cli-kit 4.0.0 → 5.0.0
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/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
|