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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +3 -0
  3. data/.github/workflows/cla.yml +22 -0
  4. data/.github/workflows/ruby.yml +16 -2
  5. data/.gitignore +2 -0
  6. data/.rubocop.sorbet.yml +47 -0
  7. data/.rubocop.yml +32 -1
  8. data/.ruby-version +1 -0
  9. data/Gemfile +10 -1
  10. data/Gemfile.lock +102 -29
  11. data/README.md +46 -3
  12. data/Rakefile +1 -0
  13. data/bin/onchange +30 -0
  14. data/bin/tapioca +28 -0
  15. data/bin/testunit +1 -0
  16. data/cli-kit.gemspec +9 -4
  17. data/dev.yml +38 -3
  18. data/examples/minimal/example.rb +11 -6
  19. data/examples/single-file/example.rb +25 -35
  20. data/gen/lib/gen/commands/help.rb +8 -10
  21. data/gen/lib/gen/commands/new.rb +23 -9
  22. data/gen/lib/gen/commands.rb +21 -9
  23. data/gen/lib/gen/entry_point.rb +12 -3
  24. data/gen/lib/gen/generator.rb +32 -11
  25. data/gen/lib/gen/help.rb +63 -0
  26. data/gen/lib/gen.rb +18 -23
  27. data/gen/template/bin/update-deps +2 -2
  28. data/gen/template/dev-gems.yml +1 -1
  29. data/gen/template/dev-vendor.yml +1 -1
  30. data/gen/template/lib/__app__/commands.rb +1 -4
  31. data/gen/template/lib/__app__.rb +8 -17
  32. data/gen/template/test/example_test.rb +1 -1
  33. data/lib/cli/kit/args/definition.rb +344 -0
  34. data/lib/cli/kit/args/evaluation.rb +234 -0
  35. data/lib/cli/kit/args/parser/node.rb +132 -0
  36. data/lib/cli/kit/args/parser.rb +129 -0
  37. data/lib/cli/kit/args/tokenizer.rb +133 -0
  38. data/lib/cli/kit/args.rb +16 -0
  39. data/lib/cli/kit/base_command.rb +17 -32
  40. data/lib/cli/kit/command_help.rb +271 -0
  41. data/lib/cli/kit/command_registry.rb +72 -20
  42. data/lib/cli/kit/config.rb +25 -22
  43. data/lib/cli/kit/core_ext.rb +30 -0
  44. data/lib/cli/kit/error_handler.rb +131 -70
  45. data/lib/cli/kit/executor.rb +20 -3
  46. data/lib/cli/kit/ini.rb +31 -38
  47. data/lib/cli/kit/levenshtein.rb +12 -4
  48. data/lib/cli/kit/logger.rb +16 -2
  49. data/lib/cli/kit/opts.rb +301 -0
  50. data/lib/cli/kit/parse_args.rb +55 -0
  51. data/lib/cli/kit/resolver.rb +8 -0
  52. data/lib/cli/kit/sorbet_runtime_stub.rb +154 -0
  53. data/lib/cli/kit/support/test_helper.rb +27 -16
  54. data/lib/cli/kit/support.rb +2 -0
  55. data/lib/cli/kit/system.rb +194 -57
  56. data/lib/cli/kit/util.rb +48 -103
  57. data/lib/cli/kit/version.rb +3 -1
  58. data/lib/cli/kit.rb +104 -7
  59. metadata +30 -14
  60. data/.github/probots.yml +0 -2
  61. data/lib/cli/kit/autocall.rb +0 -21
  62. data/lib/cli/kit/ruby_backports/enumerable.rb +0 -6
@@ -0,0 +1,154 @@
1
+ # typed: ignore
2
+ # frozen_string_literal: true
3
+
4
+ module T
5
+ class << self
6
+ def absurd(value); end
7
+ def all(type_a, type_b, *types); end
8
+ def any(type_a, type_b, *types); end
9
+ def attached_class; end
10
+ def class_of(klass); end
11
+ def enum(values); end
12
+ def nilable(type); end
13
+ def noreturn; end
14
+ def self_type; end
15
+ def type_alias(type = nil, &_blk); end
16
+ def type_parameter(name); end
17
+ def untyped; end
18
+
19
+ def assert_type!(value, _type, _checked: true)
20
+ value
21
+ end
22
+
23
+ def cast(value, _type, _checked: true)
24
+ value
25
+ end
26
+
27
+ def let(value, _type, _checked: true)
28
+ value
29
+ end
30
+
31
+ def must(arg, _msg = nil)
32
+ arg
33
+ end
34
+
35
+ def proc
36
+ T::Proc.new
37
+ end
38
+
39
+ def reveal_type(value)
40
+ value
41
+ end
42
+
43
+ def unsafe(value)
44
+ value
45
+ end
46
+ end
47
+
48
+ module Sig
49
+ def sig(arg0 = nil, &blk); end
50
+ end
51
+
52
+ module Helpers
53
+ def abstract!; end
54
+ def interface!; end
55
+ def final!; end
56
+ def sealed!; end
57
+ def mixes_in_class_methods(mod); end
58
+ end
59
+
60
+ module Generic
61
+ include(T::Helpers)
62
+
63
+ def type_parameters(*params); end
64
+ def type_member(variance = :invariant, fixed: nil, lower: nil, upper: BasicObject); end
65
+ def type_template(variance = :invariant, fixed: nil, lower: nil, upper: BasicObject); end
66
+
67
+ def [](*types)
68
+ self
69
+ end
70
+ end
71
+
72
+ module Array
73
+ class << self
74
+ def [](type); end
75
+ end
76
+ end
77
+
78
+ Boolean = Object.new.freeze
79
+
80
+ module Configuration
81
+ class << self
82
+ def call_validation_error_handler(signature, opts); end
83
+ def call_validation_error_handler=(value); end
84
+ def default_checked_level=(default_checked_level); end
85
+ def enable_checking_for_sigs_marked_checked_tests; end
86
+ def enable_final_checks_on_hooks; end
87
+ def enable_legacy_t_enum_migration_mode; end
88
+ def reset_final_checks_on_hooks; end
89
+ def hard_assert_handler(str, extra); end
90
+ def hard_assert_handler=(value); end
91
+ def inline_type_error_handler(error); end
92
+ def inline_type_error_handler=(value); end
93
+ def log_info_handler(str, extra); end
94
+ def log_info_handler=(value); end
95
+ def scalar_types; end
96
+ def scalar_types=(values); end
97
+ def sealed_violation_whitelist; end
98
+ def sealed_violation_whitelist=(sealed_violation_whitelist); end
99
+ def sig_builder_error_handler=(value); end
100
+ def sig_validation_error_handler(error, opts); end
101
+ def sig_validation_error_handler=(value); end
102
+ def soft_assert_handler(str, extra); end
103
+ def soft_assert_handler=(value); end
104
+ end
105
+ end
106
+
107
+ module Enumerable
108
+ class << self
109
+ def [](type); end
110
+ end
111
+ end
112
+
113
+ module Enumerator
114
+ class << self
115
+ def [](type); end
116
+ end
117
+ end
118
+
119
+ module Hash
120
+ class << self
121
+ def [](keys, values); end
122
+ end
123
+ end
124
+
125
+ class Proc
126
+ def bind(*_)
127
+ self
128
+ end
129
+
130
+ def params(*_param)
131
+ self
132
+ end
133
+
134
+ def void
135
+ self
136
+ end
137
+
138
+ def returns(_type)
139
+ self
140
+ end
141
+ end
142
+
143
+ module Range
144
+ class << self
145
+ def [](type); end
146
+ end
147
+ end
148
+
149
+ module Set
150
+ class << self
151
+ def [](type); end
152
+ end
153
+ end
154
+ end
@@ -1,3 +1,5 @@
1
+ require 'cli/kit'
2
+
1
3
  module CLI
2
4
  module Kit
3
5
  module Support
@@ -10,13 +12,17 @@ module CLI
10
12
  def assert_all_commands_run(should_raise: true)
11
13
  errors = CLI::Kit::System.error_message
12
14
  CLI::Kit::System.reset!
13
- assert(false, errors) if should_raise && !errors.nil?
15
+ # this is in minitest, but sorbet doesn't know that. probably we
16
+ # could structure this better.
17
+ T.unsafe(self).assert(false, errors) if should_raise && !errors.nil?
14
18
  errors
15
19
  end
16
20
 
17
21
  def teardown
18
22
  super
19
23
  assert_all_commands_run
24
+ rescue Errno::EACCES
25
+ # this sometimes happens on windows builds - let's ignore it
20
26
  end
21
27
 
22
28
  module FakeConfig
@@ -52,30 +58,32 @@ module CLI
52
58
  module System
53
59
  class << self
54
60
  alias_method :original_system, :system
55
- def system(*a, sudo: false, env: {}, **kwargs)
56
- expected_command = expected_command(*a, sudo: sudo, env: env)
61
+ def system(cmd, *a, sudo: false, env: {}, stdin: nil, **kwargs)
62
+ a.unshift(cmd)
63
+ expected_command = expected_command(a, sudo: sudo, env: env)
57
64
 
58
65
  # In the case of an unexpected command, expected_command will be nil
59
66
  return FakeSuccess.new(false) if expected_command.nil?
60
67
 
61
68
  # Otherwise handle the command
62
69
  if expected_command[:allow]
63
- original_system(*a, sudo: sudo, env: env, **kwargs)
70
+ T.unsafe(self).original_system(*a, sudo: sudo, env: env, **kwargs)
64
71
  else
65
72
  FakeSuccess.new(expected_command[:success])
66
73
  end
67
74
  end
68
75
 
69
76
  alias_method :original_capture2, :capture2
70
- def capture2(*a, sudo: false, env: {}, **kwargs)
71
- expected_command = expected_command(*a, sudo: sudo, env: env)
77
+ def capture2(cmd, *a, sudo: false, env: {}, **kwargs)
78
+ a.unshift(cmd)
79
+ expected_command = expected_command(a, sudo: sudo, env: env)
72
80
 
73
81
  # In the case of an unexpected command, expected_command will be nil
74
82
  return [nil, FakeSuccess.new(false)] if expected_command.nil?
75
83
 
76
84
  # Otherwise handle the command
77
85
  if expected_command[:allow]
78
- original_capture2(*a, sudo: sudo, env: env, **kwargs)
86
+ T.unsafe(self).original_capture2(*a, sudo: sudo, env: env, **kwargs)
79
87
  else
80
88
  [
81
89
  expected_command[:stdout],
@@ -85,15 +93,16 @@ module CLI
85
93
  end
86
94
 
87
95
  alias_method :original_capture2e, :capture2e
88
- def capture2e(*a, sudo: false, env: {}, **kwargs)
89
- expected_command = expected_command(*a, sudo: sudo, env: env)
96
+ def capture2e(cmd, *a, sudo: false, env: {}, **kwargs)
97
+ a.unshift(cmd)
98
+ expected_command = expected_command(a, sudo: sudo, env: env)
90
99
 
91
100
  # In the case of an unexpected command, expected_command will be nil
92
101
  return [nil, FakeSuccess.new(false)] if expected_command.nil?
93
102
 
94
103
  # Otherwise handle the command
95
104
  if expected_command[:allow]
96
- original_capture2ecapture2e(*a, sudo: sudo, env: env, **kwargs)
105
+ T.unsafe(self).original_capture2e(*a, sudo: sudo, env: env, **kwargs)
97
106
  else
98
107
  [
99
108
  expected_command[:stdout],
@@ -103,15 +112,16 @@ module CLI
103
112
  end
104
113
 
105
114
  alias_method :original_capture3, :capture3
106
- def capture3(*a, sudo: false, env: {}, **kwargs)
107
- expected_command = expected_command(*a, sudo: sudo, env: env)
115
+ def capture3(cmd, *a, sudo: false, env: {}, **kwargs)
116
+ a.unshift(cmd)
117
+ expected_command = expected_command(a, sudo: sudo, env: env)
108
118
 
109
119
  # In the case of an unexpected command, expected_command will be nil
110
120
  return [nil, nil, FakeSuccess.new(false)] if expected_command.nil?
111
121
 
112
122
  # Otherwise handle the command
113
123
  if expected_command[:allow]
114
- original_capture3(*a, sudo: sudo, env: env, **kwargs)
124
+ T.unsafe(self).original_capture3(*a, sudo: sudo, env: env, **kwargs)
115
125
  else
116
126
  [
117
127
  expected_command[:stdout],
@@ -215,18 +225,19 @@ module CLI
215
225
  EOF
216
226
  end
217
227
 
218
- return nil if final_error.empty?
228
+ return if final_error.empty?
229
+
219
230
  "\n" + final_error.join("\n") # Initial new line for formatting reasons
220
231
  end
221
232
 
222
233
  private
223
234
 
224
- def expected_command(*a, sudo: raise, env: raise)
235
+ def expected_command(a, sudo: raise, env: raise)
225
236
  expected_cmd = @delegate_open3[a.join(' ')]
226
237
 
227
238
  if expected_cmd.nil?
228
239
  @delegate_open3[a.join(' ')] = { unexpected: true }
229
- return nil
240
+ return
230
241
  end
231
242
 
232
243
  expected_cmd[:run] = true
@@ -1,3 +1,5 @@
1
+ # typed: true
2
+
1
3
  require 'cli/kit'
2
4
 
3
5
  module CLI
@@ -1,5 +1,6 @@
1
- require 'cli/kit'
1
+ # typed: true
2
2
 
3
+ require 'cli/kit'
3
4
  require 'open3'
4
5
  require 'English'
5
6
 
@@ -8,6 +9,8 @@ module CLI
8
9
  module System
9
10
  SUDO_PROMPT = CLI::UI.fmt('{{info:(sudo)}} Password: ')
10
11
  class << self
12
+ extend T::Sig
13
+
11
14
  # Ask for sudo access with a message explaning the need for it
12
15
  # Will make subsequent commands capable of running with sudo for a period of time
13
16
  #
@@ -17,10 +20,12 @@ module CLI
17
20
  # #### Usage
18
21
  # `ctx.sudo_reason("We need to do a thing")`
19
22
  #
23
+ sig { params(msg: String).void }
20
24
  def sudo_reason(msg)
21
25
  # See if sudo has a cached password
22
- %x(env SUDO_ASKPASS=/usr/bin/false sudo -A true)
26
+ %x(env SUDO_ASKPASS=/usr/bin/false sudo -A true > /dev/null 2>&1)
23
27
  return if $CHILD_STATUS.success?
28
+
24
29
  CLI::UI.with_frame_color(:blue) do
25
30
  puts(CLI::UI.fmt("{{i}} #{msg}"))
26
31
  end
@@ -43,8 +48,18 @@ module CLI
43
48
  # #### Usage
44
49
  # `out, stat = CLI::Kit::System.capture2('ls', 'a_folder')`
45
50
  #
46
- def capture2(*a, sudo: false, env: ENV, **kwargs)
47
- delegate_open3(*a, sudo: sudo, env: env, method: :capture2, **kwargs)
51
+ sig do
52
+ params(
53
+ cmd: String,
54
+ args: String,
55
+ sudo: T.any(T::Boolean, String),
56
+ env: T::Hash[String, T.nilable(String)],
57
+ kwargs: T.untyped,
58
+ )
59
+ .returns([String, Process::Status])
60
+ end
61
+ def capture2(cmd, *args, sudo: false, env: ENV.to_h, **kwargs)
62
+ delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :capture2)
48
63
  end
49
64
 
50
65
  # Execute a command in the user's environment
@@ -64,8 +79,18 @@ module CLI
64
79
  # #### Usage
65
80
  # `out_and_err, stat = CLI::Kit::System.capture2e('ls', 'a_folder')`
66
81
  #
67
- def capture2e(*a, sudo: false, env: ENV, **kwargs)
68
- delegate_open3(*a, sudo: sudo, env: env, method: :capture2e, **kwargs)
82
+ sig do
83
+ params(
84
+ cmd: String,
85
+ args: String,
86
+ sudo: T.any(T::Boolean, String),
87
+ env: T::Hash[String, T.nilable(String)],
88
+ kwargs: T.untyped,
89
+ )
90
+ .returns([String, Process::Status])
91
+ end
92
+ def capture2e(cmd, *args, sudo: false, env: ENV.to_h, **kwargs)
93
+ delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :capture2e)
69
94
  end
70
95
 
71
96
  # Execute a command in the user's environment
@@ -86,20 +111,72 @@ module CLI
86
111
  # #### Usage
87
112
  # `out, err, stat = CLI::Kit::System.capture3('ls', 'a_folder')`
88
113
  #
89
- def capture3(*a, sudo: false, env: ENV, **kwargs)
90
- delegate_open3(*a, sudo: sudo, env: env, method: :capture3, **kwargs)
114
+ sig do
115
+ params(
116
+ cmd: String,
117
+ args: String,
118
+ sudo: T.any(T::Boolean, String),
119
+ env: T::Hash[String, T.nilable(String)],
120
+ kwargs: T.untyped,
121
+ )
122
+ .returns([String, String, Process::Status])
123
+ end
124
+ def capture3(cmd, *args, sudo: false, env: ENV.to_h, **kwargs)
125
+ delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :capture3)
91
126
  end
92
127
 
93
- def popen2(*a, sudo: false, env: ENV, **kwargs, &block)
94
- delegate_open3(*a, sudo: sudo, env: env, method: :popen2, **kwargs, &block)
128
+ sig do
129
+ params(
130
+ cmd: String,
131
+ args: String,
132
+ sudo: T.any(T::Boolean, String),
133
+ env: T::Hash[String, T.nilable(String)],
134
+ kwargs: T.untyped,
135
+ block: T.nilable(
136
+ T.proc.params(stdin: IO, stdout: IO, wait_thr: Process::Waiter)
137
+ .returns([IO, IO, Process::Waiter]),
138
+ ),
139
+ )
140
+ .returns([IO, IO, Process::Waiter])
141
+ end
142
+ def popen2(cmd, *args, sudo: false, env: ENV.to_h, **kwargs, &block)
143
+ delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :popen2, &block)
95
144
  end
96
145
 
97
- def popen2e(*a, sudo: false, env: ENV, **kwargs, &block)
98
- delegate_open3(*a, sudo: sudo, env: env, method: :popen2e, **kwargs, &block)
146
+ sig do
147
+ params(
148
+ cmd: String,
149
+ args: String,
150
+ sudo: T.any(T::Boolean, String),
151
+ env: T::Hash[String, T.nilable(String)],
152
+ kwargs: T.untyped,
153
+ block: T.nilable(
154
+ T.proc.params(stdin: IO, stdout: IO, wait_thr: Process::Waiter)
155
+ .returns([IO, IO, Process::Waiter]),
156
+ ),
157
+ )
158
+ .returns([IO, IO, Process::Waiter])
159
+ end
160
+ def popen2e(cmd, *args, sudo: false, env: ENV.to_h, **kwargs, &block)
161
+ delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :popen2e, &block)
99
162
  end
100
163
 
101
- def popen3(*a, sudo: false, env: ENV, **kwargs, &block)
102
- delegate_open3(*a, sudo: sudo, env: env, method: :popen3, **kwargs, &block)
164
+ sig do
165
+ params(
166
+ cmd: String,
167
+ args: String,
168
+ sudo: T.any(T::Boolean, String),
169
+ env: T::Hash[String, T.nilable(String)],
170
+ kwargs: T.untyped,
171
+ block: T.nilable(
172
+ T.proc.params(stdin: IO, stdout: IO, stderr: IO, wait_thr: Process::Waiter)
173
+ .returns([IO, IO, IO, Process::Waiter]),
174
+ ),
175
+ )
176
+ .returns([IO, IO, IO, Process::Waiter])
177
+ end
178
+ def popen3(cmd, *args, sudo: false, env: ENV.to_h, **kwargs, &block)
179
+ delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :popen3, &block)
103
180
  end
104
181
 
105
182
  # Execute a command in the user's environment
@@ -117,13 +194,32 @@ module CLI
117
194
  # #### Usage
118
195
  # `stat = CLI::Kit::System.system('ls', 'a_folder')`
119
196
  #
120
- def system(*a, sudo: false, env: ENV, **kwargs)
121
- a = apply_sudo(*a, sudo)
197
+ sig do
198
+ params(
199
+ cmd: String,
200
+ args: String,
201
+ sudo: T.any(T::Boolean, String),
202
+ env: T::Hash[String, T.nilable(String)],
203
+ stdin: T.nilable(T.any(IO, String, Integer, Symbol)),
204
+ kwargs: T.untyped,
205
+ block: T.nilable(T.proc.params(out: String, err: String).void),
206
+ )
207
+ .returns(Process::Status)
208
+ end
209
+ def system(cmd, *args, sudo: false, env: ENV.to_h, stdin: nil, **kwargs, &block)
210
+ cmd, args = apply_sudo(cmd, args, sudo)
122
211
 
123
212
  out_r, out_w = IO.pipe
124
213
  err_r, err_w = IO.pipe
125
- in_stream = STDIN.closed? ? :close : STDIN
126
- pid = Process.spawn(env, *resolve_path(a, env), 0 => in_stream, :out => out_w, :err => err_w, **kwargs)
214
+ in_stream = if stdin
215
+ stdin
216
+ elsif STDIN.closed?
217
+ :close
218
+ else
219
+ STDIN
220
+ end
221
+ cmd, args = resolve_path(cmd, args, env)
222
+ pid = T.unsafe(Process).spawn(env, cmd, *args, 0 => in_stream, :out => out_w, :err => err_w, **kwargs)
127
223
  out_w.close
128
224
  err_w.close
129
225
 
@@ -141,10 +237,14 @@ module CLI
141
237
 
142
238
  previous_trailing = Hash.new('')
143
239
  loop do
240
+ break if Process.wait(pid, Process::WNOHANG)
241
+
144
242
  ios = [err_r, out_r].reject(&:closed?)
145
- break if ios.empty?
243
+ next if ios.empty?
244
+
245
+ readers, = IO.select(ios, [], [], 1)
246
+ next if readers.nil? # If IO.select times out we iterate again so we can check if the process has exited
146
247
 
147
- readers, = IO.select(ios)
148
248
  readers.each do |io|
149
249
  data, trailing = split_partial_characters(io.readpartial(4096))
150
250
  handlers[io].call(previous_trailing[io] + data)
@@ -154,50 +254,100 @@ module CLI
154
254
  end
155
255
  end
156
256
 
157
- Process.wait(pid)
158
257
  $CHILD_STATUS
159
258
  end
160
259
 
161
260
  # Split off trailing partial UTF-8 Characters. UTF-8 Multibyte characters start with a 11xxxxxx byte that tells
162
261
  # how many following bytes are part of this character, followed by some number of 10xxxxxx bytes. This simple
163
262
  # algorithm will split off a whole trailing multi-byte character.
263
+ sig { params(data: String).returns([String, String]) }
164
264
  def split_partial_characters(data)
165
- last_byte = data.getbyte(-1)
265
+ last_byte = T.must(data.getbyte(-1))
166
266
  return [data, ''] if (last_byte & 0b1000_0000).zero?
167
267
 
168
- # UTF-8 is up to 6 characters per rune, so we could never want to trim more than that, and we want to avoid
268
+ # UTF-8 is up to 4 characters per rune, so we could never want to trim more than that, and we want to avoid
169
269
  # allocating an array for the whole of data with bytes
170
- min_bound = -[6, data.bytesize].min
171
- final_bytes = data.byteslice(min_bound..-1).bytes
270
+ min_bound = -[4, data.bytesize].min
271
+ final_bytes = T.must(data.byteslice(min_bound..-1)).bytes
172
272
  partial_character_sub_index = final_bytes.rindex { |byte| byte & 0b1100_0000 == 0b1100_0000 }
273
+
173
274
  # Bail out for non UTF-8
174
275
  return [data, ''] unless partial_character_sub_index
276
+
277
+ start_byte = final_bytes[partial_character_sub_index]
278
+ full_size = if start_byte & 0b1111_1000 == 0b1111_0000
279
+ 4
280
+ elsif start_byte & 0b1111_0000 == 0b1110_0000
281
+ 3
282
+ elsif start_byte & 0b1110_0000 == 0b110_00000
283
+ 2
284
+ else
285
+ nil # Not a valid UTF-8 character
286
+ end
287
+ return [data, ''] if full_size.nil? # Bail out for non UTF-8
288
+
289
+ if final_bytes.size - partial_character_sub_index == full_size
290
+ # We have a full UTF-8 character, so we can just return the data
291
+ return [data, '']
292
+ end
293
+
175
294
  partial_character_index = min_bound + partial_character_sub_index
176
295
 
177
- [data.byteslice(0...partial_character_index), data.byteslice(partial_character_index..-1)]
296
+ [T.must(data.byteslice(0...partial_character_index)), T.must(data.byteslice(partial_character_index..-1))]
178
297
  end
179
298
 
299
+ sig { returns(Symbol) }
180
300
  def os
181
301
  return :mac if /darwin/.match(RUBY_PLATFORM)
182
302
  return :linux if /linux/.match(RUBY_PLATFORM)
183
- return :windows if /mingw32/.match(RUBY_PLATFORM)
303
+ return :windows if /mingw/.match(RUBY_PLATFORM)
184
304
 
185
305
  raise "Could not determine OS from platform #{RUBY_PLATFORM}"
186
306
  end
187
307
 
308
+ sig { params(cmd: String, env: T::Hash[String, T.nilable(String)]).returns(T.nilable(String)) }
309
+ def which(cmd, env)
310
+ exts = os == :windows ? (env['PATHEXT'] || 'exe').split(';') : ['']
311
+ (env['PATH'] || '').split(File::PATH_SEPARATOR).each do |path|
312
+ exts.each do |ext|
313
+ exe = File.join(path, "#{cmd}#{ext}")
314
+ return exe if File.executable?(exe) && !File.directory?(exe)
315
+ end
316
+ end
317
+
318
+ nil
319
+ end
320
+
188
321
  private
189
322
 
190
- def apply_sudo(*a, sudo)
191
- a.unshift('sudo', '-S', '-p', SUDO_PROMPT, '--') if sudo
323
+ sig do
324
+ params(cmd: String, args: T::Array[String], sudo: T.any(T::Boolean, String))
325
+ .returns([String, T::Array[String]])
326
+ end
327
+ def apply_sudo(cmd, args, sudo)
328
+ return [cmd, args] if !sudo || Process.uid.zero?
329
+
192
330
  sudo_reason(sudo) if sudo.is_a?(String)
193
- a
331
+ ['sudo', args.unshift('-E', '-S', '-p', SUDO_PROMPT, '--', cmd)]
194
332
  end
195
333
 
196
- def delegate_open3(*a, sudo: raise, env: raise, method: raise, **kwargs, &block)
197
- a = apply_sudo(*a, sudo)
198
- Open3.send(method, env, *resolve_path(a, env), **kwargs, &block)
334
+ sig do
335
+ params(
336
+ cmd: String,
337
+ args: T::Array[String],
338
+ kwargs: T::Hash[Symbol, T.untyped],
339
+ sudo: T.any(T::Boolean, String),
340
+ env: T::Hash[String, T.nilable(String)],
341
+ method: Symbol,
342
+ block: T.untyped,
343
+ ).returns(T.untyped)
344
+ end
345
+ def delegate_open3(cmd, args, kwargs, sudo: raise, env: raise, method: raise, &block)
346
+ cmd, args = apply_sudo(cmd, args, sudo)
347
+ cmd, args = resolve_path(cmd, args, env)
348
+ T.unsafe(Open3).send(method, env, cmd, *args, **kwargs, &block)
199
349
  rescue Errno::EINTR
200
- raise(Errno::EINTR, "command interrupted: #{a.join(" ")}")
350
+ raise(Errno::EINTR, "command interrupted: #{cmd} #{args.join(" ")}")
201
351
  end
202
352
 
203
353
  # Ruby resolves the program to execute using its own PATH, but we want it to
@@ -209,32 +359,19 @@ module CLI
209
359
  # project.
210
360
  #
211
361
  # See https://github.com/Shopify/dev/pull/625 for more details.
212
- def resolve_path(a, env)
213
- # If only one argument was provided, make sure it's interpreted by a shell.
214
- if a.size == 1
215
- if os == :windows
216
- return ['break && ' + a[0]]
217
- else
218
- return ['true ; ' + a[0]]
219
- end
220
- end
221
- return a if a.first.include?('/')
222
-
223
- item = which(a.first, env)
224
- a[0] = item if item
225
- a
362
+ sig do
363
+ params(cmd: String, args: T::Array[String], env: T::Hash[String, T.nilable(String)])
364
+ .returns([String, T::Array[String]])
226
365
  end
227
-
228
- def which(cmd, env)
229
- exts = os == :windows ? env.fetch('PATHEXT').split(';') : ['']
230
- env.fetch('PATH', '').split(File::PATH_SEPARATOR).each do |path|
231
- exts.each do |ext|
232
- exe = File.join(path, "#{cmd}#{ext}")
233
- return exe if File.executable?(exe) && !File.directory?(exe)
234
- end
366
+ def resolve_path(cmd, args, env)
367
+ # If only one argument was provided, make sure it's interpreted by a shell.
368
+ if args.empty?
369
+ prefix = os == :windows ? 'break && ' : 'true ; '
370
+ return [prefix + cmd, []]
235
371
  end
372
+ return [cmd, args] if cmd.include?('/')
236
373
 
237
- nil
374
+ [which(cmd, env) || cmd, args]
238
375
  end
239
376
  end
240
377
  end