command_kit 0.1.0.pre1 → 0.2.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 (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