command_kit 0.1.0 → 0.3.0

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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +18 -3
  3. data/.rubocop.yml +141 -0
  4. data/ChangeLog.md +165 -0
  5. data/Gemfile +3 -0
  6. data/README.md +186 -118
  7. data/Rakefile +3 -2
  8. data/command_kit.gemspec +4 -4
  9. data/examples/command.rb +1 -1
  10. data/gemspec.yml +7 -0
  11. data/lib/command_kit/arguments/argument.rb +2 -2
  12. data/lib/command_kit/arguments.rb +36 -7
  13. data/lib/command_kit/colors.rb +702 -53
  14. data/lib/command_kit/command.rb +2 -3
  15. data/lib/command_kit/commands/auto_load.rb +8 -1
  16. data/lib/command_kit/commands/help.rb +3 -2
  17. data/lib/command_kit/commands/subcommand.rb +1 -1
  18. data/lib/command_kit/commands.rb +24 -9
  19. data/lib/command_kit/env/path.rb +1 -1
  20. data/lib/command_kit/file_utils.rb +46 -0
  21. data/lib/command_kit/help/man.rb +17 -33
  22. data/lib/command_kit/inflector.rb +47 -17
  23. data/lib/command_kit/interactive.rb +9 -0
  24. data/lib/command_kit/main.rb +7 -9
  25. data/lib/command_kit/man.rb +44 -0
  26. data/lib/command_kit/open_app.rb +69 -0
  27. data/lib/command_kit/options/option.rb +41 -27
  28. data/lib/command_kit/options/option_value.rb +3 -2
  29. data/lib/command_kit/options/parser.rb +17 -22
  30. data/lib/command_kit/options.rb +102 -14
  31. data/lib/command_kit/os/linux.rb +157 -0
  32. data/lib/command_kit/os.rb +159 -11
  33. data/lib/command_kit/package_manager.rb +200 -0
  34. data/lib/command_kit/pager.rb +46 -4
  35. data/lib/command_kit/printing/indent.rb +4 -4
  36. data/lib/command_kit/printing.rb +14 -3
  37. data/lib/command_kit/program_name.rb +9 -0
  38. data/lib/command_kit/sudo.rb +40 -0
  39. data/lib/command_kit/terminal.rb +5 -0
  40. data/lib/command_kit/version.rb +1 -1
  41. data/spec/arguments/argument_spec.rb +1 -1
  42. data/spec/arguments_spec.rb +84 -1
  43. data/spec/colors_spec.rb +357 -70
  44. data/spec/command_spec.rb +77 -6
  45. data/spec/commands/auto_load_spec.rb +33 -2
  46. data/spec/commands_spec.rb +101 -29
  47. data/spec/env/path_spec.rb +6 -0
  48. data/spec/exception_handler_spec.rb +1 -1
  49. data/spec/file_utils_spec.rb +59 -0
  50. data/spec/fixtures/template.erb +5 -0
  51. data/spec/help/man_spec.rb +54 -57
  52. data/spec/inflector_spec.rb +70 -8
  53. data/spec/man_spec.rb +46 -0
  54. data/spec/open_app_spec.rb +85 -0
  55. data/spec/options/option_spec.rb +38 -2
  56. data/spec/options/option_value_spec.rb +55 -0
  57. data/spec/options/parser_spec.rb +0 -10
  58. data/spec/options_spec.rb +328 -0
  59. data/spec/os/linux_spec.rb +164 -0
  60. data/spec/os_spec.rb +200 -13
  61. data/spec/package_manager_spec.rb +806 -0
  62. data/spec/pager_spec.rb +71 -6
  63. data/spec/printing/indent_spec.rb +7 -5
  64. data/spec/printing_spec.rb +23 -1
  65. data/spec/program_name_spec.rb +8 -0
  66. data/spec/sudo_spec.rb +51 -0
  67. data/spec/terminal_spec.rb +30 -0
  68. data/spec/usage_spec.rb +1 -1
  69. metadata +23 -4
@@ -8,8 +8,7 @@ require 'command_kit/options'
8
8
  require 'command_kit/examples'
9
9
  require 'command_kit/description'
10
10
  require 'command_kit/exception_handler'
11
-
12
- require 'fileutils'
11
+ require 'command_kit/file_utils'
13
12
 
14
13
  module CommandKit
15
14
  #
@@ -56,7 +55,7 @@ module CommandKit
56
55
  # end
57
56
  # end
58
57
  #
59
- # ### initialize and using ivars
58
+ # ### initialize and using instance variables
60
59
  #
61
60
  # option :verbose, short: '-v', desc: "Increase verbose level" do
62
61
  # @verbose += 1
@@ -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
@@ -81,10 +88,10 @@ module CommandKit
81
88
  #
82
89
  def commands
83
90
  @commands ||= if superclass.kind_of?(ClassMethods)
84
- superclass.commands.dup
85
- else
86
- {}
87
- end
91
+ superclass.commands.dup
92
+ else
93
+ {}
94
+ end
88
95
  end
89
96
 
90
97
  #
@@ -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
@@ -0,0 +1,46 @@
1
+ require 'fileutils'
2
+ require 'erb'
3
+
4
+ module CommandKit
5
+ #
6
+ # File manipulation related methods.
7
+ #
8
+ # @since 0.3.0
9
+ #
10
+ # @api public
11
+ #
12
+ module FileUtils
13
+ include ::FileUtils
14
+
15
+ #
16
+ # Renders an erb file and optionally writes it out to a destination file.
17
+ #
18
+ # @param [String] source
19
+ # The path to the erb template file.
20
+ #
21
+ # @param [String, nil] dest
22
+ # The path to the destination file.
23
+ #
24
+ # @return [String, nil]
25
+ # If no destination path argument is given, the rendered erb template
26
+ # String will be returned.
27
+ #
28
+ # @example Rendering a ERB template and saving it's output:
29
+ # erb File.join(template_dir,'README.md.erb'), 'README.md'
30
+ #
31
+ # @example Rendering a ERB template and capturing it's output:
32
+ # output = erb(File.join(template_dir,'_partial.erb'))
33
+ #
34
+ def erb(source,dest=nil)
35
+ erb = ERB.new(File.read(source), trim_mode: '-')
36
+ result = erb.result(binding)
37
+
38
+ if dest
39
+ File.write(dest,result)
40
+ return
41
+ else
42
+ return result
43
+ end
44
+ end
45
+ end
46
+ end
@@ -3,6 +3,7 @@
3
3
  require 'command_kit/command_name'
4
4
  require 'command_kit/help'
5
5
  require 'command_kit/stdio'
6
+ require 'command_kit/man'
6
7
 
7
8
  module CommandKit
8
9
  module Help
@@ -23,6 +24,7 @@ module CommandKit
23
24
  include CommandName
24
25
  include Help
25
26
  include Stdio
27
+ include CommandKit::Man
26
28
 
27
29
  #
28
30
  # @api private
@@ -68,7 +70,7 @@ module CommandKit
68
70
  #
69
71
  def man_dir(new_man_dir=nil)
70
72
  if new_man_dir
71
- @man_dir = new_man_dir
73
+ @man_dir = File.expand_path(new_man_dir)
72
74
  else
73
75
  @man_dir || if superclass.kind_of?(ClassMethods)
74
76
  superclass.man_dir
@@ -96,29 +98,6 @@ module CommandKit
96
98
  end
97
99
  end
98
100
 
99
- #
100
- # Displays the given man page.
101
- #
102
- # @param [String] page
103
- # The man page file name.
104
- #
105
- # @param [Integer, String, nil] section
106
- # The optional section number to specify.
107
- #
108
- # @return [Boolean, nil]
109
- # Specifies whether the `man` command was successful or not.
110
- # Returns `nil` when the `man` command is not installed.
111
- #
112
- # @api public
113
- #
114
- def man(page, section: nil)
115
- if section
116
- system('man',section.to_s,page.to_s)
117
- else
118
- system('man',page.to_s)
119
- end
120
- end
121
-
122
101
  #
123
102
  # Provides help information by showing one of the man pages within
124
103
  # {ClassMethods#man_dir .man_dir}.
@@ -131,13 +110,13 @@ module CommandKit
131
110
  # Returns `nil` when the `man` command is not installed.
132
111
  #
133
112
  # @raise [NotImplementedError]
134
- # {ClassMethods#man_dir .man_dir} does not have a value.
113
+ # {ClassMethods#man_dir .man_dir} was not set in the class.
135
114
  #
136
115
  # @api semipublic
137
116
  #
138
117
  def help_man(man_page=self.class.man_page)
139
118
  unless self.class.man_dir
140
- raise(NotImplementedError,"#{self.class}.man_dir not set")
119
+ raise(NotImplementedError,"man_dir was not set in #{self.class}")
141
120
  end
142
121
 
143
122
  man_path = File.join(self.class.man_dir,man_page)
@@ -149,22 +128,27 @@ module CommandKit
149
128
  # Displays the {ClassMethods#man_page .man_page} in
150
129
  # {ClassMethods#man_dir .man_dir} instead of the usual `--help` output.
151
130
  #
152
- # @raise [NotImplementedError]
153
- # {ClassMethods#man_dir .man_dir} does not have a value.
154
- #
155
131
  # @note
156
- # if `TERM` is `dumb` or `$stdout` is not a TTY, fallsback to printing
157
- # the usual `--help` output.
132
+ # if `TERM` is `dumb` or `$stdout` is not a TTY, will fall back to
133
+ # printing the usual `--help` output.
158
134
  #
159
135
  # @api public
160
136
  #
161
137
  def help
162
138
  if stdout.tty?
163
- if help_man.nil?
164
- # the `man` command is not installed
139
+ if self.class.man_dir
140
+ status = help_man
141
+
142
+ if status.nil?
143
+ # the `man` command is not installed
144
+ super
145
+ end
146
+ else
147
+ # man_dir was not set
165
148
  super
166
149
  end
167
150
  else
151
+ # stdout is not a TTY
168
152
  super
169
153
  end
170
154
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'strscan'
4
+
3
5
  module CommandKit
4
6
  #
5
7
  # A very simple inflector.
@@ -33,16 +35,37 @@ module CommandKit
33
35
  # @return [String]
34
36
  # The resulting under_scored name.
35
37
  #
38
+ # @raise [ArgumentError]
39
+ # The given string contained non-alpha-numeric characters.
40
+ #
36
41
  def self.underscore(name)
37
- # sourced from: https://github.com/dry-rb/dry-inflector/blob/c918f967ff82611da374eb0847a77b7e012d3fa8/lib/dry/inflector.rb#L286-L287
38
- name = name.to_s.dup
42
+ scanner = StringScanner.new(name.to_s)
43
+ new_string = String.new
44
+
45
+ until scanner.eos?
46
+ if (separator = scanner.scan(/[_-]+/))
47
+ new_string << ('_' * separator.length)
48
+ else
49
+ if (capitalized = scanner.scan(/[A-Z][a-z\d]+/))
50
+ new_string << capitalized
51
+ elsif (uppercase = scanner.scan(/[A-Z][A-Z\d]*(?=[A-Z_-]|$)/))
52
+ new_string << uppercase
53
+ elsif (lowercase = scanner.scan(/[a-z][a-z\d]*/))
54
+ new_string << lowercase
55
+ else
56
+ raise(ArgumentError,"cannot convert string to underscored: #{scanner.string.inspect}")
57
+ end
39
58
 
40
- name.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
41
- name.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
42
- name.tr!('-','_')
43
- name.downcase!
59
+ if (separator = scanner.scan(/[_-]+/))
60
+ new_string << ('_' * separator.length)
61
+ elsif !scanner.eos?
62
+ new_string << '_'
63
+ end
64
+ end
65
+ end
44
66
 
45
- name
67
+ new_string.downcase!
68
+ new_string
46
69
  end
47
70
 
48
71
  #
@@ -67,20 +90,27 @@ module CommandKit
67
90
  # @return [String]
68
91
  # The CamelCased name.
69
92
  #
93
+ # @raise [ArgumentError]
94
+ # The given under_scored string contained non-alpha-numeric characters.
95
+ #
70
96
  def self.camelize(name)
71
- name = name.to_s.dup
72
-
73
- # sourced from: https://github.com/dry-rb/dry-inflector/blob/c918f967ff82611da374eb0847a77b7e012d3fa8/lib/dry/inflector.rb#L329-L334
74
- name.sub!(/^[a-z\d]*/,&:capitalize)
75
- name.gsub!(%r{(?:[_-]|(/))([a-z\d]*)}i) do |match|
76
- slash = Regexp.last_match(1)
77
- word = Regexp.last_match(2)
97
+ scanner = StringScanner.new(name.to_s)
98
+ new_string = String.new
78
99
 
79
- "#{slash}#{word.capitalize}"
100
+ until scanner.eos?
101
+ if (word = scanner.scan(/[A-Za-z\d]+/))
102
+ word.capitalize!
103
+ new_string << word
104
+ elsif scanner.scan(/[_-]+/)
105
+ # skip
106
+ elsif scanner.scan(/\//)
107
+ new_string << '::'
108
+ else
109
+ raise(ArgumentError,"cannot convert string to CamelCase: #{scanner.string.inspect}")
110
+ end
80
111
  end
81
112
 
82
- name.gsub!('/','::')
83
- name
113
+ new_string
84
114
  end
85
115
  end
86
116
  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:
@@ -49,15 +49,13 @@ module CommandKit
49
49
  # @api public
50
50
  #
51
51
  def start(argv=ARGV, **kwargs)
52
- begin
53
- exit main(argv, **kwargs)
54
- rescue Interrupt
55
- # https://tldp.org/LDP/abs/html/exitcodes.html
56
- exit 130
57
- rescue Errno::EPIPE
58
- # STDOUT pipe broken
59
- exit 0
60
- end
52
+ exit main(argv, **kwargs)
53
+ rescue Interrupt
54
+ # https://tldp.org/LDP/abs/html/exitcodes.html
55
+ exit 130
56
+ rescue Errno::EPIPE
57
+ # STDOUT pipe broken
58
+ exit 0
61
59
  end
62
60
 
63
61
  #
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandKit
4
+ #
5
+ # Allows displaying man pages.
6
+ #
7
+ # ## Examples
8
+ #
9
+ # man "passwd"
10
+ # man "passwd", section: 5
11
+ #
12
+ # @since 0.2.0
13
+ #
14
+ module Man
15
+ #
16
+ # Displays the given man page.
17
+ #
18
+ # @param [String] page
19
+ # The man page file name.
20
+ #
21
+ # @param [Integer, String, nil] section
22
+ # The optional section number to specify.
23
+ #
24
+ # @return [Boolean, nil]
25
+ # Specifies whether the `man` command was successful or not.
26
+ # Returns `nil` when the `man` command is not installed.
27
+ #
28
+ # @example
29
+ # man "passwd"
30
+ #
31
+ # @example Display a man-page from a specific section:
32
+ # man "passwd", section: 5
33
+ #
34
+ # @api public
35
+ #
36
+ def man(page, section: nil)
37
+ if section
38
+ system('man',section.to_s,page.to_s)
39
+ else
40
+ system('man',page.to_s)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,69 @@
1
+ require 'command_kit/os'
2
+ require 'command_kit/env/path'
3
+
4
+ module CommandKit
5
+ #
6
+ # Allows opening a file or a URI with the system's preferred application for
7
+ # that file type or URI scheme.
8
+ #
9
+ # ## Examples
10
+ #
11
+ # open_app_for "movie.avi"
12
+ # open_app_for "https://github.com/postmodern/command_kit.rb#readme"
13
+ #
14
+ # @since 0.2.0
15
+ #
16
+ module OpenApp
17
+ include OS
18
+ include Env::Path
19
+
20
+ #
21
+ # Initializes the command and determines which open command to use.
22
+ #
23
+ # @param [Hash{Symbol => Object}] kwargs
24
+ # Additional keyword arguments.
25
+ #
26
+ # @api public
27
+ #
28
+ def initialize(**kwargs)
29
+ super(**kwargs)
30
+
31
+ @open_command = if macos?
32
+ 'open'
33
+ elsif linux? || bsd?
34
+ if command_installed?('xdg-open')
35
+ 'xdg-open'
36
+ end
37
+ elsif windows?
38
+ if command_installed?('invoke-item')
39
+ 'invoke-item'
40
+ else
41
+ 'start'
42
+ end
43
+ end
44
+ end
45
+
46
+ #
47
+ # Opens a file or URI using the system's preferred application for that
48
+ # file type or URI scheme.
49
+ #
50
+ # @param [String, URI] file_or_uri
51
+ # The file path or URI to open.
52
+ #
53
+ # @return [Boolean, nil]
54
+ # Specifies whether the file or URI was successfully opened or not.
55
+ # If the open command could not be determined, `nil` is returned.
56
+ #
57
+ # @example Open a file:
58
+ # open_app_for "movie.avi"
59
+ #
60
+ # @example Open a URI:
61
+ # open_app_for "https://github.com/postmodern/command_kit.rb"
62
+ #
63
+ def open_app_for(file_or_uri)
64
+ if @open_command
65
+ system(@open_command,file_or_uri.to_s)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -35,16 +35,18 @@ module CommandKit
35
35
  # @return [OptionValue, nil]
36
36
  attr_reader :value
37
37
 
38
- # The option's description.
39
- #
40
- # @return [String]
41
- attr_reader :desc
42
-
43
38
  # The optional block that will receive the parsed option value.
44
39
  #
45
40
  # @return [Proc, nil]
46
41
  attr_reader :block
47
42
 
43
+ # The optional category to group the option under.
44
+ #
45
+ # @return [String, nil]
46
+ #
47
+ # @since 0.3.0
48
+ attr_reader :category
49
+
48
50
  #
49
51
  # Initializes the option.
50
52
  #
@@ -70,9 +72,12 @@ module CommandKit
70
72
  # @option value [String, nil] usage
71
73
  # The usage string for the option's value.
72
74
  #
73
- # @param [String] desc
75
+ # @param [String, Array<String>] desc
74
76
  # The description for the option.
75
77
  #
78
+ # @param [String, nil] category
79
+ # The optional category to group the option under.
80
+ #
76
81
  # @yield [(value)]
77
82
  # If a block is given, it will be called when the option is parsed.
78
83
  #
@@ -83,25 +88,27 @@ module CommandKit
83
88
  # The `value` keyword argument was not a `Hash`, `true`, `false`, or
84
89
  # `nil`.
85
90
  #
86
- def initialize(name, short: nil,
87
- long: self.class.default_long_opt(name),
88
- equals: false,
89
- value: nil,
90
- desc: ,
91
+ def initialize(name, short: nil,
92
+ long: self.class.default_long_opt(name),
93
+ equals: false,
94
+ value: nil,
95
+ desc: ,
96
+ category: nil,
91
97
  &block)
92
- @name = name
93
- @short = short
94
- @long = long
95
- @equals = equals
96
- @value = case value
97
- when Hash then OptionValue.new(**value)
98
- when true then OptionValue.new()
99
- when false, nil then nil
100
- else
101
- raise(TypeError,"value: keyword must be Hash, true, false, or nil")
102
- end
103
- @desc = desc
104
- @block = block
98
+ @name = name
99
+ @short = short
100
+ @long = long
101
+ @equals = equals
102
+ @value = case value
103
+ when Hash then OptionValue.new(**value)
104
+ when true then OptionValue.new()
105
+ when false, nil then nil
106
+ else
107
+ raise(TypeError,"value: keyword must be Hash, true, false, or nil")
108
+ end
109
+ @desc = desc
110
+ @category = category
111
+ @block = block
105
112
  end
106
113
 
107
114
  #
@@ -128,7 +135,7 @@ module CommandKit
128
135
  end
129
136
 
130
137
  #
131
- # The separator characer between the option and option value.
138
+ # The separator character between the option and option value.
132
139
  #
133
140
  # @return ['=', ' ', nil]
134
141
  #
@@ -184,7 +191,7 @@ module CommandKit
184
191
  #
185
192
  # The option description.
186
193
  #
187
- # @return [String]
194
+ # @return [String, Array<String>]
188
195
  #
189
196
  # @note
190
197
  # If {#default_value} returns a value, the description will contain the
@@ -192,7 +199,14 @@ module CommandKit
192
199
  #
193
200
  def desc
194
201
  if (value = default_value)
195
- "#{@desc} (Default: #{value})"
202
+ default_text = "(Default: #{value})"
203
+
204
+ case @desc
205
+ when Array
206
+ @desc + [default_text]
207
+ else
208
+ "#{@desc} #{default_text}"
209
+ end
196
210
  else
197
211
  @desc
198
212
  end
@@ -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