nub 0.0.54 → 0.0.55
Sign up to get free protection for your applications and to get access to all the features.
- 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?
|