consoler 1.0.2 → 1.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardopts +2 -0
- data/README.md +249 -9
- data/lib/consoler/application.rb +55 -1
- data/lib/consoler/matcher.rb +92 -22
- data/lib/consoler/option.rb +38 -0
- data/lib/consoler/options.rb +7 -1
- data/lib/consoler/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 317d28b9a15f0ea07c01410191cac9d0d59a41dd
|
4
|
+
data.tar.gz: f9a363ba2b596a02df673c1600441f2f6d7b7010
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b42c0a91ed2df03ef7d76796f1da5e986782d2d0415a14167159f34b10b76eb939e97f378109328bdd768c7f234607907871bcd03e2e5ad097180c7edaf385c6
|
7
|
+
data.tar.gz: df6627a850424d6053c4525bea35014c1c8ec42bbe9cbe43f981119b535a747af53c4130dec92242309d36891c9e7368450c49838c37c55ea2662a682582fb26
|
data/.yardopts
ADDED
data/README.md
CHANGED
@@ -1,27 +1,47 @@
|
|
1
|
-
# Consoler [![Build Status](https://api.travis-ci.org/justim/consoler-rb.svg?branch=master)](
|
1
|
+
# Consoler [![Build Status](https://api.travis-ci.org/justim/consoler-rb.svg?branch=master)](https://travis-ci.org/justim/consoler-rb) [![Gem Version](https://badge.fury.io/rb/consoler.svg)](https://badge.fury.io/rb/consoler)
|
2
2
|
|
3
3
|
> Sinatra-like application builder for the console
|
4
4
|
|
5
|
-
##
|
5
|
+
## Quick usage
|
6
6
|
|
7
7
|
```ruby
|
8
|
-
# create
|
8
|
+
# create an application
|
9
9
|
app = Consoler::Application.new description: 'A simple app'
|
10
10
|
|
11
11
|
# define a command
|
12
|
-
app.build '
|
12
|
+
app.build '[--clean] output_dir' do |clean, output_dir|
|
13
13
|
# clean contains a boolean
|
14
14
|
clean_up if clean
|
15
15
|
|
16
|
-
#
|
17
|
-
build_project
|
16
|
+
# output_dir contains a string
|
17
|
+
build_project output_dir
|
18
18
|
end
|
19
19
|
app.run(['build', 'production', '--clean'])
|
20
20
|
|
21
21
|
# this does not match, nothing is executed and the usage message is printed
|
22
22
|
app.run(['deploy', 'production'])
|
23
|
+
|
24
|
+
# defaults to ARGV
|
25
|
+
app.run
|
23
26
|
```
|
24
27
|
|
28
|
+
## Features
|
29
|
+
|
30
|
+
- No fiddling with `ARGV` and friends
|
31
|
+
- Arguments filled based on their name, for easy access
|
32
|
+
- Grouped optionals
|
33
|
+
|
34
|
+
## Requirements
|
35
|
+
|
36
|
+
Tests are run against multiple ruby versions (latest supported):
|
37
|
+
|
38
|
+
- `2.2.x`
|
39
|
+
- `2.3.x`
|
40
|
+
- `2.4.x`
|
41
|
+
- `2.5.x`
|
42
|
+
|
43
|
+
No other requirements exist.
|
44
|
+
|
25
45
|
## Installation
|
26
46
|
|
27
47
|
```sh
|
@@ -31,13 +51,233 @@ gem install consoler
|
|
31
51
|
or add to your `Gemfile` for applications
|
32
52
|
|
33
53
|
```ruby
|
34
|
-
gem 'consoler', '~> 1.0.
|
54
|
+
gem 'consoler', '~> 1.0.3'
|
35
55
|
```
|
36
56
|
|
37
57
|
or to your `.gemspec` file for gems
|
38
58
|
|
39
59
|
```ruby
|
40
|
-
Gem::Specification.new do |
|
41
|
-
|
60
|
+
Gem::Specification.new do |spec|
|
61
|
+
spec.add_dependency 'consoler', '~> 1.0.3'
|
62
|
+
end
|
63
|
+
```
|
64
|
+
|
65
|
+
## Docs
|
66
|
+
|
67
|
+
Full API documentation can the found here: http://www.rubydoc.info/github/justim/consoler-rb/
|
68
|
+
|
69
|
+
### API
|
70
|
+
|
71
|
+
#### Creating an application
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
# create an application
|
75
|
+
app = Consoler::Application.new
|
76
|
+
|
77
|
+
# .. or with a description that will show up in the usage message
|
78
|
+
app = Consoler::Application.new description: 'A simple app'
|
79
|
+
```
|
80
|
+
|
81
|
+
#### Adding commands
|
82
|
+
|
83
|
+
All actions you want to create must have a command name, there is no top-level matching available at this point.
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
# create an application
|
87
|
+
app = Consoler::Application.new
|
88
|
+
|
89
|
+
# in the most simple way, you provide a name (the method name) and a block you
|
90
|
+
# want to execute if it matches
|
91
|
+
app.build do
|
92
|
+
# your code here
|
93
|
+
end
|
94
|
+
|
95
|
+
# to add options to your command, provide a options definition as an argument
|
96
|
+
app.build '[--clean] [--env=] [-v] output_dir' do |clean, env, v, output_dir|
|
97
|
+
# parameters are matched based on name
|
98
|
+
# `clean` contains a boolean
|
99
|
+
# `env` contains a string or nil if not provided
|
100
|
+
# `v` contains a number, counting the times it occurred in the arguments
|
101
|
+
# `output_dir` contains a string, if needed to match this command
|
42
102
|
end
|
43
103
|
```
|
104
|
+
|
105
|
+
#### Running the application
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
# filename: app.rb
|
109
|
+
|
110
|
+
# create an application
|
111
|
+
app = Consoler::Application.new
|
112
|
+
|
113
|
+
# a build command
|
114
|
+
app.build '[--clean] [--env=] [-v] output_dir' do |clean, env, v, output_dir|
|
115
|
+
puts 'Starting build...' if v > 0
|
116
|
+
|
117
|
+
do_clean_up if clean
|
118
|
+
|
119
|
+
do_build env || 'development', output_dir
|
120
|
+
|
121
|
+
puts 'Build complete' if v > 0
|
122
|
+
end
|
123
|
+
|
124
|
+
# a deploy command
|
125
|
+
app.build '[-v] [--env=]' do |env|
|
126
|
+
puts 'Starting deploy...' if v > 0
|
127
|
+
|
128
|
+
do_deploy env || 'development'
|
129
|
+
|
130
|
+
puts 'Deploy complete' if v > 0
|
131
|
+
end
|
132
|
+
```
|
133
|
+
|
134
|
+
_Shell commands:_
|
135
|
+
```sh
|
136
|
+
# start a build
|
137
|
+
ruby app.rb build --env production dist
|
138
|
+
|
139
|
+
# deploy
|
140
|
+
ruby app.rb deploy
|
141
|
+
```
|
142
|
+
|
143
|
+
#### Options definition
|
144
|
+
|
145
|
+
| Option | Meaning | Example |
|
146
|
+
| ------------ | ------------------------------- | -------------------------------------- |
|
147
|
+
| `-f` | Short option with name `f` | `ruby app.rb build -f` |
|
148
|
+
| `--clean` | Long option with name `clean` | `ruby app.rb build --clean` |
|
149
|
+
| `output_dir` | Argument with name `output_dir` | `ruby app.rb build dist/` |
|
150
|
+
| `--env=` | Long option with value | `ruby app.rb build --env production` |
|
151
|
+
| `-e=` | Short option with value | `ruby app.rb build -e production` |
|
152
|
+
| `[ .. ]` | Optional options/arguments | `ruby app.rb build` would match `[-v]` |
|
153
|
+
|
154
|
+
Optional-tokens can occur anywhere in the options, as long as they are not nested and properly closed. You can group optional parts, meaning that both options/arguments should be available or both not.
|
155
|
+
|
156
|
+
Options and/or arguments are mandatory unless specified otherwise.
|
157
|
+
|
158
|
+
Grouping of optionals allows you do things like this:
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
app = Consoler::Application.new
|
162
|
+
app.shout '[first_name last_name] [name]' do |first_name, last_name, name|
|
163
|
+
# by definition, `last_name` is also filled
|
164
|
+
unless first_name.nil? then
|
165
|
+
puts "Hello #{first_name} #{last_name}!"
|
166
|
+
end
|
167
|
+
|
168
|
+
unless name.nil? then
|
169
|
+
puts "Hello #{name}!"
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# calling with two arguments can fill the first group
|
174
|
+
# prints "Hello John Doe!"
|
175
|
+
app.run(['shout', 'John', 'Doe'])
|
176
|
+
|
177
|
+
# calling with one argument it is not possible to fill the first group
|
178
|
+
# prints "Hello Mr. White!"
|
179
|
+
app.run(['shout', 'Mr. White!'])
|
180
|
+
```
|
181
|
+
|
182
|
+
#### Return types in action block
|
183
|
+
|
184
|
+
| Option type | Return type (ex.) | Default (if optional) |
|
185
|
+
| ----------- | ----------------------- | --------------------- |
|
186
|
+
| Short | `Integer` (`1`) | `0` |
|
187
|
+
| Long | `Boolean` (`true`) | `false` |
|
188
|
+
| Value | `String` (`production`) | `nil` |
|
189
|
+
| Argument | `String` (`dist/`) | `nil` |
|
190
|
+
|
191
|
+
#### Subapplications
|
192
|
+
|
193
|
+
To make application nesting possible you can provide a complete application to a command, instead of an action block.
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
# filename: app.rb
|
197
|
+
|
198
|
+
# create a subapplication
|
199
|
+
android = Consoler::Application.new
|
200
|
+
android.build do; end
|
201
|
+
|
202
|
+
# options are supported just like regular apps
|
203
|
+
android.clean '--force' do; end
|
204
|
+
|
205
|
+
# create an application
|
206
|
+
app = Consoler::Application.new
|
207
|
+
|
208
|
+
# mount the android application on top of the android command
|
209
|
+
# note that the command does not support options
|
210
|
+
app.android android
|
211
|
+
```
|
212
|
+
|
213
|
+
You can now run the next bit to run the android build command:
|
214
|
+
|
215
|
+
```sh
|
216
|
+
ruby app.rb android build
|
217
|
+
```
|
218
|
+
|
219
|
+
You can build them as complicated as you like :)
|
220
|
+
|
221
|
+
### Complete example
|
222
|
+
|
223
|
+
```ruby
|
224
|
+
#!/usr/bin/env ruby
|
225
|
+
|
226
|
+
db = Consoler::Application.new
|
227
|
+
db.migrate '-- run all pending migrations' do
|
228
|
+
run_migrate
|
229
|
+
end
|
230
|
+
|
231
|
+
db.rollback '[--migrations=] -- rollback a number of migrations' do |migrations|
|
232
|
+
run_rollback migrations || 1
|
233
|
+
end
|
234
|
+
|
235
|
+
cache = Consoler::Application.new
|
236
|
+
cache.clear '[--env=] -- clear the cache for a given environment' do |env|
|
237
|
+
run_cache_clear env || 'development'
|
238
|
+
end
|
239
|
+
|
240
|
+
cache.warmup '[--env=] -- warmup the cache for a given environment' do |env|
|
241
|
+
run_cache_warmup env || 'development'
|
242
|
+
end
|
243
|
+
|
244
|
+
app = Consoler::Application.new
|
245
|
+
app.db db
|
246
|
+
app.cache cache
|
247
|
+
|
248
|
+
app.build '[--clean] [--env=] [-v] output_dir -- build the project' do |clean, env, v, output_dir|
|
249
|
+
puts 'Starting build...' if v > 0
|
250
|
+
|
251
|
+
if clean
|
252
|
+
puts 'Starting clean...' if v > 1
|
253
|
+
do_clean_up if clean
|
254
|
+
end
|
255
|
+
|
256
|
+
do_build env || 'development', output_dir
|
257
|
+
|
258
|
+
puts 'Build complete' if v > 0
|
259
|
+
end
|
260
|
+
```
|
261
|
+
|
262
|
+
Make the file executable with `chmod a+x app.rb`, you can now call it with `./app.rb` without `ruby` in front of it, saves a couple keystrokes.
|
263
|
+
|
264
|
+
```sh
|
265
|
+
# run all migration
|
266
|
+
./app.rb db migrate
|
267
|
+
# rollback the last 4 migrations
|
268
|
+
./app.rb db rollback 4
|
269
|
+
# clean production cache
|
270
|
+
./app.rb cache clear --env production
|
271
|
+
# build the project, including cleaning and logging
|
272
|
+
./app.rb build --clean --env production -vv dist/
|
273
|
+
# print the usage message
|
274
|
+
./app.rb
|
275
|
+
```
|
276
|
+
|
277
|
+
### Current wish-list
|
278
|
+
|
279
|
+
- Aliases, mostly to "document" short options, `-v|--verbose`
|
280
|
+
|
281
|
+
## Final notes
|
282
|
+
|
283
|
+
If you like what you see, feel free to use it anyway you like. Also, don't hold back if you have suggestions/question and create an issue to let me know, and we can talk about it. And if you don't like what you see, PRs are welcome. You should probably file an issue first to make sure it's something worth doing, and the right way to do it.
|
data/lib/consoler/application.rb
CHANGED
@@ -98,12 +98,18 @@ module Consoler
|
|
98
98
|
|
99
99
|
protected
|
100
100
|
|
101
|
+
# Run the app
|
102
|
+
#
|
103
|
+
# @param [Array<String>] args Arguments
|
104
|
+
# @return [(mixed, Boolean)] Result of the command, and, did the args match a command at all
|
101
105
|
def _run(args)
|
102
106
|
arg = args.shift
|
103
107
|
arguments = Consoler::Arguments.new args
|
104
108
|
|
105
109
|
@commands.each do |command|
|
106
110
|
if command.command == arg then
|
111
|
+
# the matched command contains a subapp, run subapp with the same
|
112
|
+
# arguments (excluding the arg that matched this command)
|
107
113
|
if command.action.instance_of? Consoler::Application then
|
108
114
|
result, matched = command.action._run(args)
|
109
115
|
|
@@ -123,8 +129,13 @@ module Consoler
|
|
123
129
|
return nil, false
|
124
130
|
end
|
125
131
|
|
132
|
+
# Print the usage message for this command
|
133
|
+
#
|
134
|
+
# @param [String] prefix A prefix for the command from a parent app
|
135
|
+
# @return [Consoler::Application]
|
126
136
|
def _commands_usage(prefix='')
|
127
137
|
@commands.each do |command|
|
138
|
+
# print the usage message of a subapp with a prefix from the current command
|
128
139
|
if command.action.instance_of? Consoler::Application then
|
129
140
|
command.action._commands_usage "#{prefix} #{command.command}"
|
130
141
|
else
|
@@ -141,24 +152,67 @@ module Consoler
|
|
141
152
|
print "\n"
|
142
153
|
end
|
143
154
|
end
|
155
|
+
|
156
|
+
self
|
144
157
|
end
|
145
158
|
|
146
159
|
private
|
147
160
|
|
161
|
+
# Add a command
|
162
|
+
#
|
163
|
+
# @param [String] command Command name
|
164
|
+
# @param [String] options_def Definition of options
|
165
|
+
# @param [Proc, Consoler::Application] action Action or subapp
|
166
|
+
# @return [Consoler::Application]
|
148
167
|
def _add_command(command, options_def, action)
|
149
168
|
@commands.push(Consoler::Command.new(
|
150
169
|
command: command,
|
151
170
|
options: Consoler::Options.new(options_def),
|
152
171
|
action: action,
|
153
172
|
))
|
173
|
+
|
174
|
+
self
|
154
175
|
end
|
155
176
|
|
177
|
+
# Execute an action with argument match info
|
178
|
+
#
|
179
|
+
# @param [Proc] action Action
|
180
|
+
# @param [Hash] match Argument match information
|
156
181
|
def _dispatch(action, match)
|
182
|
+
# match parameter names to indices of match information
|
157
183
|
arguments = action.parameters.map do |parameter|
|
158
|
-
|
184
|
+
parameter_name = parameter[1].to_s
|
185
|
+
|
186
|
+
if match.has_key? parameter_name then
|
187
|
+
match[parameter_name]
|
188
|
+
else
|
189
|
+
# check for the normalized name of every match to see
|
190
|
+
# if it fits the parameter name
|
191
|
+
match.each do |name, value|
|
192
|
+
normalized_name = _normalize name
|
193
|
+
|
194
|
+
if parameter_name == normalized_name then
|
195
|
+
return value
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
159
199
|
end
|
160
200
|
|
161
201
|
action.call(*arguments)
|
162
202
|
end
|
203
|
+
|
204
|
+
# Normalize a name to be used as a variable name
|
205
|
+
#
|
206
|
+
# @param [String] name Name
|
207
|
+
# @return [String] Normalized name
|
208
|
+
def _normalize(name)
|
209
|
+
# maybe do something more, maybe not.. ruby does allow for
|
210
|
+
# some weird stuff to be used as a variable name. the user
|
211
|
+
# should use some common sense. and, other things might
|
212
|
+
# also be an syntax error, like starting with a number.
|
213
|
+
# this normalization is more of a comvenience than anything
|
214
|
+
# else
|
215
|
+
name.gsub('-', '_')
|
216
|
+
end
|
163
217
|
end
|
164
218
|
end
|
data/lib/consoler/matcher.rb
CHANGED
@@ -26,12 +26,14 @@ module Consoler
|
|
26
26
|
def match
|
27
27
|
parse_options = true
|
28
28
|
|
29
|
-
|
29
|
+
_loop_args do |arg|
|
30
30
|
unless parse_options then
|
31
31
|
@argument_values.push arg
|
32
32
|
next
|
33
33
|
end
|
34
34
|
|
35
|
+
# when "argument" is --, then stop parsing the rest of the arguments
|
36
|
+
# and treat the rest as regular arguments
|
35
37
|
if arg == '--' then
|
36
38
|
parse_options = false
|
37
39
|
next
|
@@ -57,6 +59,10 @@ module Consoler
|
|
57
59
|
|
58
60
|
private
|
59
61
|
|
62
|
+
# Analyze a single argument
|
63
|
+
#
|
64
|
+
# @param [String] arg Single argument
|
65
|
+
# @return [true, nil] true on success, nil on failure
|
60
66
|
def _analyze(arg)
|
61
67
|
is_long = false
|
62
68
|
is_short = false
|
@@ -70,12 +76,14 @@ module Consoler
|
|
70
76
|
name = arg[1..-1]
|
71
77
|
end
|
72
78
|
|
73
|
-
|
79
|
+
# arg is not a long/short option, add to arguments values
|
80
|
+
unless is_long or is_short then
|
74
81
|
@argument_values.push arg
|
75
82
|
return true
|
76
83
|
end
|
77
84
|
|
78
85
|
unless name.nil? then
|
86
|
+
# get the name of the option, short options use the first character
|
79
87
|
option_name = if is_short then
|
80
88
|
name[0]
|
81
89
|
else
|
@@ -84,11 +92,13 @@ module Consoler
|
|
84
92
|
|
85
93
|
option = @options.get option_name
|
86
94
|
|
95
|
+
# no option by this name in options
|
87
96
|
return nil if option.nil?
|
88
97
|
|
89
98
|
needs_short = option.is_short
|
90
99
|
needs_long = option.is_long
|
91
100
|
|
101
|
+
# see if the type if right, short or long
|
92
102
|
if needs_long and not is_long then
|
93
103
|
return nil
|
94
104
|
elsif needs_short and not is_short then
|
@@ -97,9 +107,10 @@ module Consoler
|
|
97
107
|
|
98
108
|
if is_long then
|
99
109
|
if option.is_value then
|
100
|
-
|
101
|
-
|
102
|
-
|
110
|
+
# is_value needs a next argument for its value
|
111
|
+
return nil if _peek_next.nil?
|
112
|
+
@matched_options[name] = _peek_next
|
113
|
+
_skip_next
|
103
114
|
else
|
104
115
|
@matched_options[name] = true
|
105
116
|
end
|
@@ -107,11 +118,16 @@ module Consoler
|
|
107
118
|
|
108
119
|
if is_short then
|
109
120
|
if name.size == 1 and option.is_value then
|
110
|
-
|
111
|
-
|
112
|
-
|
121
|
+
# is_value needs a next argument for its value
|
122
|
+
return nil if _peek_next.nil?
|
123
|
+
@matched_options[name] = _peek_next
|
124
|
+
_skip_next
|
113
125
|
else
|
126
|
+
# for every character (short option) increment the option value
|
114
127
|
name.split('').each do |n|
|
128
|
+
short_option = @options.get n
|
129
|
+
return nil if short_option.nil?
|
130
|
+
|
115
131
|
if @matched_options[n].nil? then
|
116
132
|
@matched_options[n] = 0
|
117
133
|
end
|
@@ -125,29 +141,49 @@ module Consoler
|
|
125
141
|
return true
|
126
142
|
end
|
127
143
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
def
|
133
|
-
@arguments.args[@index + 1]
|
134
|
-
end
|
135
|
-
|
136
|
-
def loop_args
|
144
|
+
# Loop through the arguments
|
145
|
+
#
|
146
|
+
# @yield [String] An argument
|
147
|
+
# @return [Consoler::Matcher]
|
148
|
+
def _loop_args
|
137
149
|
@index = 0
|
138
150
|
size = @arguments.args.size
|
139
151
|
|
152
|
+
# use an incrementing index, to be able to peek to the next in the list
|
153
|
+
# and to skip an item
|
140
154
|
while @index < size do
|
141
|
-
yield
|
155
|
+
yield @arguments.args[@index]
|
142
156
|
|
143
|
-
|
157
|
+
_skip_next
|
144
158
|
end
|
159
|
+
|
160
|
+
self
|
161
|
+
end
|
162
|
+
|
163
|
+
# Peek at the next argument
|
164
|
+
#
|
165
|
+
# Only useful inside {Consoler::Matcher#_loop_args}
|
166
|
+
#
|
167
|
+
# @return [String, nil]
|
168
|
+
def _peek_next
|
169
|
+
@arguments.args[@index + 1]
|
145
170
|
end
|
146
171
|
|
147
|
-
|
172
|
+
# Skip to the next argument
|
173
|
+
#
|
174
|
+
# Useful if you use a peeked argument
|
175
|
+
#
|
176
|
+
# @return [nil]
|
177
|
+
# @return [Consoler::Matcher]
|
178
|
+
def _skip_next
|
148
179
|
@index += 1
|
180
|
+
|
181
|
+
self
|
149
182
|
end
|
150
183
|
|
184
|
+
# Match arguments to defined option arguments
|
185
|
+
#
|
186
|
+
# @return [Array<String>] The remaining args
|
151
187
|
def _match_arguments
|
152
188
|
@optionals_before = {}
|
153
189
|
@optionals_before_has_remaining = false
|
@@ -157,6 +193,8 @@ module Consoler
|
|
157
193
|
_match_arguments_optionals_before
|
158
194
|
|
159
195
|
@optionals_before.each do |mandatory_arg_name, optionals|
|
196
|
+
# fill the optional argument option with a value if there are enough
|
197
|
+
# arguments supplied (info available from optionals map)
|
160
198
|
optionals.each do |_, optional|
|
161
199
|
optional.each do |before|
|
162
200
|
if before[:included] then
|
@@ -166,6 +204,7 @@ module Consoler
|
|
166
204
|
end
|
167
205
|
end
|
168
206
|
|
207
|
+
# only fill mandatory argument if its not the :REMAINING key
|
169
208
|
if mandatory_arg_name != :REMAINING then
|
170
209
|
@matched_options[mandatory_arg_name] = @argument_values[argument_values_index]
|
171
210
|
argument_values_index += 1
|
@@ -174,6 +213,7 @@ module Consoler
|
|
174
213
|
|
175
214
|
remaining = []
|
176
215
|
|
216
|
+
# left over arguments
|
177
217
|
while argument_values_index < @argument_values.size do
|
178
218
|
remaining.push @argument_values[argument_values_index]
|
179
219
|
argument_values_index += 1
|
@@ -182,6 +222,9 @@ module Consoler
|
|
182
222
|
remaining
|
183
223
|
end
|
184
224
|
|
225
|
+
# Create a map of all optionals and before which mandatory argument they appear
|
226
|
+
#
|
227
|
+
# @return [Consoler::Matcher]
|
185
228
|
def _match_arguments_optionals_before
|
186
229
|
@optionals_before = {}
|
187
230
|
tracker = {}
|
@@ -190,8 +233,10 @@ module Consoler
|
|
190
233
|
next unless option.is_argument
|
191
234
|
|
192
235
|
if option.is_optional then
|
236
|
+
# setup tracker for optional group
|
193
237
|
tracker[option.is_optional] = [] if tracker[option.is_optional].nil?
|
194
238
|
|
239
|
+
# mark all optionals as not-included
|
195
240
|
tracker[option.is_optional].push({
|
196
241
|
included: false,
|
197
242
|
name: option.name,
|
@@ -202,24 +247,35 @@ module Consoler
|
|
202
247
|
end
|
203
248
|
end
|
204
249
|
|
250
|
+
# make sure all optionals are accounted for in the map
|
205
251
|
if tracker != {} then
|
252
|
+
# use a special key so we can handle it differently in the filling process
|
206
253
|
@optionals_before[:REMAINING] = tracker
|
207
254
|
@optionals_before_has_remaining = true
|
208
255
|
end
|
209
256
|
|
210
|
-
|
257
|
+
_match_arguments_options_before_matcher
|
258
|
+
|
259
|
+
self
|
211
260
|
end
|
212
261
|
|
213
|
-
|
262
|
+
# Match remaining args against the optionals map
|
263
|
+
#
|
264
|
+
# @return [Consoler::Matcher]
|
265
|
+
def _match_arguments_options_before_matcher
|
266
|
+
# number of arguments that are needed to fill our mandatory argument options
|
214
267
|
mandatories_matched = @optionals_before.size
|
215
268
|
|
269
|
+
# there are optionals at the end of the options, don't match the void
|
216
270
|
if @optionals_before_has_remaining then
|
217
271
|
mandatories_matched -= 1
|
218
272
|
end
|
219
273
|
|
220
274
|
total = 0
|
221
275
|
|
276
|
+
# loop through optional map
|
222
277
|
_each_optional_before_sorted do |before|
|
278
|
+
# are there enough arguments left to fill this optional group
|
223
279
|
if (total + before.size + mandatories_matched) <= @argument_values.size then
|
224
280
|
total += before.size
|
225
281
|
|
@@ -228,8 +284,13 @@ module Consoler
|
|
228
284
|
end
|
229
285
|
end
|
230
286
|
end
|
287
|
+
|
288
|
+
self
|
231
289
|
end
|
232
290
|
|
291
|
+
# Give all unmatched optional options there default value
|
292
|
+
#
|
293
|
+
# @return [Consoler::Matcher]
|
233
294
|
def _fill_defaults
|
234
295
|
@options.each do |option|
|
235
296
|
if option.is_optional then
|
@@ -238,8 +299,15 @@ module Consoler
|
|
238
299
|
end
|
239
300
|
end
|
240
301
|
end
|
302
|
+
|
303
|
+
self
|
241
304
|
end
|
242
305
|
|
306
|
+
# Loop through the optionals before map
|
307
|
+
#
|
308
|
+
# Sorted by number of optionals in a group
|
309
|
+
#
|
310
|
+
# @return [Consoler::Matcher]
|
243
311
|
def _each_optional_before_sorted
|
244
312
|
@optionals_before.each do |_, optionals|
|
245
313
|
tmp = []
|
@@ -254,6 +322,8 @@ module Consoler
|
|
254
322
|
yield optionals[item[:index]]
|
255
323
|
end
|
256
324
|
end
|
325
|
+
|
326
|
+
self
|
257
327
|
end
|
258
328
|
end
|
259
329
|
end
|
data/lib/consoler/option.rb
CHANGED
@@ -27,10 +27,13 @@ module Consoler
|
|
27
27
|
def self.create(option_def, tracker)
|
28
28
|
option = Option.new option_def, tracker
|
29
29
|
|
30
|
+
# split short options with more than 1 char in multiple options
|
30
31
|
if option.is_short and option.name.size > 1 then
|
32
|
+
# remember state
|
31
33
|
old_tracking = tracker.is_tracking
|
32
34
|
old_is_value = option.is_value
|
33
35
|
|
36
|
+
# if the complete option is optional, fake the tracker
|
34
37
|
if option.is_optional then
|
35
38
|
tracker.is_tracking = true
|
36
39
|
end
|
@@ -40,6 +43,7 @@ module Consoler
|
|
40
43
|
names.each_with_index do |name, i|
|
41
44
|
new_name = "-#{name}"
|
42
45
|
|
46
|
+
# if the short option should have a value, this only counts for the last option
|
43
47
|
if old_is_value and i == names.count - 1 then
|
44
48
|
new_name = "#{new_name}="
|
45
49
|
end
|
@@ -47,6 +51,7 @@ module Consoler
|
|
47
51
|
yield Option.new new_name, tracker
|
48
52
|
end
|
49
53
|
|
54
|
+
# reset to saved state
|
50
55
|
tracker.is_tracking = old_tracking
|
51
56
|
else
|
52
57
|
yield option
|
@@ -92,7 +97,12 @@ module Consoler
|
|
92
97
|
#
|
93
98
|
# @param [String] option_def Definition of the option
|
94
99
|
# @param [Consoler::Tracker] tracker tracker
|
100
|
+
# @raise [RuntimeError] if the option name is empty
|
101
|
+
# @raise [RuntimeError] if the option is long _and_ short
|
95
102
|
def initialize(option_def, tracker)
|
103
|
+
# Check for multiple attributes in the option definition till we got the
|
104
|
+
# final name and all of its attributes
|
105
|
+
|
96
106
|
option, @is_optional = _is_optional option_def, tracker
|
97
107
|
option, @is_long = _is_long option
|
98
108
|
option, @is_short = _is_short option
|
@@ -112,9 +122,22 @@ module Consoler
|
|
112
122
|
|
113
123
|
private
|
114
124
|
|
125
|
+
# Check optional definition
|
126
|
+
#
|
127
|
+
# Does it open an optional group
|
128
|
+
# Does it close an optional group (can be both)
|
129
|
+
# Updates the tracker
|
130
|
+
# Removes leading [ and trailing ]
|
131
|
+
#
|
132
|
+
# @param [String] option Option definition
|
133
|
+
# @param [Consoler::Tracker] tracker Optional tracker
|
134
|
+
# @raise [RuntimeError] if you try to nest optional groups
|
135
|
+
# @raise [RuntimeError] if you try to close an unopened optional
|
136
|
+
# @return [(String, Integer|nil)] Remaining option definition, and, optional group if available
|
115
137
|
def _is_optional(option, tracker)
|
116
138
|
if option[0] == '[' then
|
117
139
|
if !tracker.is_tracking then
|
140
|
+
# mark tracker as tracking
|
118
141
|
tracker.is_tracking = true
|
119
142
|
tracker.index += 1
|
120
143
|
option = option[1..-1]
|
@@ -123,6 +146,7 @@ module Consoler
|
|
123
146
|
end
|
124
147
|
end
|
125
148
|
|
149
|
+
# get optional group index from tracking, if tracking
|
126
150
|
optional = if tracker.is_tracking then
|
127
151
|
tracker.index
|
128
152
|
else
|
@@ -131,6 +155,7 @@ module Consoler
|
|
131
155
|
|
132
156
|
if option[-1] == ']' then
|
133
157
|
if tracker.is_tracking then
|
158
|
+
# mark tracker as non-tracking
|
134
159
|
tracker.is_tracking = false
|
135
160
|
option = option[0..-2]
|
136
161
|
else
|
@@ -141,6 +166,10 @@ module Consoler
|
|
141
166
|
return option, optional
|
142
167
|
end
|
143
168
|
|
169
|
+
# Check long definition
|
170
|
+
#
|
171
|
+
# @param [String] option Option definition
|
172
|
+
# @return [(String, Boolean)]
|
144
173
|
def _is_long(option)
|
145
174
|
if option[0..1] == '--' then
|
146
175
|
long = true
|
@@ -152,6 +181,10 @@ module Consoler
|
|
152
181
|
return option, long
|
153
182
|
end
|
154
183
|
|
184
|
+
# Check short definition
|
185
|
+
#
|
186
|
+
# @param [String] option Option definition
|
187
|
+
# @return [(String, Boolean)]
|
155
188
|
def _is_short(option)
|
156
189
|
if option[0] == '-' then
|
157
190
|
short = true
|
@@ -163,6 +196,11 @@ module Consoler
|
|
163
196
|
return option, short
|
164
197
|
end
|
165
198
|
|
199
|
+
# Check value definition
|
200
|
+
#
|
201
|
+
# @param [String] option Option definition
|
202
|
+
# @raise [RuntimeError] if you try to assign a value to an argument
|
203
|
+
# @return [(String, Boolean)]
|
166
204
|
def _value(option, argument)
|
167
205
|
if option[-1] == '=' then
|
168
206
|
if argument then
|
data/lib/consoler/options.rb
CHANGED
@@ -13,12 +13,14 @@ module Consoler
|
|
13
13
|
# Create a list of option based on a string definition
|
14
14
|
#
|
15
15
|
# @param options_def [String] A string definition of the desired options
|
16
|
+
# @raise [RuntimeError] if you try to use a duplicate name
|
16
17
|
def initialize(options_def)
|
17
18
|
@options = []
|
18
19
|
@description = nil
|
19
20
|
|
20
21
|
return if options_def.nil?
|
21
22
|
|
23
|
+
# strip the description
|
22
24
|
if match = /(^|\s+)-- (?<description>.*)$/.match(options_def) then
|
23
25
|
@description = match[:description]
|
24
26
|
options_def = options_def[0...-match[0].size]
|
@@ -44,7 +46,7 @@ module Consoler
|
|
44
46
|
# @param name [String] Name of the option
|
45
47
|
# @return [Consoler::Option, nil]
|
46
48
|
def get(name)
|
47
|
-
each do |option|
|
49
|
+
each do |option, _|
|
48
50
|
if option.name == name then
|
49
51
|
return option
|
50
52
|
end
|
@@ -72,6 +74,9 @@ module Consoler
|
|
72
74
|
@options.size
|
73
75
|
end
|
74
76
|
|
77
|
+
# Get the definition for these options
|
78
|
+
#
|
79
|
+
# @return [String] Options definition
|
75
80
|
def to_definition
|
76
81
|
definition = ''
|
77
82
|
optional = nil
|
@@ -87,6 +92,7 @@ module Consoler
|
|
87
92
|
definition += option.to_definition
|
88
93
|
|
89
94
|
if option.is_optional then
|
95
|
+
# only close when the next option is not optional, or another optional group
|
90
96
|
if @options[i + 1].nil? or optional != @options[i + 1].is_optional then
|
91
97
|
definition += ']'
|
92
98
|
optional = nil
|
data/lib/consoler/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: consoler
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tim
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-03-
|
11
|
+
date: 2018-03-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -89,6 +89,7 @@ files:
|
|
89
89
|
- ".editorconfig"
|
90
90
|
- ".gitignore"
|
91
91
|
- ".travis.yml"
|
92
|
+
- ".yardopts"
|
92
93
|
- Gemfile
|
93
94
|
- LICENSE.txt
|
94
95
|
- README.md
|