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.
- checksums.yaml +4 -4
- data/.github/workflows/cla.yml +22 -0
- data/.github/workflows/ruby.yml +34 -2
- data/.gitignore +2 -0
- data/.rubocop.sorbet.yml +47 -0
- data/.rubocop.yml +16 -1
- data/Gemfile +10 -1
- data/Gemfile.lock +94 -18
- data/README.md +46 -3
- data/Rakefile +1 -0
- data/bin/onchange +30 -0
- data/bin/tapioca +29 -0
- data/bin/testunit +1 -0
- data/cli-kit.gemspec +2 -2
- data/dev.yml +35 -3
- data/examples/minimal/example.rb +3 -1
- 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 +28 -7
- 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/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 +245 -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 +69 -17
- 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 +19 -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/resolver.rb +8 -0
- data/lib/cli/kit/sorbet_runtime_stub.rb +156 -0
- data/lib/cli/kit/support/test_helper.rb +23 -14
- data/lib/cli/kit/support.rb +2 -0
- data/lib/cli/kit/system.rb +188 -54
- data/lib/cli/kit/util.rb +48 -103
- data/lib/cli/kit/version.rb +3 -1
- data/lib/cli/kit.rb +103 -7
- metadata +22 -10
- data/.github/probots.yml +0 -2
- data/lib/cli/kit/autocall.rb +0 -21
- data/lib/cli/kit/ruby_backports/enumerable.rb +0 -6
@@ -0,0 +1,156 @@
|
|
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
|
+
# rubocop:disable Naming/InclusiveLanguage
|
98
|
+
def sealed_violation_whitelist; end
|
99
|
+
def sealed_violation_whitelist=(sealed_violation_whitelist); end
|
100
|
+
# rubocop:enable Naming/InclusiveLanguage
|
101
|
+
def sig_builder_error_handler=(value); end
|
102
|
+
def sig_validation_error_handler(error, opts); end
|
103
|
+
def sig_validation_error_handler=(value); end
|
104
|
+
def soft_assert_handler(str, extra); end
|
105
|
+
def soft_assert_handler=(value); end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
module Enumerable
|
110
|
+
class << self
|
111
|
+
def [](type); end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
module Enumerator
|
116
|
+
class << self
|
117
|
+
def [](type); end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
module Hash
|
122
|
+
class << self
|
123
|
+
def [](keys, values); end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
class Proc
|
128
|
+
def bind(*_)
|
129
|
+
self
|
130
|
+
end
|
131
|
+
|
132
|
+
def params(*_param)
|
133
|
+
self
|
134
|
+
end
|
135
|
+
|
136
|
+
def void
|
137
|
+
self
|
138
|
+
end
|
139
|
+
|
140
|
+
def returns(_type)
|
141
|
+
self
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
module Range
|
146
|
+
class << self
|
147
|
+
def [](type); end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
module Set
|
152
|
+
class << self
|
153
|
+
def [](type); end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'cli/kit'
|
2
|
+
|
1
3
|
module CLI
|
2
4
|
module Kit
|
3
5
|
module Support
|
@@ -10,7 +12,9 @@ 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
|
-
|
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
|
|
@@ -52,30 +56,32 @@ module CLI
|
|
52
56
|
module System
|
53
57
|
class << self
|
54
58
|
alias_method :original_system, :system
|
55
|
-
def system(*a, sudo: false, env: {}, **kwargs)
|
56
|
-
|
59
|
+
def system(cmd, *a, sudo: false, env: {}, stdin: nil, **kwargs)
|
60
|
+
a.unshift(cmd)
|
61
|
+
expected_command = expected_command(a, sudo: sudo, env: env)
|
57
62
|
|
58
63
|
# In the case of an unexpected command, expected_command will be nil
|
59
64
|
return FakeSuccess.new(false) if expected_command.nil?
|
60
65
|
|
61
66
|
# Otherwise handle the command
|
62
67
|
if expected_command[:allow]
|
63
|
-
original_system(*a, sudo: sudo, env: env, **kwargs)
|
68
|
+
T.unsafe(self).original_system(*a, sudo: sudo, env: env, **kwargs)
|
64
69
|
else
|
65
70
|
FakeSuccess.new(expected_command[:success])
|
66
71
|
end
|
67
72
|
end
|
68
73
|
|
69
74
|
alias_method :original_capture2, :capture2
|
70
|
-
def capture2(*a, sudo: false, env: {}, **kwargs)
|
71
|
-
|
75
|
+
def capture2(cmd, *a, sudo: false, env: {}, **kwargs)
|
76
|
+
a.unshift(cmd)
|
77
|
+
expected_command = expected_command(a, sudo: sudo, env: env)
|
72
78
|
|
73
79
|
# In the case of an unexpected command, expected_command will be nil
|
74
80
|
return [nil, FakeSuccess.new(false)] if expected_command.nil?
|
75
81
|
|
76
82
|
# Otherwise handle the command
|
77
83
|
if expected_command[:allow]
|
78
|
-
original_capture2(*a, sudo: sudo, env: env, **kwargs)
|
84
|
+
T.unsafe(self).original_capture2(*a, sudo: sudo, env: env, **kwargs)
|
79
85
|
else
|
80
86
|
[
|
81
87
|
expected_command[:stdout],
|
@@ -85,15 +91,16 @@ module CLI
|
|
85
91
|
end
|
86
92
|
|
87
93
|
alias_method :original_capture2e, :capture2e
|
88
|
-
def capture2e(*a, sudo: false, env: {}, **kwargs)
|
89
|
-
|
94
|
+
def capture2e(cmd, *a, sudo: false, env: {}, **kwargs)
|
95
|
+
a.unshift(cmd)
|
96
|
+
expected_command = expected_command(a, sudo: sudo, env: env)
|
90
97
|
|
91
98
|
# In the case of an unexpected command, expected_command will be nil
|
92
99
|
return [nil, FakeSuccess.new(false)] if expected_command.nil?
|
93
100
|
|
94
101
|
# Otherwise handle the command
|
95
102
|
if expected_command[:allow]
|
96
|
-
|
103
|
+
T.unsafe(self).original_capture2e(*a, sudo: sudo, env: env, **kwargs)
|
97
104
|
else
|
98
105
|
[
|
99
106
|
expected_command[:stdout],
|
@@ -103,15 +110,16 @@ module CLI
|
|
103
110
|
end
|
104
111
|
|
105
112
|
alias_method :original_capture3, :capture3
|
106
|
-
def capture3(*a, sudo: false, env: {}, **kwargs)
|
107
|
-
|
113
|
+
def capture3(cmd, *a, sudo: false, env: {}, **kwargs)
|
114
|
+
a.unshift(cmd)
|
115
|
+
expected_command = expected_command(a, sudo: sudo, env: env)
|
108
116
|
|
109
117
|
# In the case of an unexpected command, expected_command will be nil
|
110
118
|
return [nil, nil, FakeSuccess.new(false)] if expected_command.nil?
|
111
119
|
|
112
120
|
# Otherwise handle the command
|
113
121
|
if expected_command[:allow]
|
114
|
-
original_capture3(*a, sudo: sudo, env: env, **kwargs)
|
122
|
+
T.unsafe(self).original_capture3(*a, sudo: sudo, env: env, **kwargs)
|
115
123
|
else
|
116
124
|
[
|
117
125
|
expected_command[:stdout],
|
@@ -216,12 +224,13 @@ module CLI
|
|
216
224
|
end
|
217
225
|
|
218
226
|
return nil if final_error.empty?
|
227
|
+
|
219
228
|
"\n" + final_error.join("\n") # Initial new line for formatting reasons
|
220
229
|
end
|
221
230
|
|
222
231
|
private
|
223
232
|
|
224
|
-
def expected_command(
|
233
|
+
def expected_command(a, sudo: raise, env: raise)
|
225
234
|
expected_cmd = @delegate_open3[a.join(' ')]
|
226
235
|
|
227
236
|
if expected_cmd.nil?
|
data/lib/cli/kit/support.rb
CHANGED
data/lib/cli/kit/system.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
|
-
|
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
|
-
|
47
|
-
|
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
|
-
|
68
|
-
|
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
|
-
|
90
|
-
|
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
|
-
|
94
|
-
|
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
|
-
|
98
|
-
|
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
|
-
|
102
|
-
|
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
|
-
|
121
|
-
|
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 =
|
126
|
-
|
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
|
|
@@ -145,7 +241,7 @@ module CLI
|
|
145
241
|
break if ios.empty?
|
146
242
|
|
147
243
|
readers, = IO.select(ios)
|
148
|
-
readers.each do |io|
|
244
|
+
(readers || []).each do |io|
|
149
245
|
data, trailing = split_partial_characters(io.readpartial(4096))
|
150
246
|
handlers[io].call(previous_trailing[io] + data)
|
151
247
|
previous_trailing[io] = trailing
|
@@ -161,22 +257,43 @@ module CLI
|
|
161
257
|
# Split off trailing partial UTF-8 Characters. UTF-8 Multibyte characters start with a 11xxxxxx byte that tells
|
162
258
|
# how many following bytes are part of this character, followed by some number of 10xxxxxx bytes. This simple
|
163
259
|
# algorithm will split off a whole trailing multi-byte character.
|
260
|
+
sig { params(data: String).returns([String, String]) }
|
164
261
|
def split_partial_characters(data)
|
165
|
-
last_byte = data.getbyte(-1)
|
262
|
+
last_byte = T.must(data.getbyte(-1))
|
166
263
|
return [data, ''] if (last_byte & 0b1000_0000).zero?
|
167
264
|
|
168
|
-
# UTF-8 is up to
|
265
|
+
# 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
266
|
# allocating an array for the whole of data with bytes
|
170
|
-
min_bound = -[
|
171
|
-
final_bytes = data.byteslice(min_bound..-1).bytes
|
267
|
+
min_bound = -[4, data.bytesize].min
|
268
|
+
final_bytes = T.must(data.byteslice(min_bound..-1)).bytes
|
172
269
|
partial_character_sub_index = final_bytes.rindex { |byte| byte & 0b1100_0000 == 0b1100_0000 }
|
270
|
+
|
173
271
|
# Bail out for non UTF-8
|
174
272
|
return [data, ''] unless partial_character_sub_index
|
273
|
+
|
274
|
+
start_byte = final_bytes[partial_character_sub_index]
|
275
|
+
full_size = if start_byte & 0b1111_1000 == 0b1111_0000
|
276
|
+
4
|
277
|
+
elsif start_byte & 0b1111_0000 == 0b1110_0000
|
278
|
+
3
|
279
|
+
elsif start_byte & 0b1110_0000 == 0b110_00000
|
280
|
+
2
|
281
|
+
else
|
282
|
+
nil # Not a valid UTF-8 character
|
283
|
+
end
|
284
|
+
return [data, ''] if full_size.nil? # Bail out for non UTF-8
|
285
|
+
|
286
|
+
if final_bytes.size - partial_character_sub_index == full_size
|
287
|
+
# We have a full UTF-8 character, so we can just return the data
|
288
|
+
return [data, '']
|
289
|
+
end
|
290
|
+
|
175
291
|
partial_character_index = min_bound + partial_character_sub_index
|
176
292
|
|
177
|
-
[data.byteslice(0...partial_character_index), data.byteslice(partial_character_index..-1)]
|
293
|
+
[T.must(data.byteslice(0...partial_character_index)), T.must(data.byteslice(partial_character_index..-1))]
|
178
294
|
end
|
179
295
|
|
296
|
+
sig { returns(Symbol) }
|
180
297
|
def os
|
181
298
|
return :mac if /darwin/.match(RUBY_PLATFORM)
|
182
299
|
return :linux if /linux/.match(RUBY_PLATFORM)
|
@@ -185,19 +302,49 @@ module CLI
|
|
185
302
|
raise "Could not determine OS from platform #{RUBY_PLATFORM}"
|
186
303
|
end
|
187
304
|
|
305
|
+
sig { params(cmd: String, env: T::Hash[String, T.nilable(String)]).returns(T.nilable(String)) }
|
306
|
+
def which(cmd, env)
|
307
|
+
exts = os == :windows ? (env['PATHEXT'] || 'exe').split(';') : ['']
|
308
|
+
(env['PATH'] || '').split(File::PATH_SEPARATOR).each do |path|
|
309
|
+
exts.each do |ext|
|
310
|
+
exe = File.join(path, "#{cmd}#{ext}")
|
311
|
+
return exe if File.executable?(exe) && !File.directory?(exe)
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
nil
|
316
|
+
end
|
317
|
+
|
188
318
|
private
|
189
319
|
|
190
|
-
|
191
|
-
|
320
|
+
sig do
|
321
|
+
params(cmd: String, args: T::Array[String], sudo: T.any(T::Boolean, String))
|
322
|
+
.returns([String, T::Array[String]])
|
323
|
+
end
|
324
|
+
def apply_sudo(cmd, args, sudo)
|
325
|
+
return [cmd, args] unless sudo
|
326
|
+
|
192
327
|
sudo_reason(sudo) if sudo.is_a?(String)
|
193
|
-
|
328
|
+
['sudo', args.unshift('-E', '-S', '-p', SUDO_PROMPT, '--', cmd)]
|
194
329
|
end
|
195
330
|
|
196
|
-
|
197
|
-
|
198
|
-
|
331
|
+
sig do
|
332
|
+
params(
|
333
|
+
cmd: String,
|
334
|
+
args: T::Array[String],
|
335
|
+
kwargs: T::Hash[Symbol, T.untyped],
|
336
|
+
sudo: T.any(T::Boolean, String),
|
337
|
+
env: T::Hash[String, T.nilable(String)],
|
338
|
+
method: Symbol,
|
339
|
+
block: T.untyped,
|
340
|
+
).returns(T.untyped)
|
341
|
+
end
|
342
|
+
def delegate_open3(cmd, args, kwargs, sudo: raise, env: raise, method: raise, &block)
|
343
|
+
cmd, args = apply_sudo(cmd, args, sudo)
|
344
|
+
cmd, args = resolve_path(cmd, args, env)
|
345
|
+
T.unsafe(Open3).send(method, env, cmd, *args, **kwargs, &block)
|
199
346
|
rescue Errno::EINTR
|
200
|
-
raise(Errno::EINTR, "command interrupted: #{
|
347
|
+
raise(Errno::EINTR, "command interrupted: #{cmd} #{args.join(" ")}")
|
201
348
|
end
|
202
349
|
|
203
350
|
# Ruby resolves the program to execute using its own PATH, but we want it to
|
@@ -209,32 +356,19 @@ module CLI
|
|
209
356
|
# project.
|
210
357
|
#
|
211
358
|
# See https://github.com/Shopify/dev/pull/625 for more details.
|
212
|
-
|
213
|
-
|
214
|
-
|
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
|
359
|
+
sig do
|
360
|
+
params(cmd: String, args: T::Array[String], env: T::Hash[String, T.nilable(String)])
|
361
|
+
.returns([String, T::Array[String]])
|
226
362
|
end
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
exe = File.join(path, "#{cmd}#{ext}")
|
233
|
-
return exe if File.executable?(exe) && !File.directory?(exe)
|
234
|
-
end
|
363
|
+
def resolve_path(cmd, args, env)
|
364
|
+
# If only one argument was provided, make sure it's interpreted by a shell.
|
365
|
+
if args.empty?
|
366
|
+
prefix = os == :windows ? 'break && ' : 'true ; '
|
367
|
+
return [prefix + cmd, []]
|
235
368
|
end
|
369
|
+
return [cmd, args] if cmd.include?('/')
|
236
370
|
|
237
|
-
|
371
|
+
[which(cmd, env) || cmd, args]
|
238
372
|
end
|
239
373
|
end
|
240
374
|
end
|