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