ark-cli 0.3.3.pre → 0.4.4

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 (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