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.
- checksums.yaml +4 -4
- data/README.md +13 -11
- data/lib/nub/commander.rb +53 -36
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d6492bcf60374552fe23049d74396793bccae8f0e11ed9521d58feb19bc592da
|
4
|
+
data.tar.gz: 1113ee70f0fdef09151c7551c840dec18142af299efc7938050f49e3579e714b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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, :
|
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
|
133
|
-
def add(cmd, desc,
|
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
|
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
|
-
#
|
140
|
-
|
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
|
-
|
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
|
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.
|
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.
|
221
|
-
cmd_named_opts = cmd.
|
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.
|
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.
|
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.
|
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.
|
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
|
455
|
+
# @param opts [List] list of command options
|
438
456
|
# @return [Command] new command
|
439
|
-
def add_cmd(cmd, desc,
|
440
|
-
|
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
|
-
|
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 =
|
454
|
-
sorted_options +=
|
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?
|