cli-kit 4.0.0 → 5.0.1
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/dependabot.yml +3 -0
- data/.github/workflows/cla.yml +22 -0
- data/.github/workflows/ruby.yml +16 -2
- data/.gitignore +2 -0
- data/.rubocop.sorbet.yml +47 -0
- data/.rubocop.yml +32 -1
- data/.ruby-version +1 -0
- data/Gemfile +10 -1
- data/Gemfile.lock +102 -29
- data/README.md +46 -3
- data/Rakefile +1 -0
- data/bin/onchange +30 -0
- data/bin/tapioca +28 -0
- data/bin/testunit +1 -0
- data/cli-kit.gemspec +9 -4
- data/dev.yml +38 -3
- data/examples/minimal/example.rb +11 -6
- data/examples/single-file/example.rb +25 -35
- data/gen/lib/gen/commands/help.rb +8 -10
- data/gen/lib/gen/commands/new.rb +23 -9
- data/gen/lib/gen/commands.rb +21 -9
- data/gen/lib/gen/entry_point.rb +12 -3
- data/gen/lib/gen/generator.rb +32 -11
- data/gen/lib/gen/help.rb +63 -0
- data/gen/lib/gen.rb +18 -23
- data/gen/template/bin/update-deps +2 -2
- data/gen/template/dev-gems.yml +1 -1
- data/gen/template/dev-vendor.yml +1 -1
- data/gen/template/lib/__app__/commands.rb +1 -4
- data/gen/template/lib/__app__.rb +8 -17
- data/gen/template/test/example_test.rb +1 -1
- data/lib/cli/kit/args/definition.rb +344 -0
- data/lib/cli/kit/args/evaluation.rb +234 -0
- data/lib/cli/kit/args/parser/node.rb +132 -0
- data/lib/cli/kit/args/parser.rb +129 -0
- data/lib/cli/kit/args/tokenizer.rb +133 -0
- data/lib/cli/kit/args.rb +16 -0
- data/lib/cli/kit/base_command.rb +17 -32
- data/lib/cli/kit/command_help.rb +271 -0
- data/lib/cli/kit/command_registry.rb +72 -20
- data/lib/cli/kit/config.rb +25 -22
- data/lib/cli/kit/core_ext.rb +30 -0
- data/lib/cli/kit/error_handler.rb +131 -70
- data/lib/cli/kit/executor.rb +20 -3
- data/lib/cli/kit/ini.rb +31 -38
- data/lib/cli/kit/levenshtein.rb +12 -4
- data/lib/cli/kit/logger.rb +16 -2
- data/lib/cli/kit/opts.rb +301 -0
- data/lib/cli/kit/parse_args.rb +55 -0
- data/lib/cli/kit/resolver.rb +8 -0
- data/lib/cli/kit/sorbet_runtime_stub.rb +154 -0
- data/lib/cli/kit/support/test_helper.rb +27 -16
- data/lib/cli/kit/support.rb +2 -0
- data/lib/cli/kit/system.rb +194 -57
- data/lib/cli/kit/util.rb +48 -103
- data/lib/cli/kit/version.rb +3 -1
- data/lib/cli/kit.rb +104 -7
- metadata +30 -14
- data/.github/probots.yml +0 -2
- data/lib/cli/kit/autocall.rb +0 -21
- data/lib/cli/kit/ruby_backports/enumerable.rb +0 -6
data/lib/cli/kit/ini.rb
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
3
|
+
require 'cli/kit'
|
4
|
+
|
1
5
|
module CLI
|
2
6
|
module Kit
|
3
7
|
# INI is a language similar to JSON or YAML, but simplied
|
@@ -13,87 +17,76 @@ module CLI
|
|
13
17
|
# See the ini_test.rb file for more examples
|
14
18
|
#
|
15
19
|
class Ini
|
20
|
+
extend T::Sig
|
21
|
+
|
22
|
+
sig { returns(T::Hash[String, T::Hash[String, String]]) }
|
16
23
|
attr_accessor :ini
|
17
24
|
|
18
|
-
|
25
|
+
sig do
|
26
|
+
params(path: T.nilable(String), config: T.nilable(String), default_section: String).void
|
27
|
+
end
|
28
|
+
def initialize(path = nil, config: nil, default_section: '[global]')
|
19
29
|
@config = if path && File.exist?(path)
|
20
30
|
File.readlines(path)
|
21
31
|
elsif config
|
22
32
|
config.lines
|
23
33
|
end
|
24
34
|
@ini = {}
|
25
|
-
@current_key =
|
26
|
-
@default_section = default_section
|
27
|
-
@convert_types = convert_types
|
35
|
+
@current_key = default_section
|
28
36
|
end
|
29
37
|
|
38
|
+
sig { returns(T::Hash[String, T::Hash[String, String]]) }
|
30
39
|
def parse
|
31
40
|
return @ini if @config.nil?
|
32
41
|
|
33
42
|
@config.each do |l|
|
34
43
|
l.strip!
|
35
44
|
|
36
|
-
# If section, then set current key, this will nest the setting
|
37
45
|
if section_designator?(l)
|
38
46
|
@current_key = l
|
39
|
-
|
40
|
-
# A new line will reset the current key
|
41
|
-
elsif l.strip.empty?
|
42
|
-
@current_key = nil
|
43
|
-
|
44
|
-
# Otherwise set the values
|
45
47
|
else
|
46
48
|
k, v = l.split('=', 2).map(&:strip)
|
47
|
-
set_val(k, v)
|
49
|
+
set_val(k, v) if k && v
|
48
50
|
end
|
49
51
|
end
|
52
|
+
|
50
53
|
@ini
|
51
54
|
end
|
52
55
|
|
56
|
+
sig { returns(String) }
|
53
57
|
def git_format
|
54
|
-
to_ini(
|
58
|
+
to_ini(git_format: true)
|
55
59
|
end
|
56
60
|
|
61
|
+
sig { returns(String) }
|
57
62
|
def to_s
|
58
|
-
to_ini
|
63
|
+
to_ini
|
59
64
|
end
|
60
65
|
|
61
66
|
private
|
62
67
|
|
63
|
-
|
68
|
+
sig { params(git_format: T::Boolean).returns(String) }
|
69
|
+
def to_ini(git_format: false)
|
64
70
|
optional_tab = git_format ? "\t" : ''
|
65
71
|
str = []
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
str << to_ini(v, git_format: git_format)
|
71
|
-
else
|
72
|
+
@ini.each do |section_designator, section|
|
73
|
+
str << '' unless str.empty? || git_format
|
74
|
+
str << section_designator
|
75
|
+
section.each do |k, v|
|
72
76
|
str << "#{optional_tab}#{k} = #{v}"
|
73
77
|
end
|
74
78
|
end
|
75
|
-
str
|
79
|
+
str.join("\n")
|
76
80
|
end
|
77
81
|
|
82
|
+
sig { params(key: String, val: String).void }
|
78
83
|
def set_val(key, val)
|
79
|
-
|
80
|
-
|
81
|
-
current_key =
|
82
|
-
if current_key
|
83
|
-
@ini[current_key] ||= {}
|
84
|
-
@ini[current_key][key] = typed_val(val)
|
85
|
-
else
|
86
|
-
@ini[key] = typed_val(val)
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
def typed_val(val)
|
91
|
-
return val.to_s unless @convert_types
|
92
|
-
return val.to_i if val =~ /^-?[0-9]+$/
|
93
|
-
return val.to_f if val =~ /^-?[0-9]+\.[0-9]*$/
|
94
|
-
val.to_s
|
84
|
+
current_key = @current_key
|
85
|
+
@ini[current_key] ||= {}
|
86
|
+
@ini[current_key][key] = val
|
95
87
|
end
|
96
88
|
|
89
|
+
sig { params(k: String).returns(T::Boolean) }
|
97
90
|
def section_designator?(k)
|
98
91
|
k.start_with?('[') && k.end_with?(']')
|
99
92
|
end
|
data/lib/cli/kit/levenshtein.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
1
3
|
# Copyright (c) 2014-2016 Yuki Nishijima
|
2
4
|
|
3
5
|
# MIT License
|
@@ -21,13 +23,18 @@
|
|
21
23
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
24
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
23
25
|
|
26
|
+
require 'cli/kit'
|
27
|
+
|
24
28
|
module CLI
|
25
29
|
module Kit
|
26
30
|
module Levenshtein
|
31
|
+
extend T::Sig
|
32
|
+
|
27
33
|
# This code is based directly on the Text gem implementation
|
28
34
|
# Copyright (c) 2006-2013 Paul Battley, Michael Neumann, Tim Fletcher.
|
29
35
|
#
|
30
36
|
# Returns a value representing the "cost" of transforming str1 into str2
|
37
|
+
sig { params(str1: String, str2: String).returns(Integer) }
|
31
38
|
def distance(str1, str2)
|
32
39
|
n = str1.length
|
33
40
|
m = str2.length
|
@@ -35,7 +42,7 @@ module CLI
|
|
35
42
|
return n if m.zero?
|
36
43
|
|
37
44
|
d = (0..m).to_a
|
38
|
-
x =
|
45
|
+
x = 0
|
39
46
|
|
40
47
|
# to avoid duplicating an enumerable object, create it outside of the loop
|
41
48
|
str2_codepoints = str2.codepoints
|
@@ -45,9 +52,9 @@ module CLI
|
|
45
52
|
while j < m
|
46
53
|
cost = char1 == str2_codepoints[j] ? 0 : 1
|
47
54
|
x = min3(
|
48
|
-
d[j + 1] + 1, # insertion
|
49
|
-
i + 1,
|
50
|
-
d[j] + cost # substitution
|
55
|
+
T.must(d[j + 1]) + 1, # insertion
|
56
|
+
i + 1, # deletion
|
57
|
+
T.must(d[j]) + cost, # substitution
|
51
58
|
)
|
52
59
|
d[j] = i
|
53
60
|
i = x
|
@@ -67,6 +74,7 @@ module CLI
|
|
67
74
|
# faster than `[a, b, c].min` and puts less GC pressure.
|
68
75
|
# See https://github.com/yuki24/did_you_mean/pull/1 for a performance
|
69
76
|
# benchmark.
|
77
|
+
sig { params(a: Integer, b: Integer, c: Integer).returns(Integer) }
|
70
78
|
def min3(a, b, c)
|
71
79
|
if a < b && a < c
|
72
80
|
a
|
data/lib/cli/kit/logger.rb
CHANGED
@@ -1,15 +1,21 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
3
|
+
require 'cli/kit'
|
1
4
|
require 'logger'
|
2
5
|
require 'fileutils'
|
3
6
|
|
4
7
|
module CLI
|
5
8
|
module Kit
|
6
9
|
class Logger
|
10
|
+
extend T::Sig
|
11
|
+
|
7
12
|
MAX_LOG_SIZE = 5 * 1024 * 1000 # 5MB
|
8
13
|
MAX_NUM_LOGS = 10
|
9
14
|
|
10
15
|
# Constructor for CLI::Kit::Logger
|
11
16
|
#
|
12
17
|
# @param debug_log_file [String] path to the file where debug logs should be stored
|
18
|
+
sig { params(debug_log_file: String, env_debug_name: String).void }
|
13
19
|
def initialize(debug_log_file:, env_debug_name: 'DEBUG')
|
14
20
|
FileUtils.mkpath(File.dirname(debug_log_file))
|
15
21
|
@debug_logger = ::Logger.new(debug_log_file, MAX_NUM_LOGS, MAX_LOG_SIZE)
|
@@ -21,6 +27,7 @@ module CLI
|
|
21
27
|
#
|
22
28
|
# @param msg [String] the message to log
|
23
29
|
# @param debug [Boolean] determines if the debug logger will receive the log (default true)
|
30
|
+
sig { params(msg: String, debug: T::Boolean).void }
|
24
31
|
def info(msg, debug: true)
|
25
32
|
$stdout.puts CLI::UI.fmt(msg)
|
26
33
|
@debug_logger.info(format_debug(msg)) if debug
|
@@ -31,6 +38,7 @@ module CLI
|
|
31
38
|
#
|
32
39
|
# @param msg [String] the message to log
|
33
40
|
# @param debug [Boolean] determines if the debug logger will receive the log (default true)
|
41
|
+
sig { params(msg: String, debug: T::Boolean).void }
|
34
42
|
def warn(msg, debug: true)
|
35
43
|
$stdout.puts CLI::UI.fmt("{{yellow:#{msg}}}")
|
36
44
|
@debug_logger.warn(format_debug(msg)) if debug
|
@@ -41,6 +49,7 @@ module CLI
|
|
41
49
|
#
|
42
50
|
# @param msg [String] the message to log
|
43
51
|
# @param debug [Boolean] determines if the debug logger will receive the log (default true)
|
52
|
+
sig { params(msg: String, debug: T::Boolean).void }
|
44
53
|
def error(msg, debug: true)
|
45
54
|
$stderr.puts CLI::UI.fmt("{{red:#{msg}}}")
|
46
55
|
@debug_logger.error(format_debug(msg)) if debug
|
@@ -51,6 +60,7 @@ module CLI
|
|
51
60
|
#
|
52
61
|
# @param msg [String] the message to log
|
53
62
|
# @param debug [Boolean] determines if the debug logger will receive the log (default true)
|
63
|
+
sig { params(msg: String, debug: T::Boolean).void }
|
54
64
|
def fatal(msg, debug: true)
|
55
65
|
$stderr.puts CLI::UI.fmt("{{red:{{bold:Fatal:}} #{msg}}}")
|
56
66
|
@debug_logger.fatal(format_debug(msg)) if debug
|
@@ -60,6 +70,7 @@ module CLI
|
|
60
70
|
# Logs to the debug file, taking into account CLI::UI::StdoutRouter.current_id
|
61
71
|
#
|
62
72
|
# @param msg [String] the message to log
|
73
|
+
sig { params(msg: String).void }
|
63
74
|
def debug(msg)
|
64
75
|
$stdout.puts CLI::UI.fmt(msg) if debug?
|
65
76
|
@debug_logger.debug(format_debug(msg))
|
@@ -67,15 +78,18 @@ module CLI
|
|
67
78
|
|
68
79
|
private
|
69
80
|
|
81
|
+
sig { params(msg: String).returns(String) }
|
70
82
|
def format_debug(msg)
|
71
83
|
msg = CLI::UI.fmt(msg)
|
72
84
|
return msg unless CLI::UI::StdoutRouter.current_id
|
73
|
-
|
85
|
+
|
86
|
+
"[#{CLI::UI::StdoutRouter.current_id&.fetch(:id, nil)}] #{msg}"
|
74
87
|
end
|
75
88
|
|
89
|
+
sig { returns(T::Boolean) }
|
76
90
|
def debug?
|
77
91
|
val = ENV[@env_debug_name]
|
78
|
-
val && val != '0' && val != ''
|
92
|
+
!!val && val != '0' && val != ''
|
79
93
|
end
|
80
94
|
end
|
81
95
|
end
|
data/lib/cli/kit/opts.rb
ADDED
@@ -0,0 +1,301 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
3
|
+
require 'cli/kit'
|
4
|
+
|
5
|
+
module CLI
|
6
|
+
module Kit
|
7
|
+
class Opts
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
module Mixin
|
11
|
+
extend T::Sig
|
12
|
+
include Kernel
|
13
|
+
|
14
|
+
module MixinClassMethods
|
15
|
+
extend T::Sig
|
16
|
+
|
17
|
+
sig { params(included_module: Module).void }
|
18
|
+
def include(included_module)
|
19
|
+
super
|
20
|
+
return unless included_module.is_a?(MixinClassMethods)
|
21
|
+
|
22
|
+
included_module.tracked_methods.each { |m| track_method(m) }
|
23
|
+
end
|
24
|
+
|
25
|
+
# No signature - Sorbet uses method_added internally, so can't verify it
|
26
|
+
def method_added(method_name) # rubocop:disable Sorbet/EnforceSignatures
|
27
|
+
super
|
28
|
+
track_method(method_name)
|
29
|
+
end
|
30
|
+
|
31
|
+
sig { params(method_name: Symbol).void }
|
32
|
+
def track_method(method_name)
|
33
|
+
@tracked_methods ||= []
|
34
|
+
@tracked_methods << method_name unless @tracked_methods.include?(method_name)
|
35
|
+
end
|
36
|
+
|
37
|
+
sig { returns(T::Array[Symbol]) }
|
38
|
+
def tracked_methods
|
39
|
+
@tracked_methods || []
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class << self
|
44
|
+
extend T::Sig
|
45
|
+
|
46
|
+
sig { params(klass: Module).void }
|
47
|
+
def included(klass)
|
48
|
+
klass.extend(MixinClassMethods)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
sig do
|
53
|
+
params(
|
54
|
+
name: Symbol,
|
55
|
+
short: T.nilable(String),
|
56
|
+
long: T.nilable(String),
|
57
|
+
desc: T.nilable(String),
|
58
|
+
default: T.any(NilClass, String, T.proc.returns(String)),
|
59
|
+
).returns(T.nilable(String))
|
60
|
+
end
|
61
|
+
def option(name: infer_name, short: nil, long: nil, desc: nil, default: nil)
|
62
|
+
unless default.nil?
|
63
|
+
raise(ArgumentError, 'declare options with non-nil defaults using `option!` instead of `option`')
|
64
|
+
end
|
65
|
+
|
66
|
+
case @obj
|
67
|
+
when Args::Definition
|
68
|
+
@obj.add_option(
|
69
|
+
name, short: short, long: long, desc: desc, default: default
|
70
|
+
)
|
71
|
+
'(result unavailable)'
|
72
|
+
when Args::Evaluation
|
73
|
+
@obj.opt.send(name)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
sig do
|
78
|
+
params(
|
79
|
+
name: Symbol,
|
80
|
+
short: T.nilable(String),
|
81
|
+
long: T.nilable(String),
|
82
|
+
desc: T.nilable(String),
|
83
|
+
default: T.any(NilClass, String, T.proc.returns(String)),
|
84
|
+
).returns(String)
|
85
|
+
end
|
86
|
+
def option!(name: infer_name, short: nil, long: nil, desc: nil, default: nil)
|
87
|
+
case @obj
|
88
|
+
when Args::Definition
|
89
|
+
@obj.add_option(
|
90
|
+
name, short: short, long: long, desc: desc, default: default
|
91
|
+
)
|
92
|
+
'(result unavailable)'
|
93
|
+
when Args::Evaluation
|
94
|
+
@obj.opt.send(name)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
sig do
|
99
|
+
params(
|
100
|
+
name: Symbol,
|
101
|
+
short: T.nilable(String),
|
102
|
+
long: T.nilable(String),
|
103
|
+
desc: T.nilable(String),
|
104
|
+
default: T.any(T::Array[String], T.proc.returns(T::Array[String])),
|
105
|
+
).returns(T::Array[String])
|
106
|
+
end
|
107
|
+
def multi_option(name: infer_name, short: nil, long: nil, desc: nil, default: [])
|
108
|
+
case @obj
|
109
|
+
when Args::Definition
|
110
|
+
@obj.add_option(
|
111
|
+
name, short: short, long: long, desc: desc, default: default, multi: true
|
112
|
+
)
|
113
|
+
['(result unavailable)']
|
114
|
+
when Args::Evaluation
|
115
|
+
@obj.opt.send(name)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
sig do
|
120
|
+
params(
|
121
|
+
name: Symbol,
|
122
|
+
short: T.nilable(String),
|
123
|
+
long: T.nilable(String),
|
124
|
+
desc: T.nilable(String),
|
125
|
+
).returns(T::Boolean)
|
126
|
+
end
|
127
|
+
def flag(name: infer_name, short: nil, long: nil, desc: nil)
|
128
|
+
case @obj
|
129
|
+
when Args::Definition
|
130
|
+
@obj.add_flag(name, short: short, long: long, desc: desc)
|
131
|
+
false
|
132
|
+
when Args::Evaluation
|
133
|
+
@obj.flag.send(name)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
sig { params(name: Symbol, desc: T.nilable(String)).returns(String) }
|
138
|
+
def position!(name: infer_name, desc: nil)
|
139
|
+
case @obj
|
140
|
+
when Args::Definition
|
141
|
+
@obj.add_position(name, desc: desc, required: true, multi: false)
|
142
|
+
'(result unavailable)'
|
143
|
+
when Args::Evaluation
|
144
|
+
@obj.position.send(name)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
sig do
|
149
|
+
params(
|
150
|
+
name: Symbol,
|
151
|
+
desc: T.nilable(String),
|
152
|
+
default: T.any(NilClass, String, T.proc.returns(String)),
|
153
|
+
skip: T.any(
|
154
|
+
NilClass,
|
155
|
+
T.proc.returns(T::Boolean),
|
156
|
+
T.proc.params(arg0: String).returns(T::Boolean),
|
157
|
+
),
|
158
|
+
).returns(T.nilable(String))
|
159
|
+
end
|
160
|
+
def position(name: infer_name, desc: nil, default: nil, skip: nil)
|
161
|
+
case @obj
|
162
|
+
when Args::Definition
|
163
|
+
@obj.add_position(name, desc: desc, required: false, multi: false, default: default, skip: skip)
|
164
|
+
'(result unavailable)'
|
165
|
+
when Args::Evaluation
|
166
|
+
@obj.position.send(name)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
sig { params(name: Symbol, desc: T.nilable(String)).returns(T::Array[String]) }
|
171
|
+
def rest(name: infer_name, desc: nil)
|
172
|
+
case @obj
|
173
|
+
when Args::Definition
|
174
|
+
@obj.add_position(name, desc: desc, required: false, multi: true)
|
175
|
+
['(result unavailable)']
|
176
|
+
when Args::Evaluation
|
177
|
+
@obj.position.send(name)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
private
|
182
|
+
|
183
|
+
sig { returns(Symbol) }
|
184
|
+
def infer_name
|
185
|
+
to_skip = 1
|
186
|
+
Kernel.caller_locations.each do |loc|
|
187
|
+
next if loc.path =~ /sorbet-runtime/
|
188
|
+
|
189
|
+
if to_skip > 0
|
190
|
+
to_skip -= 1
|
191
|
+
next
|
192
|
+
end
|
193
|
+
return(T.must(loc.label&.to_sym))
|
194
|
+
end
|
195
|
+
raise(ArgumentError, 'could not infer name')
|
196
|
+
end
|
197
|
+
end
|
198
|
+
include(Mixin)
|
199
|
+
|
200
|
+
DEFAULT_OPTIONS = [:helpflag]
|
201
|
+
|
202
|
+
sig { returns(T::Boolean) }
|
203
|
+
def helpflag
|
204
|
+
flag(name: :help, short: '-h', long: '--help', desc: 'Show this help message')
|
205
|
+
end
|
206
|
+
|
207
|
+
sig { returns(T::Array[String]) }
|
208
|
+
def unparsed
|
209
|
+
obj = assert_result!
|
210
|
+
obj.unparsed
|
211
|
+
end
|
212
|
+
|
213
|
+
sig do
|
214
|
+
params(
|
215
|
+
block: T.nilable(
|
216
|
+
T.proc.params(arg0: Symbol, arg1: T.nilable(String)).void,
|
217
|
+
),
|
218
|
+
).returns(T.untyped)
|
219
|
+
end
|
220
|
+
def each_option(&block)
|
221
|
+
return(enum_for(:each_option)) unless block_given?
|
222
|
+
|
223
|
+
obj = assert_result!
|
224
|
+
obj.defn.options.each do |opt|
|
225
|
+
name = opt.name
|
226
|
+
value = obj.opt.send(name)
|
227
|
+
yield(name, value)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
sig do
|
232
|
+
params(
|
233
|
+
block: T.nilable(
|
234
|
+
T.proc.params(arg0: Symbol, arg1: T::Boolean).void,
|
235
|
+
),
|
236
|
+
).returns(T.untyped)
|
237
|
+
end
|
238
|
+
def each_flag(&block)
|
239
|
+
return(enum_for(:each_flag)) unless block_given?
|
240
|
+
|
241
|
+
obj = assert_result!
|
242
|
+
obj.defn.flags.each do |flag|
|
243
|
+
name = flag.name
|
244
|
+
value = obj.flag.send(name)
|
245
|
+
yield(name, value)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
sig { params(name: String).returns(T.nilable(T.any(String, T::Boolean))) }
|
250
|
+
def [](name)
|
251
|
+
obj = assert_result!
|
252
|
+
if obj.opt.respond_to?(name)
|
253
|
+
obj.opt.send(name)
|
254
|
+
elsif obj.flag.respond_to?(name)
|
255
|
+
obj.flag.send(name)
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
sig { params(name: String).returns(T.nilable(String)) }
|
260
|
+
def lookup_option(name)
|
261
|
+
obj = assert_result!
|
262
|
+
obj.opt.send(name)
|
263
|
+
rescue NoMethodError
|
264
|
+
# TODO: should we raise a KeyError?
|
265
|
+
nil
|
266
|
+
end
|
267
|
+
|
268
|
+
sig { params(name: String).returns(T::Boolean) }
|
269
|
+
def lookup_flag(name)
|
270
|
+
obj = assert_result!
|
271
|
+
obj.flag.send(name)
|
272
|
+
rescue NoMethodError
|
273
|
+
false
|
274
|
+
end
|
275
|
+
|
276
|
+
sig { returns(Args::Evaluation) }
|
277
|
+
def assert_result!
|
278
|
+
raise(NotImplementedError, 'not implemented') if @obj.is_a?(Args::Definition)
|
279
|
+
|
280
|
+
@obj
|
281
|
+
end
|
282
|
+
|
283
|
+
sig { params(defn: Args::Definition).void }
|
284
|
+
def define!(defn)
|
285
|
+
@obj = defn
|
286
|
+
T.cast(self.class, Mixin::MixinClassMethods).tracked_methods.each do |m|
|
287
|
+
send(m)
|
288
|
+
end
|
289
|
+
DEFAULT_OPTIONS.each do |m|
|
290
|
+
send(m)
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
sig { params(ev: Args::Evaluation).void }
|
295
|
+
def evaluate!(ev)
|
296
|
+
@obj = ev
|
297
|
+
ev.resolve_positions!
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
3
|
+
module CLI
|
4
|
+
module Kit
|
5
|
+
module ParseArgs
|
6
|
+
# because sorbet type-checking takes the pedantic route that module doesn't include Kernel, therefore
|
7
|
+
# this is necessary (even tho it's ~probably fine~)
|
8
|
+
include Kernel
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
# T.untyped is used in two places. The interpretation of dynamic values from the provided `opts`
|
12
|
+
# and the resulting args[:opts] is pretty broad. There seems to be minimal value in expressing a
|
13
|
+
# tighter subset of T.untyped.
|
14
|
+
|
15
|
+
sig { params(args: String, opts_defn: T::Hash[Symbol, T::Array[T.untyped]]).returns(T::Hash[Symbol, T.untyped]) }
|
16
|
+
def parse_args(args, opts_defn)
|
17
|
+
start_opts, parser_config = opts_defn.reduce([{}, []]) do |(ini, pcfg), (n, cfg)|
|
18
|
+
(vals, desc, short, klass) = cfg
|
19
|
+
(init_val, def_val) = Array(vals)
|
20
|
+
|
21
|
+
[
|
22
|
+
init_val.nil? ? ini : ini.merge(n => init_val),
|
23
|
+
pcfg + [[n, short, desc, def_val, klass]],
|
24
|
+
]
|
25
|
+
end
|
26
|
+
|
27
|
+
require('optparse')
|
28
|
+
|
29
|
+
acc_opts = {}
|
30
|
+
prsr = OptionParser.new do |opt_p|
|
31
|
+
parser_config.each do |(n, short, desc, def_val, klass)|
|
32
|
+
(_, mark) = short.split(' ')
|
33
|
+
long = "--#{n.to_s.tr("_", "-")}" + (mark.nil? ? '' : " #{mark}")
|
34
|
+
opt_args = klass.nil? ? [short, long, desc] : [short, long, klass, desc]
|
35
|
+
|
36
|
+
T.unsafe(opt_p).on(*opt_args) do |v|
|
37
|
+
acc_opts[n] = if acc_opts.key?(n)
|
38
|
+
Array(acc_opts[n]) + Array(v || def_val)
|
39
|
+
else
|
40
|
+
v || def_val
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
arg_v = args.strip.split(/\s+/).map(&:strip)
|
47
|
+
sub = prsr.parse(arg_v)
|
48
|
+
|
49
|
+
{ opts: start_opts.merge(acc_opts) }.tap do |a|
|
50
|
+
a[:sub] = sub if sub
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/cli/kit/resolver.rb
CHANGED
@@ -1,13 +1,19 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
1
3
|
require 'cli/kit'
|
2
4
|
|
3
5
|
module CLI
|
4
6
|
module Kit
|
5
7
|
class Resolver
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
sig { params(tool_name: String, command_registry: CLI::Kit::CommandRegistry).void }
|
6
11
|
def initialize(tool_name:, command_registry:)
|
7
12
|
@tool_name = tool_name
|
8
13
|
@command_registry = command_registry
|
9
14
|
end
|
10
15
|
|
16
|
+
sig { params(args: T::Array[String]).returns([T.class_of(CLI::Kit::BaseCommand), String, T::Array[String]]) }
|
11
17
|
def call(args)
|
12
18
|
args = args.dup
|
13
19
|
command_name = args.shift
|
@@ -24,6 +30,7 @@ module CLI
|
|
24
30
|
|
25
31
|
private
|
26
32
|
|
33
|
+
sig { params(name: T.nilable(String)).void }
|
27
34
|
def command_not_found(name)
|
28
35
|
CLI::UI::Frame.open('Command not found', color: :red, timing: false) do
|
29
36
|
$stderr.puts(CLI::UI.fmt("{{command:#{@tool_name} #{name}}} was not found"))
|
@@ -52,6 +59,7 @@ module CLI
|
|
52
59
|
end
|
53
60
|
end
|
54
61
|
|
62
|
+
sig { returns(T::Array[String]) }
|
55
63
|
def commands_and_aliases
|
56
64
|
@command_registry.command_names + @command_registry.aliases.keys
|
57
65
|
end
|