gorails 0.1.1 → 0.1.4
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/CHANGELOG.md +18 -1
- data/Gemfile.lock +1 -6
- data/README.md +41 -12
- data/bin/update-deps +95 -0
- data/exe/gorails +2 -1
- data/gorails.gemspec +0 -2
- data/lib/gorails/commands/railsbytes.rb +45 -4
- data/lib/gorails/commands/version.rb +15 -0
- data/lib/gorails/commands.rb +2 -5
- data/lib/gorails/version.rb +1 -1
- data/lib/gorails.rb +11 -20
- data/vendor/deps/cli-kit/REVISION +1 -0
- data/vendor/deps/cli-kit/lib/cli/kit/args/definition.rb +301 -0
- data/vendor/deps/cli-kit/lib/cli/kit/args/evaluation.rb +237 -0
- data/vendor/deps/cli-kit/lib/cli/kit/args/parser/node.rb +131 -0
- data/vendor/deps/cli-kit/lib/cli/kit/args/parser.rb +128 -0
- data/vendor/deps/cli-kit/lib/cli/kit/args/tokenizer.rb +132 -0
- data/vendor/deps/cli-kit/lib/cli/kit/args.rb +15 -0
- data/vendor/deps/cli-kit/lib/cli/kit/base_command.rb +29 -0
- data/vendor/deps/cli-kit/lib/cli/kit/command_help.rb +256 -0
- data/vendor/deps/cli-kit/lib/cli/kit/command_registry.rb +141 -0
- data/vendor/deps/cli-kit/lib/cli/kit/config.rb +137 -0
- data/vendor/deps/cli-kit/lib/cli/kit/core_ext.rb +30 -0
- data/vendor/deps/cli-kit/lib/cli/kit/error_handler.rb +165 -0
- data/vendor/deps/cli-kit/lib/cli/kit/executor.rb +99 -0
- data/vendor/deps/cli-kit/lib/cli/kit/ini.rb +94 -0
- data/vendor/deps/cli-kit/lib/cli/kit/levenshtein.rb +89 -0
- data/vendor/deps/cli-kit/lib/cli/kit/logger.rb +95 -0
- data/vendor/deps/cli-kit/lib/cli/kit/opts.rb +284 -0
- data/vendor/deps/cli-kit/lib/cli/kit/resolver.rb +67 -0
- data/vendor/deps/cli-kit/lib/cli/kit/sorbet_runtime_stub.rb +142 -0
- data/vendor/deps/cli-kit/lib/cli/kit/support/test_helper.rb +253 -0
- data/vendor/deps/cli-kit/lib/cli/kit/support.rb +10 -0
- data/vendor/deps/cli-kit/lib/cli/kit/system.rb +350 -0
- data/vendor/deps/cli-kit/lib/cli/kit/util.rb +133 -0
- data/vendor/deps/cli-kit/lib/cli/kit/version.rb +7 -0
- data/vendor/deps/cli-kit/lib/cli/kit.rb +151 -0
- data/vendor/deps/cli-ui/REVISION +1 -0
- data/vendor/deps/cli-ui/lib/cli/ui/ansi.rb +180 -0
- data/vendor/deps/cli-ui/lib/cli/ui/color.rb +98 -0
- data/vendor/deps/cli-ui/lib/cli/ui/formatter.rb +216 -0
- data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_stack.rb +116 -0
- data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style/box.rb +176 -0
- data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style/bracket.rb +149 -0
- data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style.rb +112 -0
- data/vendor/deps/cli-ui/lib/cli/ui/frame.rb +300 -0
- data/vendor/deps/cli-ui/lib/cli/ui/glyph.rb +92 -0
- data/vendor/deps/cli-ui/lib/cli/ui/os.rb +58 -0
- data/vendor/deps/cli-ui/lib/cli/ui/printer.rb +72 -0
- data/vendor/deps/cli-ui/lib/cli/ui/progress.rb +102 -0
- data/vendor/deps/cli-ui/lib/cli/ui/prompt/interactive_options.rb +534 -0
- data/vendor/deps/cli-ui/lib/cli/ui/prompt/options_handler.rb +36 -0
- data/vendor/deps/cli-ui/lib/cli/ui/prompt.rb +354 -0
- data/vendor/deps/cli-ui/lib/cli/ui/sorbet_runtime_stub.rb +143 -0
- data/vendor/deps/cli-ui/lib/cli/ui/spinner/async.rb +46 -0
- data/vendor/deps/cli-ui/lib/cli/ui/spinner/spin_group.rb +292 -0
- data/vendor/deps/cli-ui/lib/cli/ui/spinner.rb +82 -0
- data/vendor/deps/cli-ui/lib/cli/ui/stdout_router.rb +264 -0
- data/vendor/deps/cli-ui/lib/cli/ui/terminal.rb +53 -0
- data/vendor/deps/cli-ui/lib/cli/ui/truncater.rb +107 -0
- data/vendor/deps/cli-ui/lib/cli/ui/version.rb +6 -0
- data/vendor/deps/cli-ui/lib/cli/ui/widgets/base.rb +37 -0
- data/vendor/deps/cli-ui/lib/cli/ui/widgets/status.rb +75 -0
- data/vendor/deps/cli-ui/lib/cli/ui/widgets.rb +91 -0
- data/vendor/deps/cli-ui/lib/cli/ui/wrap.rb +63 -0
- data/vendor/deps/cli-ui/lib/cli/ui.rb +356 -0
- metadata +59 -30
@@ -0,0 +1,253 @@
|
|
1
|
+
require 'cli/kit'
|
2
|
+
|
3
|
+
module CLI
|
4
|
+
module Kit
|
5
|
+
module Support
|
6
|
+
module TestHelper
|
7
|
+
def setup
|
8
|
+
super
|
9
|
+
CLI::Kit::System.reset!
|
10
|
+
end
|
11
|
+
|
12
|
+
def assert_all_commands_run(should_raise: true)
|
13
|
+
errors = CLI::Kit::System.error_message
|
14
|
+
CLI::Kit::System.reset!
|
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?
|
18
|
+
errors
|
19
|
+
end
|
20
|
+
|
21
|
+
def teardown
|
22
|
+
super
|
23
|
+
assert_all_commands_run
|
24
|
+
end
|
25
|
+
|
26
|
+
module FakeConfig
|
27
|
+
require 'tmpdir'
|
28
|
+
require 'fileutils'
|
29
|
+
|
30
|
+
def setup
|
31
|
+
super
|
32
|
+
@tmpdir = Dir.mktmpdir
|
33
|
+
@prev_xdg = ENV['XDG_CONFIG_HOME']
|
34
|
+
ENV['XDG_CONFIG_HOME'] = @tmpdir
|
35
|
+
end
|
36
|
+
|
37
|
+
def teardown
|
38
|
+
FileUtils.rm_rf(@tmpdir)
|
39
|
+
ENV['XDG_CONFIG_HOME'] = @prev_xdg
|
40
|
+
super
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class FakeSuccess
|
45
|
+
def initialize(success)
|
46
|
+
@success = success
|
47
|
+
end
|
48
|
+
|
49
|
+
def success?
|
50
|
+
@success
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
module ::CLI
|
55
|
+
module Kit
|
56
|
+
module System
|
57
|
+
class << self
|
58
|
+
alias_method :original_system, :system
|
59
|
+
def system(cmd, *a, sudo: false, env: {}, **kwargs)
|
60
|
+
a.unshift(cmd)
|
61
|
+
expected_command = expected_command(a, sudo: sudo, env: env)
|
62
|
+
|
63
|
+
# In the case of an unexpected command, expected_command will be nil
|
64
|
+
return FakeSuccess.new(false) if expected_command.nil?
|
65
|
+
|
66
|
+
# Otherwise handle the command
|
67
|
+
if expected_command[:allow]
|
68
|
+
T.unsafe(self).original_system(*a, sudo: sudo, env: env, **kwargs)
|
69
|
+
else
|
70
|
+
FakeSuccess.new(expected_command[:success])
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
alias_method :original_capture2, :capture2
|
75
|
+
def capture2(cmd, *a, sudo: false, env: {}, **kwargs)
|
76
|
+
a.unshift(cmd)
|
77
|
+
expected_command = expected_command(a, sudo: sudo, env: env)
|
78
|
+
|
79
|
+
# In the case of an unexpected command, expected_command will be nil
|
80
|
+
return [nil, FakeSuccess.new(false)] if expected_command.nil?
|
81
|
+
|
82
|
+
# Otherwise handle the command
|
83
|
+
if expected_command[:allow]
|
84
|
+
T.unsafe(self).original_capture2(*a, sudo: sudo, env: env, **kwargs)
|
85
|
+
else
|
86
|
+
[
|
87
|
+
expected_command[:stdout],
|
88
|
+
FakeSuccess.new(expected_command[:success]),
|
89
|
+
]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
alias_method :original_capture2e, :capture2e
|
94
|
+
def capture2e(cmd, *a, sudo: false, env: {}, **kwargs)
|
95
|
+
a.unshift(cmd)
|
96
|
+
expected_command = expected_command(a, sudo: sudo, env: env)
|
97
|
+
|
98
|
+
# In the case of an unexpected command, expected_command will be nil
|
99
|
+
return [nil, FakeSuccess.new(false)] if expected_command.nil?
|
100
|
+
|
101
|
+
# Otherwise handle the command
|
102
|
+
if expected_command[:allow]
|
103
|
+
T.unsafe(self).original_capture2e(*a, sudo: sudo, env: env, **kwargs)
|
104
|
+
else
|
105
|
+
[
|
106
|
+
expected_command[:stdout],
|
107
|
+
FakeSuccess.new(expected_command[:success]),
|
108
|
+
]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
alias_method :original_capture3, :capture3
|
113
|
+
def capture3(cmd, *a, sudo: false, env: {}, **kwargs)
|
114
|
+
a.unshift(cmd)
|
115
|
+
expected_command = expected_command(a, sudo: sudo, env: env)
|
116
|
+
|
117
|
+
# In the case of an unexpected command, expected_command will be nil
|
118
|
+
return [nil, nil, FakeSuccess.new(false)] if expected_command.nil?
|
119
|
+
|
120
|
+
# Otherwise handle the command
|
121
|
+
if expected_command[:allow]
|
122
|
+
T.unsafe(self).original_capture3(*a, sudo: sudo, env: env, **kwargs)
|
123
|
+
else
|
124
|
+
[
|
125
|
+
expected_command[:stdout],
|
126
|
+
expected_command[:stderr],
|
127
|
+
FakeSuccess.new(expected_command[:success]),
|
128
|
+
]
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Sets up an expectation for a command and stubs out the call (unless allow is true)
|
133
|
+
#
|
134
|
+
# #### Parameters
|
135
|
+
# `*a` : the command, represented as a splat
|
136
|
+
# `stdout` : stdout to stub the command with (defaults to empty string)
|
137
|
+
# `stderr` : stderr to stub the command with (defaults to empty string)
|
138
|
+
# `allow` : allow determines if the command will be actually run, or stubbed. Defaults to nil (stub)
|
139
|
+
# `success` : success status to stub the command with (Defaults to nil)
|
140
|
+
# `sudo` : expectation of sudo being set or not (defaults to false)
|
141
|
+
# `env` : expectation of env being set or not (defaults to {})
|
142
|
+
#
|
143
|
+
# Note: Must set allow or success
|
144
|
+
#
|
145
|
+
def fake(*a, stdout: '', stderr: '', allow: nil, success: nil, sudo: false, env: {})
|
146
|
+
raise ArgumentError, 'success or allow must be set' if success.nil? && allow.nil?
|
147
|
+
|
148
|
+
@delegate_open3 ||= {}
|
149
|
+
@delegate_open3[a.join(' ')] = {
|
150
|
+
expected: {
|
151
|
+
sudo: sudo,
|
152
|
+
env: env,
|
153
|
+
},
|
154
|
+
actual: {
|
155
|
+
sudo: nil,
|
156
|
+
env: nil,
|
157
|
+
},
|
158
|
+
stdout: stdout,
|
159
|
+
stderr: stderr,
|
160
|
+
allow: allow,
|
161
|
+
success: success,
|
162
|
+
run: false,
|
163
|
+
}
|
164
|
+
end
|
165
|
+
|
166
|
+
# Resets the faked commands
|
167
|
+
#
|
168
|
+
def reset!
|
169
|
+
@delegate_open3 = {}
|
170
|
+
end
|
171
|
+
|
172
|
+
# Returns the errors associated to a test run
|
173
|
+
#
|
174
|
+
# #### Returns
|
175
|
+
# `errors` (String) a string representing errors found on this run, nil if none
|
176
|
+
def error_message
|
177
|
+
errors = {
|
178
|
+
unexpected: [],
|
179
|
+
not_run: [],
|
180
|
+
other: {},
|
181
|
+
}
|
182
|
+
|
183
|
+
@delegate_open3.each do |cmd, opts|
|
184
|
+
if opts[:unexpected]
|
185
|
+
errors[:unexpected] << cmd
|
186
|
+
elsif opts[:run]
|
187
|
+
error = []
|
188
|
+
|
189
|
+
if opts[:expected][:sudo] != opts[:actual][:sudo]
|
190
|
+
error << "- sudo was supposed to be #{opts[:expected][:sudo]} but was #{opts[:actual][:sudo]}"
|
191
|
+
end
|
192
|
+
|
193
|
+
if opts[:expected][:env] != opts[:actual][:env]
|
194
|
+
error << "- env was supposed to be #{opts[:expected][:env]} but was #{opts[:actual][:env]}"
|
195
|
+
end
|
196
|
+
|
197
|
+
errors[:other][cmd] = error.join("\n") unless error.empty?
|
198
|
+
else
|
199
|
+
errors[:not_run] << cmd
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
final_error = []
|
204
|
+
|
205
|
+
unless errors[:unexpected].empty?
|
206
|
+
final_error << CLI::UI.fmt(<<~EOF)
|
207
|
+
{{bold:Unexpected command invocations:}}
|
208
|
+
{{command:#{errors[:unexpected].join("\n")}}}
|
209
|
+
EOF
|
210
|
+
end
|
211
|
+
|
212
|
+
unless errors[:not_run].empty?
|
213
|
+
final_error << CLI::UI.fmt(<<~EOF)
|
214
|
+
{{bold:Expected commands were not run:}}
|
215
|
+
{{command:#{errors[:not_run].join("\n")}}}
|
216
|
+
EOF
|
217
|
+
end
|
218
|
+
|
219
|
+
unless errors[:other].empty?
|
220
|
+
final_error << CLI::UI.fmt(<<~EOF)
|
221
|
+
{{bold:Commands were not run as expected:}}
|
222
|
+
#{errors[:other].map { |cmd, msg| "{{command:#{cmd}}}\n#{msg}" }.join("\n\n")}
|
223
|
+
EOF
|
224
|
+
end
|
225
|
+
|
226
|
+
return nil if final_error.empty?
|
227
|
+
|
228
|
+
"\n" + final_error.join("\n") # Initial new line for formatting reasons
|
229
|
+
end
|
230
|
+
|
231
|
+
private
|
232
|
+
|
233
|
+
def expected_command(a, sudo: raise, env: raise)
|
234
|
+
expected_cmd = @delegate_open3[a.join(' ')]
|
235
|
+
|
236
|
+
if expected_cmd.nil?
|
237
|
+
@delegate_open3[a.join(' ')] = { unexpected: true }
|
238
|
+
return nil
|
239
|
+
end
|
240
|
+
|
241
|
+
expected_cmd[:run] = true
|
242
|
+
expected_cmd[:actual][:sudo] = sudo
|
243
|
+
expected_cmd[:actual][:env] = env
|
244
|
+
expected_cmd
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
@@ -0,0 +1,350 @@
|
|
1
|
+
# typed: true
|
2
|
+
require 'cli/kit'
|
3
|
+
require 'open3'
|
4
|
+
require 'English'
|
5
|
+
|
6
|
+
module CLI
|
7
|
+
module Kit
|
8
|
+
module System
|
9
|
+
SUDO_PROMPT = CLI::UI.fmt('{{info:(sudo)}} Password: ')
|
10
|
+
class << self
|
11
|
+
extend T::Sig
|
12
|
+
|
13
|
+
# Ask for sudo access with a message explaning the need for it
|
14
|
+
# Will make subsequent commands capable of running with sudo for a period of time
|
15
|
+
#
|
16
|
+
# #### Parameters
|
17
|
+
# - `msg`: A message telling the user why sudo is needed
|
18
|
+
#
|
19
|
+
# #### Usage
|
20
|
+
# `ctx.sudo_reason("We need to do a thing")`
|
21
|
+
#
|
22
|
+
sig { params(msg: String).void }
|
23
|
+
def sudo_reason(msg)
|
24
|
+
# See if sudo has a cached password
|
25
|
+
%x(env SUDO_ASKPASS=/usr/bin/false sudo -A true > /dev/null 2>&1)
|
26
|
+
return if $CHILD_STATUS.success?
|
27
|
+
|
28
|
+
CLI::UI.with_frame_color(:blue) do
|
29
|
+
puts(CLI::UI.fmt("{{i}} #{msg}"))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Execute a command in the user's environment
|
34
|
+
# This is meant to be largely equivalent to backticks, only with the env passed in.
|
35
|
+
# Captures the results of the command without output to the console
|
36
|
+
#
|
37
|
+
# #### Parameters
|
38
|
+
# - `*a`: A splat of arguments evaluated as a command. (e.g. `'rm', folder` is equivalent to `rm #{folder}`)
|
39
|
+
# - `sudo`: If truthy, run this command with sudo. If String, pass to `sudo_reason`
|
40
|
+
# - `env`: process environment with which to execute this command
|
41
|
+
# - `**kwargs`: additional arguments to pass to Open3.capture2
|
42
|
+
#
|
43
|
+
# #### Returns
|
44
|
+
# - `output`: output (STDOUT) of the command execution
|
45
|
+
# - `status`: boolean success status of the command execution
|
46
|
+
#
|
47
|
+
# #### Usage
|
48
|
+
# `out, stat = CLI::Kit::System.capture2('ls', 'a_folder')`
|
49
|
+
#
|
50
|
+
sig do
|
51
|
+
params(
|
52
|
+
cmd: String,
|
53
|
+
args: String,
|
54
|
+
sudo: T.any(T::Boolean, String),
|
55
|
+
env: T::Hash[String, T.nilable(String)],
|
56
|
+
kwargs: T.untyped
|
57
|
+
)
|
58
|
+
.returns([String, Process::Status])
|
59
|
+
end
|
60
|
+
def capture2(cmd, *args, sudo: false, env: ENV.to_h, **kwargs)
|
61
|
+
delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :capture2)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Execute a command in the user's environment
|
65
|
+
# This is meant to be largely equivalent to backticks, only with the env passed in.
|
66
|
+
# Captures the results of the command without output to the console
|
67
|
+
#
|
68
|
+
# #### Parameters
|
69
|
+
# - `*a`: A splat of arguments evaluated as a command. (e.g. `'rm', folder` is equivalent to `rm #{folder}`)
|
70
|
+
# - `sudo`: If truthy, run this command with sudo. If String, pass to `sudo_reason`
|
71
|
+
# - `env`: process environment with which to execute this command
|
72
|
+
# - `**kwargs`: additional arguments to pass to Open3.capture2e
|
73
|
+
#
|
74
|
+
# #### Returns
|
75
|
+
# - `output`: output (STDOUT merged with STDERR) of the command execution
|
76
|
+
# - `status`: boolean success status of the command execution
|
77
|
+
#
|
78
|
+
# #### Usage
|
79
|
+
# `out_and_err, stat = CLI::Kit::System.capture2e('ls', 'a_folder')`
|
80
|
+
#
|
81
|
+
sig do
|
82
|
+
params(
|
83
|
+
cmd: String,
|
84
|
+
args: String,
|
85
|
+
sudo: T.any(T::Boolean, String),
|
86
|
+
env: T::Hash[String, T.nilable(String)],
|
87
|
+
kwargs: T.untyped
|
88
|
+
)
|
89
|
+
.returns([String, Process::Status])
|
90
|
+
end
|
91
|
+
def capture2e(cmd, *args, sudo: false, env: ENV.to_h, **kwargs)
|
92
|
+
delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :capture2e)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Execute a command in the user's environment
|
96
|
+
# This is meant to be largely equivalent to backticks, only with the env passed in.
|
97
|
+
# Captures the results of the command without output to the console
|
98
|
+
#
|
99
|
+
# #### Parameters
|
100
|
+
# - `*a`: A splat of arguments evaluated as a command. (e.g. `'rm', folder` is equivalent to `rm #{folder}`)
|
101
|
+
# - `sudo`: If truthy, run this command with sudo. If String, pass to `sudo_reason`
|
102
|
+
# - `env`: process environment with which to execute this command
|
103
|
+
# - `**kwargs`: additional arguments to pass to Open3.capture3
|
104
|
+
#
|
105
|
+
# #### Returns
|
106
|
+
# - `output`: STDOUT of the command execution
|
107
|
+
# - `error`: STDERR of the command execution
|
108
|
+
# - `status`: boolean success status of the command execution
|
109
|
+
#
|
110
|
+
# #### Usage
|
111
|
+
# `out, err, stat = CLI::Kit::System.capture3('ls', 'a_folder')`
|
112
|
+
#
|
113
|
+
sig do
|
114
|
+
params(
|
115
|
+
cmd: String,
|
116
|
+
args: String,
|
117
|
+
sudo: T.any(T::Boolean, String),
|
118
|
+
env: T::Hash[String, T.nilable(String)],
|
119
|
+
kwargs: T.untyped
|
120
|
+
)
|
121
|
+
.returns([String, String, Process::Status])
|
122
|
+
end
|
123
|
+
def capture3(cmd, *args, sudo: false, env: ENV.to_h, **kwargs)
|
124
|
+
delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :capture3)
|
125
|
+
end
|
126
|
+
|
127
|
+
sig do
|
128
|
+
params(
|
129
|
+
cmd: String,
|
130
|
+
args: String,
|
131
|
+
sudo: T.any(T::Boolean, String),
|
132
|
+
env: T::Hash[String, T.nilable(String)],
|
133
|
+
kwargs: T.untyped,
|
134
|
+
block: T.nilable(
|
135
|
+
T.proc.params(stdin: IO, stdout: IO, wait_thr: Process::Waiter)
|
136
|
+
.returns([IO, IO, Process::Waiter])
|
137
|
+
)
|
138
|
+
)
|
139
|
+
.returns([IO, IO, Process::Waiter])
|
140
|
+
end
|
141
|
+
def popen2(cmd, *args, sudo: false, env: ENV.to_h, **kwargs, &block)
|
142
|
+
delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :popen2, &block)
|
143
|
+
end
|
144
|
+
|
145
|
+
sig do
|
146
|
+
params(
|
147
|
+
cmd: String,
|
148
|
+
args: String,
|
149
|
+
sudo: T.any(T::Boolean, String),
|
150
|
+
env: T::Hash[String, T.nilable(String)],
|
151
|
+
kwargs: T.untyped,
|
152
|
+
block: T.nilable(
|
153
|
+
T.proc.params(stdin: IO, stdout: IO, wait_thr: Process::Waiter)
|
154
|
+
.returns([IO, IO, Process::Waiter])
|
155
|
+
)
|
156
|
+
)
|
157
|
+
.returns([IO, IO, Process::Waiter])
|
158
|
+
end
|
159
|
+
def popen2e(cmd, *args, sudo: false, env: ENV.to_h, **kwargs, &block)
|
160
|
+
delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :popen2e, &block)
|
161
|
+
end
|
162
|
+
|
163
|
+
sig do
|
164
|
+
params(
|
165
|
+
cmd: String,
|
166
|
+
args: String,
|
167
|
+
sudo: T.any(T::Boolean, String),
|
168
|
+
env: T::Hash[String, T.nilable(String)],
|
169
|
+
kwargs: T.untyped,
|
170
|
+
block: T.nilable(
|
171
|
+
T.proc.params(stdin: IO, stdout: IO, stderr: IO, wait_thr: Process::Waiter)
|
172
|
+
.returns([IO, IO, IO, Process::Waiter])
|
173
|
+
)
|
174
|
+
)
|
175
|
+
.returns([IO, IO, IO, Process::Waiter])
|
176
|
+
end
|
177
|
+
def popen3(cmd, *args, sudo: false, env: ENV.to_h, **kwargs, &block)
|
178
|
+
delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :popen3, &block)
|
179
|
+
end
|
180
|
+
|
181
|
+
# Execute a command in the user's environment
|
182
|
+
# Outputs result of the command without capturing it
|
183
|
+
#
|
184
|
+
# #### Parameters
|
185
|
+
# - `*a`: A splat of arguments evaluated as a command. (e.g. `'rm', folder` is equivalent to `rm #{folder}`)
|
186
|
+
# - `sudo`: If truthy, run this command with sudo. If String, pass to `sudo_reason`
|
187
|
+
# - `env`: process environment with which to execute this command
|
188
|
+
# - `**kwargs`: additional keyword arguments to pass to Process.spawn
|
189
|
+
#
|
190
|
+
# #### Returns
|
191
|
+
# - `status`: The `Process:Status` result for the command execution
|
192
|
+
#
|
193
|
+
# #### Usage
|
194
|
+
# `stat = CLI::Kit::System.system('ls', 'a_folder')`
|
195
|
+
#
|
196
|
+
sig do
|
197
|
+
params(
|
198
|
+
cmd: String,
|
199
|
+
args: String,
|
200
|
+
sudo: T.any(T::Boolean, String),
|
201
|
+
env: T::Hash[String, T.nilable(String)],
|
202
|
+
kwargs: T.untyped,
|
203
|
+
block: T.nilable(T.proc.params(out: String, err: String).void)
|
204
|
+
)
|
205
|
+
.returns(Process::Status)
|
206
|
+
end
|
207
|
+
def system(cmd, *args, sudo: false, env: ENV.to_h, **kwargs, &block)
|
208
|
+
cmd, args = apply_sudo(cmd, args, sudo)
|
209
|
+
|
210
|
+
out_r, out_w = IO.pipe
|
211
|
+
err_r, err_w = IO.pipe
|
212
|
+
in_stream = STDIN.closed? ? :close : STDIN
|
213
|
+
cmd, args = resolve_path(cmd, args, env)
|
214
|
+
pid = T.unsafe(Process).spawn(env, cmd, *args, 0 => in_stream, :out => out_w, :err => err_w, **kwargs)
|
215
|
+
out_w.close
|
216
|
+
err_w.close
|
217
|
+
|
218
|
+
handlers = if block_given?
|
219
|
+
{
|
220
|
+
out_r => ->(data) { yield(data.force_encoding(Encoding::UTF_8), '') },
|
221
|
+
err_r => ->(data) { yield('', data.force_encoding(Encoding::UTF_8)) },
|
222
|
+
}
|
223
|
+
else
|
224
|
+
{
|
225
|
+
out_r => ->(data) { STDOUT.write(data) },
|
226
|
+
err_r => ->(data) { STDOUT.write(data) },
|
227
|
+
}
|
228
|
+
end
|
229
|
+
|
230
|
+
previous_trailing = Hash.new('')
|
231
|
+
loop do
|
232
|
+
ios = [err_r, out_r].reject(&:closed?)
|
233
|
+
break if ios.empty?
|
234
|
+
|
235
|
+
readers, = IO.select(ios)
|
236
|
+
(readers || []).each do |io|
|
237
|
+
data, trailing = split_partial_characters(io.readpartial(4096))
|
238
|
+
handlers[io].call(previous_trailing[io] + data)
|
239
|
+
previous_trailing[io] = trailing
|
240
|
+
rescue IOError
|
241
|
+
io.close
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
Process.wait(pid)
|
246
|
+
$CHILD_STATUS
|
247
|
+
end
|
248
|
+
|
249
|
+
# Split off trailing partial UTF-8 Characters. UTF-8 Multibyte characters start with a 11xxxxxx byte that tells
|
250
|
+
# how many following bytes are part of this character, followed by some number of 10xxxxxx bytes. This simple
|
251
|
+
# algorithm will split off a whole trailing multi-byte character.
|
252
|
+
sig { params(data: String).returns([String, String]) }
|
253
|
+
def split_partial_characters(data)
|
254
|
+
last_byte = T.must(data.getbyte(-1))
|
255
|
+
return [data, ''] if (last_byte & 0b1000_0000).zero?
|
256
|
+
|
257
|
+
# UTF-8 is up to 6 characters per rune, so we could never want to trim more than that, and we want to avoid
|
258
|
+
# allocating an array for the whole of data with bytes
|
259
|
+
min_bound = -[6, data.bytesize].min
|
260
|
+
final_bytes = T.must(data.byteslice(min_bound..-1)).bytes
|
261
|
+
partial_character_sub_index = final_bytes.rindex { |byte| byte & 0b1100_0000 == 0b1100_0000 }
|
262
|
+
# Bail out for non UTF-8
|
263
|
+
return [data, ''] unless partial_character_sub_index
|
264
|
+
|
265
|
+
partial_character_index = min_bound + partial_character_sub_index
|
266
|
+
|
267
|
+
[T.must(data.byteslice(0...partial_character_index)), T.must(data.byteslice(partial_character_index..-1))]
|
268
|
+
end
|
269
|
+
|
270
|
+
sig { returns(Symbol) }
|
271
|
+
def os
|
272
|
+
return :mac if /darwin/.match(RUBY_PLATFORM)
|
273
|
+
return :linux if /linux/.match(RUBY_PLATFORM)
|
274
|
+
return :windows if /mingw32/.match(RUBY_PLATFORM)
|
275
|
+
|
276
|
+
raise "Could not determine OS from platform #{RUBY_PLATFORM}"
|
277
|
+
end
|
278
|
+
|
279
|
+
private
|
280
|
+
|
281
|
+
sig do
|
282
|
+
params(cmd: String, args: T::Array[String], sudo: T.any(T::Boolean, String))
|
283
|
+
.returns([String, T::Array[String]])
|
284
|
+
end
|
285
|
+
def apply_sudo(cmd, args, sudo)
|
286
|
+
return [cmd, args] unless sudo
|
287
|
+
|
288
|
+
sudo_reason(sudo) if sudo.is_a?(String)
|
289
|
+
['sudo', args.unshift('-S', '-p', SUDO_PROMPT, '--', cmd)]
|
290
|
+
end
|
291
|
+
|
292
|
+
sig do
|
293
|
+
params(
|
294
|
+
cmd: String,
|
295
|
+
args: T::Array[String],
|
296
|
+
kwargs: T::Hash[Symbol, T.untyped],
|
297
|
+
sudo: T.any(T::Boolean, String),
|
298
|
+
env: T::Hash[String, T.nilable(String)],
|
299
|
+
method: Symbol,
|
300
|
+
block: T.untyped
|
301
|
+
).returns(T.untyped)
|
302
|
+
end
|
303
|
+
def delegate_open3(cmd, args, kwargs, sudo: raise, env: raise, method: raise, &block)
|
304
|
+
cmd, args = apply_sudo(cmd, args, sudo)
|
305
|
+
cmd, args = resolve_path(cmd, args, env)
|
306
|
+
T.unsafe(Open3).send(method, env, cmd, *args, **kwargs, &block)
|
307
|
+
rescue Errno::EINTR
|
308
|
+
raise(Errno::EINTR, "command interrupted: #{cmd} #{args.join(" ")}")
|
309
|
+
end
|
310
|
+
|
311
|
+
# Ruby resolves the program to execute using its own PATH, but we want it to
|
312
|
+
# use the provided one, so we ensure ruby chooses to spawn a shell, which will
|
313
|
+
# parse our command and properly spawn our target using the provided environment.
|
314
|
+
#
|
315
|
+
# This is important because dev clobbers its own environment such that ruby
|
316
|
+
# means /usr/bin/ruby, but we want it to select the ruby targeted by the active
|
317
|
+
# project.
|
318
|
+
#
|
319
|
+
# See https://github.com/Shopify/dev/pull/625 for more details.
|
320
|
+
sig do
|
321
|
+
params(cmd: String, args: T::Array[String], env: T::Hash[String, T.nilable(String)])
|
322
|
+
.returns([String, T::Array[String]])
|
323
|
+
end
|
324
|
+
def resolve_path(cmd, args, env)
|
325
|
+
# If only one argument was provided, make sure it's interpreted by a shell.
|
326
|
+
if args.empty?
|
327
|
+
prefix = os == :windows ? 'break && ' : 'true ; '
|
328
|
+
return [prefix + cmd, []]
|
329
|
+
end
|
330
|
+
return [cmd, args] if cmd.include?('/')
|
331
|
+
|
332
|
+
[which(cmd, env) || cmd, args]
|
333
|
+
end
|
334
|
+
|
335
|
+
sig { params(cmd: T.untyped, env: T.untyped).returns(T.untyped) }
|
336
|
+
def which(cmd, env)
|
337
|
+
exts = os == :windows ? env.fetch('PATHEXT').split(';') : ['']
|
338
|
+
env.fetch('PATH', '').split(File::PATH_SEPARATOR).each do |path|
|
339
|
+
exts.each do |ext|
|
340
|
+
exe = File.join(path, "#{cmd}#{ext}")
|
341
|
+
return exe if File.executable?(exe) && !File.directory?(exe)
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
nil
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|