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
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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(
|
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
|
240
|
+
return
|
230
241
|
end
|
231
242
|
|
232
243
|
expected_cmd[:run] = true
|
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
|
|
@@ -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
|
-
|
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
|
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 = -[
|
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 /
|
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
|
-
|
191
|
-
|
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
|
-
|
331
|
+
['sudo', args.unshift('-E', '-S', '-p', SUDO_PROMPT, '--', cmd)]
|
194
332
|
end
|
195
333
|
|
196
|
-
|
197
|
-
|
198
|
-
|
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: #{
|
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
|
-
|
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
|
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
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
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
|
-
|
374
|
+
[which(cmd, env) || cmd, args]
|
238
375
|
end
|
239
376
|
end
|
240
377
|
end
|