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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +15 -0
- data/.rubocop.yml +138 -0
- data/ChangeLog.md +34 -2
- data/Gemfile +3 -0
- data/README.md +135 -214
- data/Rakefile +3 -2
- data/command_kit.gemspec +4 -4
- data/examples/colors.rb +30 -0
- data/examples/command.rb +65 -0
- data/examples/pager.rb +30 -0
- data/gemspec.yml +10 -2
- data/lib/command_kit/arguments/argument.rb +16 -44
- data/lib/command_kit/arguments/argument_value.rb +3 -30
- data/lib/command_kit/arguments.rb +66 -20
- data/lib/command_kit/colors.rb +253 -45
- data/lib/command_kit/command.rb +50 -3
- data/lib/command_kit/command_name.rb +9 -0
- data/lib/command_kit/commands/auto_load/subcommand.rb +3 -0
- data/lib/command_kit/commands/auto_load.rb +16 -0
- data/lib/command_kit/commands/auto_require.rb +16 -0
- data/lib/command_kit/commands/command.rb +3 -0
- data/lib/command_kit/commands/help.rb +2 -0
- data/lib/command_kit/commands/parent_command.rb +7 -0
- data/lib/command_kit/commands/subcommand.rb +15 -0
- data/lib/command_kit/commands.rb +40 -4
- data/lib/command_kit/description.rb +15 -2
- data/lib/command_kit/env/home.rb +9 -0
- data/lib/command_kit/env/path.rb +15 -0
- data/lib/command_kit/env.rb +4 -0
- data/lib/command_kit/examples.rb +15 -2
- data/lib/command_kit/exception_handler.rb +4 -0
- data/lib/command_kit/help/man.rb +74 -47
- data/lib/command_kit/help.rb +10 -1
- data/lib/command_kit/inflector.rb +49 -17
- data/lib/command_kit/interactive.rb +239 -0
- data/lib/command_kit/main.rb +20 -9
- data/lib/command_kit/man.rb +44 -0
- data/lib/command_kit/open_app.rb +69 -0
- data/lib/command_kit/options/option.rb +36 -9
- data/lib/command_kit/options/option_value.rb +42 -3
- data/lib/command_kit/options/parser.rb +44 -17
- data/lib/command_kit/options/quiet.rb +3 -0
- data/lib/command_kit/options/verbose.rb +5 -0
- data/lib/command_kit/options/version.rb +6 -0
- data/lib/command_kit/options.rb +59 -10
- data/lib/command_kit/os/linux.rb +157 -0
- data/lib/command_kit/os.rb +165 -11
- data/lib/command_kit/package_manager.rb +200 -0
- data/lib/command_kit/pager.rb +84 -9
- data/lib/command_kit/printing/indent.rb +25 -2
- data/lib/command_kit/printing.rb +23 -0
- data/lib/command_kit/program_name.rb +7 -0
- data/lib/command_kit/stdio.rb +24 -0
- data/lib/command_kit/sudo.rb +40 -0
- data/lib/command_kit/terminal.rb +159 -0
- data/lib/command_kit/usage.rb +14 -0
- data/lib/command_kit/version.rb +1 -1
- data/lib/command_kit/xdg.rb +21 -1
- data/lib/command_kit.rb +1 -0
- data/spec/arguments/argument_spec.rb +5 -41
- data/spec/arguments/argument_value_spec.rb +1 -61
- data/spec/arguments_spec.rb +8 -25
- data/spec/colors_spec.rb +277 -13
- data/spec/command_name_spec.rb +1 -1
- data/spec/command_spec.rb +4 -1
- data/spec/commands/auto_load/subcommand_spec.rb +1 -1
- data/spec/commands/auto_load_spec.rb +1 -1
- data/spec/commands/auto_require_spec.rb +2 -2
- data/spec/commands/help_spec.rb +1 -1
- data/spec/commands/parent_command_spec.rb +1 -1
- data/spec/commands/subcommand_spec.rb +1 -1
- data/spec/commands_spec.rb +2 -2
- data/spec/description_spec.rb +1 -25
- data/spec/env/home_spec.rb +1 -1
- data/spec/env/path_spec.rb +1 -1
- data/spec/examples_spec.rb +1 -25
- data/spec/exception_handler_spec.rb +1 -1
- data/spec/help/man_spec.rb +316 -0
- data/spec/help_spec.rb +0 -25
- data/spec/inflector_spec.rb +71 -9
- data/spec/interactive_spec.rb +415 -0
- data/spec/main_spec.rb +7 -7
- data/spec/man_spec.rb +46 -0
- data/spec/open_app_spec.rb +85 -0
- data/spec/options/option_spec.rb +48 -9
- data/spec/options/option_value_spec.rb +53 -4
- data/spec/options_spec.rb +1 -1
- data/spec/os/linux_spec.rb +154 -0
- data/spec/os_spec.rb +201 -14
- data/spec/package_manager_spec.rb +806 -0
- data/spec/pager_spec.rb +78 -15
- data/spec/printing/indent_spec.rb +1 -1
- data/spec/printing_spec.rb +10 -2
- data/spec/program_name_spec.rb +1 -1
- data/spec/spec_helper.rb +0 -3
- data/spec/sudo_spec.rb +51 -0
- data/spec/{console_spec.rb → terminal_spec.rb} +65 -35
- data/spec/usage_spec.rb +2 -2
- data/spec/xdg_spec.rb +1 -1
- metadata +32 -13
- data/lib/command_kit/arguments/usage.rb +0 -6
- data/lib/command_kit/console.rb +0 -141
- data/lib/command_kit/options/usage.rb +0 -6
data/lib/command_kit/env/path.rb
CHANGED
@@ -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
|
data/lib/command_kit/env.rb
CHANGED
@@ -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
|
|
data/lib/command_kit/examples.rb
CHANGED
@@ -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 ||
|
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
|
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)
|
data/lib/command_kit/help/man.rb
CHANGED
@@ -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
|
-
# ##
|
13
|
+
# ## Examples
|
14
|
+
#
|
15
|
+
# class Foo < CommandKit::Command
|
16
|
+
#
|
17
|
+
# include CommandKit::Help::Man
|
9
18
|
#
|
10
|
-
#
|
11
|
-
#
|
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
|
-
#
|
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
|
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 =
|
73
|
+
@man_dir = new_man_dir
|
54
74
|
else
|
55
|
-
@man_dir ||
|
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
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
-
#
|
102
|
+
# Provides help information by showing one of the man pages within
|
103
|
+
# {ClassMethods#man_dir .man_dir}.
|
86
104
|
#
|
87
|
-
# @param [String]
|
88
|
-
# The man page
|
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
|
-
|
95
|
-
|
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}
|
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,
|
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
|
110
|
-
|
111
|
-
|
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
|
data/lib/command_kit/help.rb
CHANGED
@@ -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
|
-
|
36
|
-
|
42
|
+
scanner = StringScanner.new(name.to_s)
|
43
|
+
new_string = String.new
|
37
44
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|