clive 0.8.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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