nub 0.0.53 → 0.0.54

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