nub 0.0.54 → 0.0.55

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 +13 -11
  3. data/lib/nub/commander.rb +53 -36
  4. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 00de24079eb9ea5d62262d9453e4bb8745a004fec49d0c300bd8b01a430e4589
4
- data.tar.gz: f1c7b1a86cb4637a80b48814193305b2c7a27fb50a46b7ac35ff22cb952e1b52
3
+ metadata.gz: d6492bcf60374552fe23049d74396793bccae8f0e11ed9521d58feb19bc592da
4
+ data.tar.gz: 1113ee70f0fdef09151c7551c840dec18142af299efc7938050f49e3579e714b
5
5
  SHA512:
6
- metadata.gz: 3b86079c3a02cf90b74d9db8ff3240f9d261c4ab5671af4d068e76686a45ac50682265d69bb04eb82aaee3e9c49739afe76462a19a3a08c139a03edbde992585
7
- data.tar.gz: 2093bb7ee390f121324b39ab66beb74f8a8fd5c7baa45ff9671a26459e785f937203e03d80c278d744b4340db2fe27e242d9322d893e112f7198c15c596902b1
6
+ metadata.gz: ab873f93ede81f44697af52730f61035503a0290769f132f22118567096c1910b94a4adcd4c77b657bb68167dc6303a52bda1f6ab06b1d23be1e5f5bf1b4cc28
7
+ data.tar.gz: 39fe7de14e98f37b1f2d224691c801aed3c27ff44731595bc0c72a76255a2e3f847b86e6f29a7e0632c75f028860fc61bf602ce4b58de9f8f09c54e3f81885bb
data/README.md CHANGED
@@ -38,23 +38,25 @@ 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 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.
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.
52
51
 
53
52
  ***Global*** options are options that are added with the ***add_global*** function and will show up
54
53
  set in the command results using the ***:global*** symbol. Global positional options must be given
55
54
  before any other commands but global named options may appear anywhere in the command line
56
55
  expression.
57
56
 
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
+
58
60
  ***Commander.new*** must be run from the app's executable file for it to pick up the app's filename
59
61
  properly.
60
62
 
data/lib/nub/commander.rb CHANGED
@@ -34,6 +34,7 @@ class Option
34
34
  attr_reader(:type)
35
35
  attr_accessor(:allowed)
36
36
  attr_accessor(:required)
37
+ attr_accessor(:shared)
37
38
 
38
39
  # Create a new option instance
39
40
  # @param key [String] option short hand, long hand and hint e.g. -s|--skip=COMPONENTS
@@ -46,12 +47,13 @@ class Option
46
47
  @long = nil
47
48
  @short = nil
48
49
  @desc = desc
50
+ @shared = false
49
51
  @allowed = allowed || []
50
52
  @required = required || false
51
53
 
52
54
  # Parse the key into its components (short hand, long hand, and hint)
53
55
  #https://bneijt.nl/pr/ruby-regular-expressions/
54
- # Valid forms to look for with chars [a-zA-Z0-9-_=|]
56
+ # Valid forms to look for with chars [a-zA-Z0-9-_=|]
55
57
  # --help, --help=HINT, -h|--help, -h|--help=HINT
56
58
  Log.die("invalid option key #{key}") if key && (key.count('=') > 1 or key.count('|') > 1 or !key[/[^\w\-=|]/].nil? or
57
59
  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?)
@@ -87,7 +89,7 @@ class Commander
87
89
  attr_reader(:banner)
88
90
  attr_accessor(:cmds)
89
91
 
90
- Command = Struct.new(:name, :desc, :nodes, :help)
92
+ Command = Struct.new(:name, :desc, :opts, :help)
91
93
 
92
94
  # Initialize the commands for your application
93
95
  # @param app [String] application name e.g. reduce
@@ -112,6 +114,9 @@ class Commander
112
114
  # Configuration - ordered list of commands
113
115
  @config = []
114
116
 
117
+ # List of options that will be added to all commands
118
+ @shared = []
119
+
115
120
  # Configure default global options
116
121
  add_global(Option.new('-h|--help', 'Print command/options help'))
117
122
  end
@@ -129,39 +134,46 @@ class Commander
129
134
  # Add a command to the command list
130
135
  # @param cmd [String] name of the command
131
136
  # @param desc [String] description of the command
132
- # @param nodes [List] list of command nodes (i.e. options or commands)
133
- def add(cmd, desc, nodes:[])
137
+ # @param opts [List] list of command options
138
+ def add(cmd, desc, options:[])
134
139
  Log.die("'global' is a reserved command name") if cmd == 'global'
140
+ Log.die("'shared' is a reserved command name") if cmd == 'shared'
135
141
  Log.die("'#{cmd}' already exists") if @config.any?{|x| x.name == cmd}
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]/
142
+ Log.die("'help' is a reserved option name") if options.any?{|x| !x.key.nil? && x.key.include?('help')}
138
143
 
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)}
144
+ # Add shared options
145
+ @shared.each{|x| options.unshift(x)}
147
146
 
148
- @config << add_cmd(cmd, desc, nodes)
147
+ cmd = add_cmd(cmd, desc, options)
148
+ @config << cmd
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 != Array
155
- Log.die("only options are allowed as globals") if options.any?{|x| x.class != Option}
154
+ options = [options] if options.class == Option
156
155
 
157
156
  # Aggregate global options
158
157
  if (global = @config.find{|x| x.name == 'global'})
159
- global.nodes.each{|x| options << x}
158
+ global.opts.each{|x| options << x}
160
159
  @config.reject!{|x| x.name == 'global'}
161
160
  end
162
161
  @config << add_cmd('global', 'Global options:', options)
163
162
  end
164
163
 
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
+
165
177
  # Returns banner string
166
178
  # @return [String] the app's banner
167
179
  def banner
@@ -194,7 +206,7 @@ class Commander
194
206
 
195
207
  # Set help if nothing was given
196
208
  ARGV.clear and ARGV << '-h' if ARGV.empty?
197
-
209
+
198
210
  # Process command options
199
211
  #---------------------------------------------------------------------------
200
212
  order_globals!
@@ -209,7 +221,7 @@ class Commander
209
221
  # Collect command options from args to compare against
210
222
  opts = ARGV.take_while{|x| !cmd_names.include?(x) }
211
223
  ARGV.shift(opts.size)
212
-
224
+
213
225
  # Handle help upfront before anything else
214
226
  if opts.any?{|x| m = match_named(x, cmd); m.hit? && m.sym == :help }
215
227
  !puts(help) and exit if cmd.name == 'global'
@@ -217,8 +229,8 @@ class Commander
217
229
  end
218
230
 
219
231
  # Check that all required options were given
220
- cmd_pos_opts = cmd.nodes.select{|x| x.key.nil? }
221
- cmd_named_opts = cmd.nodes.select{|x| !x.key.nil? }
232
+ cmd_pos_opts = cmd.opts.select{|x| x.key.nil? }
233
+ cmd_named_opts = cmd.opts.select{|x| !x.key.nil? }
222
234
 
223
235
  !puts("Error: positional option required!".colorize(:red)) && !puts(cmd.help) and
224
236
  exit if opts.select{|x| !x.start_with?('-')}.size < cmd_pos_opts.select{|x| x.required}.size
@@ -266,12 +278,18 @@ class Commander
266
278
  # --------------------------------------------------------------------
267
279
  !puts("Error: unknown named option '#{opt}' given!".colorize(:red)) && !puts(cmd.help) and exit if !sym
268
280
  @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
269
286
  }
270
287
  end
271
288
  }
272
289
 
273
- # Ensure specials (global) are always set
290
+ # Ensure specials (global, shared) are always set
274
291
  @cmds[:global] = {} if !@cmds[:global]
292
+ @cmds[:shared] = {} if !@cmds[:shared]
275
293
 
276
294
  # Ensure all options were consumed
277
295
  Log.die("invalid options #{ARGV}") if ARGV.any?
@@ -335,13 +353,13 @@ class Commander
335
353
  args = ARGV[0..-1]
336
354
  results = {}
337
355
  cmd_names = @config.map{|x| x.name }
338
-
356
+
339
357
  chained = []
340
358
  while args.any? do
341
359
  if !(cmd = @config.find{|x| x.name == args.first}).nil?
342
360
  results[args.shift] = [] # Add the command to the results
343
361
  cmd_names.reject!{|x| x == cmd.name} # Remove command from possible commands
344
- cmd_required = cmd.nodes.select{|x| x.required}
362
+ cmd_required = cmd.opts.select{|x| x.required}
345
363
 
346
364
  # Collect command options from args to compare against
347
365
  opts = args.take_while{|x| !cmd_names.include?(x)}
@@ -351,7 +369,7 @@ class Commander
351
369
  results[cmd.name].concat(opts) and next if cmd.name == 'global'
352
370
 
353
371
  # Chained case is when no options are given but some are required
354
- if opts.size == 0 && cmd.nodes.any?{|x| x.required}
372
+ if opts.size == 0 && cmd.opts.any?{|x| x.required}
355
373
  chained << cmd
356
374
  else
357
375
  # Add cmd with options
@@ -359,7 +377,7 @@ class Commander
359
377
 
360
378
  # Check chained cmds against current cmd
361
379
  chained.each{|x|
362
- other_required = x.nodes.select{|x| x.required}
380
+ other_required = x.opts.select{|x| x.required}
363
381
  !puts("Error: chained commands must satisfy required options!".colorize(:red)) && !puts(x.help) and
364
382
  exit if cmd_required.size < other_required.size
365
383
  other_required.each_with_index{|y,i|
@@ -382,7 +400,7 @@ class Commander
382
400
  # @return [OptionMatch]] struct with some helper functions
383
401
  def match_named(opt, cmd)
384
402
  match = OptionMatch.new(opt)
385
- cmd_named_opts = cmd.nodes.select{|x| !x.key.nil? }
403
+ cmd_named_opts = cmd.opts.select{|x| !x.key.nil? }
386
404
 
387
405
  if opt.start_with?('-')
388
406
  short = opt[@short_regex, 1]
@@ -434,26 +452,25 @@ class Commander
434
452
  # Add a command to the command list
435
453
  # @param cmd [String] name of the command
436
454
  # @param desc [String] description of the command
437
- # @param nodes [List] list of command nodes (i.e. options or commands)
455
+ # @param opts [List] list of command options
438
456
  # @return [Command] new command
439
- def add_cmd(cmd, desc, nodes)
440
- #nodes.select{|x| x.class != Option}.each{|x| validate_sub_cmd.(x)}
457
+ def add_cmd(cmd, desc, options)
458
+ Log.die("command names must be pure lowercase letters") if cmd =~ /[^a-z]/
441
459
 
442
460
  # Build help for command
443
- #---------------------------------------------------------------------------
444
461
  app = @app || @app_default
445
462
  help = "#{desc}\n"
446
463
  help += "\nUsage: ./#{app} #{cmd} [options]\n" if cmd != 'global'
447
464
  help = "#{banner}\n#{help}" if @app && cmd != 'global'
448
465
 
449
466
  # Add help option if not global command
450
- nodes << @config.find{|x| x.name == 'global'}.nodes.find{|x| x.long == '--help'} if cmd != 'global'
467
+ options << @config.find{|x| x.name == 'global'}.opts.find{|x| x.long == '--help'} if cmd != 'global'
451
468
 
452
469
  # Add positional options first
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
+ sorted_options = options.select{|x| x.key.nil?}
471
+ sorted_options += options.select{|x| !x.key.nil?}.sort{|x,y| x.key <=> y.key}
455
472
  positional_index = -1
456
- sorted_options.each{|x|
473
+ sorted_options.each{|x|
457
474
  required = x.required ? ", Required" : ""
458
475
  allowed = x.allowed.empty? ? "" : " (#{x.allowed * ','})"
459
476
  positional_index += 1 if x.key.nil?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nub
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.54
4
+ version: 0.0.55
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrick Crummett