clin 0.3.0 → 0.4.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/.lint-ci.yml +2 -0
- data/.simplecov +5 -0
- data/.travis.yml +8 -0
- data/CHANGELOG.md +11 -0
- data/README.md +5 -4
- data/benchmarks/bench.rb +21 -0
- data/benchmarks/text_bench.rb +78 -0
- data/clin.gemspec +2 -1
- data/examples/reusable_options.rb +19 -0
- data/examples/simple.rb +8 -3
- data/examples/test.rb +5 -5
- data/examples/text_builder.rb +40 -0
- data/lib/clin/argument.rb +19 -2
- data/lib/clin/command_mixin/core.rb +13 -18
- data/lib/clin/command_mixin/options.rb +37 -26
- data/lib/clin/command_parser.rb +46 -57
- data/lib/clin/common/help_options.rb +1 -0
- data/lib/clin/errors.rb +50 -4
- data/lib/clin/line_reader/basic.rb +38 -0
- data/lib/clin/line_reader/readline.rb +53 -0
- data/lib/clin/line_reader.rb +16 -0
- data/lib/clin/option.rb +24 -11
- data/lib/clin/option_parser.rb +159 -0
- data/lib/clin/shell.rb +36 -15
- data/lib/clin/shell_interaction/choose.rb +19 -11
- data/lib/clin/shell_interaction/file_conflict.rb +4 -1
- data/lib/clin/shell_interaction/select.rb +44 -0
- data/lib/clin/shell_interaction.rb +1 -0
- data/lib/clin/text/table.rb +270 -0
- data/lib/clin/text.rb +152 -0
- data/lib/clin/version.rb +1 -1
- data/lib/clin.rb +10 -1
- data/spec/clin/command_dispacher_spec.rb +1 -1
- data/spec/clin/command_mixin/options_spec.rb +38 -15
- data/spec/clin/command_parser_spec.rb +27 -51
- data/spec/clin/line_reader/basic_spec.rb +54 -0
- data/spec/clin/line_reader/readline_spec.rb +64 -0
- data/spec/clin/line_reader_spec.rb +17 -0
- data/spec/clin/option_parser_spec.rb +217 -0
- data/spec/clin/option_spec.rb +5 -7
- data/spec/clin/shell_interaction/choose_spec.rb +30 -0
- data/spec/clin/shell_interaction/file_interaction_spec.rb +18 -0
- data/spec/clin/shell_interaction/select_spec.rb +96 -0
- data/spec/clin/shell_spec.rb +42 -0
- data/spec/clin/text/table_cell_spec.rb +72 -0
- data/spec/clin/text/table_row_spec.rb +74 -0
- data/spec/clin/text/table_separator_row_spec.rb +82 -0
- data/spec/clin/text/table_spec.rb +259 -0
- data/spec/clin/text_spec.rb +158 -0
- data/spec/examples/list_option_spec.rb +6 -2
- data/spec/examples/reusable_options_spec.rb +21 -0
- data/spec/examples/simple_spec.rb +9 -9
- data/spec/spec_helper.rb +3 -2
- metadata +54 -3
data/lib/clin/errors.rb
CHANGED
@@ -4,16 +4,32 @@ module Clin
|
|
4
4
|
Error = Class.new(RuntimeError)
|
5
5
|
|
6
6
|
# Error cause by the user input(when parsing command)
|
7
|
-
CommandLineError
|
7
|
+
class CommandLineError < Error
|
8
|
+
def self.severity(value = @severity)
|
9
|
+
@severity = value
|
10
|
+
@severity ||= 1
|
11
|
+
end
|
12
|
+
end
|
8
13
|
|
9
14
|
# Error when the help needs to be shown
|
10
|
-
HelpError
|
15
|
+
class HelpError < CommandLineError
|
16
|
+
def initialize(command)
|
17
|
+
if command.class == Class && command < Clin::Command
|
18
|
+
super(command.help)
|
19
|
+
@command = command
|
20
|
+
else
|
21
|
+
super(command)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
11
25
|
|
12
26
|
# Error when an positional argument is wrong
|
13
27
|
ArgumentError = Class.new(CommandLineError)
|
14
28
|
|
15
29
|
# Error when a fixed argument is not matched
|
16
|
-
class
|
30
|
+
class RequiredArgumentError < ArgumentError
|
31
|
+
severity 100
|
32
|
+
|
17
33
|
# Create a new FixedArgumentError
|
18
34
|
# @param argument [String] Name of the fixed argument
|
19
35
|
# @param got [String] What argument was in place of the fixed argument
|
@@ -32,5 +48,35 @@ module Clin
|
|
32
48
|
end
|
33
49
|
|
34
50
|
# Error when a option is wrong
|
35
|
-
OptionError
|
51
|
+
class OptionError < CommandLineError
|
52
|
+
def initialize(message, option)
|
53
|
+
super(message)
|
54
|
+
@option = option
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Error when undefined options are found in argv
|
59
|
+
class UnknownOptionError < OptionError
|
60
|
+
def initialize(option)
|
61
|
+
message = "Unknown option #{option}"
|
62
|
+
super(message, option)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Error when a flag option has an unexpected argument
|
67
|
+
class OptionUnexpectedArgumentError < OptionError
|
68
|
+
def initialize(option, value)
|
69
|
+
@value = value
|
70
|
+
message = "Unexpected argument '#{value}' for option #{option}"
|
71
|
+
super message, option
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# When a option is missing it's required argument
|
76
|
+
class MissingOptionArgumentError < OptionError
|
77
|
+
def initialize(option)
|
78
|
+
message = "Missing argument for option #{option}"
|
79
|
+
super(message, option)
|
80
|
+
end
|
81
|
+
end
|
36
82
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'clin'
|
2
|
+
require 'clin/line_reader'
|
3
|
+
require 'io/console'
|
4
|
+
|
5
|
+
# Basic line scanner.
|
6
|
+
# Use stdin#gets
|
7
|
+
class Clin::LineReader::Basic
|
8
|
+
attr_reader :statement
|
9
|
+
attr_reader :options
|
10
|
+
|
11
|
+
def self.available?
|
12
|
+
true
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(shell, statement, options = {})
|
16
|
+
@shell = shell
|
17
|
+
@statement = statement
|
18
|
+
@options = options
|
19
|
+
end
|
20
|
+
|
21
|
+
def readline
|
22
|
+
@shell.out.print(@statement)
|
23
|
+
scan
|
24
|
+
end
|
25
|
+
|
26
|
+
protected def scan
|
27
|
+
return @shell.in.gets if echo?
|
28
|
+
begin
|
29
|
+
@shell.in.noecho(&:gets)
|
30
|
+
rescue Errno::EBADF # If console doesn't support noecho
|
31
|
+
@shell.in.gets
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
protected def echo?
|
36
|
+
@options.fetch(:echo, true)
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'readline'
|
2
|
+
|
3
|
+
# Readline line scanner.
|
4
|
+
# Allow autocomplete and history.
|
5
|
+
# Use Readline.readline
|
6
|
+
# Valid options:
|
7
|
+
# echo: [Boolean] Set to false not to show on screen what you type(e.g. password)
|
8
|
+
# autocomplete: List of values to autocomplete or proc that return the values
|
9
|
+
# add_to_history: [Boolean] Add the reply to the history, default: true
|
10
|
+
class Clin::LineReader::Readline < Clin::LineReader::Basic
|
11
|
+
def self.available?
|
12
|
+
Clin.use_readline?
|
13
|
+
end
|
14
|
+
|
15
|
+
def readline
|
16
|
+
if echo?
|
17
|
+
Readline.completion_append_character = nil
|
18
|
+
set_completion_proc
|
19
|
+
Readline.readline(statement, add_to_history?)
|
20
|
+
else # Use basic method to fetch
|
21
|
+
super
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Set the auto-completion process if applicable
|
26
|
+
protected def set_completion_proc
|
27
|
+
proc = completion_proc
|
28
|
+
Readline.completion_proc = proc unless proc.nil?
|
29
|
+
end
|
30
|
+
|
31
|
+
# Return nil if no completion given as option
|
32
|
+
# @return [Proc] Auto-completion process
|
33
|
+
protected def completion_proc
|
34
|
+
return nil unless autocomplete?
|
35
|
+
if autocomplete.is_a? Proc
|
36
|
+
autocomplete
|
37
|
+
else
|
38
|
+
proc { |s| autocomplete.grep(/^#{Regexp.escape(s)}/) }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
protected def autocomplete
|
43
|
+
options[:autocomplete]
|
44
|
+
end
|
45
|
+
|
46
|
+
protected def autocomplete?
|
47
|
+
options.fetch(:autocomplete, false)
|
48
|
+
end
|
49
|
+
|
50
|
+
protected def add_to_history?
|
51
|
+
options.fetch(:add_to_history, true)
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'clin'
|
2
|
+
|
3
|
+
# Handle to delegate the scan method to the right module.
|
4
|
+
# It will use Readline unless the disabled using Clin.use_readline = false
|
5
|
+
module Clin::LineReader
|
6
|
+
def self.scan(shell, statement, options = {})
|
7
|
+
readers.detect(&:available?).new(shell, statement, options).readline
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.readers
|
11
|
+
@readers ||= [Clin::LineReader::Readline, Clin::LineReader::Basic]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
require 'clin/line_reader/basic'
|
16
|
+
require 'clin/line_reader/readline'
|
data/lib/clin/option.rb
CHANGED
@@ -53,19 +53,12 @@ class Clin::Option
|
|
53
53
|
@default = default
|
54
54
|
end
|
55
55
|
|
56
|
-
|
57
|
-
|
58
|
-
# @param out [Hash] Out options mapping
|
59
|
-
def register(opts, out)
|
60
|
-
load_default(out)
|
56
|
+
def trigger(opts, out, value)
|
57
|
+
value = cast(value)
|
61
58
|
if @block.nil?
|
62
|
-
|
63
|
-
on(value, out)
|
64
|
-
end
|
59
|
+
on(value, out)
|
65
60
|
else
|
66
|
-
|
67
|
-
block.call(opts, out, value)
|
68
|
-
end
|
61
|
+
block.call(opts, out, value)
|
69
62
|
end
|
70
63
|
end
|
71
64
|
|
@@ -153,6 +146,10 @@ class Clin::Option
|
|
153
146
|
@argument.eql? false
|
154
147
|
end
|
155
148
|
|
149
|
+
def argument_optional?
|
150
|
+
@optional_argument
|
151
|
+
end
|
152
|
+
|
156
153
|
# Init the output Hash with the default values. Must be called before parsing.
|
157
154
|
# @param out [Hash]
|
158
155
|
def load_default(out)
|
@@ -187,4 +184,20 @@ class Clin::Option
|
|
187
184
|
end
|
188
185
|
out
|
189
186
|
end
|
187
|
+
|
188
|
+
def banner
|
189
|
+
args = [short, long_argument, description]
|
190
|
+
args.compact.join(' ')
|
191
|
+
end
|
192
|
+
|
193
|
+
def cast(str)
|
194
|
+
return str if type.nil?
|
195
|
+
if type == Integer
|
196
|
+
Integer(str)
|
197
|
+
elsif type == Float
|
198
|
+
Float(str)
|
199
|
+
else
|
200
|
+
str
|
201
|
+
end
|
202
|
+
end
|
190
203
|
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
require 'clin'
|
2
|
+
|
3
|
+
# Class that handler the option parsing part of command parsing.
|
4
|
+
# It separate the options from the arguments
|
5
|
+
class Clin::OptionParser
|
6
|
+
LONG_OPTION_REGEX = /\A(?<name>--[^=]*)(?:=(?<value>.*))?/m
|
7
|
+
SHORT_OPTION_REGEX = /\A(?<name>-.)(?<value>(=).*|.+)?/m
|
8
|
+
|
9
|
+
# List of arguments(i.e. Argv segments that are not options)
|
10
|
+
attr_reader :arguments
|
11
|
+
|
12
|
+
# List of errors encountered
|
13
|
+
attr_reader :errors
|
14
|
+
|
15
|
+
# Parsed options are store here
|
16
|
+
attr_reader :options
|
17
|
+
|
18
|
+
# Any option skipped(if the command allow it) will be listed in here
|
19
|
+
attr_reader :skipped_options
|
20
|
+
|
21
|
+
def initialize(command, argv)
|
22
|
+
@errors = []
|
23
|
+
@command = command
|
24
|
+
@options = {}
|
25
|
+
@original_argv = argv
|
26
|
+
@argv = argv.clone
|
27
|
+
@arguments = []
|
28
|
+
@skipped_options = []
|
29
|
+
end
|
30
|
+
|
31
|
+
# Parse the argument for the command.
|
32
|
+
# @return [Hash] return the options parsed
|
33
|
+
# Options can also be accessed with #options
|
34
|
+
# ```
|
35
|
+
# # Suppose verbose and opt are defined option for the command.
|
36
|
+
# parser = OptionParser.new(command, %w(arg1 arg2 -v --opt val))
|
37
|
+
# parser.parse #=> {verbose: true, opt: 'val'}
|
38
|
+
# Get the arguments
|
39
|
+
# parser.argv # => ['arg1', 'arg2']
|
40
|
+
# ```
|
41
|
+
def parse
|
42
|
+
while parse_next
|
43
|
+
end
|
44
|
+
@options
|
45
|
+
end
|
46
|
+
|
47
|
+
# Fetch the next next argument and parse the option or store it as an argument
|
48
|
+
# @return [Boolean] true if it parsed anything, false if there are no more argument to parse
|
49
|
+
def parse_next
|
50
|
+
return false if @argv.empty?
|
51
|
+
case (arg = @argv.shift)
|
52
|
+
when LONG_OPTION_REGEX
|
53
|
+
name = Regexp.last_match[:name]
|
54
|
+
value = Regexp.last_match[:value]
|
55
|
+
parse_long(name, value)
|
56
|
+
when SHORT_OPTION_REGEX
|
57
|
+
name = Regexp.last_match[:name]
|
58
|
+
value = Regexp.last_match[:value]
|
59
|
+
parse_short(name, value)
|
60
|
+
else
|
61
|
+
@arguments << arg
|
62
|
+
end
|
63
|
+
true
|
64
|
+
end
|
65
|
+
|
66
|
+
# Parse a long option
|
67
|
+
# @param name [String] name of the option(--verbose)
|
68
|
+
# @param value [String] value of the option
|
69
|
+
# If the value is nil and the option allow argument it will try to use the next argument
|
70
|
+
def parse_long(name, value)
|
71
|
+
option = @command.find_option_by(long: name)
|
72
|
+
parse_option(option, name, value, false)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Parse a long option
|
76
|
+
# @param name [String] name of the option(-v)
|
77
|
+
# @param value [String] value of the option
|
78
|
+
# If the value is nil and the option allow argument it will try to use the next argument
|
79
|
+
def parse_short(name, value)
|
80
|
+
option = @command.find_option_by(short: name)
|
81
|
+
parse_option(option, name, value, true)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Parse the given option.
|
85
|
+
# @param option [Clin::Option]
|
86
|
+
# @param name [String] name it was given in the command
|
87
|
+
# @param value [String] value of the option
|
88
|
+
# If the value is nil and the option allow argument it will try to use the next argument
|
89
|
+
def parse_option(option, name, value, short)
|
90
|
+
return handle_unknown_option(name, value) if option.nil?
|
91
|
+
return parse_flag_option(option, value, short) if option.flag?
|
92
|
+
|
93
|
+
value = complete(value)
|
94
|
+
if value.nil? && !option.argument_optional?
|
95
|
+
return add_error Clin::MissingOptionArgumentError.new(option)
|
96
|
+
end
|
97
|
+
value ||= true
|
98
|
+
option.trigger(self, @options, value)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Get the next possible argument in the list if the value is nil.
|
102
|
+
# @param value [String] current option value.
|
103
|
+
# Only get the next argument in the list if:
|
104
|
+
# - value is nil
|
105
|
+
# - the next argument is not an option(start with '-')
|
106
|
+
def complete(value)
|
107
|
+
if value.nil? && @argv.any? && !@argv.first.start_with?('-')
|
108
|
+
@argv.shift
|
109
|
+
else
|
110
|
+
value
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Parse a flag option(No argument)
|
115
|
+
# Add [OptionUnexpectedArgumentError] If value is defined and the long version was used.
|
116
|
+
# Short flag option can be merged together(i.e these are equivalent: -abc, -a -b -c)
|
117
|
+
# In that case the value will be 'bc'. It will then try to parse b and c as flag options.
|
118
|
+
def parse_flag_option(option, value, short)
|
119
|
+
return option.trigger(self, @options, true) if value.nil?
|
120
|
+
unless short # Short can also have the format -abc
|
121
|
+
return add_error Clin::OptionUnexpectedArgumentError.new(option, value)
|
122
|
+
end
|
123
|
+
|
124
|
+
option.trigger(self, @options, true)
|
125
|
+
# The value is expected to be other flag options
|
126
|
+
parse_compact_flag_options(value)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Parse compact flag_options(e.g. For -abc it will be called with 'bc')
|
130
|
+
# @param options [String] List of options where each char should correspond to a short option
|
131
|
+
def parse_compact_flag_options(options)
|
132
|
+
options.each_char do |s|
|
133
|
+
option = @command.find_option_by(short: "-#{s}")
|
134
|
+
if option && !option.flag?
|
135
|
+
message = "Cannot combine short options that expect argument: #{option}"
|
136
|
+
add_error Clin::OptionError.new(message, option)
|
137
|
+
break
|
138
|
+
end
|
139
|
+
parse_flag_option(option, nil, true)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Handle the case where the option was not defined in the command.
|
144
|
+
# @param name [String] name used in the command.
|
145
|
+
# @param value [String] Value of the option if applicable.
|
146
|
+
# Add [UnknownOptionError] if the command doesn't allow unknown options.
|
147
|
+
def handle_unknown_option(name, value)
|
148
|
+
unless @command.skip_options?
|
149
|
+
add_error Clin::UnknownOptionError.new(name)
|
150
|
+
return
|
151
|
+
end
|
152
|
+
value = complete(value)
|
153
|
+
@skipped_options += [name, value]
|
154
|
+
end
|
155
|
+
|
156
|
+
def add_error(err)
|
157
|
+
@errors << err
|
158
|
+
end
|
159
|
+
end
|
data/lib/clin/shell.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'clin'
|
2
|
+
require 'clin/line_reader'
|
2
3
|
|
3
4
|
# Class the offer helper method to interact with the user using the command line
|
4
5
|
class Clin::Shell
|
@@ -8,19 +9,34 @@ class Clin::Shell
|
|
8
9
|
# Output stream, default: STDOUT
|
9
10
|
attr_accessor :out
|
10
11
|
|
12
|
+
# Text builder instance that is used to stream
|
13
|
+
attr_accessor :text
|
14
|
+
|
11
15
|
def initialize(input: STDIN, output: STDOUT)
|
12
16
|
@in = input
|
13
17
|
@out = output
|
14
18
|
@yes_or_no_persist = false
|
15
19
|
@override_persist = false
|
20
|
+
@text = Clin::Text.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def say(line, indent: '')
|
24
|
+
@out.puts text.line(line, indent: indent)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Indent the current output
|
28
|
+
def indent(indent, &block)
|
29
|
+
text.indent(indent, &block)
|
16
30
|
end
|
17
31
|
|
18
32
|
# Ask a question
|
19
33
|
# @param statement [String]
|
20
34
|
# @param default [String]
|
21
|
-
# @param autocomplete [Array|Proc] Filter for autocomplete
|
22
|
-
|
23
|
-
|
35
|
+
# @param autocomplete [Array|Proc] Filter for autocomplete (Need Readline)
|
36
|
+
# @param echo [Boolean] If false no character will be displayed during input
|
37
|
+
# @param add_to_history [Boolean] If the answer should be added to history. (Need Readline)
|
38
|
+
def ask(statement, default: nil, autocomplete: nil, echo: true, add_to_history: true)
|
39
|
+
answer = scan(statement, autocomplete: autocomplete, echo: echo, add_to_history: add_to_history)
|
24
40
|
if answer.blank?
|
25
41
|
default
|
26
42
|
else
|
@@ -28,6 +44,10 @@ class Clin::Shell
|
|
28
44
|
end
|
29
45
|
end
|
30
46
|
|
47
|
+
def password(statement, default: nil)
|
48
|
+
ask(statement, default: default, echo: false, add_to_history: false)
|
49
|
+
end
|
50
|
+
|
31
51
|
# Ask a question and expect the result to be in the list of choices
|
32
52
|
# Will continue asking until the input is correct
|
33
53
|
# or if a default value is supplied then empty will return.
|
@@ -43,6 +63,17 @@ class Clin::Shell
|
|
43
63
|
default: default, allow_initials: allow_initials)
|
44
64
|
end
|
45
65
|
|
66
|
+
# Ask a question with a list of possible answer.
|
67
|
+
# Answer can either be selected using their name or their index
|
68
|
+
# e.g.
|
69
|
+
# Select answer:
|
70
|
+
# 1. Choice A
|
71
|
+
# 2. Choice B
|
72
|
+
# 3. Choice C
|
73
|
+
def select(statement, choices, default: nil)
|
74
|
+
Clin::ShellInteraction::Select.new(self).run(statement, choices, default: default)
|
75
|
+
end
|
76
|
+
|
46
77
|
# Expect the user the return yes or no(y/n also works)
|
47
78
|
# @param statement [String] Question to ask
|
48
79
|
# @param default [String] Default value(yes/no)
|
@@ -93,18 +124,8 @@ class Clin::Shell
|
|
93
124
|
end
|
94
125
|
|
95
126
|
# Prompt the statement to the user and return his reply.
|
96
|
-
|
97
|
-
|
98
|
-
protected def scan(statement, autocomplete: nil)
|
99
|
-
unless autocomplete.nil?
|
100
|
-
Readline.completion_proc = if autocomplete.is_a? Proc
|
101
|
-
autocomplete
|
102
|
-
else
|
103
|
-
proc { |s| autocomplete.grep(/^#{Regexp.escape(s)}/) }
|
104
|
-
end
|
105
|
-
end
|
106
|
-
Readline.completion_append_character = nil
|
107
|
-
Readline.readline(statement + ' ', true)
|
127
|
+
protected def scan(statement, options = {})
|
128
|
+
Clin::LineReader.scan(self, statement + ' ', options)
|
108
129
|
end
|
109
130
|
end
|
110
131
|
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'clin'
|
2
|
+
require 'clin/text'
|
2
3
|
|
3
4
|
# Handle a choose question
|
4
5
|
class Clin::ShellInteraction::Choose < Clin::ShellInteraction
|
@@ -41,19 +42,26 @@ class Clin::ShellInteraction::Choose < Clin::ShellInteraction
|
|
41
42
|
end
|
42
43
|
end
|
43
44
|
|
45
|
+
# Print help
|
44
46
|
protected def print_choices_help(choices, allow_initials: false)
|
45
|
-
|
47
|
+
shell.say choice_help(choices, allow_initals: allow_initials)
|
48
|
+
end
|
49
|
+
|
50
|
+
def choice_help(choices, allow_initials: false)
|
46
51
|
used_initials = Set.new
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
52
|
+
Clin::Text.new do |t|
|
53
|
+
t.line 'Choose from:'
|
54
|
+
t.table(indent: 2, border: false, separate_blank: false) do |m|
|
55
|
+
m.column_delimiter(allow_initials ? [' - ', ' '] : [' '])
|
56
|
+
choices.each do |choice, description|
|
57
|
+
if allow_initials
|
58
|
+
inital = used_initials.add?(choice[0]) ? choice[0] : nil
|
59
|
+
m.row inital, choice.to_s, description
|
60
|
+
else
|
61
|
+
m.row choice.to_s, description
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
57
65
|
end
|
58
66
|
end
|
59
67
|
end
|
@@ -15,6 +15,9 @@ class Clin::ShellInteraction::FileConflict < Clin::ShellInteraction
|
|
15
15
|
result
|
16
16
|
end
|
17
17
|
|
18
|
+
# Handle the use choice
|
19
|
+
# @return [Boolean] true/false if the user made a choice or
|
20
|
+
# nil if the question needs to be asked again
|
18
21
|
protected def handle_choice(choice, filename, &block)
|
19
22
|
case choice
|
20
23
|
when :yes
|
@@ -24,7 +27,7 @@ class Clin::ShellInteraction::FileConflict < Clin::ShellInteraction
|
|
24
27
|
when :always
|
25
28
|
return persist!
|
26
29
|
when :quit
|
27
|
-
|
30
|
+
shell.say 'Aborting...'
|
28
31
|
fail SystemExit
|
29
32
|
when :diff
|
30
33
|
show_diff(filename, block.call)
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'clin'
|
2
|
+
require 'clin/text'
|
3
|
+
|
4
|
+
# Handle a choose question. Where you select the choices with a number
|
5
|
+
# $ Choose:
|
6
|
+
# 1. Choice A
|
7
|
+
# 2. Choice B
|
8
|
+
# 3. Choice B
|
9
|
+
#
|
10
|
+
class Clin::ShellInteraction::Select < Clin::ShellInteraction::Choose
|
11
|
+
def run(statement, choices, default: nil, start_index: 1)
|
12
|
+
choices = convert_choices(choices)
|
13
|
+
loop do
|
14
|
+
shell.say statement
|
15
|
+
shell.say choice_help(choices, start_index)
|
16
|
+
answer = @shell.ask('>', default: default, autocomplete: choices.keys)
|
17
|
+
next if answer.nil?
|
18
|
+
choice = get_choice(choices, answer, start_index)
|
19
|
+
return choice unless choice.nil?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def choice_help(choices, start_index)
|
24
|
+
Clin::Text::Table.new(border: false, col_delim: ' ') do |t|
|
25
|
+
i = start_index
|
26
|
+
choices.each do |key, description|
|
27
|
+
key = "#{key}," unless description.blank?
|
28
|
+
row = ["#{i}.", key]
|
29
|
+
row << description unless description.blank?
|
30
|
+
t.row row
|
31
|
+
i += 1
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def get_choice(choices, answer, start_index)
|
37
|
+
i = start_index
|
38
|
+
choices.each do |choice, _|
|
39
|
+
return choice if choice.casecmp(answer) == 0 || i.to_s == answer
|
40
|
+
i += 1
|
41
|
+
end
|
42
|
+
nil
|
43
|
+
end
|
44
|
+
end
|