nub 0.0.55 → 0.0.56
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +80 -54
- data/lib/nub/commander.rb +364 -195
- 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: 6840a65c5b757eb9cf92cdbfd1548577f2a0abf4b91470477cb133a048259372
|
4
|
+
data.tar.gz: 3c5dd5de7ce42a2a2ba91b4ac2d5aa6cf773cda0320723bdd13f79ddbd918de8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 20051bea8cd2af4aa22c09683673e3a99c6deb0b3e166c6e18175c7e87d96035ce3497b38337de3956274879ad52a47cb172bd80eee7f3be7b2030278c4ddb84
|
7
|
+
data.tar.gz: eddef00f72ad07ab8b384317113633d5256d489221f8411128e0db86dbee7da51c514264b93c8c3d749f0ae391d9d2f4d93396fd5806988c254118f44bc14f12
|
data/README.md
CHANGED
@@ -10,53 +10,95 @@ Collection of ruby utils I've used in several of my projects and wanted re-usabl
|
|
10
10
|
### Table of Contents
|
11
11
|
* [Deploy](#deploy)
|
12
12
|
* [Commander](#commander)
|
13
|
-
|
14
|
-
* [
|
15
|
-
* [
|
13
|
+
* [Commands](#commands)
|
14
|
+
* [Command Parameters ](#command-paramaters)
|
15
|
+
* [Chained Commands](#chained-commands)
|
16
|
+
* [Options](#options)
|
17
|
+
* [Positional Options](#positional-options)
|
18
|
+
* [Value Types](#value-types)
|
19
|
+
* [Allowed Values](#allowed-values)
|
20
|
+
* [Global Options](#global-options)
|
21
|
+
* [Configuration](#configuration)
|
22
|
+
* [Help](#help)
|
23
|
+
* [Examples](#examples)
|
24
|
+
* [Indicators](#indicators)
|
16
25
|
* [Config](#config)
|
17
26
|
* [Ruby Gem Creation](#ruby-gem-creation)
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
27
|
+
* [Package Layout](#package-layout)
|
28
|
+
* [Build Gem](#build-gem)
|
29
|
+
* [Install Gem](#install-gem)
|
30
|
+
* [Push Gem](#push-gem)
|
22
31
|
* [Integrate with Travis-CI](#integrate-with-travis-ci)
|
23
|
-
|
24
|
-
|
32
|
+
* [Install Travis Client](#install-travis-client)
|
33
|
+
* [Deploy Ruby Gem on Tag](#deploy-ruby-gem-on-tag)
|
25
34
|
|
26
35
|
## Deploy <a name="deploy"></a>
|
27
36
|
Run: `bundle install --system`
|
28
37
|
|
29
38
|
## Commander <a name="commander"></a>
|
30
|
-
Commander was created mainly because all available options parsers seemed complicated and
|
31
|
-
and partly because I enjoyed understanding every bit going into it. Commander offers
|
32
|
-
command syntax that is becoming so popular.
|
33
|
-
faster to type.
|
39
|
+
Commander was created mainly because all available options parsers seemed overly complicated and
|
40
|
+
overweight and partly because I enjoyed understanding every bit going into it. Commander offers
|
41
|
+
***git*** like command syntax that is becoming so popular.
|
34
42
|
|
35
43
|
There are two kinds of paramaters that commander deals with ***commands*** and ***options***.
|
36
|
-
Commands are specific named parameters that may or may not have options specific to it. Commands
|
37
|
-
have their own help to display their usage and available options.
|
38
44
|
|
39
45
|
### Commands <a name="commands"></a>
|
40
|
-
Commands are
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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.
|
46
|
+
Commands are specific named parameters that may or may not have options specific to it. Commands
|
47
|
+
have their own help to display their usage and available options. Commands are used to trigger
|
48
|
+
different branches of functionality in an application.
|
49
|
+
|
50
|
+
#### Command Parameters <a name="command-parameters"></a>
|
51
|
+
Each command may have zero or more command parameters. Command parameters may be either a
|
52
|
+
sub-command, which follow the same rules in a recursive fashion as any command, or an option.
|
53
|
+
Command options modify how the command behaves.
|
51
54
|
|
55
|
+
#### Chained Commands <a name="chained-commands"></a>
|
56
|
+
The chained command expressions allow a cleaner multi-command type expression with reusable options.
|
57
|
+
|
58
|
+
Whenever more than one command is used in the command line expression the expression is interpreted
|
59
|
+
as being a ***chained command expression*** a.k.a ***chained commands***. Chained commands are
|
60
|
+
executed left to right, such that you can execute the first command then the second command or more
|
61
|
+
in a single command line expression. Each command in a chained command expression may have its own
|
62
|
+
specific options (those coming after the command but before the next command) or if options are
|
63
|
+
omitted the options from the next command will be used in the order they are given to satisfy the
|
64
|
+
options of the command before. Only options of the same type and position will be used.
|
65
|
+
|
66
|
+
### Options <a name="options"></a>
|
67
|
+
Options are additional parameters that are given that modify the behavior of a command. There are
|
68
|
+
two kinds of options available for use, ***positional*** and ***named***.
|
69
|
+
|
70
|
+
#### Positional Options <a name="positional-options"></a>
|
71
|
+
Positional options are identified by the absence of preceding dash/dashes and are interpreted
|
72
|
+
according to the order in which they were found. Positional options always pass a value into the
|
73
|
+
application. Positional options are named internally with the command name concatted with a zero
|
74
|
+
based int representing its order ***e.g. clean0*** where *clean* is the command name and *0* is the
|
75
|
+
positional options order given during configuration. Positional options are given sequentially so
|
76
|
+
you can't skip one and specify the second, it must be one then two etc...
|
77
|
+
|
78
|
+
#### Named Options
|
79
|
+
Named options have a name that is prefixed with a dash (short hand) or two dashes (long hand) e.g.
|
80
|
+
***-h*** or ***--help*** and may be a value passed in or simply a boolean flag. **Long Hand** form
|
81
|
+
is always required for named options, short hand may or may not be given. An incoming
|
82
|
+
**value/values** are indicated by the hint configuration e.g. ***-s|--skip=COMPONENTS*** indicates
|
83
|
+
there is an incoming value/values to be expected because of the hint ***COMPONENTS***.
|
84
|
+
|
85
|
+
#### Value Types <a name="value-types"></a>
|
86
|
+
Option values require a ***type*** so that Commander can interpret how to use them. The supported
|
87
|
+
value types are ***true, false, Integer, String, Array***. Positional options default to
|
88
|
+
type String while named options default to false. The named option flag default of false can be
|
89
|
+
changed to default to true by setting the ***type:true*** configuration param.
|
90
|
+
|
91
|
+
#### Allowed Values <a name="allowed-values"></a>
|
92
|
+
Commander will check the values given against an allowed list if so desired. This is done via the
|
93
|
+
***allowed*** configuration parameter.
|
94
|
+
|
95
|
+
#### Global Options <a name="global-options"></a>
|
52
96
|
***Global*** options are options that are added with the ***add_global*** function and will show up
|
53
97
|
set in the command results using the ***:global*** symbol. Global positional options must be given
|
54
98
|
before any other commands but global named options may appear anywhere in the command line
|
55
99
|
expression.
|
56
100
|
|
57
|
-
|
58
|
-
should be added before any commands are added. They are added to each command as an explicit option.
|
59
|
-
|
101
|
+
### Configuration <a name="configuration"></a>
|
60
102
|
***Commander.new*** must be run from the app's executable file for it to pick up the app's filename
|
61
103
|
properly.
|
62
104
|
|
@@ -98,24 +140,6 @@ Example command line expressions:
|
|
98
140
|
./app clean all build all
|
99
141
|
```
|
100
142
|
|
101
|
-
### Options <a name="options"></a>
|
102
|
-
There are two kinds of options available for use, ***positional*** and ***named***. Positional
|
103
|
-
options are identified by the absence of preceding dash/dashes and interpreted according to the
|
104
|
-
order and number in which they were found. Positional options are a value being passed into the
|
105
|
-
application. Named options have a name that is prefixed with a dash (short hand) or two dashes
|
106
|
-
(long hand) e.g. ***-h*** or ***--help*** and may simply be a flag or pass in a value. Option
|
107
|
-
values require a ***type*** so that Commander can interpret how to use them. The supported value
|
108
|
-
types are ***Flag, Integer, String, Array***. Values may be checked or not checked via the
|
109
|
-
***allowed*** config param. Positional options default to type String while named options default to
|
110
|
-
type Flag. Positional options are named internally with the command concatted with a an int for
|
111
|
-
order ***e.g. clean0*** zero based. Positional options are given sequentially so you can't
|
112
|
-
skip one and specify the second, it must be one then two etc...
|
113
|
-
|
114
|
-
**Long Hand** form is always required for named options, short hand may or may not be given.
|
115
|
-
|
116
|
-
**Values** are indicated by the hint given e.g. ***-s|--skip=COMPONENTS*** indicates there is an
|
117
|
-
incoming value/values to be expected because of the hint ***COMPONENTS***.
|
118
|
-
|
119
143
|
Example ruby configuration:
|
120
144
|
```ruby
|
121
145
|
# Creates a new instance of commander with app settings as given
|
@@ -155,10 +179,15 @@ Example command line expressions:
|
|
155
179
|
### Help <a name="help"></a>
|
156
180
|
Help for your appliation and commands is automatically supported with the ***-h*** and ***--help***
|
157
181
|
flags and is generated from the app ***name***, ***version***, ***examples***, ***commands***,
|
158
|
-
***descriptions*** and ***options*** given in Commander's configuration.
|
159
|
-
|
160
|
-
|
161
|
-
|
182
|
+
***descriptions*** and ***options*** given in Commander's configuration.
|
183
|
+
|
184
|
+
#### Examples <a name="examples"></a>
|
185
|
+
Examples is just a free form string that is displayed before usage so user's have an idea of how to
|
186
|
+
put together the commands and options.
|
187
|
+
|
188
|
+
#### Indicators <a name="indicators"></a>
|
189
|
+
Allowed checks, types, and required flags specified in the configuration are known as indicators in
|
190
|
+
the help context and are added to the end of the option descriptions.
|
162
191
|
|
163
192
|
Example ruby configuration:
|
164
193
|
```ruby
|
@@ -235,9 +264,6 @@ Usage: ./builder build [options]
|
|
235
264
|
-h|--help Print command/options help
|
236
265
|
```
|
237
266
|
|
238
|
-
**Required**
|
239
|
-
Options can be required using the ***required:true*** options param
|
240
|
-
|
241
267
|
## Config <a name="config"></a>
|
242
268
|
Config is a simple YAML wrapper with some extra features. Since it implements the ***Singleton***
|
243
269
|
pattern you can easily use it through out your app without carrying around instances everywhere.
|
data/lib/nub/commander.rb
CHANGED
@@ -18,8 +18,8 @@
|
|
18
18
|
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
19
|
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
20
20
|
#SOFTWARE.
|
21
|
-
|
22
21
|
require 'colorize'
|
22
|
+
require 'ostruct'
|
23
23
|
require_relative 'log'
|
24
24
|
require_relative 'sys'
|
25
25
|
require_relative 'string'
|
@@ -34,26 +34,26 @@ 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
|
41
40
|
# @param desc [String] the option's description
|
42
41
|
# @param type [Type] the option's type
|
43
42
|
# @param required [Bool] require the option if true else optional
|
44
|
-
# @param allowed [
|
45
|
-
def initialize(key, desc, type:nil, required:false, allowed:
|
43
|
+
# @param allowed [Hash] hash of allowed strings to descriptions maps
|
44
|
+
def initialize(key, desc, type:nil, required:false, allowed:{})
|
46
45
|
@hint = nil
|
47
46
|
@long = nil
|
48
47
|
@short = nil
|
49
48
|
@desc = desc
|
50
|
-
@
|
51
|
-
@allowed = allowed || []
|
49
|
+
@allowed = allowed || {}
|
52
50
|
@required = required || false
|
51
|
+
Log.die("allowed should be a hash of values to descriptions") if allowed.class != Hash
|
52
|
+
Log.die("required should be a boolean value") if ![TrueClass, FalseClass].include?(required.class)
|
53
53
|
|
54
54
|
# Parse the key into its components (short hand, long hand, and hint)
|
55
55
|
#https://bneijt.nl/pr/ruby-regular-expressions/
|
56
|
-
# Valid forms to look for with chars [a-zA-Z0-9-_=|]
|
56
|
+
# Valid forms to look for with chars [a-zA-Z0-9-_=|]
|
57
57
|
# --help, --help=HINT, -h|--help, -h|--help=HINT
|
58
58
|
Log.die("invalid option key #{key}") if key && (key.count('=') > 1 or key.count('|') > 1 or !key[/[^\w\-=|]/].nil? or
|
59
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?)
|
@@ -64,22 +64,72 @@ class Option
|
|
64
64
|
@long = key[/(--[\w\-]+)(=.+)*$/, 1]
|
65
65
|
end
|
66
66
|
|
67
|
-
#
|
68
|
-
|
67
|
+
# Convert true/false to TrueClass/FalseClass
|
68
|
+
type = TrueClass if type.class == TrueClass
|
69
|
+
type = FalseClass if type.class == FalseClass
|
70
|
+
|
71
|
+
# Validate and set type, allow Flag defaults to be true or false
|
72
|
+
Log.die("invalid option type #{type}") if ![String, Integer, Array, TrueClass, FalseClass, nil].any?{|x| type == x}
|
69
73
|
Log.die("option type must be set") if @hint && !type
|
70
74
|
@type = String if !key && !type
|
71
75
|
@type = FalseClass if key and !type
|
72
76
|
@type = type if type
|
73
77
|
|
74
78
|
# Validate hint is given for non flags
|
75
|
-
Log.die("option hint must be set") if @key && !@hint && @type != FalseClass
|
79
|
+
Log.die("option hint must be set") if @key && !@hint && @type != FalseClass && @type != TrueClass
|
76
80
|
|
77
81
|
# Validate allowed
|
78
82
|
if @allowed.any?
|
79
|
-
allowed_type = @allowed.first.class
|
80
|
-
Log.die("mixed allowed types") if @allowed.any?{|
|
83
|
+
allowed_type = @allowed.first.first.class
|
84
|
+
Log.die("mixed allowed types") if @allowed.any?{|k,v| k.class != allowed_type}
|
81
85
|
end
|
82
86
|
end
|
87
|
+
|
88
|
+
# Get a symbol representing the command
|
89
|
+
# @returns symbol
|
90
|
+
def to_sym
|
91
|
+
return @long[2..-1].gsub("-", "_").to_sym
|
92
|
+
end
|
93
|
+
|
94
|
+
# Return a human readable string of this object
|
95
|
+
# @param level [Integer] level to indent
|
96
|
+
def to_s(level:0)
|
97
|
+
return "#{" " * level * 2}Option => key:#{@key}, desc:'#{@desc}', type:#{@type}, allowed:#{@allowed}, required:#{@required}"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class Command
|
102
|
+
attr_reader(:name)
|
103
|
+
attr_reader(:desc)
|
104
|
+
attr_accessor(:nodes)
|
105
|
+
attr_accessor(:help)
|
106
|
+
|
107
|
+
# Create a new command
|
108
|
+
# @param name [String] command name used on command line
|
109
|
+
# @param desc [String] the command's description
|
110
|
+
# @param nodes [String] the command's description
|
111
|
+
def initialize(name, desc, nodes:[])
|
112
|
+
@name = name
|
113
|
+
@desc = desc
|
114
|
+
@nodes = nodes
|
115
|
+
@help = ""
|
116
|
+
end
|
117
|
+
|
118
|
+
# Get a symbol representing the command
|
119
|
+
# @returns symbol
|
120
|
+
def to_sym
|
121
|
+
return @name.gsub('-', '_').to_sym
|
122
|
+
end
|
123
|
+
|
124
|
+
# Return a human readable string of this object
|
125
|
+
# @param level [Integer] level to indent
|
126
|
+
def to_s(level:0)
|
127
|
+
str = "#{" " * level * 2}Command => name:#{@name}, desc:'#{@desc}'"
|
128
|
+
@nodes.each{|x|
|
129
|
+
str += "\n#{x.to_s(level: level + 1)}"
|
130
|
+
}
|
131
|
+
return str
|
132
|
+
end
|
83
133
|
end
|
84
134
|
|
85
135
|
# An implementation of git like command syntax for ruby applications:
|
@@ -89,13 +139,15 @@ class Commander
|
|
89
139
|
attr_reader(:banner)
|
90
140
|
attr_accessor(:cmds)
|
91
141
|
|
92
|
-
Command = Struct.new(:name, :desc, :opts, :help)
|
93
|
-
|
94
142
|
# Initialize the commands for your application
|
95
143
|
# @param app [String] application name e.g. reduce
|
96
144
|
# @param version [String] version of the application e.g. 1.0.0
|
97
145
|
# @param examples [String] optional examples to list after the title before usage
|
98
146
|
def initialize(app:nil, version:nil, examples:nil)
|
147
|
+
@k = OpenStruct.new({
|
148
|
+
global: 'global'
|
149
|
+
})
|
150
|
+
|
99
151
|
@app = app
|
100
152
|
@app_default = Sys.caller_filename
|
101
153
|
@version = version
|
@@ -107,18 +159,15 @@ class Commander
|
|
107
159
|
@long_regex = /(--[\w\-]+)(=.+)*$/
|
108
160
|
@value_regex = /.*=(.*)$/
|
109
161
|
|
110
|
-
#
|
162
|
+
# Command line expression results
|
111
163
|
# {command_name => {}}
|
112
164
|
@cmds = {}
|
113
165
|
|
114
166
|
# Configuration - ordered list of commands
|
115
167
|
@config = []
|
116
168
|
|
117
|
-
# List of options that will be added to all commands
|
118
|
-
@shared = []
|
119
|
-
|
120
169
|
# Configure default global options
|
121
|
-
add_global(
|
170
|
+
add_global('-h|--help', 'Print command/options help')
|
122
171
|
end
|
123
172
|
|
124
173
|
# Hash like accessor for checking if a command or option is set
|
@@ -134,44 +183,41 @@ class Commander
|
|
134
183
|
# Add a command to the command list
|
135
184
|
# @param cmd [String] name of the command
|
136
185
|
# @param desc [String] description of the command
|
137
|
-
# @param
|
138
|
-
def add(cmd, desc,
|
139
|
-
Log.die("'global' is a reserved command name") if cmd ==
|
140
|
-
Log.die("'shared' is a reserved command name") if cmd == 'shared'
|
186
|
+
# @param nodes [List] list of command nodes (i.e. options or commands)
|
187
|
+
def add(cmd, desc, nodes:[])
|
188
|
+
Log.die("'#{@k.global}' is a reserved command name") if cmd == @k.global
|
141
189
|
Log.die("'#{cmd}' already exists") if @config.any?{|x| x.name == cmd}
|
142
|
-
Log.die("'help' is a reserved option name") if
|
143
|
-
|
144
|
-
|
145
|
-
|
190
|
+
Log.die("'help' is a reserved option name") if nodes.any?{|x| x.class == Option && !x.key.nil? && x.key.include?('help')}
|
191
|
+
Log.die("command names must be pure lowercase letters or hypen") if cmd =~ /[^a-z-]/
|
192
|
+
|
193
|
+
# Validate sub command key words
|
194
|
+
validate_subcmd = ->(subcmd){
|
195
|
+
subcmd.nodes = [] if !subcmd.nodes
|
196
|
+
Log.die("'#{@k.global}' is a reserved command name") if subcmd.name == @k.global
|
197
|
+
Log.die("'help' is a reserved option name") if subcmd.nodes.any?{|x| x.class == Option && !x.key.nil? && x.key.include?('help')}
|
198
|
+
Log.die("command names must be pure lowercase letters or hypen") if subcmd.name =~ /[^a-z-]/
|
199
|
+
subcmd.nodes.select{|x| x.class != Option}.each{|x| validate_subcmd.(x)}
|
200
|
+
}
|
201
|
+
nodes.select{|x| x.class != Option}.each{|x| validate_subcmd.(x)}
|
146
202
|
|
147
|
-
|
148
|
-
@config << cmd
|
203
|
+
@config << add_cmd(cmd, desc, nodes)
|
149
204
|
end
|
150
205
|
|
151
206
|
# Add global options (any option coming before all commands)
|
152
|
-
# @param
|
153
|
-
|
154
|
-
|
207
|
+
# @param key [String] option short hand, long hand and hint e.g. -s|--skip=COMPONENTS
|
208
|
+
# @param desc [String] the option's description
|
209
|
+
# @param type [Type] the option's type
|
210
|
+
# @param required [Bool] require the option if true else optional
|
211
|
+
# @param allowed [Hash] hash of allowed values to description map
|
212
|
+
def add_global(key, desc, type:nil, required:false, allowed:{})
|
213
|
+
options = [Option.new(key, desc, type:type, required:required, allowed:allowed)]
|
155
214
|
|
156
215
|
# Aggregate global options
|
157
|
-
if (global = @config.find{|x| x.name ==
|
158
|
-
global.
|
159
|
-
@config.reject!{|x| x.name ==
|
216
|
+
if (global = @config.find{|x| x.name == @k.global})
|
217
|
+
global.nodes.each{|x| options << x}
|
218
|
+
@config.reject!{|x| x.name == @k.global}
|
160
219
|
end
|
161
|
-
@config << add_cmd(
|
162
|
-
end
|
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
|
-
}
|
220
|
+
@config << add_cmd(@k.global, 'Global options:', options)
|
175
221
|
end
|
176
222
|
|
177
223
|
# Returns banner string
|
@@ -185,6 +231,8 @@ class Commander
|
|
185
231
|
# Return the app's help string
|
186
232
|
# @return [String] the app's help string
|
187
233
|
def help
|
234
|
+
|
235
|
+
# Global help
|
188
236
|
help = @app.nil? ? "" : "#{banner}\n"
|
189
237
|
if !@examples.nil? && !@examples.empty?
|
190
238
|
newline = @examples.strip_color[-1] != "\n" ? "\n" : ""
|
@@ -192,9 +240,9 @@ class Commander
|
|
192
240
|
end
|
193
241
|
app = @app || @app_default
|
194
242
|
help += "Usage: ./#{app} [commands] [options]\n"
|
195
|
-
help += @config.find{|x| x.name ==
|
243
|
+
help += @config.find{|x| x.name == @k.global}.help
|
196
244
|
help += "COMMANDS:\n"
|
197
|
-
@config.select{|x| x.name !=
|
245
|
+
@config.select{|x| x.name != @k.global}.each{|x| help += " #{x.name.ljust(@just)}#{x.desc}\n" }
|
198
246
|
help += "\nsee './#{app} COMMAND --help' for specific command help\n"
|
199
247
|
|
200
248
|
return help
|
@@ -202,94 +250,22 @@ class Commander
|
|
202
250
|
|
203
251
|
# Construct the command line parser and parse
|
204
252
|
def parse!
|
205
|
-
|
253
|
+
|
254
|
+
# Clear out the previous run every time, in case run more than once
|
255
|
+
@cmds = {}
|
206
256
|
|
207
257
|
# Set help if nothing was given
|
208
258
|
ARGV.clear and ARGV << '-h' if ARGV.empty?
|
209
|
-
|
210
|
-
# Process command options
|
211
|
-
#---------------------------------------------------------------------------
|
212
|
-
order_globals!
|
213
|
-
expand_chained_options!
|
214
|
-
loop {
|
215
|
-
break if ARGV.first.nil?
|
216
|
-
|
217
|
-
if !(cmd = @config.find{|x| x.name == ARGV.first}).nil?
|
218
|
-
@cmds[ARGV.shift.to_sym] = {} # Create command results entry
|
219
|
-
cmd_names.reject!{|x| x == cmd.name} # Remove command from possible commands
|
220
|
-
|
221
|
-
# Collect command options from args to compare against
|
222
|
-
opts = ARGV.take_while{|x| !cmd_names.include?(x) }
|
223
|
-
ARGV.shift(opts.size)
|
224
|
-
|
225
|
-
# Handle help upfront before anything else
|
226
|
-
if opts.any?{|x| m = match_named(x, cmd); m.hit? && m.sym == :help }
|
227
|
-
!puts(help) and exit if cmd.name == 'global'
|
228
|
-
!puts(cmd.help) and exit
|
229
|
-
end
|
230
259
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
named_opts = opts.select{|x| x.start_with?('-')}
|
239
|
-
cmd_named_opts.select{|x| x.required}.each{|x|
|
240
|
-
!puts("Error: required option #{x.key} not given!".colorize(:red)) && !puts(cmd.help) and
|
241
|
-
exit if !named_opts.find{|y| y.start_with?(x.short) || y.start_with?(x.long)}
|
242
|
-
}
|
243
|
-
|
244
|
-
# Process command options
|
245
|
-
pos = -1
|
246
|
-
loop {
|
247
|
-
break if opts.first.nil?
|
248
|
-
opt = opts.shift
|
249
|
-
cmd_opt = nil
|
250
|
-
value = nil
|
251
|
-
sym = nil
|
252
|
-
|
253
|
-
# Validate/set named options
|
254
|
-
# --------------------------------------------------------------------
|
255
|
-
# e.g. -s, --skip, --skip=VALUE
|
256
|
-
if (match = match_named(opt, cmd)).hit?
|
257
|
-
sym = match.sym
|
258
|
-
cmd_opt = match.opt
|
259
|
-
value = match.value
|
260
|
-
value = match.flag? || opts.shift if !value
|
261
|
-
|
262
|
-
# Validate/set positional options
|
263
|
-
# --------------------------------------------------------------------
|
264
|
-
else
|
265
|
-
pos += 1
|
266
|
-
value = opt
|
267
|
-
cmd_opt = cmd_pos_opts.shift
|
268
|
-
!puts("Error: invalid positional option '#{opt}'!".colorize(:red)) && !puts(cmd.help) and
|
269
|
-
exit if cmd_opt.nil? || cmd_names.include?(value)
|
270
|
-
sym = "#{cmd.name}#{pos}".to_sym
|
271
|
-
end
|
272
|
-
|
273
|
-
# Convert value to appropriate type and validate against allowed
|
274
|
-
# --------------------------------------------------------------------
|
275
|
-
value = convert_value(value, cmd, cmd_opt)
|
276
|
-
|
277
|
-
# Set option with value
|
278
|
-
# --------------------------------------------------------------------
|
279
|
-
!puts("Error: unknown named option '#{opt}' given!".colorize(:red)) && !puts(cmd.help) and exit if !sym
|
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
|
286
|
-
}
|
287
|
-
end
|
288
|
-
}
|
260
|
+
# Parse commands recursively
|
261
|
+
move_globals_to_front!
|
262
|
+
expand_chained_options!
|
263
|
+
while (cmd = @config.find{|x| x.name == ARGV.first})
|
264
|
+
ARGV.shift && parse_commands(cmd, nil, @config.select{|x| x.name != cmd.name}, ARGV, @cmds)
|
265
|
+
end
|
289
266
|
|
290
|
-
# Ensure specials (global
|
267
|
+
# Ensure specials (global) are always set
|
291
268
|
@cmds[:global] = {} if !@cmds[:global]
|
292
|
-
@cmds[:shared] = {} if !@cmds[:shared]
|
293
269
|
|
294
270
|
# Ensure all options were consumed
|
295
271
|
Log.die("invalid options #{ARGV}") if ARGV.any?
|
@@ -307,15 +283,132 @@ class Commander
|
|
307
283
|
return !!sym
|
308
284
|
end
|
309
285
|
def flag?
|
310
|
-
return opt.type == FalseClass
|
286
|
+
return opt.type == FalseClass || opt.type == TrueClass
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
# Parse the given args recursively
|
291
|
+
# @param cmd [Command] command to work with
|
292
|
+
# @param parent [Command] command to work with
|
293
|
+
# @param others [Array] sibling cmds to cmd
|
294
|
+
# @param args [Array] array of arguments
|
295
|
+
# @param results [Hash] of cmd results
|
296
|
+
def parse_commands(cmd, parent, others, args, results)
|
297
|
+
results[cmd.to_sym] = {} # Create command results entry
|
298
|
+
cmd_names = others.map{|x| x.name} # Get other command names as markers
|
299
|
+
subcmds = cmd.nodes.select{|x| x.class == Command} # Get sub-commands for this command
|
300
|
+
|
301
|
+
# Collect all params until the next sibling command
|
302
|
+
#---------------------------------------------------------------------------
|
303
|
+
params = args.take_while{|x| !cmd_names.include?(x)}
|
304
|
+
args.shift(params.size)
|
305
|
+
|
306
|
+
# Strip off this command's preceeding options
|
307
|
+
opts = subcmds.any? ? params.take_while{|x| !subcmds.any?{|y| x == y.name}} : params
|
308
|
+
otherparams = params[opts.size..-1]
|
309
|
+
|
310
|
+
#---------------------------------------------------------------------------
|
311
|
+
# Handle sub-commands recursively first
|
312
|
+
#---------------------------------------------------------------------------
|
313
|
+
while subcmds.any? && (subcmd = subcmds.find{|x| x.name == otherparams.first})
|
314
|
+
otherparams.shift # Consume sub-cmd from opts
|
315
|
+
subcmds.reject!{|x| x.name == subcmd.name} # Drop sub-command from further use
|
316
|
+
parse_commands(subcmd, cmd, subcmds, otherparams, results[cmd.to_sym])
|
317
|
+
end
|
318
|
+
|
319
|
+
# Account for any left over options
|
320
|
+
otherparams.reverse.each{|x| opts.unshift(x)}
|
321
|
+
|
322
|
+
#---------------------------------------------------------------------------
|
323
|
+
# Base case: dealing with options for a given command.
|
324
|
+
# Only consume options for this command and bubble up unused to parent
|
325
|
+
#---------------------------------------------------------------------------
|
326
|
+
|
327
|
+
# Handle help upfront before anything else
|
328
|
+
#---------------------------------------------------------------------------
|
329
|
+
if opts.any?{|x| m = match_named(x, cmd); m.hit? && m.sym == :help }
|
330
|
+
!puts(help) and exit if cmd.name == @k.global
|
331
|
+
!puts(cmd.help) and exit
|
332
|
+
end
|
333
|
+
|
334
|
+
# Parse/consume named options first
|
335
|
+
#---------------------------------------------------------------------------
|
336
|
+
|
337
|
+
# Check that all required named options were given
|
338
|
+
cmd.nodes.select{|x| x.class == Option && !x.key.nil? && x.required}.each{|x|
|
339
|
+
!puts("Error: required option #{x.key} not given!".colorize(:red)) && !puts(cmd.help) and
|
340
|
+
exit if !match_named(x, opts).hit?
|
341
|
+
}
|
342
|
+
|
343
|
+
# Consume and set all named options
|
344
|
+
i = 0
|
345
|
+
while i < opts.size
|
346
|
+
if (match = match_named(opts[i], cmd)).hit?
|
347
|
+
value = match.flag? || match.value # Inline or Flag value
|
348
|
+
|
349
|
+
# Separate value
|
350
|
+
separate = false
|
351
|
+
if !value && i + 1 < opts.size
|
352
|
+
separate = true
|
353
|
+
value = opts[i + 1]
|
354
|
+
elsif !value
|
355
|
+
!puts("Error: named option '#{opts[i]}' value not found!".colorize(:red)) and
|
356
|
+
!puts(cmd.help) and exit
|
357
|
+
end
|
358
|
+
|
359
|
+
# Set result and consume options
|
360
|
+
results[cmd.to_sym][match.sym] = convert_value(value, cmd, match.opt)
|
361
|
+
opts.delete_at(i) # Consume option
|
362
|
+
opts.delete_at(i) if separate # Consume separate value
|
363
|
+
else
|
364
|
+
i += 1
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
# Parse/consume positional options next
|
369
|
+
#---------------------------------------------------------------------------
|
370
|
+
cmd_pos_opts = cmd.nodes.select{|x| x.class == Option && x.key.nil?}
|
371
|
+
|
372
|
+
# Check that all required positionals were given
|
373
|
+
!puts("Error: positional option required!".colorize(:red)) && !puts(cmd.help) and
|
374
|
+
exit if opts.select{|x| !x.start_with?('-')}.size < cmd_pos_opts.select{|x| x.required}.size
|
375
|
+
|
376
|
+
# Consume and set all positional options
|
377
|
+
i = 0
|
378
|
+
pos = -1
|
379
|
+
while i < opts.size && cmd_pos_opts.any?
|
380
|
+
if !opts[i].start_with?('-')
|
381
|
+
pos += 1
|
382
|
+
cmd_opt = cmd_pos_opts.shift
|
383
|
+
!puts("Error: invalid positional option '#{opts[i]}'!".colorize(:red)) and
|
384
|
+
!puts(cmd.help) and exit if cmd_opt.nil?
|
385
|
+
|
386
|
+
# Set result and consume options
|
387
|
+
results[cmd.to_sym]["#{cmd.to_sym}#{pos}".to_sym] = convert_value(opts[i], cmd, cmd_opt)
|
388
|
+
opts.delete_at(i) # Consume option
|
389
|
+
else
|
390
|
+
i += 1
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
# Add any unconsumed options back to parent to ensure everything is accounted for
|
395
|
+
if parent
|
396
|
+
opts.reverse.each{|x| args.unshift(x)}
|
397
|
+
else
|
398
|
+
opts.each{|x|
|
399
|
+
!puts("Error: invalid positional option '#{x}'!".colorize(:red)) and
|
400
|
+
!puts(cmd.help) and exit if !x.start_with?('-')
|
401
|
+
!puts("Error: invalid named option '#{x}'!".colorize(:red)) and
|
402
|
+
!puts(cmd.help) and exit if x.start_with?('-')
|
403
|
+
}
|
311
404
|
end
|
312
405
|
end
|
313
406
|
|
314
407
|
# Parses the command line, moving all global options to the begining
|
315
408
|
# and inserting the global command
|
316
|
-
def
|
317
|
-
if !(global_cmd = @config.find{|x| x.name ==
|
318
|
-
ARGV.delete(
|
409
|
+
def move_globals_to_front!
|
410
|
+
if !(global_cmd = @config.find{|x| x.name == @k.global}).nil?
|
411
|
+
ARGV.delete(@k.global)
|
319
412
|
|
320
413
|
# Collect positional and named options from begining
|
321
414
|
globals = ARGV.take_while{|x| !@config.any?{|y| y.name == x}}
|
@@ -324,7 +417,7 @@ class Commander
|
|
324
417
|
# Collect named options throughout
|
325
418
|
i = -1
|
326
419
|
cmd = nil
|
327
|
-
while (i += 1) < ARGV.size
|
420
|
+
while (i += 1) < ARGV.size
|
328
421
|
|
329
422
|
# Set command and skip command and matching options
|
330
423
|
if !(_cmd = @config.find{|x| x.name == ARGV[i]}).nil?
|
@@ -342,7 +435,7 @@ class Commander
|
|
342
435
|
|
343
436
|
# Re-insert options in correct order at end with command
|
344
437
|
globals.reverse.each{|x| ARGV.unshift(x)}
|
345
|
-
ARGV.unshift(
|
438
|
+
ARGV.unshift(@k.global)
|
346
439
|
end
|
347
440
|
end
|
348
441
|
|
@@ -352,66 +445,118 @@ class Commander
|
|
352
445
|
def expand_chained_options!
|
353
446
|
args = ARGV[0..-1]
|
354
447
|
results = {}
|
355
|
-
|
356
|
-
|
448
|
+
cmd_order = []
|
449
|
+
cmd_names = @config.map{|x| x.name}
|
450
|
+
|
357
451
|
chained = []
|
358
|
-
while args.any?
|
452
|
+
while args.any?
|
359
453
|
if !(cmd = @config.find{|x| x.name == args.first}).nil?
|
360
|
-
|
361
|
-
|
362
|
-
|
454
|
+
cmd_order << args.shift # Maintain oder of given commands
|
455
|
+
results[cmd.name] = [] # Add the command to the results
|
456
|
+
cmd_names.reject!{|x| x == cmd.name} # Remove command from possible commands
|
363
457
|
|
364
458
|
# Collect command options from args to compare against
|
365
459
|
opts = args.take_while{|x| !cmd_names.include?(x)}
|
366
460
|
args.shift(opts.size)
|
367
461
|
|
368
462
|
# Globals are not to be considered for chaining
|
369
|
-
results[cmd.name].concat(opts) and next if cmd.name ==
|
463
|
+
results[cmd.name].concat(opts) and next if cmd.name == @k.global
|
370
464
|
|
371
|
-
# Chained case is when no options are given but
|
372
|
-
|
465
|
+
# Chained case is when no options are given but the command has options
|
466
|
+
cmd_options = cmd.nodes.select{|x| x.class == Option}
|
467
|
+
if opts.size == 0 && cmd_options.any?
|
373
468
|
chained << cmd
|
374
469
|
else
|
375
470
|
# Add cmd with options
|
376
471
|
results[cmd.name].concat(opts)
|
377
472
|
|
378
|
-
#
|
379
|
-
chained.each{|
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
473
|
+
# Add applicable options to chained as well
|
474
|
+
chained.each{|other|
|
475
|
+
_opts = opts[0..-1]
|
476
|
+
named_results = []
|
477
|
+
positional_results = []
|
478
|
+
|
479
|
+
# Add all matching named options
|
480
|
+
#-------------------------------------------------------------------
|
481
|
+
i = 0
|
482
|
+
while i < _opts.size
|
483
|
+
if (match = match_named(_opts[i], other)).hit?
|
484
|
+
named_results << _opts[i];
|
485
|
+
_opts.delete_at(i)
|
486
|
+
|
487
|
+
# Get the next option to as the value was separate
|
488
|
+
if i < _opts.size && !(match.flag? || match.value)
|
489
|
+
named_results << _opts[i]
|
490
|
+
_opts.delete_at(i)
|
491
|
+
end
|
492
|
+
else
|
493
|
+
i += 1
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
# Add all matching positional options
|
498
|
+
#-------------------------------------------------------------------
|
499
|
+
i = 0
|
500
|
+
other_positional = other.nodes.select{|x| x.class == Option && x.key.nil?}
|
501
|
+
while i < _opts.size
|
502
|
+
if !_opts[i].start_with?('-') && other_positional.any?
|
503
|
+
positional_results << _opts[i]
|
504
|
+
other_positional.shift
|
505
|
+
_opts.delete_at(i)
|
506
|
+
else
|
507
|
+
i += 1
|
508
|
+
end
|
509
|
+
end
|
510
|
+
|
511
|
+
positional_results.each{|x| results[other.name] << x}
|
512
|
+
named_results.each{|x| results[other.name] << x}
|
388
513
|
}
|
389
514
|
end
|
390
515
|
end
|
391
516
|
end
|
392
517
|
|
393
518
|
# Set results as new ARGV command line expression
|
394
|
-
ARGV.clear and
|
519
|
+
ARGV.clear and cmd_order.each{|x| ARGV << x; ARGV.concat(results[x]) }
|
395
520
|
end
|
396
521
|
|
397
522
|
# Match the given command line arg with a configured named option
|
398
|
-
#
|
399
|
-
# @param
|
523
|
+
# or match configured named option against a list of command line args
|
524
|
+
# @param arg [String/Option] the command line argument or configured Option
|
525
|
+
# @param other [Command/Array] configured command or command line args
|
400
526
|
# @return [OptionMatch]] struct with some helper functions
|
401
|
-
def match_named(
|
402
|
-
match = OptionMatch.new
|
403
|
-
|
404
|
-
|
405
|
-
if
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
match.
|
413
|
-
|
527
|
+
def match_named(arg, other)
|
528
|
+
match = OptionMatch.new
|
529
|
+
|
530
|
+
# Match command line arg against command options
|
531
|
+
if arg.class == String && other.class == Command
|
532
|
+
match.arg = arg
|
533
|
+
options = other.nodes.select{|x| x.class == Option && !x.key.nil? }
|
534
|
+
|
535
|
+
if arg.start_with?('-')
|
536
|
+
short = arg[@short_regex, 1]
|
537
|
+
long = arg[@long_regex, 1]
|
538
|
+
match.value = arg[@value_regex, 1]
|
539
|
+
|
540
|
+
# Set symbol converting dashes to underscores for named options
|
541
|
+
if (match.opt = options.find{|x| x.short == short || x.long == long})
|
542
|
+
match.sym = match.opt.to_sym
|
543
|
+
end
|
414
544
|
end
|
545
|
+
|
546
|
+
# Match command option against command line args
|
547
|
+
elsif arg.class == Option && other.class == Array
|
548
|
+
match.arg = arg.key
|
549
|
+
|
550
|
+
other.select{|x| x.start_with?('-')}.any?{|x|
|
551
|
+
short = x[@short_regex, 1]
|
552
|
+
long = x[@long_regex, 1]
|
553
|
+
value = x[@value_regex, 1]
|
554
|
+
if short == arg.short || long == arg.long
|
555
|
+
match.opt = arg
|
556
|
+
match.value = value
|
557
|
+
match.sym = match.opt.to_sym
|
558
|
+
end
|
559
|
+
}
|
415
560
|
end
|
416
561
|
|
417
562
|
return match
|
@@ -427,20 +572,20 @@ class Commander
|
|
427
572
|
if opt.type == String
|
428
573
|
if opt.allowed.any?
|
429
574
|
!puts("Error: invalid string value '#{value}'!".colorize(:red)) && !puts(cmd.help) and
|
430
|
-
exit if !opt.allowed.
|
575
|
+
exit if !opt.allowed.key?(value) && !opt.allowed.key?(value.to_sym)
|
431
576
|
end
|
432
577
|
elsif opt.type == Integer
|
433
578
|
value = value.to_i
|
434
579
|
if opt.allowed.any?
|
435
580
|
!puts("Error: invalid integer value '#{value}'!".colorize(:red)) && !puts(cmd.help) and
|
436
|
-
exit if !opt.allowed.
|
581
|
+
exit if !opt.allowed.key?(value)
|
437
582
|
end
|
438
583
|
elsif opt.type == Array
|
439
584
|
value = value.split(',')
|
440
585
|
if opt.allowed.any?
|
441
586
|
value.each{|x|
|
442
587
|
!puts("Error: invalid array value '#{x}'!".colorize(:red)) && !puts(cmd.help) and
|
443
|
-
exit if !opt.allowed.
|
588
|
+
exit if !opt.allowed.key?(x) && !opt.allowed.key?(x.to_sym)
|
444
589
|
}
|
445
590
|
end
|
446
591
|
end
|
@@ -450,37 +595,61 @@ class Commander
|
|
450
595
|
end
|
451
596
|
|
452
597
|
# Add a command to the command list
|
453
|
-
# @param
|
598
|
+
# @param name [String] name of the command
|
454
599
|
# @param desc [String] description of the command
|
455
|
-
# @param
|
600
|
+
# @param nodes [Array] list of command nodes (i.e. options or commands)
|
601
|
+
# @param hierarchy [Array] list of commands
|
456
602
|
# @return [Command] new command
|
457
|
-
def add_cmd(
|
458
|
-
|
603
|
+
def add_cmd(name, desc, nodes, hierarchy:[])
|
604
|
+
hierarchy << name
|
605
|
+
cmd = Command.new(name, desc)
|
606
|
+
subcmds = nodes.select{|x| x.class == Command}.sort{|x,y| x.name <=> y.name}
|
459
607
|
|
460
608
|
# Build help for command
|
609
|
+
#---------------------------------------------------------------------------
|
610
|
+
cmd.help = "#{desc}\n"
|
461
611
|
app = @app || @app_default
|
462
|
-
|
463
|
-
help += "\nUsage: ./#{app} #{
|
464
|
-
help = "#{banner}\n#{help}" if @app &&
|
612
|
+
cmd_prompt = subcmds.any? ? "[commands] " : ""
|
613
|
+
cmd.help += "\nUsage: ./#{app} #{hierarchy * ' '} #{cmd_prompt}[options]\n" if name != @k.global
|
614
|
+
cmd.help = "#{banner}\n#{cmd.help}" if @app && name != @k.global
|
465
615
|
|
466
|
-
# Add help
|
467
|
-
|
616
|
+
# Add help for each sub-command before options
|
617
|
+
cmd.help += "COMMANDS:\n" if subcmds.any?
|
618
|
+
subcmds.each{|x| cmd.help += " #{x.name.ljust(@just)}#{x.desc}\n" }
|
619
|
+
|
620
|
+
# Insert standard help option for command (re-using one from global, all identical)
|
621
|
+
nodes << @config.find{|x| x.name == @k.global}.nodes.find{|x| x.long == '--help'} if name != @k.global
|
468
622
|
|
469
623
|
# Add positional options first
|
470
|
-
sorted_options =
|
471
|
-
sorted_options +=
|
624
|
+
sorted_options = nodes.select{|x| x.class == Option && x.key.nil?}
|
625
|
+
sorted_options += nodes.select{|x| x.class == Option && !x.key.nil?}.sort{|x,y| x.key <=> y.key}
|
626
|
+
cmd.help += "OPTIONS:\n" if subcmds.any? && sorted_options.any?
|
472
627
|
positional_index = -1
|
473
|
-
sorted_options.each{|x|
|
628
|
+
sorted_options.each{|x|
|
474
629
|
required = x.required ? ", Required" : ""
|
475
|
-
allowed = x.allowed.empty? ? "" : " (#{x.allowed * ','})"
|
476
630
|
positional_index += 1 if x.key.nil?
|
477
|
-
key = x.key.nil? ? "#{
|
478
|
-
|
479
|
-
|
631
|
+
key = x.key.nil? ? "#{name}#{positional_index}" : x.key
|
632
|
+
if x.type == FalseClass || x.type == TrueClass
|
633
|
+
type = "Flag(#{x.type.to_s[/(.*)Class/, 1].downcase})"
|
634
|
+
elsif x.type == Array
|
635
|
+
type = "#{x.type}(String)"
|
636
|
+
else
|
637
|
+
type = x.type
|
638
|
+
end
|
639
|
+
cmd.help += " #{key.ljust(@just)}#{x.desc}: #{type}#{required}\n"
|
640
|
+
x.allowed.sort{|x,y| x <=> y}.each{|x| cmd.help += " #{''.ljust(@just)} #{x.first}: #{x.last}\n" }
|
480
641
|
}
|
481
642
|
|
482
|
-
#
|
483
|
-
|
643
|
+
# Add hint as to how to get specific sub command help
|
644
|
+
cmd.help += "\nsee './#{app} #{name} COMMAND --help' for specific command help\n" if subcmds.any?
|
645
|
+
|
646
|
+
# Configure help for each sub command
|
647
|
+
subcmds.each{|x| cmd.nodes << add_cmd(x.name, x.desc, x.nodes, hierarchy:hierarchy)}
|
648
|
+
|
649
|
+
# Add options after any sub-commands
|
650
|
+
cmd.nodes += sorted_options
|
651
|
+
|
652
|
+
return cmd
|
484
653
|
end
|
485
654
|
end
|
486
655
|
|
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.56
|
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-05-
|
11
|
+
date: 2018-05-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: colorize
|