command_kit 0.1.0.pre1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +15 -0
  3. data/.rubocop.yml +138 -0
  4. data/ChangeLog.md +34 -2
  5. data/Gemfile +3 -0
  6. data/README.md +135 -214
  7. data/Rakefile +3 -2
  8. data/command_kit.gemspec +4 -4
  9. data/examples/colors.rb +30 -0
  10. data/examples/command.rb +65 -0
  11. data/examples/pager.rb +30 -0
  12. data/gemspec.yml +10 -2
  13. data/lib/command_kit/arguments/argument.rb +16 -44
  14. data/lib/command_kit/arguments/argument_value.rb +3 -30
  15. data/lib/command_kit/arguments.rb +66 -20
  16. data/lib/command_kit/colors.rb +253 -45
  17. data/lib/command_kit/command.rb +50 -3
  18. data/lib/command_kit/command_name.rb +9 -0
  19. data/lib/command_kit/commands/auto_load/subcommand.rb +3 -0
  20. data/lib/command_kit/commands/auto_load.rb +16 -0
  21. data/lib/command_kit/commands/auto_require.rb +16 -0
  22. data/lib/command_kit/commands/command.rb +3 -0
  23. data/lib/command_kit/commands/help.rb +2 -0
  24. data/lib/command_kit/commands/parent_command.rb +7 -0
  25. data/lib/command_kit/commands/subcommand.rb +15 -0
  26. data/lib/command_kit/commands.rb +40 -4
  27. data/lib/command_kit/description.rb +15 -2
  28. data/lib/command_kit/env/home.rb +9 -0
  29. data/lib/command_kit/env/path.rb +15 -0
  30. data/lib/command_kit/env.rb +4 -0
  31. data/lib/command_kit/examples.rb +15 -2
  32. data/lib/command_kit/exception_handler.rb +4 -0
  33. data/lib/command_kit/help/man.rb +74 -47
  34. data/lib/command_kit/help.rb +10 -1
  35. data/lib/command_kit/inflector.rb +49 -17
  36. data/lib/command_kit/interactive.rb +239 -0
  37. data/lib/command_kit/main.rb +20 -9
  38. data/lib/command_kit/man.rb +44 -0
  39. data/lib/command_kit/open_app.rb +69 -0
  40. data/lib/command_kit/options/option.rb +36 -9
  41. data/lib/command_kit/options/option_value.rb +42 -3
  42. data/lib/command_kit/options/parser.rb +44 -17
  43. data/lib/command_kit/options/quiet.rb +3 -0
  44. data/lib/command_kit/options/verbose.rb +5 -0
  45. data/lib/command_kit/options/version.rb +6 -0
  46. data/lib/command_kit/options.rb +59 -10
  47. data/lib/command_kit/os/linux.rb +157 -0
  48. data/lib/command_kit/os.rb +165 -11
  49. data/lib/command_kit/package_manager.rb +200 -0
  50. data/lib/command_kit/pager.rb +84 -9
  51. data/lib/command_kit/printing/indent.rb +25 -2
  52. data/lib/command_kit/printing.rb +23 -0
  53. data/lib/command_kit/program_name.rb +7 -0
  54. data/lib/command_kit/stdio.rb +24 -0
  55. data/lib/command_kit/sudo.rb +40 -0
  56. data/lib/command_kit/terminal.rb +159 -0
  57. data/lib/command_kit/usage.rb +14 -0
  58. data/lib/command_kit/version.rb +1 -1
  59. data/lib/command_kit/xdg.rb +21 -1
  60. data/lib/command_kit.rb +1 -0
  61. data/spec/arguments/argument_spec.rb +5 -41
  62. data/spec/arguments/argument_value_spec.rb +1 -61
  63. data/spec/arguments_spec.rb +8 -25
  64. data/spec/colors_spec.rb +277 -13
  65. data/spec/command_name_spec.rb +1 -1
  66. data/spec/command_spec.rb +4 -1
  67. data/spec/commands/auto_load/subcommand_spec.rb +1 -1
  68. data/spec/commands/auto_load_spec.rb +1 -1
  69. data/spec/commands/auto_require_spec.rb +2 -2
  70. data/spec/commands/help_spec.rb +1 -1
  71. data/spec/commands/parent_command_spec.rb +1 -1
  72. data/spec/commands/subcommand_spec.rb +1 -1
  73. data/spec/commands_spec.rb +2 -2
  74. data/spec/description_spec.rb +1 -25
  75. data/spec/env/home_spec.rb +1 -1
  76. data/spec/env/path_spec.rb +1 -1
  77. data/spec/examples_spec.rb +1 -25
  78. data/spec/exception_handler_spec.rb +1 -1
  79. data/spec/help/man_spec.rb +316 -0
  80. data/spec/help_spec.rb +0 -25
  81. data/spec/inflector_spec.rb +71 -9
  82. data/spec/interactive_spec.rb +415 -0
  83. data/spec/main_spec.rb +7 -7
  84. data/spec/man_spec.rb +46 -0
  85. data/spec/open_app_spec.rb +85 -0
  86. data/spec/options/option_spec.rb +48 -9
  87. data/spec/options/option_value_spec.rb +53 -4
  88. data/spec/options_spec.rb +1 -1
  89. data/spec/os/linux_spec.rb +154 -0
  90. data/spec/os_spec.rb +201 -14
  91. data/spec/package_manager_spec.rb +806 -0
  92. data/spec/pager_spec.rb +78 -15
  93. data/spec/printing/indent_spec.rb +1 -1
  94. data/spec/printing_spec.rb +10 -2
  95. data/spec/program_name_spec.rb +1 -1
  96. data/spec/spec_helper.rb +0 -3
  97. data/spec/sudo_spec.rb +51 -0
  98. data/spec/{console_spec.rb → terminal_spec.rb} +65 -35
  99. data/spec/usage_spec.rb +2 -2
  100. data/spec/xdg_spec.rb +1 -1
  101. metadata +32 -13
  102. data/lib/command_kit/arguments/usage.rb +0 -6
  103. data/lib/command_kit/console.rb +0 -141
  104. data/lib/command_kit/options/usage.rb +0 -6
@@ -17,6 +17,8 @@ module CommandKit
17
17
  # The home directory.
18
18
  #
19
19
  # @return [String]
20
+ #
21
+ # @api semipublic
20
22
  attr_reader :path_dirs
21
23
 
22
24
  #
@@ -25,6 +27,8 @@ module CommandKit
25
27
  # @param [Hash{Symbol => Object}] kwargs
26
28
  # Additional keyword arguments.
27
29
  #
30
+ # @api public
31
+ #
28
32
  def initialize(**kwargs)
29
33
  super(**kwargs)
30
34
 
@@ -41,6 +45,8 @@ module CommandKit
41
45
  # The absolute path to the executable file, or `nil` if the command
42
46
  # could not be found in any of the {#path_dirs}.
43
47
  #
48
+ # @api public
49
+ #
44
50
  def find_command(name)
45
51
  name = name.to_s
46
52
 
@@ -63,6 +69,15 @@ module CommandKit
63
69
  # Specifies whether a command with the given name exists in one of the
64
70
  # {#path_dirs}.
65
71
  #
72
+ # @example
73
+ # if command_installed?("docker")
74
+ # # ...
75
+ # else
76
+ # abort "Docker is not installed. Aborting"
77
+ # end
78
+ #
79
+ # @api public
80
+ #
66
81
  def command_installed?(name)
67
82
  !find_command(name).nil?
68
83
  end
@@ -23,6 +23,8 @@ module CommandKit
23
23
  # The environment variables hash.
24
24
  #
25
25
  # @return [Hash{String => String}]
26
+ #
27
+ # @api public
26
28
  attr_reader :env
27
29
 
28
30
  #
@@ -34,6 +36,8 @@ module CommandKit
34
36
  # @param [Hash{Symbol => Object}] kwargs
35
37
  # Additional keyword arguments.
36
38
  #
39
+ # @api public
40
+ #
37
41
  def initialize(env: ENV, **kwargs)
38
42
  @env = env
39
43
 
@@ -17,6 +17,9 @@ module CommandKit
17
17
  include Help
18
18
  include CommandName
19
19
 
20
+ #
21
+ # @api private
22
+ #
20
23
  module ModuleMethods
21
24
  #
22
25
  # Extends {ClassMethods} or {ModuleMethods}, depending on whether
@@ -57,11 +60,15 @@ module CommandKit
57
60
  # "-o output.txt path/to/file"
58
61
  # ]
59
62
  #
63
+ # @api public
64
+ #
60
65
  def examples(new_examples=nil)
61
66
  if new_examples
62
67
  @examples = Array(new_examples)
63
68
  else
64
- @examples || (superclass.examples if superclass.kind_of?(ClassMethods))
69
+ @examples || if superclass.kind_of?(ClassMethods)
70
+ superclass.examples
71
+ end
65
72
  end
66
73
  end
67
74
  end
@@ -69,6 +76,8 @@ module CommandKit
69
76
  #
70
77
  # @see ClassMethods#examples
71
78
  #
79
+ # @api semipublic
80
+ #
72
81
  def examples
73
82
  self.class.examples
74
83
  end
@@ -76,6 +85,8 @@ module CommandKit
76
85
  #
77
86
  # Prints the command class'es example commands.
78
87
  #
88
+ # @api semipublic
89
+ #
79
90
  def help_examples
80
91
  if (examples = self.examples)
81
92
  puts
@@ -90,8 +101,10 @@ module CommandKit
90
101
  # Calls the superclass'es `#help` method, if it's defined, then calls
91
102
  # {#help_examples}.
92
103
  #
104
+ # @api public
105
+ #
93
106
  def help
94
- super if defined?(super)
107
+ super
95
108
 
96
109
  help_examples
97
110
  end
@@ -33,6 +33,8 @@ module CommandKit
33
33
  # @return [Integer]
34
34
  # The exit status of the command.
35
35
  #
36
+ # @api public
37
+ #
36
38
  def main(argv=[])
37
39
  super(argv)
38
40
  rescue Interrupt, Errno::EPIPE => error
@@ -47,6 +49,8 @@ module CommandKit
47
49
  # @param [Exception] error
48
50
  # The raised exception.
49
51
  #
52
+ # @api semipublic
53
+ #
50
54
  def on_exception(error)
51
55
  print_exception(error)
52
56
  exit(1)
@@ -1,16 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'command_kit/command_name'
4
+ require 'command_kit/help'
5
+ require 'command_kit/stdio'
6
+ require 'command_kit/man'
7
+
3
8
  module CommandKit
4
9
  module Help
5
10
  #
6
11
  # Allows displaying a man-page instead of the usual `--help` output.
7
12
  #
8
- # ## Environment Variables
13
+ # ## Examples
14
+ #
15
+ # class Foo < CommandKit::Command
16
+ #
17
+ # include CommandKit::Help::Man
9
18
  #
10
- # * `TERM` - Specifies the type of terminal. When set to `DUMB`, it will
11
- # disable man-page help output.
19
+ # man_dir "#{__dir__}/../../man"
20
+ #
21
+ # end
12
22
  #
13
23
  module Man
24
+ include CommandName
25
+ include Help
26
+ include Stdio
27
+ include CommandKit::Man
28
+
29
+ #
30
+ # @api private
31
+ #
14
32
  module ModuleMethods
15
33
  #
16
34
  # Extends {ClassMethods} or {ModuleMethods}, depending on whether
@@ -33,7 +51,7 @@ module CommandKit
33
51
  extend ModuleMethods
34
52
 
35
53
  #
36
- # Defines class-level methods.
54
+ # Class-level methods.
37
55
  #
38
56
  module ClassMethods
39
57
  #
@@ -46,74 +64,83 @@ module CommandKit
46
64
  # The class'es or superclass'es man-page directory.
47
65
  #
48
66
  # @example
49
- # man_dir File.expand_path('../../../man',__FILE__)
67
+ # man_dir "#{__dir__}/../../man"
68
+ #
69
+ # @api public
50
70
  #
51
71
  def man_dir(new_man_dir=nil)
52
72
  if new_man_dir
53
- @man_dir = File.expand_path(new_man_dir)
73
+ @man_dir = new_man_dir
54
74
  else
55
- @man_dir || (superclass.man_dir if superclass.kind_of?(ClassMethods))
75
+ @man_dir || if superclass.kind_of?(ClassMethods)
76
+ superclass.man_dir
77
+ end
56
78
  end
57
79
  end
58
- end
59
80
 
60
- #
61
- # Determines if displaying man pages is supported.
62
- #
63
- # @return [Boolean]
64
- # Indicates whether the `TERM` environment variable is not `dumb`
65
- # and `$stdout` is a TTY.
66
- #
67
- def self.supported?
68
- ENV['TERM'] != 'dumb' && $stdout.tty?
69
- end
70
-
71
- #
72
- # Returns the man-page file name for the given command name.
73
- #
74
- # @param [String] command
75
- # The given command name.
76
- #
77
- # @return [String]
78
- # The man-page file name.
79
- #
80
- def man_page(command=command_name)
81
- "#{command}.1"
81
+ #
82
+ # Gets or sets the class'es man-page file name.
83
+ #
84
+ # @param [String, nil] new_man_page
85
+ # If a String is given, the class'es man-page file name will be set.
86
+ #
87
+ # @return [String]
88
+ # The class'es or superclass'es man-page file name.
89
+ #
90
+ # @api public
91
+ #
92
+ def man_page(new_man_page=nil)
93
+ if new_man_page
94
+ @man_page = new_man_page
95
+ else
96
+ @man_page || "#{command_name}.1"
97
+ end
98
+ end
82
99
  end
83
100
 
84
101
  #
85
- # Displays the given man page.
102
+ # Provides help information by showing one of the man pages within
103
+ # {ClassMethods#man_dir .man_dir}.
86
104
  #
87
- # @param [String] page
88
- # The man page file name.
105
+ # @param [String] man_page
106
+ # The file name of the man page to display.
89
107
  #
90
108
  # @return [Boolean, nil]
91
109
  # Specifies whether the `man` command was successful or not.
92
110
  # Returns `nil` when the `man` command is not installed.
93
111
  #
94
- def man(page=man_page)
95
- system('man',page)
112
+ # @raise [NotImplementedError]
113
+ # {ClassMethods#man_dir .man_dir} does not have a value.
114
+ #
115
+ # @api semipublic
116
+ #
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
+ man_path = File.join(self.class.man_dir,man_page)
123
+
124
+ man(man_path)
96
125
  end
97
126
 
98
127
  #
99
- # Displays the {#man_page} instead of the usual `--help` output.
128
+ # Displays the {ClassMethods#man_page .man_page} in
129
+ # {ClassMethods#man_dir .man_dir} instead of the usual `--help` output.
100
130
  #
101
131
  # @raise [NotImplementedError]
102
- # {ClassMethods#man_dir man_dir} does not have a value.
132
+ # {ClassMethods#man_dir .man_dir} does not have a value.
103
133
  #
104
134
  # @note
105
- # if `TERM` is `dumb` or `$stdout` is not a TTY, fallsback to printing
106
- # the usual `--help` output.
135
+ # if `TERM` is `dumb` or `$stdout` is not a TTY, will fall back to
136
+ # printing the usual `--help` output.
137
+ #
138
+ # @api public
107
139
  #
108
140
  def help
109
- if Man.supported?
110
- unless self.class.man_dir
111
- raise(NotImplementedError,"#{self.class}.man_dir not set")
112
- end
113
-
114
- man_path = File.join(self.class.man_dir,man_page)
115
-
116
- if man(man_path).nil?
141
+ if stdout.tty?
142
+ if help_man.nil?
143
+ # the `man` command is not installed
117
144
  super
118
145
  end
119
146
  else
@@ -15,6 +15,9 @@ module CommandKit
15
15
  # MyCmd.help
16
16
  #
17
17
  module Help
18
+ #
19
+ # @api private
20
+ #
18
21
  module ModuleMethods
19
22
  #
20
23
  # Extends {ClassMethods} or {ModuleMethods}, depending on whether {Help}
@@ -36,6 +39,9 @@ module CommandKit
36
39
 
37
40
  extend ModuleMethods
38
41
 
42
+ #
43
+ # Class-level methods.
44
+ #
39
45
  module ClassMethods
40
46
  #
41
47
  # Prints `--help` information.
@@ -45,6 +51,8 @@ module CommandKit
45
51
  #
46
52
  # @see Help#help
47
53
  #
54
+ # @api public
55
+ #
48
56
  def help(**kwargs)
49
57
  new(**kwargs).help
50
58
  end
@@ -55,8 +63,9 @@ module CommandKit
55
63
  #
56
64
  # @abstract
57
65
  #
66
+ # @api public
67
+ #
58
68
  def help
59
- super if defined?(super)
60
69
  end
61
70
  end
62
71
  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.
@@ -8,6 +10,8 @@ module CommandKit
8
10
  # If you need something more powerful, checkout
9
11
  # [dry-inflector](https://dry-rb.org/gems/dry-inflector/0.1/)
10
12
  #
13
+ # @api semipublic
14
+ #
11
15
  module Inflector
12
16
  #
13
17
  # Removes the namespace from a constant name.
@@ -31,16 +35,37 @@ module CommandKit
31
35
  # @return [String]
32
36
  # The resulting under_scored name.
33
37
  #
38
+ # @raise [ArgumentError]
39
+ # The given string contained non-alpha-numeric characters.
40
+ #
34
41
  def self.underscore(name)
35
- # sourced from: https://github.com/dry-rb/dry-inflector/blob/c918f967ff82611da374eb0847a77b7e012d3fa8/lib/dry/inflector.rb#L286-L287
36
- name = name.to_s.dup
42
+ scanner = StringScanner.new(name.to_s)
43
+ new_string = String.new
37
44
 
38
- name.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
39
- name.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
40
- name.tr!('-','_')
41
- name.downcase!
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
42
58
 
43
- name
59
+ if (separator = scanner.scan(/[_-]+/))
60
+ new_string << '_' * separator.length
61
+ elsif !scanner.eos?
62
+ new_string << '_'
63
+ end
64
+ end
65
+ end
66
+
67
+ new_string.downcase!
68
+ new_string
44
69
  end
45
70
 
46
71
  #
@@ -65,20 +90,27 @@ module CommandKit
65
90
  # @return [String]
66
91
  # The CamelCased name.
67
92
  #
93
+ # @raise [ArgumentError]
94
+ # The given under_scored string contained non-alpha-numeric characters.
95
+ #
68
96
  def self.camelize(name)
69
- name = name.to_s.dup
70
-
71
- # sourced from: https://github.com/dry-rb/dry-inflector/blob/c918f967ff82611da374eb0847a77b7e012d3fa8/lib/dry/inflector.rb#L329-L334
72
- name.sub!(/^[a-z\d]*/,&:capitalize)
73
- name.gsub!(%r{(?:[_-]|(/))([a-z\d]*)}i) do |match|
74
- slash = Regexp.last_match(1)
75
- word = Regexp.last_match(2)
97
+ scanner = StringScanner.new(name.to_s)
98
+ new_string = String.new
76
99
 
77
- "#{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
78
111
  end
79
112
 
80
- name.gsub!('/','::')
81
- name
113
+ new_string
82
114
  end
83
115
  end
84
116
  end
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'command_kit/stdio'
4
+
5
+ module CommandKit
6
+ #
7
+ # Provides methods for asking the user for input.
8
+ #
9
+ # ## Examples
10
+ #
11
+ # first_name = ask("First name")
12
+ # last_name = ask("Last name")
13
+ #
14
+ # ### Asking for secret input
15
+ #
16
+ # password = ask_secret("Password")
17
+ #
18
+ # ### Asking Y/N?
19
+ #
20
+ # if ask_yes_or_no("Proceed anyways?")
21
+ # # ...
22
+ # else
23
+ # stderr.puts "Aborting!"
24
+ # end
25
+ #
26
+ # ### Asking multi-choice questions
27
+ #
28
+ # ask_multiple_choice("Select a flavor", %w[Apple Orange Lemon Lime])
29
+ # # 1) Apple
30
+ # # 2) Orange
31
+ # # 3) Lemon
32
+ # # 4) Lime
33
+ # # Select a flavor: 4
34
+ # #
35
+ # # => "Lime"
36
+ #
37
+ module Interactive
38
+ include Stdio
39
+
40
+ #
41
+ # Asks the user for input.
42
+ #
43
+ # @param [String] prompt
44
+ # The prompt that will be printed before reading input.
45
+ #
46
+ # @param [String, nil] default
47
+ # The default value to return if no input is given.
48
+ #
49
+ # @param [Boolean] required
50
+ # Requires non-empty input.
51
+ #
52
+ # @return [String]
53
+ # The user input.
54
+ #
55
+ # @example
56
+ # first_name = ask("First name")
57
+ # last_name = ask("Last name")
58
+ #
59
+ # @example Default value:
60
+ # ask("Country", default: "EU")
61
+ # # Country [EU]: <enter>
62
+ # # => "EU"
63
+ #
64
+ # @example Required non-empty input:
65
+ # ask("Email", required: true)
66
+ # # Email: <enter>
67
+ # # Email: bob@example.com<enter>
68
+ # # => "bob@example.com"
69
+ #
70
+ # @api public
71
+ #
72
+ def ask(prompt, default: nil, required: false)
73
+ prompt = prompt.chomp
74
+ prompt << " [#{default}]" if default
75
+ prompt << ": "
76
+
77
+ stdout.print(prompt)
78
+
79
+ loop do
80
+ value = stdin.gets
81
+ value ||= '' # convert nil values (ctrl^D) to an empty String
82
+
83
+ if value.empty?
84
+ if required
85
+ next
86
+ else
87
+ return (default || value)
88
+ end
89
+ else
90
+ return value
91
+ end
92
+ end
93
+ end
94
+
95
+ #
96
+ # Asks the user a yes or no question.
97
+ #
98
+ # @param [String] prompt
99
+ # The prompt that will be printed before reading input.
100
+ #
101
+ # @param [true, false, nil] default
102
+ #
103
+ # @return [Boolean]
104
+ # Specifies whether the user entered Y/yes.
105
+ #
106
+ # @example
107
+ # ask_yes_or_no("Proceed anyways?")
108
+ # # Proceed anyways? (Y/N): Y
109
+ # # => true
110
+ #
111
+ # @example Default value:
112
+ # ask_yes_or_no("Proceed anyways?", default: true)
113
+ # # Proceed anyways? (Y/N) [Y]: <enter>
114
+ # # => true
115
+ #
116
+ # @api public
117
+ #
118
+ def ask_yes_or_no(prompt, default: nil, **kwargs)
119
+ default = case default
120
+ when true then 'Y'
121
+ when false then 'N'
122
+ when nil then nil
123
+ else
124
+ raise(ArgumentError,"invalid default: #{default.inspect}")
125
+ end
126
+
127
+ prompt = "#{prompt} (Y/N)"
128
+
129
+ loop do
130
+ answer = ask(prompt, **kwargs, default: default)
131
+
132
+ case answer.downcase
133
+ when 'y', 'yes'
134
+ return true
135
+ else
136
+ return false
137
+ end
138
+ end
139
+ end
140
+
141
+ #
142
+ # Asks the user to select a choice from a list of options.
143
+ #
144
+ # @param [String] prompt
145
+ # The prompt that will be printed before reading input.
146
+ #
147
+ # @param [Hash{String => String}, Array<String>] choices
148
+ # The choices to select from.
149
+ #
150
+ # @param [Hash{Symbol => Object}] kwargs
151
+ # Additional keyword arguments for {#ask}.
152
+ #
153
+ # @option kwargs [String, nil] default
154
+ # The default option to fallback to, if no input is given.
155
+ #
156
+ # @option kwargs [Boolean] required
157
+ # Requires non-empty input.
158
+ #
159
+ # @return [String]
160
+ # The selected choice.
161
+ #
162
+ # @example Array of choices:
163
+ # ask_multiple_choice("Select a flavor", %w[Apple Orange Lemon Lime])
164
+ # # 1) Apple
165
+ # # 2) Orange
166
+ # # 3) Lemon
167
+ # # 4) Lime
168
+ # # Select a flavor: 4
169
+ # #
170
+ # # => "Lime"
171
+ #
172
+ # @example Hash of choices:
173
+ # ask_multiple_choice("Select an option", {'A' => 'Foo',
174
+ # 'B' => 'Bar',
175
+ # 'X' => 'All of the above'})
176
+ # # A) Foo
177
+ # # B) Bar
178
+ # # X) All of the above
179
+ # # Select an option: X
180
+ # #
181
+ # # => "All of the above"
182
+ #
183
+ # @api public
184
+ #
185
+ def ask_multiple_choice(prompt,choices,**kwargs)
186
+ choices = case choices
187
+ when Array
188
+ Hash[choices.each_with_index.map { |value,i|
189
+ [(i+1).to_s, value]
190
+ }]
191
+ when Hash
192
+ choices
193
+ else
194
+ raise(TypeError,"unsupported choices class #{choices.class}: #{choices.inspect}")
195
+ end
196
+
197
+ prompt = "#{prompt} (#{choices.keys.join(', ')})"
198
+
199
+ loop do
200
+ # print the choices
201
+ choices.each do |choice,value|
202
+ stdout.puts " #{choice}) #{value}"
203
+ end
204
+ stdout.puts
205
+
206
+ # read the choice
207
+ choice = ask(prompt,**kwargs)
208
+
209
+ if choices.has_key?(choice)
210
+ # if a valid choice is given, return the value
211
+ return choices[choice]
212
+ else
213
+ stderr.puts "Invalid selection: #{choice}"
214
+ end
215
+ end
216
+ end
217
+
218
+ #
219
+ # Asks the user for secret input.
220
+ #
221
+ # @example
222
+ # ask_secret("Password")
223
+ # # Password:
224
+ # # => "s3cr3t"
225
+ #
226
+ # @api public
227
+ #
228
+ def ask_secret(prompt, required: true)
229
+ if stdin.respond_to?(:noecho)
230
+ stdin.noecho do
231
+ ask(prompt, required: required)
232
+ end
233
+ else
234
+ ask(prompt, required: required)
235
+ end
236
+ end
237
+
238
+ end
239
+ end