ark-cli 0.3.3.pre → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ark/cli.rb +377 -197
  3. metadata +5 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 72720815ec557828109dd2b41d70c7696d2af6d1
4
- data.tar.gz: cb7c8672236156022779ef6efd10611bbc072791
3
+ metadata.gz: 8f81a9ef56e28f05713802328520747a8cd55f28
4
+ data.tar.gz: c3c8d30d31f3f89fbf32eeb1f2de9082a45b494b
5
5
  SHA512:
6
- metadata.gz: 4cc0f8a933c6d7aae990c0ca27f400553715063cdbeceba75eebef89f987ab037e906990df1ab7e725022ca8d776dca33aa42053bc2b4ca9d8aaa77f4f4b1f09
7
- data.tar.gz: 7fe81ef9f2a608955236343baf419394fa2bb726f7b9fdc3e6c4de91066891a2c08485a7f42105ce8f18e4cea27c7dc2c69acca6164b3c23e04bc41e5d254a27
6
+ metadata.gz: a971ddca64327ee1f4931fa09bf6597c3e43d6cb0b8f70b119d644f5814d57ce794c2a8b47ca7638027792491ccb503f653ce1bfbeef12e4866cc5d49c6b9b75
7
+ data.tar.gz: 3a15696e5b260bb06537e05f078d954784dad19e9621dc4871e0f5905a65be77cca3e50d178127d6b17a66ade5f2227041203730fa54c85f568c821cf3990e62
@@ -25,117 +25,118 @@ module Ark # :nodoc:
25
25
  #
26
26
  # Call #begin to define a new interface and parse the command line. See
27
27
  # +README.md+ or +example/hello.rb+ for more information.
28
- class CLI
29
28
 
30
- # Raised when a nonexistent option is received
31
- class NoSuchOptionError < ArgumentError
29
+ # Represents an option and stores the option's current state, as well as
30
+ # usage information.
31
+ class Option
32
+ # Initialize a new Option instance
33
+ # [+keys+] A list of names this option will be identified by
34
+ # [+args+] A list of argument named this option will expect
35
+ # [+desc+] A short description of this option
36
+ def initialize(long, short=nil, args=nil, desc=nil)
37
+ @long = long
38
+ @short = short
39
+ @args = args || []
40
+ @vals = []
41
+ @flag = false
42
+ @count = 0
43
+ @desc = desc || ''
32
44
  end
33
-
34
- # Raised when the command line is malformed
35
- class SyntaxError < ArgumentError
45
+ # A count of how many times this option has been given on the command line.
46
+ # Useful for flags that might be specified repeatedly, like +-vvv+ to raise
47
+ # verbosity three times.
48
+ attr_reader :count
49
+ # A short description of the option, if given
50
+ attr_reader :desc
51
+ # Long name for this option
52
+ attr_reader :long
53
+ # Short name for this option
54
+ attr_reader :short
55
+
56
+ # Return the number of arguments this option expects
57
+ def arity()
58
+ return @args.length
36
59
  end
37
60
 
38
- # Represents an option and stores the option's current state, as well as
39
- # usage information.
40
- class Option
41
- # Initialize a new Option instance
42
- # [+keys+] A list of names this option will be identified by
43
- # [+args+] A list of argument named this option will expect
44
- # [+desc+] A short description of this option
45
- def initialize(keys, args=nil, desc=nil)
46
- @keys = keys
47
- @args = args || []
48
- @vals = []
49
- @flag = false
50
- @count = 0
51
- @desc = desc || ''
52
- end
53
- # A count of how many times this option has been given on the command line.
54
- # Useful for flags that might be specified repeatedly, like +-vvv+ to raise
55
- # verbosity three times.
56
- attr_reader :count
57
- # A short description of the option, if given
58
- attr_reader :desc
59
-
60
- # Return the number of arguments this option expects
61
- def arity()
62
- return @args.length
61
+ # Return a count of how many arguments this option still expects
62
+ def vals_needed()
63
+ if self.flag?
64
+ return 0
65
+ else
66
+ return @args.length - @vals.length
63
67
  end
68
+ end
64
69
 
65
- # Return a count of how many arguments this option still expects
66
- def vals_needed()
67
- if self.flag?
68
- return 0
69
- else
70
- return @args.length - @vals.length
71
- end
72
- end
70
+ # True if this option has received all the arguments it expects, or if this
71
+ # option expects no arguments
72
+ def full?
73
+ return self.vals_needed == 0
74
+ end
73
75
 
74
- # True if this option has received all the arguments it expects, or if this
75
- # option expects no arguments
76
- def full?
77
- return self.vals_needed == 0
78
- end
76
+ # True if this option expects no arguments; opposite of #has_args?
77
+ def flag?
78
+ return @args.empty?
79
+ end
79
80
 
80
- # True if this option expects no arguments; opposite of #has_args?
81
- def flag?
82
- return @args.empty?
81
+ # Toggle this option to the true state and increment the toggle count. Only
82
+ # valid for options which expect no argument (flags). Attempting to toggle
83
+ # a option with expected arguments will raise an error.
84
+ def toggle()
85
+ if self.flag?
86
+ @count += 1
87
+ @flag = true
88
+ else
89
+ raise StandardError, "Tried to toggle an option which expects an argument"
83
90
  end
91
+ end
84
92
 
85
- # Toggle this option to the true state and increment the toggle count. Only
86
- # valid for options which expect no argument (flags). Attempting to toggle
87
- # a option with expected arguments will raise an error.
88
- def toggle()
89
- if self.flag?
90
- @count += 1
91
- @flag = true
93
+ # Pass an argument +arg+ to this option
94
+ def push(arg)
95
+ @vals << arg
96
+ end
97
+
98
+ # Return the current value of this option
99
+ def value()
100
+ if self.flag?
101
+ return @flag
102
+ else
103
+ if self.full? && @vals.length == 1
104
+ return @vals[0]
105
+ elsif self.full?
106
+ return @vals
92
107
  else
93
- raise StandardError, "Tried to toggle an option which expects an argument"
108
+ return nil
94
109
  end
95
110
  end
111
+ end
96
112
 
97
- # Pass an argument +arg+ to this option
98
- def push(arg)
99
- @vals << arg
100
- end
113
+ # True if this option expects an argument. Opposite of #flag?
114
+ def has_args?
115
+ return @args.length > 0
116
+ end
101
117
 
102
- # Return the current value of this option
103
- def value()
104
- if self.flag?
105
- return @flag
106
- else
107
- if self.full? && @vals.length == 1
108
- return @vals[0]
109
- elsif self.full?
110
- return @vals
111
- else
112
- return nil
113
- end
114
- end
118
+ # Return basic usage information: the option's names and arguments
119
+ def header()
120
+ if self.flag?
121
+ args = ''
122
+ else
123
+ args = ' ' + @args.join(', ').upcase
115
124
  end
125
+ short = @short ? "-#{@short} " : ''
126
+ return "#{short}--#{@long}#{args}"
127
+ end
116
128
 
117
- # True if this option expects an argument. Opposite of #flag?
118
- def has_args?
119
- return @args.length > 0
120
- end
129
+ # Represent this option as a string
130
+ def to_s()
131
+ return "(#{self.header})"
132
+ end
133
+ end
121
134
 
122
- # Return basic usage information: the option's names and arguments
123
- def header()
124
- if self.flag?
125
- args = ''
126
- else
127
- args = ' ' + @args.join(', ').upcase
128
- end
129
- keys = @keys.sort {|a,b| a.length <=> b.length }
130
- keys = keys.map {|k| k.length > 1 ? "--#{k}" : "-#{k}" }
131
- keys = keys.join(' ')
132
- return keys + args
133
- end
134
135
 
135
- # Represent this option as a string
136
- def to_s()
137
- return "(#{self.header})"
138
- end
136
+ class CLI
137
+
138
+ # Raised when the command line is malformed
139
+ class SyntaxError < ArgumentError
139
140
  end
140
141
 
141
142
  # Convenience method for interface declarations. Yields the CLI instance and
@@ -143,42 +144,37 @@ class CLI
143
144
  # with #new, modifying it, and then calling #parse
144
145
  #
145
146
  # +args+ is an array of strings, which defaults to ARGV
146
- def self.begin(args=ARGV, &block)
147
- cli = self.new(args)
148
- yield cli
149
- cli.parse()
150
- if cli[:help]
151
- cli.print_help()
152
- exit 0
153
- else
154
- return cli
155
- end
147
+ def self.report(args=ARGV, &block)
148
+ cli = self.new(args, &block)
149
+ return cli.report
156
150
  end
157
151
 
158
152
  # Initialize a CLI instance.
159
153
  #
160
154
  # +args+ must be an array of strings, like ARGV
161
- def initialize(args)
162
- @args = args
163
- @output_args = []
164
- @scriptargs = []
165
- @refargs = []
166
- @named_args = {}
167
- @options = {}
168
- @variadic = false
169
- @variad = nil
170
- @scriptname = nil
171
- @desc = nil
155
+ def initialize(args, &block)
156
+ self.rebuild(args, &block)
157
+ end
158
+
159
+ attr_reader :report
172
160
 
173
- self.opt :help, :h, desc: "Print usage information"
161
+ def rebuild(input=ARGV, &block)
162
+ @input = input
163
+ @spec = Spec.new
164
+ yield @spec
165
+ @spec.opt :help, :h, desc: "Print usage information"
166
+ self.parse
174
167
  end
175
168
 
176
169
  # Parse the command line
177
170
  def parse()
178
171
  taking_options = true
179
172
  last_opt = nil
173
+ refargs = @spec.get_args.clone
174
+
175
+ @report = Report.new()
180
176
 
181
- @args.each do |word|
177
+ @input.each do |word|
182
178
  dbg "Parsing '#{word}'"
183
179
  if last_opt && last_opt.has_args? && !last_opt.full?
184
180
  dbg "Got argument '#{word}' for '#{last_opt}'", 1
@@ -190,10 +186,10 @@ class CLI
190
186
  shorts = word[/[^-]+$/].split('')
191
187
  shorts.each_with_index do |short, i|
192
188
  last_short = i == (shorts.length - 1)
193
- opt = self.get_opt(short)
189
+ opt = @spec.get_opt(short)
194
190
  last_opt = opt
195
191
  if opt.has_args? && shorts.length > 1 && !last_short
196
- raise SyntaxError, "Error: compound option '#{word}' expected an argument"
192
+ raise SyntaxError, "Error: -#{short} in compound option '#{word}' expects an argument"
197
193
  elsif opt.flag?
198
194
  opt.toggle()
199
195
  dbg "Toggled flag '#{opt}'", 1
@@ -202,7 +198,7 @@ class CLI
202
198
  elsif word[/^--/]
203
199
  dbg "Identified long option", 1
204
200
  key = word[/[^-]+$/]
205
- opt = self.get_opt(key)
201
+ opt = @spec.get_opt(key)
206
202
  last_opt = opt
207
203
  if opt.flag?
208
204
  opt.toggle()
@@ -212,44 +208,188 @@ class CLI
212
208
  else
213
209
  dbg "Parsed output arg", 1
214
210
  taking_options = false
215
- @output_args << word
216
- key = @scriptargs.shift
211
+ @report.args << word
212
+ key = refargs.shift
217
213
  if key
218
- if key == @variad
219
- @named_args[key] = []
220
- @named_args[key] << word
214
+ if key == @spec.get_variad
215
+ @report.arg[key] = []
216
+ @report.arg[key] << word
221
217
  else
222
- @named_args[key] = word
218
+ @report.arg[key] = word
223
219
  end
224
- elsif @variadic
225
- @named_args[@variad] << word
220
+ elsif @spec.is_variadic?
221
+ @report.arg[@spec.get_variad] << word
222
+ else
223
+ @report.trailing << word
224
+ end
225
+ end
226
+ end
227
+ end
228
+ @spec.get_opts.each do |name, opt|
229
+ @report.opt[name] = opt.value
230
+ @report.count[name] = opt.count
231
+ end
232
+ @spec.get_args.each do |name|
233
+ if @report.arg[name].nil?
234
+ if @spec.has_default?(name)
235
+ @report.arg[name] = @spec.get_default(name)
236
+ @report.args << @report.arg[name]
237
+ end
238
+ end
239
+ end
240
+ if @report.opt[:help]
241
+ self.print_usage()
242
+ end
243
+ end
244
+
245
+ # Construct usage information
246
+ def usage()
247
+ tb = TextBuilder.new()
248
+
249
+ tb.next 'USAGE:'
250
+ tb.push @spec.get_name if @spec.get_name
251
+
252
+ tb.push '[OPTION'
253
+ tb.add '...' if @spec.has_options?
254
+ tb.add ']'
255
+
256
+ if @spec.has_args?
257
+ if @spec.is_variadic?
258
+ singles = @spec.get_args[0..-2].map do |a|
259
+ if @spec.has_default?(a)
260
+ a = "[#{a}]"
261
+ end
262
+ a.upcase
263
+ end
264
+ tb.push singles
265
+ v = @spec.get_args.last.upcase
266
+ tb.push "[#{v}1 #{v}2...]"
267
+ else
268
+ argmap = @spec.get_args.map do |a|
269
+ if @spec.has_default?(a)
270
+ a = "[#{a}]"
226
271
  end
272
+ a.upcase
227
273
  end
274
+ tb.push argmap
228
275
  end
229
276
  end
230
277
 
278
+ if @spec.get_desc
279
+ tb.skip @spec.get_desc
280
+ tb.wrap(indent: 4)
281
+ end
282
+
283
+ tb.skip 'OPTIONS:'
284
+ tb.skip
285
+
286
+ @spec.get_opts.values.uniq.each do |opt|
287
+ tb.indent 4
288
+ tb.push opt.header
289
+ if opt.desc
290
+ tb.next
291
+ tb.indent 8
292
+ tb.push opt.desc
293
+ end
294
+ tb.skip
295
+ end
296
+
297
+ return tb.print
231
298
  end
232
299
 
233
- # Define an Option
234
- # [+keys+] A list of names for this option
235
- # [+args+] A list of arguments the option expects
236
- # [+desc+] A short description of the option, used to provide usage info
237
- def opt(*keys, args: nil, desc: nil)
238
- raise ArgumentError, "An option must have at least one name" if keys.empty?
239
- keys.map!(&:to_sym)
240
- args.map!(&:to_sym) if args
241
- o = Option.new(keys, args, desc)
242
- keys.each {|k| @options[k] = o }
300
+ # Print usage information and exit
301
+ def print_usage()
302
+ puts self.usage
303
+ exit 0
243
304
  end
305
+ end
306
+
307
+ class Spec
308
+
309
+ # Raised when a nonexistent option is received
310
+ class NoSuchOptionError < ArgumentError
311
+ end
312
+
313
+ # Raised when there is a syntax error in the args declaration
314
+ class ArgumentSyntaxError < ArgumentError
315
+ end
316
+
317
+ # Initialize a bare interface spec
318
+ def initialize()
319
+ @args = []
320
+ @options = {}
321
+ @variadic = false
322
+ @option_listing = false
323
+ @trailing_error = false
324
+ end
325
+ # If true, the full option list will always be displayed in the usage info
326
+ # header
327
+ attr_reader :option_listing
328
+ # If true, an error will be raised if trailing arguments are given
329
+ attr_reader :trailing_error
244
330
 
245
- # Return all command line arguments
246
- def args()
247
- return @output_args
331
+ private
332
+
333
+ def strip(arg)
334
+ return arg[/^(.+?)(\.\.\.$|_$|$)/, 1]
335
+ end
336
+
337
+ def optional?(arg)
338
+ return !arg[/_$/].nil?
248
339
  end
249
340
 
250
- # Return the value of the named argument +name+
251
- def arg(name)
252
- return @named_args[name.to_sym]
341
+ def variadic?(arg)
342
+ return !arg[/\.\.\.$/].nil?
343
+ end
344
+
345
+ def parse_arg(arg, default: nil, last: false)
346
+ stripped = strip(arg)
347
+ @args << stripped
348
+ if optional?(arg)
349
+ @optional << stripped
350
+ end
351
+ unless default.nil?
352
+ @defaults[stripped] = default
353
+ end
354
+ if variadic?(arg)
355
+ if last
356
+ @variadic = true
357
+ @variad = stripped
358
+ else
359
+ raise ArgumentSyntaxError,
360
+ "Variadic arguments must come last. Offending variad is '#{arg}'"
361
+ end
362
+ end
363
+ end
364
+
365
+ public
366
+
367
+ def get_name
368
+ return @name
369
+ end
370
+
371
+ def get_desc
372
+ return @desc
373
+ end
374
+
375
+ def get_args
376
+ return @args
377
+ end
378
+
379
+ def get_opts
380
+ return @options
381
+ end
382
+
383
+ def is_variadic?
384
+ return @variadic
385
+ end
386
+
387
+ def get_variad
388
+ return @variad
389
+ end
390
+
391
+ def get_defaults
392
+ return @defaults
253
393
  end
254
394
 
255
395
  # Get an Option object for the given option +name+
@@ -261,14 +401,24 @@ class CLI
261
401
  return @options[name]
262
402
  end
263
403
 
264
- # Get the value of a given option by +name+
265
- def [](name)
266
- return self.get_opt(name).value
404
+ def is_optional?(arg)
405
+ @optional.member?(arg.to_s)
406
+ end
407
+
408
+ def has_default?(arg)
409
+ @defaults.key?(arg.to_s)
410
+ end
411
+
412
+ def get_default(arg)
413
+ @defaults[arg.to_s]
414
+ end
415
+
416
+ def has_args?
417
+ @args.length > 0
267
418
  end
268
419
 
269
- # Get the toggle count of a flag by +name+
270
- def count(name)
271
- return self.get_opt(name).count
420
+ def has_options?
421
+ @options.values.uniq.length > 1
272
422
  end
273
423
 
274
424
  # Specify general information about the program
@@ -276,69 +426,99 @@ class CLI
276
426
  # [+desc+] Short description of the program
277
427
  # [+args+] A list of named arguments
278
428
  def header(name: nil, desc: nil, args: [])
279
- @scriptname = name
280
- @desc = desc
281
- @scriptargs = args.map(&:to_sym)
282
- if @scriptargs.last == :*
283
- if @scriptargs.length > 1
284
- @variadic = true
285
- @scriptargs.pop
286
- @refargs = @scriptargs.clone
287
- @variad = @scriptargs.last
288
- else
289
- @scriptargs = []
290
- end
291
- else
292
- @refargs = @scriptargs.clone
293
- end
429
+ self.name(name)
430
+ self.desc(desc)
431
+ self.args(args)
294
432
  end
295
433
 
296
- # Print usage information
297
- def print_help()
298
- tb = TextBuilder.new()
299
-
300
- tb.push @scriptname || 'Usage:'
434
+ # Set the name of the program to +str+
435
+ def name(str)
436
+ @name = str.to_s if str
437
+ end
301
438
 
302
- if @options.length > 0
303
- tb.push '[OPTION'
304
- tb.add '...' if @options.values.uniq.length > 1
305
- tb.add ']'
306
- end
439
+ # Set the description of the program to +str+
440
+ def desc(str)
441
+ @desc = str.to_s if str
442
+ end
307
443
 
308
- if !@refargs.empty?
309
- if @variadic
310
- singles = @refargs[0..-2].map(&:upcase)
311
- tb.push singles
312
- v = @variad.upcase
313
- tb.push "#{v}1 #{v}2..."
444
+ # Define what arguments the program will accept
445
+ def args(*input)
446
+ @args = []
447
+ @optional = []
448
+ @defaults = {}
449
+
450
+ input.flatten.each_with_index do |item, i|
451
+ list_last = (input.length - (i + 1)) == 0
452
+ if item.is_a?(Hash)
453
+ item.each_with_index do |pair,ii|
454
+ k = pair[0].to_s
455
+ v = pair[1]
456
+ hash_last = (item.length - (ii + 1)) == 0
457
+ last = hash_last && list_last
458
+ parse_arg(k, default: v, last: last)
459
+ end
314
460
  else
315
- tb.push @refargs.map(&:upcase)
461
+ parse_arg(item, last: list_last)
316
462
  end
317
463
  end
318
464
 
319
- if @desc
320
- tb.next @desc
321
- tb.wrap(indent: 4)
322
- end
465
+ @refargs = @args.clone
466
+ end
323
467
 
324
- tb.skip 'OPTIONS:'
325
- tb.skip
468
+ # Define an Option
469
+ # [+keys+] A list of names for this option
470
+ # [+args+] A list of arguments the option expects
471
+ # [+desc+] A short description of the option, used to provide usage info
472
+ def opt(long, short=nil, args: nil, desc: nil)
473
+ long = long.to_sym
474
+ short = short.to_sym if short
475
+ args = [args] if args.is_a?(String)
476
+ args.map!(&:to_sym) if args
477
+ o = Option.new(long, short, args, desc)
478
+ @options[long] = o
479
+ @options[short] = o if short
480
+ end
326
481
 
327
- @options.values.uniq.each do |opt|
328
- tb.indent 4
329
- tb.push opt.header
330
- if opt.desc
331
- tb.next
332
- tb.indent 8
333
- tb.push opt.desc
334
- end
335
- tb.skip
336
- end
482
+ # Force the full option list display in the usage info, no matter how many
483
+ # options the program has
484
+ def force_option_list()
485
+ @option_list = true
486
+ end
337
487
 
338
- puts tb.print
488
+ # The parser will raise an error on finding trailing arguments (default
489
+ # behavior is to ignore and stuff the trailing args into Report.trailing_args)
490
+ def raise_on_trailing()
491
+ @trailing_error = true
339
492
  end
340
493
 
341
494
  end
342
495
 
496
+ class Report
497
+ def initialize()
498
+ @args = []
499
+ @named_args = {}
500
+ @trailing_args = []
501
+ @counts = {}
502
+ @options = {}
503
+ end
504
+
505
+ def args
506
+ return @args
507
+ end
508
+ def arg
509
+ return @named_args
510
+ end
511
+ def trailing
512
+ return @trailing_args
513
+ end
514
+ def opt
515
+ return @options
516
+ end
517
+ def count
518
+ return @counts
519
+ end
520
+
343
521
  end
344
522
 
523
+ end # module Ark
524
+
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ark-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3.pre
4
+ version: 0.4.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Macquarie Sharpless
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-09-03 00:00:00.000000000 Z
11
+ date: 2015-09-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ark-util
@@ -56,12 +56,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
56
56
  version: '0'
57
57
  required_rubygems_version: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - ">"
59
+ - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: 1.3.1
61
+ version: '0'
62
62
  requirements: []
63
63
  rubyforge_project:
64
- rubygems_version: 2.4.5.1
64
+ rubygems_version: 2.4.5
65
65
  signing_key:
66
66
  specification_version: 4
67
67
  summary: Simple commandline interface