cli-kit 4.0.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/cla.yml +22 -0
  3. data/.github/workflows/ruby.yml +34 -2
  4. data/.gitignore +2 -0
  5. data/.rubocop.sorbet.yml +47 -0
  6. data/.rubocop.yml +16 -1
  7. data/Gemfile +10 -1
  8. data/Gemfile.lock +94 -18
  9. data/README.md +46 -3
  10. data/Rakefile +1 -0
  11. data/bin/onchange +30 -0
  12. data/bin/tapioca +29 -0
  13. data/bin/testunit +1 -0
  14. data/cli-kit.gemspec +2 -2
  15. data/dev.yml +35 -3
  16. data/examples/minimal/example.rb +3 -1
  17. data/examples/single-file/example.rb +25 -35
  18. data/gen/lib/gen/commands/help.rb +8 -10
  19. data/gen/lib/gen/commands/new.rb +23 -9
  20. data/gen/lib/gen/commands.rb +21 -9
  21. data/gen/lib/gen/entry_point.rb +12 -3
  22. data/gen/lib/gen/generator.rb +28 -7
  23. data/gen/lib/gen/help.rb +63 -0
  24. data/gen/lib/gen.rb +18 -23
  25. data/gen/template/bin/update-deps +2 -2
  26. data/gen/template/lib/__app__/commands.rb +1 -4
  27. data/gen/template/lib/__app__.rb +8 -17
  28. data/gen/template/test/example_test.rb +1 -1
  29. data/lib/cli/kit/args/definition.rb +344 -0
  30. data/lib/cli/kit/args/evaluation.rb +245 -0
  31. data/lib/cli/kit/args/parser/node.rb +132 -0
  32. data/lib/cli/kit/args/parser.rb +129 -0
  33. data/lib/cli/kit/args/tokenizer.rb +133 -0
  34. data/lib/cli/kit/args.rb +16 -0
  35. data/lib/cli/kit/base_command.rb +17 -32
  36. data/lib/cli/kit/command_help.rb +271 -0
  37. data/lib/cli/kit/command_registry.rb +69 -17
  38. data/lib/cli/kit/config.rb +25 -22
  39. data/lib/cli/kit/core_ext.rb +30 -0
  40. data/lib/cli/kit/error_handler.rb +131 -70
  41. data/lib/cli/kit/executor.rb +19 -3
  42. data/lib/cli/kit/ini.rb +31 -38
  43. data/lib/cli/kit/levenshtein.rb +12 -4
  44. data/lib/cli/kit/logger.rb +16 -2
  45. data/lib/cli/kit/opts.rb +301 -0
  46. data/lib/cli/kit/resolver.rb +8 -0
  47. data/lib/cli/kit/sorbet_runtime_stub.rb +156 -0
  48. data/lib/cli/kit/support/test_helper.rb +23 -14
  49. data/lib/cli/kit/support.rb +2 -0
  50. data/lib/cli/kit/system.rb +188 -54
  51. data/lib/cli/kit/util.rb +48 -103
  52. data/lib/cli/kit/version.rb +3 -1
  53. data/lib/cli/kit.rb +103 -7
  54. metadata +22 -10
  55. data/.github/probots.yml +0 -2
  56. data/lib/cli/kit/autocall.rb +0 -21
  57. 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
- def initialize(path = nil, config: nil, default_section: nil, convert_types: true)
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 = nil
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(@ini, git_format: true).flatten.join("\n")
58
+ to_ini(git_format: true)
55
59
  end
56
60
 
61
+ sig { returns(String) }
57
62
  def to_s
58
- to_ini(@ini).flatten.join("\n")
63
+ to_ini
59
64
  end
60
65
 
61
66
  private
62
67
 
63
- def to_ini(h, git_format: false)
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
- h.each do |k, v|
67
- if section_designator?(k)
68
- str << '' unless str.empty? || git_format
69
- str << k
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
- return if key.nil? && val.nil?
80
-
81
- current_key = @current_key || @default_section
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
@@ -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 = nil
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, # deletion
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
@@ -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
- "[#{CLI::UI::StdoutRouter.current_id[:id]}] #{msg}"
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
@@ -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
@@ -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