nub 0.0.54 → 0.0.55

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