clive 0.8.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/LICENSE +1 -1
  2. data/README.md +328 -227
  3. data/lib/clive.rb +130 -50
  4. data/lib/clive/argument.rb +170 -0
  5. data/lib/clive/arguments.rb +139 -0
  6. data/lib/clive/arguments/parser.rb +210 -0
  7. data/lib/clive/base.rb +189 -0
  8. data/lib/clive/command.rb +342 -444
  9. data/lib/clive/error.rb +66 -0
  10. data/lib/clive/formatter.rb +57 -141
  11. data/lib/clive/formatter/colour.rb +37 -0
  12. data/lib/clive/formatter/plain.rb +172 -0
  13. data/lib/clive/option.rb +185 -75
  14. data/lib/clive/option/runner.rb +163 -0
  15. data/lib/clive/output.rb +141 -16
  16. data/lib/clive/parser.rb +180 -87
  17. data/lib/clive/struct_hash.rb +109 -0
  18. data/lib/clive/type.rb +117 -0
  19. data/lib/clive/type/definitions.rb +170 -0
  20. data/lib/clive/type/lookup.rb +23 -0
  21. data/lib/clive/version.rb +3 -3
  22. data/spec/clive/a_cli_spec.rb +245 -0
  23. data/spec/clive/argument_spec.rb +148 -0
  24. data/spec/clive/arguments/parser_spec.rb +35 -0
  25. data/spec/clive/arguments_spec.rb +191 -0
  26. data/spec/clive/command_spec.rb +276 -209
  27. data/spec/clive/formatter/colour_spec.rb +129 -0
  28. data/spec/clive/formatter/plain_spec.rb +129 -0
  29. data/spec/clive/option/runner_spec.rb +92 -0
  30. data/spec/clive/option_spec.rb +149 -23
  31. data/spec/clive/output_spec.rb +86 -2
  32. data/spec/clive/parser_spec.rb +201 -81
  33. data/spec/clive/struct_hash_spec.rb +82 -0
  34. data/spec/clive/type/definitions_spec.rb +312 -0
  35. data/spec/clive/type_spec.rb +107 -0
  36. data/spec/clive_spec.rb +60 -0
  37. data/spec/extras/expectations.rb +86 -0
  38. data/spec/extras/focus.rb +22 -0
  39. data/spec/helper.rb +35 -0
  40. metadata +56 -36
  41. data/lib/clive/bool.rb +0 -67
  42. data/lib/clive/exceptions.rb +0 -54
  43. data/lib/clive/flag.rb +0 -199
  44. data/lib/clive/switch.rb +0 -31
  45. data/lib/clive/tokens.rb +0 -141
  46. data/spec/clive/bool_spec.rb +0 -54
  47. data/spec/clive/flag_spec.rb +0 -117
  48. data/spec/clive/formatter_spec.rb +0 -108
  49. data/spec/clive/switch_spec.rb +0 -14
  50. data/spec/clive/tokens_spec.rb +0 -38
  51. data/spec/shared_specs.rb +0 -16
  52. data/spec/spec_helper.rb +0 -12
@@ -0,0 +1,170 @@
1
+ # can't autoload time as the constant is already defined
2
+ require 'time'
3
+ autoload :Pathname, 'pathname'
4
+
5
+ class Clive
6
+ class Type
7
+
8
+ # Basic object, all arguments are valid and will simply return
9
+ # themselves.
10
+ class Object < Type
11
+
12
+ # Test the value to see if it is a valid value for this Tyoe.
13
+ # @param arg [String] The value to be tested
14
+ def valid?(arg)
15
+ true
16
+ end
17
+
18
+ # Cast the arg (String) to the correct type.
19
+ # @param arg [String] The value to be cast
20
+ def typecast(arg)
21
+ arg
22
+ end
23
+ end
24
+
25
+ # String will accept any argument which is not +nil+ and will
26
+ # return the argument with #to_s called on it.
27
+ class String < Object
28
+ refute :nil?
29
+ cast :to_s
30
+ end
31
+
32
+ # Symbol will accept and argument which is not +nil+ and will
33
+ # return the argument with #to_sym called on it.
34
+ class Symbol < Object
35
+ refute :nil?
36
+ cast :to_sym
37
+ end
38
+
39
+ # Integer will match anything that float matches, but will
40
+ # return an integer. If you need something that only matches
41
+ # integer values properly use {StrictInteger}.
42
+ class Integer < Object
43
+ match /^[-+]?\d*\.?\d+([eE][-+]?\d+)?$/
44
+ cast :to_i
45
+ end
46
+
47
+ # StrictInteger only matches strings that look like integers,
48
+ # it returns Integers.
49
+ # @see Integer
50
+ class StrictInteger < Object
51
+ match /^[-+]?\d+([eE][-+]?\d+)?$/
52
+ cast :to_i
53
+ end
54
+
55
+ # Binary matches any binary number which may or may not have a "0b" prefix
56
+ # and returns the number as an Integer.
57
+ class Binary < Object
58
+ match /^[-+]?(0b)?[01]*$/i
59
+ cast :to_i, 2
60
+ end
61
+
62
+ # Octal matches any octal number which may (or may not) be prefixed with "0"
63
+ # or "0o" (or even "0O") so 25, 025, 0o25 and 0O25 are all valid and will
64
+ # give the same result, the Integer 21.
65
+ class Octal < Object
66
+ match /^[-+]?(0o?)?[0-7]*$/i
67
+ cast :to_i, 8
68
+ end
69
+
70
+ # Hexadecimal matches any hexadecimal number which may or may not have a
71
+ # "0x" prefix, it returns the number as an Integer.
72
+ class Hexadecimal < Object
73
+ match /^[-+]?(0x)?[0-9a-f]*$/i
74
+ cast :to_i, 16
75
+ end
76
+
77
+ class Float < Object
78
+ match /^[-+]?\d*\.?\d+([eE][-+]?\d+)?$/
79
+ cast :to_f
80
+ end
81
+
82
+ # Boolean will accept 'true', 't', 'yes', 'y' or 'on' as +true+ and
83
+ # 'false', 'f', 'no', 'n' or 'off' as +false+.
84
+ class Boolean < Object
85
+ TRUE_VALUES = %w(true t yes y on)
86
+ FALSE_VALUES = %w(false f no n off)
87
+
88
+ def valid?(arg)
89
+ (TRUE_VALUES + FALSE_VALUES).include? arg
90
+ end
91
+
92
+ def typecast(arg)
93
+ case arg
94
+ when *TRUE_VALUES then true
95
+ when *FALSE_VALUES then false
96
+ end
97
+ end
98
+ end
99
+
100
+ class Pathname < Object
101
+ refute :nil?
102
+
103
+ def typecast(arg)
104
+ ::Pathname.new arg
105
+ end
106
+ end
107
+
108
+ # Range accepts 'a..b', 'a...b' which behave as in ruby and
109
+ # 'a-b' which behaves like 'a..b'. It returns the correct
110
+ # Range object.
111
+ class Range < Object
112
+ match /^(\w+\.\.\.?\w+|\w+\-\w+)$/
113
+
114
+ def typecast(arg)
115
+ if arg.include?('...')
116
+ a,b = arg.split('...')
117
+ ::Range.new a, b, true
118
+ elsif arg.include?('..')
119
+ a,b = arg.split('..')
120
+ ::Range.new a, b, false
121
+ else
122
+ a,b = arg.split('-')
123
+ ::Range.new a, b, false
124
+ end
125
+ end
126
+ end
127
+
128
+ # Array accepts a list of arguments separated by a comma, no
129
+ # spaces are allowed. It returns an array of the elements.
130
+ class Array < Object
131
+ match /^(.+,)*.+[^,]$/
132
+ cast :split, ','
133
+ end
134
+
135
+ # Time accepts any value which can be parsed by {::Time.parse},
136
+ # it returns the correct {::Time} object.
137
+ class Time < Object
138
+ def valid?(arg)
139
+ ::Time.parse arg
140
+ true
141
+ rescue
142
+ false
143
+ end
144
+
145
+ def typecast(arg)
146
+ ::Time.parse arg
147
+ end
148
+ end
149
+
150
+ class Regexp < Object
151
+ match /^\/.*?\/[imxou]*$/
152
+
153
+ OPTS = {
154
+ 'x' => ::Regexp::EXTENDED,
155
+ 'i' => ::Regexp::IGNORECASE,
156
+ 'm' => ::Regexp::MULTILINE
157
+ }
158
+
159
+ def typecast(arg)
160
+ parts = arg.split('/')
161
+ mods = parts.pop
162
+ arg = parts.join('')
163
+ mods = mods.split('').map {|a| OPTS[a] }.inject{|a,e| a | e }
164
+
165
+ ::Regexp.new arg, mods
166
+ end
167
+ end
168
+
169
+ end
170
+ end
@@ -0,0 +1,23 @@
1
+ class Clive
2
+ class Type
3
+
4
+ # Clive::Type::Lookup is an almost carbon copy of DataMapper::Property::Lookup
5
+ # @see https://github.com/datamapper/dm-core/blob/master/lib/dm-core/property/lookup.rb
6
+ module Lookup
7
+
8
+ protected
9
+
10
+ # Provides access to Types defined under {Type} as if accessed
11
+ # normally.
12
+ #
13
+ # @param name [#to_s] The name of the Type to lookup.
14
+ # @return [Type] The type with the given name.
15
+ # @raise [NameError] The property could not be found.
16
+ #
17
+ def const_missing(name)
18
+ Type.find_class(name.to_s) || super
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -1,3 +1,3 @@
1
- module Clive
2
- VERSION = '0.8.1'
3
- end
1
+ class Clive
2
+ VERSION = '1.0.0'
3
+ end
@@ -0,0 +1,245 @@
1
+ $: << File.dirname(__FILE__) + '/..'
2
+ require 'helper'
3
+
4
+ describe 'A CLI' do
5
+ subject {
6
+ Clive.new {
7
+
8
+ header 'Usage: clive_test.rb [command] [options]'
9
+
10
+ opt :version, :tail => true do
11
+ puts "Version 1"
12
+ end
13
+
14
+ set :something, []
15
+
16
+ bool :v, :verbose
17
+ bool :a, :auto
18
+
19
+ opt :s, :size, 'Size of thing', :arg => '<size>', :as => Float
20
+ opt :S, :super_size
21
+
22
+ opt :name, :args => '<name>'
23
+ opt :modify, :arg => '<key> <sym> [<args>]', :as => [Symbol, Symbol, Array] do
24
+ update key, sym, *args
25
+ end
26
+
27
+ desc 'Print <message> <n> times'
28
+ opt :print, :arg => '<message> <n>', :as => [String, Integer] do
29
+ n.times { puts message }
30
+ end
31
+
32
+ desc 'A super long description for a super stupid option, this should test the _extreme_ wrapping abilities as it should all be aligned. Maybe I should go for another couple of lines just for good measure. That\'s all'
33
+ opt :complex, :arg => '[<one>] <two> [<three>]', :match => [ /^\d$/, /^\d\d$/, /^\d\d\d$/ ] do |a,b,c|
34
+ puts "a: #{a}, b: #{b}, c: #{c}"
35
+ end
36
+
37
+ command :new, :create, 'Creates new things', :arg => '[<dir>]', :match => /\// do
38
+
39
+ set :something, []
40
+
41
+ # implicit arg as "<choice>", also added default
42
+ opt :T, :type, :in => %w(post page blog), :default => :page, :as => Symbol
43
+ opt :force, 'Force overwrite' do
44
+ require 'highline/import'
45
+ answer = ask("Are you sure, this could delete stuff? [y/n]\n")
46
+ set :force, true if answer == "y"
47
+ end
48
+
49
+ action do |dir|
50
+ puts "Creating #{get :type} in #{dir}" if dir
51
+ end
52
+ end
53
+ }
54
+ }
55
+
56
+ describe '--version' do
57
+ it 'prints version string' do
58
+ this {
59
+ subject.run s '--version'
60
+ }.must_output "Version 1\n"
61
+ end
62
+ end
63
+
64
+ describe 'set :something' do
65
+ it 'is set to an empty Array' do
66
+ r = subject.run []
67
+ r[:something].must_equal []
68
+ end
69
+ end
70
+
71
+ describe '-a, --[no-]auto' do
72
+ it 'sets to true' do
73
+ r = subject.run s '--auto'
74
+ r[:auto].must_be_true
75
+ r[:a].must_be_true
76
+ end
77
+
78
+ it 'sets to false if no passed' do
79
+ r = subject.run s '--no-auto'
80
+ r[:auto].must_be_false
81
+ r[:a].must_be_false
82
+ end
83
+
84
+ it 'allows the short version' do
85
+ r = subject.run s '-a'
86
+ r[:auto].must_be_true
87
+ r[:a].must_be_true
88
+ end
89
+ end
90
+
91
+ describe '-s, --size' do
92
+ it 'takes a Float as an argument' do
93
+ r = subject.run s '--size 50.56'
94
+ r[:size].must_equal 50.56
95
+ r[:s].must_equal 50.56
96
+ end
97
+
98
+ it 'raises an error if the argument is not passed' do
99
+ this {
100
+ subject.run s '--size'
101
+ }.must_raise Clive::Parser::MissingArgumentError
102
+ end
103
+
104
+ it 'raises an error if a Float is not given' do
105
+ this {
106
+ subject.run s '--size hello'
107
+ }.must_raise Clive::Parser::MissingArgumentError
108
+ end
109
+ end
110
+
111
+ describe '-S, --super-size' do
112
+ it 'can be called with dashes' do
113
+ r = subject.run s '--super-size'
114
+ r[:super_size].must_be_true
115
+ r[:S].must_be_true
116
+ end
117
+
118
+ it 'can be called with underscores' do
119
+ r = subject.run s '--super_size'
120
+ r[:super_size].must_be_true
121
+ r[:S].must_be_true
122
+ end
123
+ end
124
+
125
+ describe '--modify' do
126
+ it 'updates the key' do
127
+ r = subject.run s '--name "John Doe" --modify name count oe,e'
128
+ r[:name].must_equal 1
129
+ end
130
+ end
131
+
132
+ describe '--print' do
133
+ it 'prints a message n times' do
134
+ this {
135
+ subject.run s '--print "Hello World!" 5'
136
+ }.must_output "Hello World!\n" * 5
137
+ end
138
+ end
139
+
140
+ describe '--complex' do
141
+ it 'takes one argument' do
142
+ this {
143
+ subject.run s '--complex 55'
144
+ }.must_output "a: , b: 55, c: \n"
145
+
146
+ this {
147
+ subject.run s '--complex 4'
148
+ }.must_raise Clive::Parser::MissingArgumentError
149
+
150
+ this {
151
+ subject.run s '--complex 666'
152
+ }.must_raise Clive::Parser::MissingArgumentError
153
+ end
154
+
155
+ it 'takes two arguments' do
156
+ this {
157
+ subject.run s '--complex 4 55'
158
+ }.must_output "a: 4, b: 55, c: \n"
159
+
160
+ this {
161
+ subject.run s '--complex 55 666'
162
+ }.must_output "a: , b: 55, c: 666\n"
163
+
164
+ this {
165
+ subject.run s '--complex 4 666'
166
+ }.must_raise Clive::Parser::MissingArgumentError
167
+ end
168
+
169
+ it 'takes three arguments' do
170
+ this {
171
+ subject.run s '--complex 4 55 666'
172
+ }.must_output "a: 4, b: 55, c: 666\n"
173
+ end
174
+ end
175
+
176
+ describe 'new' do
177
+ describe 'set :something' do
178
+ it 'sets :something in :new to []' do
179
+ r = subject.run s 'new'
180
+ r[:new][:something].must_equal []
181
+ end
182
+ end
183
+
184
+ describe '-T, --type' do
185
+ it 'sets the type' do
186
+ r = subject.run s 'new --type blog'
187
+ r[:new][:type].must_equal :blog
188
+ r[:new][:T].must_equal :blog
189
+ end
190
+
191
+ it 'uses the default' do
192
+ r = subject.run s 'new --type'
193
+ r[:new][:type].must_equal :page
194
+ r[:new][:T].must_equal :page
195
+ end
196
+
197
+ it 'ignores non valid options' do
198
+ r = subject.run s 'new --type crazy'
199
+ r[:new][:type].must_equal :page
200
+ r[:new][:T].must_equal :page
201
+ r.args.must_equal ['crazy']
202
+ end
203
+ end
204
+
205
+ describe '--force' do
206
+ #it 'asks for conformation' do
207
+ # r = subject.run s 'new --force'
208
+ # r[:force].must_be_true
209
+ #end
210
+ end
211
+
212
+ describe 'action' do
213
+ it 'prints the type and dir' do
214
+ this {
215
+ subject.run s 'new --type ~/dir'
216
+ }.must_output "Creating page in ~/dir\n"
217
+ end
218
+
219
+ it 'only accepts directories' do
220
+ this {
221
+ subject.run s 'new not-a-dir'
222
+ }.must_output ""
223
+ end
224
+ end
225
+ end
226
+
227
+ it 'should be able to do this' do
228
+ this {
229
+ r = subject.run s('-v new --type post ~/my_site --no-auto arg arg2')
230
+ r.args.must_equal %w(arg arg2)
231
+ r.to_h.must_equal :something => [], :verbose => true,
232
+ :new => {:something => [], :type => :post}, :auto => false
233
+ }.must_output "Creating post in ~/my_site\n"
234
+ end
235
+
236
+ it 'should be able to do combined short switches' do
237
+ r = subject.run s '-vas 2.45'
238
+
239
+ r.to_h.must_equal :something => [], :verbose => true, :auto => true, :size => 2.45
240
+
241
+ this {
242
+ subject.run %w(-vsa 2.45)
243
+ }.must_raise Clive::Parser::MissingArgumentError
244
+ end
245
+ end
@@ -0,0 +1,148 @@
1
+ $: << File.dirname(__FILE__) + '/..'
2
+ require 'helper'
3
+
4
+ describe Clive::Argument::AlwaysTrue do
5
+ subject { Clive::Argument::AlwaysTrue }
6
+
7
+ it 'is always true for the method given' do
8
+ subject.for(:hey).hey.must_be_true
9
+ end
10
+
11
+ it 'is always true for the methods given' do
12
+ a = subject.for(:one, :two, :three)
13
+ a.one.must_be_true
14
+ a.two.must_be_true
15
+ a.three.must_be_true
16
+ end
17
+ end
18
+
19
+ describe Clive::Argument do
20
+ subject { Clive::Argument }
21
+
22
+ describe '#initialize' do
23
+ it 'converts name to Symbol' do
24
+ subject.new('arg').name.must_be_kind_of Symbol
25
+ end
26
+
27
+ it 'calls #to_proc on a Symbol constraint' do
28
+ c = mock
29
+ c.expects(:respond_to?).with(:to_proc).returns(true)
30
+ c.expects(:to_proc)
31
+
32
+ subject.new :a, :constraint => c
33
+ end
34
+
35
+ it 'merges given options with DEFAULTS' do
36
+ opts = {:optional => true}
37
+ Clive::Argument::DEFAULTS.expects(:merge).with(opts).returns({})
38
+ subject.new('arg', opts)
39
+ end
40
+
41
+ it 'finds the correct type class' do
42
+ subject.new(:a, :type => String).type.must_equal Clive::Type::String
43
+ end
44
+
45
+ it 'uses the class passed if type cannot be found' do
46
+ type = Class.new
47
+ subject.new(:a, :type => type).type.must_equal type
48
+ end
49
+ end
50
+
51
+ describe '#optional?' do
52
+ it 'is true if the argument is optional' do
53
+ subject.new(:arg, :optional => true).must_be :optional?
54
+ end
55
+
56
+ it 'is false if the argument is not optional' do
57
+ subject.new(:arg, :optional => false).wont_be :optional?
58
+ end
59
+
60
+ it 'is false by default' do
61
+ subject.new(:arg).wont_be :optional?
62
+ end
63
+ end
64
+
65
+ describe '#to_s' do
66
+ it 'surrounds the name by < and >' do
67
+ subject.new(:a).to_s.must_equal '<a>'
68
+ end
69
+
70
+ it 'surrounds optional arguments with [ and ]' do
71
+ subject.new(:a, :optional => true).to_s.must_equal '[<a>]'
72
+ end
73
+ end
74
+
75
+ describe '#choice_str' do
76
+ it 'returns the array of values allowed' do
77
+ subject.new(:a, :within => %w(a b c)).choice_str.must_equal '(a, b, c)'
78
+ end
79
+
80
+ it 'returns the range of values allowed' do
81
+ subject.new(:a, :within => 1..5).choice_str.must_equal '(1..5)'
82
+ end
83
+ end
84
+
85
+ describe '#possible?' do
86
+ describe 'for @type' do
87
+ subject { Clive::Argument.new :a, :type => Clive::Type::Time }
88
+
89
+ it 'is true for correct string values' do
90
+ subject.must_be :possible?, '12:34'
91
+ end
92
+
93
+ it 'is true for objects of type' do
94
+ subject.must_be :possible?, Time.parse('12:34')
95
+ end
96
+
97
+ unless RUBY_VERSION == '1.8.7' # No big problem so just ignore
98
+ it 'is false for incorrect values' do
99
+ subject.wont_be :possible?, 'not-a-time'
100
+ end
101
+ end
102
+ end
103
+
104
+ describe 'for @match' do
105
+ subject { Clive::Argument.new :a, :match => /^[a-e]+![f-o]+\?.$/ }
106
+
107
+ it 'is true for matching values' do
108
+ subject.must_be :possible?, 'abe!off?.'
109
+ end
110
+
111
+ it 'is false for non-matching values' do
112
+ subject.wont_be :possible?, 'off?abe!.'
113
+ end
114
+ end
115
+
116
+ describe 'for @within' do
117
+ subject { Clive::Argument.new :a, :within => %w(dog cat fish) }
118
+
119
+ it 'is true for elements included in the collection' do
120
+ subject.must_be :possible?, 'dog'
121
+ end
122
+
123
+ it 'is false for elements not in the collection' do
124
+ subject.wont_be :possible?, 'mouse'
125
+ end
126
+ end
127
+
128
+ describe 'for @constraint' do
129
+ subject { Clive::Argument.new :a, :constraint => proc {|i| i.size == 5 } }
130
+
131
+ it 'is true if the proc returns true' do
132
+ subject.must_be :possible?, 'abcde'
133
+ end
134
+
135
+ it 'is false if the proc returns false' do
136
+ subject.wont_be :possible?, 'abcd'
137
+ end
138
+ end
139
+ end
140
+
141
+ describe '#coerce' do
142
+ it 'uses @type to return the correct object' do
143
+ type = mock
144
+ type.expects(:typecast).with('str').returns(5)
145
+ subject.new(:a, :type => type).coerce("str").must_equal 5
146
+ end
147
+ end
148
+ end