clin 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|