command_kit 0.2.0 → 0.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
2
  SHA256:
3
- metadata.gz: 5dc00be598fd9cee093dbfaf32a03118897179587d4dc17c97fdde2b020d3ac7
4
- data.tar.gz: 88e498fa844b4db50dc194f6ec474d7c85bbea1619bf1340e6d2abfc4c13c0b3
3
+ metadata.gz: a218265a42b8b11c95888d07a3dcc3817d534adf830f8e7b1f1790bae73f05d6
4
+ data.tar.gz: c11cf5b2179d0701d5164b5ffcc07f2040cf8c813ea68caafdd7602e8603b754
5
5
  SHA512:
6
- metadata.gz: 5fd2683d7d3fb3f4c1258019d209b9f8ca348b0fc71c21c9f292b6799502dcfbc39d8f5ff8e50d54b822aaf229eedd85c3e151bea0f9c92d13dc08bb5b6d947e
7
- data.tar.gz: 4276756c0495b2583303c9db090cd0e1221681d7eb357b34d4078a7e59167695f3250552c58ff0be24ee27535fb49ab2ce15d8e0711ee44cebbb93a52f6dfac0
6
+ metadata.gz: 7bd20d0e6b33b1d84933ea246c51b552c8652e3f59c8ec3fe32bf1944148377799b20b8177f0025114689ff5f73a88d08fe14171bdbd48189ab4123ba9129113
7
+ data.tar.gz: 617efce051bc51bbb88ada26a83ded0e158b8e0af28ab2bad8af1478aac05a156833aa3bfd95b59e7b07daf1075c08e60f7f6165cc28b76908cb13eb694c4f4b
data/.rubocop.yml CHANGED
@@ -85,6 +85,7 @@ Style/IfUnlessModifier: { Enabled: false } # Offense count: 13
85
85
  Style/MethodCallWithoutArgsParentheses: { Enabled: false } # Offense count: 10
86
86
  Style/SpecialGlobalVars: { Enabled: false } # Offense count: 28
87
87
  Style/StringLiterals: { Enabled: false } # Offense count: 774
88
+ Lint/ElseLayout: { Enabled: false } # Offense count: 22
88
89
 
89
90
  # < 10 violations
90
91
  Layout/EmptyLinesAroundModuleBody: { Enabled: false } # Offense count: 5
@@ -135,4 +136,6 @@ Style/RegexpLiteral: { Enabled: false } # Offense count: 1
135
136
  Style/RescueStandardError: { Enabled: false } # Offense count: 1
136
137
  Style/SoleNestedConditional: { Enabled: false } # Offense count: 1
137
138
  Style/TrailingCommaInHashLiteral: { Enabled: false } # Offense count: 2
138
- Style/PercentLiteralDelimiters: { Enabled: false } # Offense count: 2
139
+
140
+ # rubocop cannot tell that rubygems_mfa_required is enabled in gemspec.yml
141
+ Gemspec/RequireMFA: { Enabled: false }
data/ChangeLog.md CHANGED
@@ -1,3 +1,67 @@
1
+ ### 0.2.1 / 2021-11-16
2
+
3
+ * Ensure that all error messages end with a period.
4
+ * Documentation fixes.
5
+ * Opt-in to [rubygem.org MFA requirement](https://guides.rubygems.org/mfa-requirement-opt-in/).
6
+
7
+ #### CommandKit::Printing
8
+
9
+ * Auto-detect whether {CommandKit::CommandName#command_name #command_name} is
10
+ available, and if so, prepend the command name to all error messages.
11
+
12
+ #### CommandKit::Help::Man
13
+
14
+ * Expand the path given to
15
+ {CommandKit::Help::Man::ClassMethods#man_dir man_dir}.
16
+ * If {CommandKit::Help::Man::ClassMethods#man_dir man_dir} is not set, fallback
17
+ to regular `--help` output.
18
+
19
+ #### CommandKit::Arguments
20
+
21
+ * Include {CommandKit::Usage} and {CommandKit::Printing} into
22
+ {CommandKit::Arguments}.
23
+
24
+ #### CommandKit::Options
25
+
26
+ * Include {CommandKit::Arguments} into {CommandKit::Options}.
27
+ * Ensure that {CommandKit::Options::Parser#main} runs before
28
+ {CommandKit::Arguments#main}.
29
+ * Ensure that {CommandKit::Options#help} also calls
30
+ {CommandKit::Arguments#help_arguments}.
31
+ * Always prepopulate {CommandKit::Options#options #options} with option's
32
+ default values.
33
+ * Note: if an option has a default value but the option's value is not
34
+ required (ex: `value: {required: false, default: "foo"}`), and the option's
35
+ flag is given but no value is given (ex: `--option-flag --some-other-flag`),
36
+ the option's value in {CommandKit::Options#options #options} will be `nil`
37
+ _not_ the option's default value (`"foo"`). This helps indicate that the
38
+ option's flag was given but no value was given with it.
39
+
40
+ #### CommandKit::Options::OptionValue
41
+
42
+ * When a `Class` is passed to {CommandKit::Options::OptionValue.default_usage},
43
+ demodularize the class name before converting it to underscored/uppercase.
44
+
45
+ #### CommandKit::Command
46
+
47
+ * Fixed the inclusion order of {CommandKit::Options} and
48
+ {CommandKit::Arguments}.
49
+
50
+ #### CommandKit::Commands
51
+
52
+ * Define the `COMMAND` and `ARGS` arguments.
53
+ * Correctly duplicate the {CommandKit::Env#env env} (which can be either `ENV`
54
+ or a `Hash`) to work on ruby-3.1.0-preview1.
55
+ * Print command aliases that were set explicitly
56
+ (ex: `command_aliases['rm'] = 'remove'`) in {CommandKit::Commands#help}.
57
+ * Print help and exit with status `1` if no command is given. This matches the
58
+ behavior of the `git` command.
59
+
60
+ #### CommandKit::Commands::AutoLoad
61
+
62
+ * Ensure that any explicit command aliases are added to the command's
63
+ {CommandKit::Commands::ClassMethods#command_aliases command_aliases}.
64
+
1
65
  ### 0.2.0 / 2021-08-31
2
66
 
3
67
  * Added {CommandKit::Colors::ANSI#on_black}.
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # command_kit
2
2
 
3
3
  [![Build Status](https://github.com/postmodern/command_kit.rb/workflows/CI/badge.svg?branch=main)](https://github.com/postmodern/command_kit.rb/actions)
4
+ [![Code Climate](https://codeclimate.com/github/postmodern/command_kit.rb.svg)](https://codeclimate.com/github/postmodern/command_kit.rb)
5
+ [![Gem Version](https://badge.fury.io/rb/command_kit.svg)](https://badge.fury.io/rb/command_kit)
4
6
 
5
7
  * [Homepage](https://github.com/postmodern/command_kit.rb#readme)
6
8
  * [Forum](https://github.com/postmodern/command_kit.rb/discussions) |
@@ -153,6 +155,52 @@ Foo::CLI::MyCmd.start
153
155
 
154
156
  Example command
155
157
 
158
+ ## Testing
159
+
160
+ ### RSpec
161
+
162
+ ```ruby
163
+ require 'spec_helper'
164
+ require 'stringio'
165
+ require 'foo/cli/my_cmd'
166
+
167
+ describe Foo::CLI::MyCmd do
168
+ let(:stdin) { StringIO.new }
169
+ let(:stdout) { StringIO.new }
170
+ let(:stderr) { StringIO.new }
171
+ let(:env) { ENV }
172
+
173
+ subject do
174
+ described_class.new(
175
+ stdin: stdin,
176
+ stdout: stdout,
177
+ stderr: stderr,
178
+ env: env
179
+ )
180
+ end
181
+
182
+ # testing with raw options/arguments
183
+ describe "#main" do
184
+ context "when executed with no arguments" do
185
+ it "must exit with -1" do
186
+ expect(subject.main([])).to eq(-1)
187
+ end
188
+ end
189
+
190
+ context "when executed with -o OUTPUT" do
191
+ let(:file) { ... }
192
+ let(:output) { ... }
193
+
194
+ before { subject.main(["-o", output, file]) }
195
+
196
+ it "must create the output file" do
197
+ ...
198
+ end
199
+ end
200
+ end
201
+ end
202
+ ```
203
+
156
204
  ### Reference
157
205
 
158
206
  * [CommandKit::Arguments](https://rubydoc.info/gems/command_kit/CommandKit/Arguments)
data/gemspec.yml CHANGED
@@ -14,7 +14,7 @@ metadata:
14
14
  source_code_uri: https://github.com/postmodern/command_kit.rb
15
15
  bug_tracker_uri: https://github.com/postmodern/command_kit.rb/issues
16
16
  changelog_uri: https://github.com/postmodern/command_kit.rb/blob/main/ChangeLog.md
17
-
17
+ rubygems_mfa_required: 'true'
18
18
 
19
19
  required_ruby_version: ">= 2.7.0"
20
20
 
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'command_kit/arguments/argument'
4
+ require 'command_kit/usage'
3
5
  require 'command_kit/main'
4
6
  require 'command_kit/help'
5
- require 'command_kit/arguments/argument'
7
+ require 'command_kit/printing'
6
8
 
7
9
  module CommandKit
8
10
  #
@@ -45,8 +47,10 @@ module CommandKit
45
47
  # end
46
48
  #
47
49
  module Arguments
50
+ include Usage
48
51
  include Main
49
52
  include Help
53
+ include Printing
50
54
 
51
55
  #
52
56
  # @api private
@@ -168,7 +172,7 @@ module CommandKit
168
172
  help_usage
169
173
  return 1
170
174
  elsif argv.length > (required_args + optional_args) && !has_repeats_arg
171
- print_error("too many arguments given")
175
+ print_error("too many arguments given.")
172
176
  help_usage
173
177
  return 1
174
178
  end
@@ -104,7 +104,7 @@ module CommandKit
104
104
  # @since 0.2.0
105
105
  ON_BLUE = "\e[44m"
106
106
 
107
- # ANSI color code for background color megenta
107
+ # ANSI color code for background color magenta
108
108
  #
109
109
  # @since 0.2.0
110
110
  ON_MAGENTA = "\e[45m"
@@ -162,7 +162,14 @@ module CommandKit
162
162
  #
163
163
  def included(command)
164
164
  command.include Commands
165
- command.commands.merge!(@commands)
165
+
166
+ @commands.each do |name,subcommand|
167
+ command.commands[name] = subcommand
168
+
169
+ subcommand.aliases.each do |alias_name|
170
+ command.command_aliases[alias_name] = name
171
+ end
172
+ end
166
173
  end
167
174
  end
168
175
  end
@@ -14,7 +14,8 @@ module CommandKit
14
14
 
15
15
  include ParentCommand
16
16
 
17
- argument :command, desc: 'Command name to lookup'
17
+ argument :command, required: false,
18
+ desc: 'Command name to lookup'
18
19
 
19
20
  #
20
21
  # Prints the given commands `--help` output or lists registered commands.
@@ -34,7 +35,7 @@ module CommandKit
34
35
 
35
36
  subcommand.help
36
37
  else
37
- print_error "#{command_name}: unknown command: #{command}"
38
+ print_error "unknown command: #{command}"
38
39
  exit(1)
39
40
  end
40
41
  end
@@ -36,7 +36,7 @@ module CommandKit
36
36
  # Optional alias names for the subcommand.
37
37
  #
38
38
  def initialize(command, summary: self.class.summary(command),
39
- aliases: [])
39
+ aliases: [])
40
40
  @command = command
41
41
  @summary = summary
42
42
  @aliases = aliases.map(&:to_s)
@@ -59,6 +59,13 @@ module CommandKit
59
59
  context.extend ModuleMethods
60
60
  else
61
61
  context.usage "[options] [COMMAND [ARGS...]]"
62
+ context.argument :command, required: false,
63
+ desc: 'The command name to run'
64
+
65
+ context.argument :args, required: false,
66
+ repeats: true,
67
+ desc: 'Additional arguments for the command'
68
+
62
69
  context.extend ClassMethods
63
70
  context.command Help
64
71
  end
@@ -140,11 +147,10 @@ module CommandKit
140
147
  end
141
148
 
142
149
  subcommand = Subcommand.new(command_class,**kwargs)
143
-
144
150
  commands[name] = subcommand
145
151
 
146
- subcommand.aliases.each do |command_alias|
147
- command_aliases[command_alias] = name
152
+ subcommand.aliases.each do |alias_name|
153
+ command_aliases[alias_name] = name
148
154
  end
149
155
 
150
156
  return subcommand
@@ -220,7 +226,9 @@ module CommandKit
220
226
  end
221
227
 
222
228
  if command_class.include?(Env)
223
- kwargs[:env] = env.dup
229
+ kwargs[:env] = if env.eql?(ENV) then env.to_h
230
+ else env.dup
231
+ end
224
232
  end
225
233
 
226
234
  if command_class.include?(Options)
@@ -296,6 +304,7 @@ module CommandKit
296
304
  exit invoke(command,*argv)
297
305
  else
298
306
  help
307
+ exit(1)
299
308
  end
300
309
  end
301
310
 
@@ -309,8 +318,14 @@ module CommandKit
309
318
  puts
310
319
  puts "Commands:"
311
320
 
321
+ command_aliases = Hash.new { |hash,key| hash[key] = [] }
322
+
323
+ self.class.command_aliases.each do |alias_name,name|
324
+ command_aliases[name] << alias_name
325
+ end
326
+
312
327
  self.class.commands.sort.each do |name,subcommand|
313
- names = [name, *subcommand.aliases].join(', ')
328
+ names = [name, *command_aliases[name]].join(', ')
314
329
 
315
330
  if subcommand.summary
316
331
  puts " #{names}\t#{subcommand.summary}"
@@ -16,7 +16,7 @@ module CommandKit
16
16
 
17
17
  # The home directory.
18
18
  #
19
- # @return [String]
19
+ # @return [Array<String>]
20
20
  #
21
21
  # @api semipublic
22
22
  attr_reader :path_dirs
@@ -70,7 +70,7 @@ module CommandKit
70
70
  #
71
71
  def man_dir(new_man_dir=nil)
72
72
  if new_man_dir
73
- @man_dir = new_man_dir
73
+ @man_dir = File.expand_path(new_man_dir)
74
74
  else
75
75
  @man_dir || if superclass.kind_of?(ClassMethods)
76
76
  superclass.man_dir
@@ -115,10 +115,6 @@ module CommandKit
115
115
  # @api semipublic
116
116
  #
117
117
  def help_man(man_page=self.class.man_page)
118
- unless self.class.man_dir
119
- raise(NotImplementedError,"#{self.class}.man_dir not set")
120
- end
121
-
122
118
  man_path = File.join(self.class.man_dir,man_page)
123
119
 
124
120
  man(man_path)
@@ -139,11 +135,19 @@ module CommandKit
139
135
  #
140
136
  def help
141
137
  if stdout.tty?
142
- if help_man.nil?
143
- # the `man` command is not installed
138
+ if self.class.man_dir
139
+ status = help_man
140
+
141
+ if status.nil?
142
+ # the `man` command is not installed
143
+ super
144
+ end
145
+ else
146
+ # man_dir was not set
144
147
  super
145
148
  end
146
149
  else
150
+ # stdout is not a TTY
147
151
  super
148
152
  end
149
153
  end
@@ -44,7 +44,7 @@ module CommandKit
44
44
 
45
45
  until scanner.eos?
46
46
  if (separator = scanner.scan(/[_-]+/))
47
- new_string << '_' * separator.length
47
+ new_string << ('_' * separator.length)
48
48
  else
49
49
  if (capitalized = scanner.scan(/[A-Z][a-z\d]+/))
50
50
  new_string << capitalized
@@ -57,7 +57,7 @@ module CommandKit
57
57
  end
58
58
 
59
59
  if (separator = scanner.scan(/[_-]+/))
60
- new_string << '_' * separator.length
60
+ new_string << ('_' * separator.length)
61
61
  elsif !scanner.eos?
62
62
  new_string << '_'
63
63
  end
@@ -218,6 +218,15 @@ module CommandKit
218
218
  #
219
219
  # Asks the user for secret input.
220
220
  #
221
+ # @param [String] prompt
222
+ # The prompt that will be printed before reading input.
223
+ #
224
+ # @param [Boolean] required
225
+ # Requires non-empty input.
226
+ #
227
+ # @return [String]
228
+ # The user input.
229
+ #
221
230
  # @example
222
231
  # ask_secret("Password")
223
232
  # # Password:
@@ -9,7 +9,7 @@ module CommandKit
9
9
  # ## Examples
10
10
  #
11
11
  # open_app_for "movie.avi"
12
- # open_app_for "https://github.com/postmodern/command_kit.rb"
12
+ # open_app_for "https://github.com/postmodern/command_kit.rb#readme"
13
13
  #
14
14
  # @since 0.2.0
15
15
  #
@@ -41,7 +41,7 @@ module CommandKit
41
41
 
42
42
  # The desired type of the argument value.
43
43
  #
44
- # @return [Class, Hash, Array, Regexp, nil]
44
+ # @return [Class, Hash, Array, Regexp]
45
45
  attr_reader :type
46
46
 
47
47
  # The default parsed value for the argument value.
@@ -92,10 +92,11 @@ module CommandKit
92
92
  def self.default_usage(type)
93
93
  USAGES.fetch(type) do
94
94
  case type
95
- when Class then Inflector.underscore(type.name).upcase
96
95
  when Hash then type.keys.join('|')
97
96
  when Array then type.join('|')
98
97
  when Regexp then type.source
98
+ when Class
99
+ Inflector.underscore(Inflector.demodularize(type.name)).upcase
99
100
  else
100
101
  raise(TypeError,"unsupported option type: #{type.inspect}")
101
102
  end
@@ -144,8 +144,8 @@ module CommandKit
144
144
  # @api semipublic
145
145
  #
146
146
  def on_parse_error(error)
147
- print_error("#{command_name}: #{error.message}")
148
- print_error("Try '#{command_name} --help' for more information.")
147
+ print_error(error.message)
148
+ stderr.puts("Try '#{command_name} --help' for more information.")
149
149
  exit(1)
150
150
  end
151
151
 
@@ -1,3 +1,4 @@
1
+ require 'command_kit/arguments'
1
2
  require 'command_kit/options/option'
2
3
  require 'command_kit/options/parser'
3
4
 
@@ -36,6 +37,7 @@ module CommandKit
36
37
  # end
37
38
  #
38
39
  module Options
40
+ include Arguments
39
41
  include Parser
40
42
 
41
43
  #
@@ -211,12 +213,12 @@ module CommandKit
211
213
  super(**kwargs)
212
214
 
213
215
  self.class.options.each_value do |option|
216
+ default_value = option.default_value
217
+
218
+ @options[option.name] = default_value unless default_value.nil?
219
+
214
220
  option_parser.on(*option.usage,option.type,option.desc) do |arg,*captures|
215
- @options[option.name] = if arg.nil?
216
- option.default_value
217
- else
218
- arg
219
- end
221
+ @options[option.name] = arg
220
222
 
221
223
  if option.block
222
224
  instance_exec(*arg,*captures,&option.block)
@@ -224,5 +226,16 @@ module CommandKit
224
226
  end
225
227
  end
226
228
  end
229
+
230
+ #
231
+ # Overrides the default {Usage#help help} method and calls {#help_options}
232
+ # and {#help_arguments}.
233
+ #
234
+ # @api public
235
+ #
236
+ def help
237
+ help_options
238
+ help_arguments
239
+ end
227
240
  end
228
241
  end
@@ -65,10 +65,10 @@ module CommandKit
65
65
  #
66
66
  def indent(n=2)
67
67
  if block_given?
68
+ original_indent = @indent
69
+
68
70
  begin
69
- original_indent = @indent
70
71
  @indent += n
71
-
72
72
  yield
73
73
  ensure
74
74
  @indent = original_indent
@@ -23,12 +23,23 @@ module CommandKit
23
23
  # The error message.
24
24
  #
25
25
  # @example
26
- # print_error "Error: invalid input"
26
+ # print_error "error: invalid input"
27
+ # # error: invalid input
28
+ #
29
+ # @example When CommandKit::CommandName is included:
30
+ # print_error "invalid input"
31
+ # # foo: invalid input
27
32
  #
28
33
  # @api public
29
34
  #
30
35
  def print_error(message)
31
- stderr.puts message
36
+ if respond_to?(:command_name)
37
+ # if #command_name is available, prefix all error messages with it
38
+ stderr.puts "#{command_name}: #{message}"
39
+ else
40
+ # if #command_name is not available, just print the error message as-is
41
+ stderr.puts message
42
+ end
32
43
  end
33
44
 
34
45
  #
@@ -1,4 +1,4 @@
1
1
  module CommandKit
2
2
  # command_kit version
3
- VERSION = "0.2.0"
3
+ VERSION = "0.2.1"
4
4
  end
@@ -137,6 +137,56 @@ describe CommandKit::Arguments do
137
137
 
138
138
  subject { command_class.new }
139
139
 
140
+ describe "#main" do
141
+ module TestArguments
142
+ class TestCommand
143
+
144
+ include CommandKit::Arguments
145
+
146
+ argument :argument1, required: true,
147
+ usage: 'ARG1',
148
+ desc: "Argument 1"
149
+
150
+ argument :argument2, required: false,
151
+ usage: 'ARG2',
152
+ desc: "Argument 2"
153
+
154
+ end
155
+ end
156
+
157
+ let(:command_class) { TestArguments::TestCommand }
158
+
159
+ context "when given the correct number of arguments" do
160
+ let(:argv) { %w[arg1 arg2] }
161
+
162
+ it "must parse options before validating the number of arguments" do
163
+ expect {
164
+ expect(subject.main(argv)).to eq(0)
165
+ }.to_not output.to_stderr
166
+ end
167
+ end
168
+
169
+ context "when given fewer than the required number of arguments" do
170
+ let(:argv) { %w[] }
171
+
172
+ it "must print an error message and return 1" do
173
+ expect {
174
+ expect(subject.main(argv)).to eq(1)
175
+ }.to output("#{subject.command_name}: insufficient number of arguments.#{$/}").to_stderr
176
+ end
177
+ end
178
+
179
+ context "when given more than the total number of arguments" do
180
+ let(:argv) { %w[foo bar baz] }
181
+
182
+ it "must print an error message and return 1" do
183
+ expect {
184
+ expect(subject.main(argv)).to eq(1)
185
+ }.to output("#{subject.command_name}: too many arguments given.#{$/}").to_stderr
186
+ end
187
+ end
188
+ end
189
+
140
190
  describe "#help_arguments" do
141
191
  context "when #arguments returns {}" do
142
192
  module TestArguments