command_kit 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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