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.
- checksums.yaml +4 -4
- data/README.md +11 -13
- data/lib/nub/commander.rb +43 -58
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 00de24079eb9ea5d62262d9453e4bb8745a004fec49d0c300bd8b01a430e4589
|
4
|
+
data.tar.gz: f1c7b1a86cb4637a80b48814193305b2c7a27fb50a46b7ac35ff22cb952e1b52
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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, :
|
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
|
138
|
-
def add(cmd, desc,
|
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
|
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
|
-
#
|
145
|
-
|
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
|
-
|
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
|
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.
|
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.
|
233
|
-
cmd_named_opts = cmd.
|
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
|
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.
|
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.
|
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.
|
379
|
-
!puts("Error: chained commands must
|
380
|
-
exit if cmd_required.size
|
381
|
-
|
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 !=
|
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.
|
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
|
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,
|
456
|
-
|
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
|
-
|
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 =
|
469
|
-
sorted_options +=
|
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.
|
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-
|
11
|
+
date: 2018-05-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: colorize
|