nub 0.0.53 → 0.0.54

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +11 -13
  3. data/lib/nub/commander.rb +43 -58
  4. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: af6ff1743615a43ecb53bb050e0f763b9f4974d24beeeb6e53607509c53719a3
4
- data.tar.gz: fbb637f9cb425e29c38a2cab27d0ba60f49c66831077a9150ae4a96d8b3945af
3
+ metadata.gz: 00de24079eb9ea5d62262d9453e4bb8745a004fec49d0c300bd8b01a430e4589
4
+ data.tar.gz: f1c7b1a86cb4637a80b48814193305b2c7a27fb50a46b7ac35ff22cb952e1b52
5
5
  SHA512:
6
- metadata.gz: 5195e80a5f024b57e4eaf6d667d3dc6e168c1933fa91b998d9781dd7f67c3c5020c941801685faf9a41c761a4c833f0ca330b79bf9fa96d4fc1ecfd86dd919fa
7
- data.tar.gz: 19bc728c493e1500accf09a4d40eb57db3f117428b6508466a295f70985dbd1cd854f9edc444c2550bb1f48a00199d6dd16e70b939fcd777e64493fb9db7ffe9
6
+ metadata.gz: 3b86079c3a02cf90b74d9db8ff3240f9d261c4ab5671af4d068e76686a45ac50682265d69bb04eb82aaee3e9c49739afe76462a19a3a08c139a03edbde992585
7
+ data.tar.gz: 2093bb7ee390f121324b39ab66beb74f8a8fd5c7baa45ff9671a26459e785f937203e03d80c278d744b4340db2fe27e242d9322d893e112f7198c15c596902b1
data/README.md CHANGED
@@ -38,25 +38,23 @@ have their own help to display their usage and available options.
38
38
 
39
39
  ### Commands <a name="commands"></a>
40
40
  Commands are defined via configuration as key words that trigger different branches of functionality
41
- for the application. Each command may have zero or more options that modify how this behaveior is
42
- invoked. Whenever more than one command is used in the command line expression the expression is
43
- interpreted as being a ***chained command expression***. Chained command expressions are executed
44
- left to right, such that you can execute the ***clean*** command then the ***build*** command or
45
- more in a single command line expression. Each command in a chained command expression may have its
46
- own specific options (those coming after the command but before the next command) or if options are
47
- omitted the required options from the next command will be used. The chained command options syntax
48
- allows one to have a cleaner multi-command line expression with reusable options. Options are said
49
- to apply in a chained command syntax when they are of the same type and position in the positional
50
- case or same type and name in the named case.
41
+ for the application. Each command may have zero or more sub commands, each of which follow the same
42
+ rules in a recursive fashion. Each command may have zero or more options that modify how this
43
+ behavior is invoked. Whenever more than one command is used in the command line expression the
44
+ expression is interpreted as being a ***chained command expression***. Chained command expressions
45
+ are executed left to right, such that you can execute the ***clean*** command then the ***build***
46
+ command or more in a single command line expression. Each command in a chained command expression
47
+ may have its own specific options (those coming after the command but before the next command) or if
48
+ options are omitted the required options from the next command will be used. The chained command
49
+ options syntax allows one to have a cleaner multi-command line expression with reusable options.
50
+ Options are said to apply in a chained command syntax when they are of the same type and position in
51
+ the positional case or same type and name in the named case.
51
52
 
52
53
  ***Global*** options are options that are added with the ***add_global*** function and will show up
53
54
  set in the command results using the ***:global*** symbol. Global positional options must be given
54
55
  before any other commands but global named options may appear anywhere in the command line
55
56
  expression.
56
57
 
57
- ***Shared*** options are options that are added with the command ***add_shared*** function. They
58
- should be added before any commands are added. They are added to each command as an explicit option.
59
-
60
58
  ***Commander.new*** must be run from the app's executable file for it to pick up the app's filename
61
59
  properly.
62
60
 
data/lib/nub/commander.rb CHANGED
@@ -34,7 +34,6 @@ class Option
34
34
  attr_reader(:type)
35
35
  attr_accessor(:allowed)
36
36
  attr_accessor(:required)
37
- attr_accessor(:shared)
38
37
 
39
38
  # Create a new option instance
40
39
  # @param key [String] option short hand, long hand and hint e.g. -s|--skip=COMPONENTS
@@ -47,13 +46,12 @@ class Option
47
46
  @long = nil
48
47
  @short = nil
49
48
  @desc = desc
50
- @shared = false
51
49
  @allowed = allowed || []
52
50
  @required = required || false
53
51
 
54
52
  # Parse the key into its components (short hand, long hand, and hint)
55
53
  #https://bneijt.nl/pr/ruby-regular-expressions/
56
- # Valid forms to look for with chars [a-zA-Z0-9-_=|]
54
+ # Valid forms to look for with chars [a-zA-Z0-9-_=|]
57
55
  # --help, --help=HINT, -h|--help, -h|--help=HINT
58
56
  Log.die("invalid option key #{key}") if key && (key.count('=') > 1 or key.count('|') > 1 or !key[/[^\w\-=|]/].nil? or
59
57
  key[/(^--[a-zA-Z0-9\-_]+$)|(^--[a-zA-Z\-_]+=\w+$)|(^-[a-zA-Z]\|--[a-zA-Z0-9\-_]+$)|(^-[a-zA-Z]\|--[a-zA-Z0-9\-_]+=\w+$)/].nil?)
@@ -89,7 +87,7 @@ class Commander
89
87
  attr_reader(:banner)
90
88
  attr_accessor(:cmds)
91
89
 
92
- Command = Struct.new(:name, :desc, :opts, :help)
90
+ Command = Struct.new(:name, :desc, :nodes, :help)
93
91
 
94
92
  # Initialize the commands for your application
95
93
  # @param app [String] application name e.g. reduce
@@ -114,9 +112,6 @@ class Commander
114
112
  # Configuration - ordered list of commands
115
113
  @config = []
116
114
 
117
- # List of options that will be added to all commands
118
- @shared = []
119
-
120
115
  # Configure default global options
121
116
  add_global(Option.new('-h|--help', 'Print command/options help'))
122
117
  end
@@ -134,46 +129,39 @@ class Commander
134
129
  # Add a command to the command list
135
130
  # @param cmd [String] name of the command
136
131
  # @param desc [String] description of the command
137
- # @param opts [List] list of command options
138
- def add(cmd, desc, options:[])
132
+ # @param nodes [List] list of command nodes (i.e. options or commands)
133
+ def add(cmd, desc, nodes:[])
139
134
  Log.die("'global' is a reserved command name") if cmd == 'global'
140
- Log.die("'shared' is a reserved command name") if cmd == 'shared'
141
135
  Log.die("'#{cmd}' already exists") if @config.any?{|x| x.name == cmd}
142
- Log.die("'help' is a reserved option name") if options.any?{|x| !x.key.nil? && x.key.include?('help')}
136
+ Log.die("'help' is a reserved option name") if nodes.any?{|x| x.class == Option && !x.key.nil? && x.key.include?('help')}
137
+ Log.die("command names must be pure lowercase letters") if cmd =~ /[^a-z]/
143
138
 
144
- # Add shared options
145
- @shared.each{|x| options.unshift(x)}
139
+ # Validate sub command key words
140
+ validate_sub_cmd = ->(sub_cmd){
141
+ Log.die("'global' is a reserved command name") if sub_cmd.name == 'global'
142
+ Log.die("'help' is a reserved option name") if sub_cmd.nodes.any?{|x| x.class == Option && !x.key.nil? && x.key.include?('help')}
143
+ Log.die("command names must be pure lowercase letters") if sub_cmd.name =~ /[^a-z]/
144
+ sub_cmd.nodes.select{|x| x.class != Option}.each{|x| validate_sub_cmd.(x)}
145
+ }
146
+ nodes.select{|x| x.class != Option}.each{|x| validate_sub_cmd.(x)}
146
147
 
147
- cmd = add_cmd(cmd, desc, options)
148
- @config << cmd
148
+ @config << add_cmd(cmd, desc, nodes)
149
149
  end
150
150
 
151
151
  # Add global options (any option coming before all commands)
152
152
  # @param option/s [Array/Option] array or single option/s
153
153
  def add_global(options)
154
- options = [options] if options.class == Option
154
+ options = [options] if options.class != Array
155
+ Log.die("only options are allowed as globals") if options.any?{|x| x.class != Option}
155
156
 
156
157
  # Aggregate global options
157
158
  if (global = @config.find{|x| x.name == 'global'})
158
- global.opts.each{|x| options << x}
159
+ global.nodes.each{|x| options << x}
159
160
  @config.reject!{|x| x.name == 'global'}
160
161
  end
161
162
  @config << add_cmd('global', 'Global options:', options)
162
163
  end
163
164
 
164
- # Add shared option (options that are added to all commands)
165
- # @param option/s [Array/Option] array or single option/s
166
- def add_shared(options)
167
- options = [options] if options.class == Option
168
- options.each{|x|
169
- Log.die("duplicate shared option '#{x.desc}' given") if @shared
170
- .any?{|y| y.key == x.key && y.desc == x.desc && y.type == x.type}
171
- x.shared = true
172
- x.required = true
173
- @shared << x
174
- }
175
- end
176
-
177
165
  # Returns banner string
178
166
  # @return [String] the app's banner
179
167
  def banner
@@ -206,7 +194,7 @@ class Commander
206
194
 
207
195
  # Set help if nothing was given
208
196
  ARGV.clear and ARGV << '-h' if ARGV.empty?
209
-
197
+
210
198
  # Process command options
211
199
  #---------------------------------------------------------------------------
212
200
  order_globals!
@@ -221,7 +209,7 @@ class Commander
221
209
  # Collect command options from args to compare against
222
210
  opts = ARGV.take_while{|x| !cmd_names.include?(x) }
223
211
  ARGV.shift(opts.size)
224
-
212
+
225
213
  # Handle help upfront before anything else
226
214
  if opts.any?{|x| m = match_named(x, cmd); m.hit? && m.sym == :help }
227
215
  !puts(help) and exit if cmd.name == 'global'
@@ -229,8 +217,8 @@ class Commander
229
217
  end
230
218
 
231
219
  # Check that all required options were given
232
- cmd_pos_opts = cmd.opts.select{|x| x.key.nil? }
233
- cmd_named_opts = cmd.opts.select{|x| !x.key.nil? }
220
+ cmd_pos_opts = cmd.nodes.select{|x| x.key.nil? }
221
+ cmd_named_opts = cmd.nodes.select{|x| !x.key.nil? }
234
222
 
235
223
  !puts("Error: positional option required!".colorize(:red)) && !puts(cmd.help) and
236
224
  exit if opts.select{|x| !x.start_with?('-')}.size < cmd_pos_opts.select{|x| x.required}.size
@@ -278,18 +266,12 @@ class Commander
278
266
  # --------------------------------------------------------------------
279
267
  !puts("Error: unknown named option '#{opt}' given!".colorize(:red)) && !puts(cmd.help) and exit if !sym
280
268
  @cmds[cmd.name.to_sym][sym] = value
281
- if cmd_opt.shared
282
- sym = "shared#{pos}".to_sym if cmd_opt.key.nil?
283
- @cmds[:shared] = {} if !@cmds.key?(:shared)
284
- @cmds[:shared][sym] = value
285
- end
286
269
  }
287
270
  end
288
271
  }
289
272
 
290
- # Ensure specials (global, shared) are always set
273
+ # Ensure specials (global) are always set
291
274
  @cmds[:global] = {} if !@cmds[:global]
292
- @cmds[:shared] = {} if !@cmds[:shared]
293
275
 
294
276
  # Ensure all options were consumed
295
277
  Log.die("invalid options #{ARGV}") if ARGV.any?
@@ -353,13 +335,13 @@ class Commander
353
335
  args = ARGV[0..-1]
354
336
  results = {}
355
337
  cmd_names = @config.map{|x| x.name }
356
-
338
+
357
339
  chained = []
358
340
  while args.any? do
359
341
  if !(cmd = @config.find{|x| x.name == args.first}).nil?
360
342
  results[args.shift] = [] # Add the command to the results
361
343
  cmd_names.reject!{|x| x == cmd.name} # Remove command from possible commands
362
- cmd_required = cmd.opts.select{|x| x.required}
344
+ cmd_required = cmd.nodes.select{|x| x.required}
363
345
 
364
346
  # Collect command options from args to compare against
365
347
  opts = args.take_while{|x| !cmd_names.include?(x)}
@@ -369,20 +351,22 @@ class Commander
369
351
  results[cmd.name].concat(opts) and next if cmd.name == 'global'
370
352
 
371
353
  # Chained case is when no options are given but some are required
372
- if opts.size == 0 && cmd.opts.any?{|x| x.required}
354
+ if opts.size == 0 && cmd.nodes.any?{|x| x.required}
373
355
  chained << cmd
374
356
  else
357
+ # Add cmd with options
375
358
  results[cmd.name].concat(opts)
376
359
 
360
+ # Check chained cmds against current cmd
377
361
  chained.each{|x|
378
- other_required = x.opts.select{|x| x.required}
379
- !puts("Error: chained commands must have equal numbers of required options!".colorize(:red)) && !puts(x.help) and
380
- exit if cmd_required.size != other_required.size
381
- cmd_required.each_with_index{|y,i|
362
+ other_required = x.nodes.select{|x| x.required}
363
+ !puts("Error: chained commands must satisfy required options!".colorize(:red)) && !puts(x.help) and
364
+ exit if cmd_required.size < other_required.size
365
+ other_required.each_with_index{|y,i|
382
366
  !puts("Error: chained command options are not type consistent!".colorize(:red)) && !puts(x.help) and
383
- exit if y.type != other_required[i].type || y.key != other_required[i].key
367
+ exit if y.type != cmd_required[i].type || y.key != cmd_required[i].key
384
368
  }
385
- results[x.name].concat(opts)
369
+ results[x.name].concat(opts.take(other_required.size))
386
370
  }
387
371
  end
388
372
  end
@@ -398,7 +382,7 @@ class Commander
398
382
  # @return [OptionMatch]] struct with some helper functions
399
383
  def match_named(opt, cmd)
400
384
  match = OptionMatch.new(opt)
401
- cmd_named_opts = cmd.opts.select{|x| !x.key.nil? }
385
+ cmd_named_opts = cmd.nodes.select{|x| !x.key.nil? }
402
386
 
403
387
  if opt.start_with?('-')
404
388
  short = opt[@short_regex, 1]
@@ -450,25 +434,26 @@ class Commander
450
434
  # Add a command to the command list
451
435
  # @param cmd [String] name of the command
452
436
  # @param desc [String] description of the command
453
- # @param opts [List] list of command options
437
+ # @param nodes [List] list of command nodes (i.e. options or commands)
454
438
  # @return [Command] new command
455
- def add_cmd(cmd, desc, options)
456
- Log.die("command names must be pure lowercase letters") if cmd =~ /[^a-z]/
439
+ def add_cmd(cmd, desc, nodes)
440
+ #nodes.select{|x| x.class != Option}.each{|x| validate_sub_cmd.(x)}
457
441
 
458
442
  # Build help for command
443
+ #---------------------------------------------------------------------------
459
444
  app = @app || @app_default
460
445
  help = "#{desc}\n"
461
446
  help += "\nUsage: ./#{app} #{cmd} [options]\n" if cmd != 'global'
462
447
  help = "#{banner}\n#{help}" if @app && cmd != 'global'
463
448
 
464
449
  # Add help option if not global command
465
- options << @config.find{|x| x.name == 'global'}.opts.find{|x| x.long == '--help'} if cmd != 'global'
450
+ nodes << @config.find{|x| x.name == 'global'}.nodes.find{|x| x.long == '--help'} if cmd != 'global'
466
451
 
467
452
  # Add positional options first
468
- sorted_options = options.select{|x| x.key.nil?}
469
- sorted_options += options.select{|x| !x.key.nil?}.sort{|x,y| x.key <=> y.key}
453
+ sorted_options = nodes.select{|x| x.key.nil?}
454
+ sorted_options += nodes.select{|x| !x.key.nil?}.sort{|x,y| x.key <=> y.key}
470
455
  positional_index = -1
471
- sorted_options.each{|x|
456
+ sorted_options.each{|x|
472
457
  required = x.required ? ", Required" : ""
473
458
  allowed = x.allowed.empty? ? "" : " (#{x.allowed * ','})"
474
459
  positional_index += 1 if x.key.nil?
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nub
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.53
4
+ version: 0.0.54
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrick Crummett
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-04-16 00:00:00.000000000 Z
11
+ date: 2018-05-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: colorize