consoler 1.0.1 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 114e6fa695bcd034b3fbeb8a7e176aaa63493e9c
4
- data.tar.gz: f16bae2dc6e43d138735e889b0d49db1280b2491
2
+ SHA256:
3
+ metadata.gz: 924c498b6db12469aef2fb31bbe7cc0bbcaa0e10cccb2a18b91584484b3cb189
4
+ data.tar.gz: f99cdb84ac04f1ce5baa60ce29ccd7667a5239c0b959b1310ce8f8a9da4292a9
5
5
  SHA512:
6
- metadata.gz: 2cd8d0e7daf27c4df2937c8e4a181b97eb759b03d3083a79df5959deac6f9d631497b8a6193d8a7c0d318ed1cc87e37f69a5ade1c52ee65367440767825307ab
7
- data.tar.gz: f21ecd390a02b5751a00ec6a4aa53e5d4bdc9b5b5cb2bbfbd8fa5451ce0a46f175589b46eed77e588ea8ba1c0acf405800ce1a9ec94f3a243d0882cd859b75da
6
+ metadata.gz: 20ff199169e12d2bff2be0d12b3bcb6e615eb5613166406c4e46963e6716fd9b70306f7b6f561525f0783866f6d8e0d4c7315b73f98150d2133bccf4a643bc48
7
+ data.tar.gz: 34fb00e04d57de3fbcfc0b8242d01f5a76010860cac2fc78a69622ee3c7c89eb656a3f6d0abe05a7c38bdf71b97a97871c6738e488d52be203c5a2cc8bb603d4
@@ -0,0 +1,34 @@
1
+ name: tests
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+ branches: [master]
8
+
9
+ jobs:
10
+ tests:
11
+ runs-on: ubuntu-latest
12
+
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ ruby: ['2.4', '2.5', '2.6', '2.7', '3.0']
17
+
18
+ name: Ruby ${{ matrix.ruby }}
19
+
20
+ steps:
21
+ - uses: actions/checkout@v2
22
+
23
+ - name: Set up Ruby
24
+ # change this to (see https://github.com/ruby/setup-ruby#versioning):
25
+ uses: ruby/setup-ruby@v1
26
+ with:
27
+ ruby-version: ${{ matrix.ruby }}
28
+ bundler-cache: true
29
+
30
+ - name: Report Ruby version
31
+ run: ruby -v
32
+
33
+ - name: Run tests
34
+ run: bundle exec rake
@@ -0,0 +1,45 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.2
3
+
4
+ Exclude:
5
+ - 'test/**/*'
6
+
7
+ # 80 is not enough these days
8
+ Metrics/LineLength:
9
+ Max: 100
10
+
11
+ # Not ready for 100 yet
12
+ Metrics/ClassLength:
13
+ Max: 250
14
+
15
+ # Not ready for 20 yet
16
+ Metrics/MethodLength:
17
+ Max: 100
18
+
19
+ Metrics/AbcSize:
20
+ Enabled: false
21
+
22
+ # not ready for 10 just yet
23
+ Metrics/CyclomaticComplexity:
24
+ Max: 30
25
+
26
+ Metrics/BlockNesting:
27
+ Enabled: false
28
+
29
+ Metrics/PerceivedComplexity:
30
+ Enabled: false
31
+
32
+ # I prefer regular if statements in some cases
33
+ Style/IfUnlessModifier:
34
+ Enabled: false
35
+ Style/GuardClause:
36
+ Enabled: false
37
+
38
+ # `super` makes no sense in this case
39
+ # `#respond_to_missing?` is implemented
40
+ Style/MethodMissingSuper:
41
+ Enabled: false
42
+
43
+ # I like the comma
44
+ Style/TrailingCommaInArguments:
45
+ Enabled: false
@@ -0,0 +1,2 @@
1
+ --private
2
+ --protected
data/README.md CHANGED
@@ -1,27 +1,51 @@
1
- # Consoler [![Build Status](https://api.travis-ci.org/justim/consoler-rb.svg?branch=master)](http://travis-ci.org/justim/consoler-rb)
1
+ # Consoler [![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
- ## Usage
5
+ ## Quick usage
6
6
 
7
7
  ```ruby
8
- # create a application
8
+ # create an application
9
9
  app = Consoler::Application.new description: 'A simple app'
10
10
 
11
11
  # define a command
12
- app.build 'target [--clean]' do |target, clean|
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
- # target contains a string
17
- build_project target
16
+ # output_dir contains a string
17
+ build_project output_dir
18
18
  end
19
- app.run(['build', 'production', '--clean'])
19
+
20
+ # run with own args
21
+ app.run ['build', 'production', '--clean']
20
22
 
21
23
  # this does not match, nothing is executed and the usage message is printed
22
- app.run(['deploy', 'production'])
24
+ app.run ['deploy', 'production']
25
+
26
+ # defaults to ARGV
27
+ app.run
23
28
  ```
24
29
 
30
+ ## Features
31
+
32
+ - No fiddling with `ARGV` and friends
33
+ - Arguments filled based on their name, for easy access
34
+ - Grouped optionals
35
+ - Easy option aliasing
36
+
37
+ ## Requirements
38
+
39
+ Tests are run against multiple ruby versions (latest supported):
40
+
41
+ - `2.4.x`
42
+ - `2.5.x`
43
+ - `2.6.x`
44
+ - `2.7.x`
45
+ - `3.0.x`
46
+
47
+ No other requirements exist.
48
+
25
49
  ## Installation
26
50
 
27
51
  ```sh
@@ -31,13 +55,242 @@ gem install consoler
31
55
  or add to your `Gemfile` for applications
32
56
 
33
57
  ```ruby
34
- gem 'consoler', '~> 1.0.0'
58
+ gem 'consoler', '~> 1.2.1'
35
59
  ```
36
60
 
37
61
  or to your `.gemspec` file for gems
38
62
 
39
63
  ```ruby
40
- Gem::Specification.new do |s|
41
- s.add_dependency 'consoler', '~> 1.0.0'
64
+ Gem::Specification.new do |spec|
65
+ spec.add_dependency 'consoler', '~> 1.2.1'
66
+ end
67
+ ```
68
+
69
+ ## Docs
70
+
71
+ Full API documentation can the found here: https://www.rubydoc.info/gems/consoler/1.2.1
72
+
73
+ ### API
74
+
75
+ #### Creating an application
76
+
77
+ ```ruby
78
+ # create an application
79
+ app = Consoler::Application.new
80
+
81
+ # .. or with a description that will show up in the usage message
82
+ app = Consoler::Application.new description: 'A simple app'
83
+ ```
84
+
85
+ #### Adding commands
86
+
87
+ All actions you want to create must have a command name, there is no top-level matching available at this point.
88
+
89
+ ```ruby
90
+ # create an application
91
+ app = Consoler::Application.new
92
+
93
+ # in the most simple way, you provide a name (the method name) and a block you
94
+ # want to execute if it matches
95
+ app.build do
96
+ # your code here
97
+ end
98
+
99
+ # to add options to your command, provide a options definition as an argument
100
+ app.build '[--clean] [--env=] [-v|--verbose] <output_dir>' do |clean, env, verbose, output_dir|
101
+ # parameters are matched based on name
102
+ # `clean` contains a boolean
103
+ # `env` contains a string or nil if not provided
104
+ # `verbose` contains a number, counting the times it occurred in the arguments
105
+ # `output_dir` contains a string, if needed to match this command
106
+ end
107
+ ```
108
+
109
+ #### Running the application
110
+
111
+ ```ruby
112
+ # filename: app.rb
113
+
114
+ # create an application
115
+ app = Consoler::Application.new
116
+
117
+ # a build command
118
+ app.build '[--clean] [--env=] [-v|--verbose] <output_dir>' do |clean, env, verbose, output_dir|
119
+ puts 'Starting build...' if verbose > 0
120
+
121
+ do_clean_up if clean
122
+
123
+ do_build env || 'development', output_dir
124
+
125
+ puts 'Build complete' if verbose > 0
42
126
  end
127
+
128
+ # a deploy command
129
+ app.build '[-v|--verbose] [--env=]' do |env|
130
+ puts 'Starting deploy...' if verbose > 0
131
+
132
+ do_deploy env || 'development'
133
+
134
+ puts 'Deploy complete' if verbose > 0
135
+ end
136
+ ```
137
+
138
+ _Shell commands:_
139
+ ```sh
140
+ # start a build
141
+ ruby app.rb build --env production dist
142
+
143
+ # deploy
144
+ ruby app.rb deploy --verbose
145
+
146
+ # or, you can use command shortcuts. this works by prefix matching and only if
147
+ # there is one match, exact matches always have priority
148
+ ruby app.rb b --env production dist
149
+ ruby app.rb d --verbose
150
+
43
151
  ```
152
+
153
+ #### Options definition
154
+
155
+ | Option | Meaning | Example |
156
+ | --------------- | ------------------------------- | -------------------------------------- |
157
+ | `-f` | Short option with name `f` | `ruby app.rb build -f` |
158
+ | `--clean` | Long option with name `clean` | `ruby app.rb build --clean` |
159
+ | `-v\|--verbose` | Alias option for `v` | `ruby app.rb build --verbose` |
160
+ | `<output_dir>` | Argument with name `output_dir` | `ruby app.rb build dist/` |
161
+ | `--env=` | Long option with value | `ruby app.rb build --env production` |
162
+ | `-e=` | Short option with value | `ruby app.rb build -e production` |
163
+ | `[ .. ]` | Optional options/arguments | `ruby app.rb build` would match `[-v]` |
164
+
165
+ Some notes about these options:
166
+
167
+ - The `<`, `>` around the argument name a optional, but are always shown in de usage message for better legibility
168
+ - 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.
169
+ - Options and/or arguments are mandatory unless specified otherwise.
170
+ - Aliases should be the same "type", meaning that if the original option expands an value, the alias must do as well
171
+ - Aliases are only allowed for short and long options, with or without value and can be optional, just like regular options
172
+
173
+ Grouping of optionals allows you do things like this:
174
+
175
+ ```ruby
176
+ app = Consoler::Application.new
177
+ app.shout '[<first_name> <last_name>] [<name>]' do |first_name, last_name, name|
178
+ # by definition, `last_name` is also filled
179
+ unless first_name.nil? then
180
+ puts "Hello #{first_name} #{last_name}!"
181
+ end
182
+
183
+ unless name.nil? then
184
+ puts "Hello #{name}!"
185
+ end
186
+ end
187
+
188
+ # calling with two arguments can fill the first group
189
+ # prints "Hello John Doe!"
190
+ app.run ['shout', 'John', 'Doe']
191
+
192
+ # calling with one argument it is not possible to fill the first group
193
+ # prints "Hello Mr. White!"
194
+ app.run ['shout', 'Mr. White!']
195
+ ```
196
+
197
+ #### Return types in action block
198
+
199
+ | Option type | Return type (ex.) | Default (if optional) |
200
+ | ----------- | ----------------------- | --------------------- |
201
+ | Short | `Integer` (`1`) | `0` |
202
+ | Long | `Boolean` (`true`) | `false` |
203
+ | Value | `String` (`production`) | `nil` |
204
+ | Argument | `String` (`dist/`) | `nil` |
205
+
206
+ Aliased keep the properties as the original options; with a definition of `-v|--verbose` and providing `--verbose` as an argument, both `v` and `verbose` contain a number (`1` in this case). Same goes the other way around; with a definition of `--force|-f` and providing `-f` as an argument, both `force` and `f` contain a boolean (`true` in this case).
207
+
208
+ #### Subapplications
209
+
210
+ To make application nesting possible you can provide a complete application to a command, instead of an action block.
211
+
212
+ ```ruby
213
+ # filename: app.rb
214
+
215
+ # create a subapplication
216
+ android = Consoler::Application.new
217
+ android.build do; end
218
+
219
+ # options are supported just like regular apps
220
+ android.clean '--force|-f' do; end
221
+
222
+ # create an application
223
+ app = Consoler::Application.new
224
+
225
+ # mount the android application on top of the android command
226
+ # note that the command does not support options
227
+ app.android android
228
+ ```
229
+
230
+ You can now run the next bit to run the android build command:
231
+
232
+ ```sh
233
+ ruby app.rb android build
234
+ ```
235
+
236
+ You can build them as complicated as you like :)
237
+
238
+ ### Complete example
239
+
240
+ ```ruby
241
+ #!/usr/bin/env ruby
242
+
243
+ db = Consoler::Application.new
244
+ db.migrate '-- run all pending migrations' do
245
+ run_migrate
246
+ end
247
+
248
+ db.rollback '[--migrations=] -- rollback a number of migrations' do |migrations|
249
+ run_rollback migrations || 1
250
+ end
251
+
252
+ cache = Consoler::Application.new
253
+ cache.clear '[--env=] -- clear the cache for a given environment' do |env|
254
+ run_cache_clear env || 'development'
255
+ end
256
+
257
+ cache.warmup '[--env=] -- warmup the cache for a given environment' do |env|
258
+ run_cache_warmup env || 'development'
259
+ end
260
+
261
+ app = Consoler::Application.new
262
+ app.db db
263
+ app.cache cache
264
+
265
+ app.build '[--clean] [--env=] [-v] <output_dir> -- build the project' do |clean, env, v, output_dir|
266
+ puts 'Starting build...' if v > 0
267
+
268
+ if clean
269
+ puts 'Starting clean...' if v > 1
270
+ do_clean_up if clean
271
+ end
272
+
273
+ do_build env || 'development', output_dir
274
+
275
+ puts 'Build complete' if v > 0
276
+ end
277
+ ```
278
+
279
+ 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.
280
+
281
+ ```sh
282
+ # run all migration
283
+ ./app.rb db migrate
284
+ # rollback the last 4 migrations
285
+ ./app.rb db rollback 4
286
+ # clean production cache
287
+ ./app.rb cache clear --env production
288
+ # build the project, including cleaning and logging
289
+ ./app.rb build --clean --env production -vv dist/
290
+ # print the usage message
291
+ ./app.rb
292
+ ```
293
+
294
+ ## Final notes
295
+
296
+ 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.
@@ -22,9 +22,10 @@ Gem::Specification.new do |spec|
22
22
  # build docs on install
23
23
  spec.metadata['yard.run'] = 'yri'
24
24
 
25
- spec.add_development_dependency 'bundler', '~> 1.15'
26
- spec.add_development_dependency 'rake', '~> 12.3.0'
25
+ spec.required_ruby_version = '>= 2.4'
26
+ spec.add_development_dependency 'bundler', '~> 2.0'
27
27
  spec.add_development_dependency 'minitest', '~> 5.11.3'
28
+ spec.add_development_dependency 'rake', '~> 12.3.0'
29
+ spec.add_development_dependency 'simplecov', '~> 0.16.1'
28
30
  spec.add_development_dependency 'yard', '~> 0.9.12'
29
- spec.add_development_dependency 'simplecov', '~> 0.15.1'
30
31
  end
@@ -4,7 +4,6 @@ require_relative 'options'
4
4
  require_relative 'arguments'
5
5
 
6
6
  module Consoler
7
-
8
7
  # Consoler application
9
8
  #
10
9
  # @example A simple application
@@ -24,16 +23,25 @@ module Consoler
24
23
  # # this does not match, nothing is executed and the usage message is printed
25
24
  # app.run(['deploy', 'production'])
26
25
  class Application
27
-
28
26
  # Create a consoler application
29
27
  #
30
28
  # @param options [Hash] Options for the application
31
29
  # @option options [String] :description The description for the application (optional)
32
- def initialize(options={})
30
+ def initialize(options = {})
33
31
  @description = options[:description]
34
32
  @commands = []
35
33
  end
36
34
 
35
+ # Accept all method_missing call
36
+ #
37
+ # We use the name as a command name, thus we accept all names
38
+ #
39
+ # @param _method_name [String] Name of the method
40
+ # @param _include_private [bool] Name of the method
41
+ def respond_to_missing?(_method_name, _include_private = false)
42
+ true
43
+ end
44
+
37
45
  # Register a command for this app
38
46
  #
39
47
  # @param command_name [Symbol] Name of the command
@@ -44,21 +52,21 @@ module Consoler
44
52
  action = nil
45
53
  options_def = ''
46
54
 
47
- unless block.nil? then
55
+ unless block.nil?
48
56
  action = block
49
57
  options_def = input
50
58
 
51
- if not options_def.nil? and not options_def.instance_of? String then
59
+ if !options_def.nil? && !options_def.instance_of?(String)
52
60
  raise 'Invalid options'
53
61
  end
54
62
  end
55
63
 
56
- if input.instance_of? Consoler::Application then
64
+ if input.instance_of? Consoler::Application
57
65
  action = input
58
66
  options_def = ''
59
67
  end
60
68
 
61
- if action.nil? then
69
+ if action.nil?
62
70
  raise 'Invalid subapp/block'
63
71
  end
64
72
 
@@ -66,7 +74,7 @@ module Consoler
66
74
 
67
75
  _add_command(command, options_def, action)
68
76
 
69
- return nil
77
+ nil
70
78
  end
71
79
 
72
80
  # Run the application with a list of arguments
@@ -75,15 +83,15 @@ module Consoler
75
83
  # @param disable_usage_message [Boolean] Disable the usage message when nothing it matched
76
84
  # @return [mixed] Result of your matched command, <tt>nil</tt> otherwise
77
85
  def run(args = ARGV, disable_usage_message = false)
78
- # TODO signal handling of some kind?
86
+ # TODO: signal handling of some kind?
79
87
 
80
- result, matched = _run(args)
88
+ result, matched = _run(args.dup)
81
89
 
82
- if not matched and not disable_usage_message
90
+ if !matched && !disable_usage_message
83
91
  usage
84
92
  end
85
93
 
86
- return result
94
+ result
87
95
  end
88
96
 
89
97
  # Show the usage message
@@ -93,21 +101,48 @@ module Consoler
93
101
  puts "#{@description}\n\n" unless @description.nil?
94
102
  puts 'Usage:'
95
103
 
96
- _commands_usage $0
104
+ _commands_usage $PROGRAM_NAME
97
105
  end
98
106
 
99
107
  protected
100
108
 
109
+ # Run the app
110
+ #
111
+ # @param [Array<String>] args Arguments
112
+ # @return [(mixed, Boolean)] Result of the command, and, did the args match a command at all
101
113
  def _run(args)
102
114
  arg = args.shift
115
+
116
+ return [nil, false] if arg.nil?
117
+
103
118
  arguments = Consoler::Arguments.new args
119
+ exact_matches = []
120
+ partial_matches = []
104
121
 
105
122
  @commands.each do |command|
106
- if command.command == arg then
107
- if command.action.instance_of? Consoler::Application then
123
+ if command.command == arg
124
+ exact_matches.push command
125
+ elsif command.command.start_with? arg
126
+ partial_matches.push command
127
+ end
128
+ end
129
+
130
+ # we only allow a single partial match to prevent ambiguity
131
+ partial_match = if partial_matches.size == 1
132
+ partial_matches[0]
133
+ end
134
+
135
+ unless exact_matches.empty? && partial_match.nil?
136
+ matches = exact_matches
137
+ matches.push partial_match unless partial_match.nil?
138
+
139
+ matches.each do |command|
140
+ # the matched command contains a subapp, run subapp with the same
141
+ # arguments (excluding the arg that matched this command)
142
+ if command.action.instance_of?(Consoler::Application)
108
143
  result, matched = command.action._run(args)
109
144
 
110
- if matched then
145
+ if matched
111
146
  return result, true
112
147
  end
113
148
  else
@@ -120,45 +155,95 @@ module Consoler
120
155
  end
121
156
  end
122
157
 
123
- return nil, false
158
+ [nil, false]
124
159
  end
125
160
 
126
- def _commands_usage(prefix='')
161
+ # Print the usage message for this command
162
+ #
163
+ # @param [String] prefix A prefix for the command from a parent app
164
+ # @return [Consoler::Application]
165
+ def _commands_usage(prefix = '')
127
166
  @commands.each do |command|
128
- if command.action.instance_of? Consoler::Application then
167
+ # print the usage message of a subapp with a prefix from the current command
168
+ if command.action.instance_of?(Consoler::Application)
129
169
  command.action._commands_usage "#{prefix} #{command.command}"
130
170
  else
131
171
  print " #{prefix} #{command.command}"
132
172
 
133
- if command.options.size then
173
+ if command.options.size
134
174
  print " #{command.options.to_definition}"
135
175
  end
136
176
 
137
- unless command.options.description.nil? then
177
+ unless command.options.description.nil?
138
178
  print " -- #{command.options.description}"
139
179
  end
140
180
 
141
181
  print "\n"
142
182
  end
143
183
  end
184
+
185
+ self
144
186
  end
145
187
 
146
188
  private
147
189
 
190
+ # Add a command
191
+ #
192
+ # @param [String] command Command name
193
+ # @param [String] options_def Definition of options
194
+ # @param [Proc, Consoler::Application] action Action or subapp
195
+ # @return [Consoler::Application]
148
196
  def _add_command(command, options_def, action)
149
- @commands.push(Consoler::Command.new(
150
- command: command,
151
- options: Consoler::Options.new(options_def),
152
- action: action,
153
- ))
197
+ @commands.push(
198
+ Consoler::Command.new(
199
+ command: command,
200
+ options: Consoler::Options.new(options_def),
201
+ action: action,
202
+ )
203
+ )
204
+
205
+ self
154
206
  end
155
207
 
208
+ # Execute an action with argument match info
209
+ #
210
+ # @param [Proc] action Action
211
+ # @param [Hash] match Argument match information
156
212
  def _dispatch(action, match)
213
+ # match parameter names to indices of match information
157
214
  arguments = action.parameters.map do |parameter|
158
- match[parameter[1].to_s]
215
+ parameter_name = parameter[1].to_s
216
+
217
+ if match.key? parameter_name
218
+ match[parameter_name]
219
+ else
220
+ # check for the normalized name of every match to see
221
+ # if it fits the parameter name
222
+ match.each do |name, value|
223
+ normalized_name = _normalize name
224
+
225
+ if parameter_name == normalized_name
226
+ break value
227
+ end
228
+ end
229
+ end
159
230
  end
160
231
 
161
232
  action.call(*arguments)
162
233
  end
234
+
235
+ # Normalize a name to be used as a variable name
236
+ #
237
+ # @param [String] name Name
238
+ # @return [String] Normalized name
239
+ def _normalize(name)
240
+ # maybe do something more, maybe not.. ruby does allow for
241
+ # some weird stuff to be used as a variable name. the user
242
+ # should use some common sense. and, other things might
243
+ # also be an syntax error, like starting with a number.
244
+ # this normalization is more of a comvenience than anything
245
+ # else
246
+ name.tr('-', '_')
247
+ end
163
248
  end
164
249
  end