Module: CLI::Kit::System
- Defined in:
- lib/cli/kit/system.rb,
lib/cli/kit/support/test_helper.rb
Constant Summary collapse
- SUDO_PROMPT =
CLI::UI.fmt('{{info:(sudo)}} Password: ')
Class Method Summary collapse
-
.capture2(cmd, *a, sudo: false, env: {}, **kwargs) ⇒ Object
Execute a command in the user’s environment This is meant to be largely equivalent to backticks, only with the env passed in.
-
.capture2e(cmd, *a, sudo: false, env: {}, **kwargs) ⇒ Object
Execute a command in the user’s environment This is meant to be largely equivalent to backticks, only with the env passed in.
-
.capture3(cmd, *a, sudo: false, env: {}, **kwargs) ⇒ Object
Execute a command in the user’s environment This is meant to be largely equivalent to backticks, only with the env passed in.
-
.error_message ⇒ Object
Returns the errors associated to a test run.
-
.fake(*a, stdout: '', stderr: '', allow: nil, success: nil, sudo: false, env: {}) ⇒ Object
Sets up an expectation for a command and stubs out the call (unless allow is true).
-
.original_capture2 ⇒ Object
Execute a command in the user’s environment This is meant to be largely equivalent to backticks, only with the env passed in.
-
.original_capture2e ⇒ Object
Execute a command in the user’s environment This is meant to be largely equivalent to backticks, only with the env passed in.
-
.original_capture3 ⇒ Object
Execute a command in the user’s environment This is meant to be largely equivalent to backticks, only with the env passed in.
-
.original_system ⇒ Object
Execute a command in the user’s environment Outputs result of the command without capturing it.
-
.os ⇒ Object
: -> Symbol.
-
.popen2(cmd, *args, sudo: false, env: ENV.to_h, **kwargs, &block) ⇒ Object
: (String cmd, *String args, ?sudo: (String | bool), ?env: Hash[String, String?], **untyped kwargs) ?{ (IO stdin, IO stdout, Process::Waiter wait_thr) -> [IO, IO, Process::Waiter] } -> [IO, IO, Process::Waiter].
-
.popen2e(cmd, *args, sudo: false, env: ENV.to_h, **kwargs, &block) ⇒ Object
: (String cmd, *String args, ?sudo: (String | bool), ?env: Hash[String, String?], **untyped kwargs) ?{ (IO stdin, IO stdout, Process::Waiter wait_thr) -> [IO, IO, Process::Waiter] } -> [IO, IO, Process::Waiter].
-
.popen3(cmd, *args, sudo: false, env: ENV.to_h, **kwargs, &block) ⇒ Object
: (String cmd, *String args, ?sudo: (String | bool), ?env: Hash[String, String?], **untyped kwargs) ?{ (IO stdin, IO stdout, IO stderr, Process::Waiter wait_thr) -> [IO, IO, IO, Process::Waiter] } -> [IO, IO, IO, Process::Waiter].
-
.reset! ⇒ Object
Resets the faked commands.
-
.split_partial_characters(data) ⇒ Object
Split off trailing partial UTF-8 Characters.
-
.sudo_reason(msg) ⇒ Object
Ask for sudo access with a message explaning the need for it Will make subsequent commands capable of running with sudo for a period of time.
-
.system(cmd, *a, sudo: false, env: {}, stdin: nil, **kwargs) ⇒ Object
Execute a command in the user’s environment Outputs result of the command without capturing it.
-
.which(cmd, env) ⇒ Object
: (String cmd, Hash[String, String?] env) -> String?.
Class Method Details
.capture2(cmd, *a, sudo: false, env: {}, **kwargs) ⇒ Object
Execute a command in the user’s environment This is meant to be largely equivalent to backticks, only with the env passed in. Captures the results of the command without output to the console
#### Parameters
-
‘*a`: A splat of arguments evaluated as a command. (e.g. `’rm’, folder` is equivalent to ‘rm #folder`)
-
sudo: If truthy, run this command with sudo. If String, pass tosudo_reason -
env: process environment with which to execute this command -
‘**kwargs`: additional arguments to pass to Open3.capture2
#### Returns
-
output: output (STDOUT) of the command execution -
status: boolean success status of the command execution
#### Usage ‘out, stat = CLI::Kit::System.capture2(’ls’, ‘a_folder’)‘
: (String cmd, *String args, ?sudo: (String | bool), ?env: Hash[String, String?], **untyped kwargs) -> [String, Process::Status]
50 51 52 |
# File 'lib/cli/kit/system.rb', line 50 def capture2(cmd, *args, sudo: false, env: ENV.to_h, **kwargs) delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :capture2) end |
.capture2e(cmd, *a, sudo: false, env: {}, **kwargs) ⇒ Object
Execute a command in the user’s environment This is meant to be largely equivalent to backticks, only with the env passed in. Captures the results of the command without output to the console
#### Parameters
-
‘*a`: A splat of arguments evaluated as a command. (e.g. `’rm’, folder` is equivalent to ‘rm #folder`)
-
sudo: If truthy, run this command with sudo. If String, pass tosudo_reason -
env: process environment with which to execute this command -
‘**kwargs`: additional arguments to pass to Open3.capture2e
#### Returns
-
output: output (STDOUT merged with STDERR) of the command execution -
status: boolean success status of the command execution
#### Usage ‘out_and_err, stat = CLI::Kit::System.capture2e(’ls’, ‘a_folder’)‘
: (String cmd, *String args, ?sudo: (String | bool), ?env: Hash[String, String?], **untyped kwargs) -> [String, Process::Status]
72 73 74 |
# File 'lib/cli/kit/system.rb', line 72 def capture2e(cmd, *args, sudo: false, env: ENV.to_h, **kwargs) delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :capture2e) end |
.capture3(cmd, *a, sudo: false, env: {}, **kwargs) ⇒ Object
Execute a command in the user’s environment This is meant to be largely equivalent to backticks, only with the env passed in. Captures the results of the command without output to the console
#### Parameters
-
‘*a`: A splat of arguments evaluated as a command. (e.g. `’rm’, folder` is equivalent to ‘rm #folder`)
-
sudo: If truthy, run this command with sudo. If String, pass tosudo_reason -
env: process environment with which to execute this command -
‘**kwargs`: additional arguments to pass to Open3.capture3
#### Returns
-
output: STDOUT of the command execution -
error: STDERR of the command execution -
status: boolean success status of the command execution
#### Usage ‘out, err, stat = CLI::Kit::System.capture3(’ls’, ‘a_folder’)‘
: (String cmd, *String args, ?sudo: (String | bool), ?env: Hash[String, String?], **untyped kwargs) -> [String, String, Process::Status]
95 96 97 |
# File 'lib/cli/kit/system.rb', line 95 def capture3(cmd, *args, sudo: false, env: ENV.to_h, **kwargs) delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :capture3) end |
.error_message ⇒ Object
Returns the errors associated to a test run
#### Returns errors (String) a string representing errors found on this run, nil if none
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 |
# File 'lib/cli/kit/support/test_helper.rb', line 183 def errors = { unexpected: [], not_run: [], other: {}, } @delegate_open3.each do |cmd, opts| if opts[:unexpected] errors[:unexpected] << cmd elsif opts[:run] error = [] if opts[:expected][:sudo] != opts[:actual][:sudo] error << "- sudo was supposed to be #{opts[:expected][:sudo]} but was #{opts[:actual][:sudo]}" end if opts[:expected][:env] != opts[:actual][:env] error << "- env was supposed to be #{opts[:expected][:env]} but was #{opts[:actual][:env]}" end errors[:other][cmd] = error.join("\n") unless error.empty? else errors[:not_run] << cmd end end final_error = [] unless errors[:unexpected].empty? final_error << CLI::UI.fmt(" {{bold:Unexpected command invocations:}}\n {{command:\#{errors[:unexpected].join(\"\\n\")}}}\n EOF\n end\n\n unless errors[:not_run].empty?\n final_error << CLI::UI.fmt(<<~EOF)\n {{bold:Expected commands were not run:}}\n {{command:\#{errors[:not_run].join(\"\\n\")}}}\n EOF\n end\n\n unless errors[:other].empty?\n final_error << CLI::UI.fmt(<<~EOF)\n {{bold:Commands were not run as expected:}}\n \#{errors[:other].map { |cmd, msg| \"{{command:\#{cmd}}}\\n\#{msg}\" }.join(\"\\n\\n\")}\n EOF\n end\n\n return if final_error.empty?\n\n \"\\n\" + final_error.join(\"\\n\") # Initial new line for formatting reasons\nend\n") |
.fake(*a, stdout: '', stderr: '', allow: nil, success: nil, sudo: false, env: {}) ⇒ Object
Sets up an expectation for a command and stubs out the call (unless allow is true)
#### Parameters ‘*a` : the command, represented as a splat stdout : stdout to stub the command with (defaults to empty string) stderr : stderr to stub the command with (defaults to empty string) allow : allow determines if the command will be actually run, or stubbed. Defaults to nil (stub) success : success status to stub the command with (Defaults to nil) sudo : expectation of sudo being set or not (defaults to false) env : expectation of env being set or not (defaults to {})
Note: Must set allow or success
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
# File 'lib/cli/kit/support/test_helper.rb', line 152 def fake(*a, stdout: '', stderr: '', allow: nil, success: nil, sudo: false, env: {}) raise ArgumentError, 'success or allow must be set' if success.nil? && allow.nil? @delegate_open3 ||= {} @delegate_open3[a.join(' ')] = { expected: { sudo: sudo, env: env, }, actual: { sudo: nil, env: nil, }, stdout: stdout, stderr: stderr, allow: allow, success: success, run: false, } end |
.original_capture2 ⇒ Object
Execute a command in the user’s environment This is meant to be largely equivalent to backticks, only with the env passed in. Captures the results of the command without output to the console
#### Parameters
-
‘*a`: A splat of arguments evaluated as a command. (e.g. `’rm’, folder` is equivalent to ‘rm #folder`)
-
sudo: If truthy, run this command with sudo. If String, pass tosudo_reason -
env: process environment with which to execute this command -
‘**kwargs`: additional arguments to pass to Open3.capture2
#### Returns
-
output: output (STDOUT) of the command execution -
status: boolean success status of the command execution
#### Usage ‘out, stat = CLI::Kit::System.capture2(’ls’, ‘a_folder’)‘
: (String cmd, *String args, ?sudo: (String | bool), ?env: Hash[String, String?], **untyped kwargs) -> [String, Process::Status]
78 79 80 |
# File 'lib/cli/kit/support/test_helper.rb', line 78 def capture2(cmd, *args, sudo: false, env: ENV.to_h, **kwargs) delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :capture2) end |
.original_capture2e ⇒ Object
Execute a command in the user’s environment This is meant to be largely equivalent to backticks, only with the env passed in. Captures the results of the command without output to the console
#### Parameters
-
‘*a`: A splat of arguments evaluated as a command. (e.g. `’rm’, folder` is equivalent to ‘rm #folder`)
-
sudo: If truthy, run this command with sudo. If String, pass tosudo_reason -
env: process environment with which to execute this command -
‘**kwargs`: additional arguments to pass to Open3.capture2e
#### Returns
-
output: output (STDOUT merged with STDERR) of the command execution -
status: boolean success status of the command execution
#### Usage ‘out_and_err, stat = CLI::Kit::System.capture2e(’ls’, ‘a_folder’)‘
: (String cmd, *String args, ?sudo: (String | bool), ?env: Hash[String, String?], **untyped kwargs) -> [String, Process::Status]
98 99 100 |
# File 'lib/cli/kit/support/test_helper.rb', line 98 def capture2e(cmd, *args, sudo: false, env: ENV.to_h, **kwargs) delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :capture2e) end |
.original_capture3 ⇒ Object
Execute a command in the user’s environment This is meant to be largely equivalent to backticks, only with the env passed in. Captures the results of the command without output to the console
#### Parameters
-
‘*a`: A splat of arguments evaluated as a command. (e.g. `’rm’, folder` is equivalent to ‘rm #folder`)
-
sudo: If truthy, run this command with sudo. If String, pass tosudo_reason -
env: process environment with which to execute this command -
‘**kwargs`: additional arguments to pass to Open3.capture3
#### Returns
-
output: STDOUT of the command execution -
error: STDERR of the command execution -
status: boolean success status of the command execution
#### Usage ‘out, err, stat = CLI::Kit::System.capture3(’ls’, ‘a_folder’)‘
: (String cmd, *String args, ?sudo: (String | bool), ?env: Hash[String, String?], **untyped kwargs) -> [String, String, Process::Status]
118 119 120 |
# File 'lib/cli/kit/support/test_helper.rb', line 118 def capture3(cmd, *args, sudo: false, env: ENV.to_h, **kwargs) delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :capture3) end |
.original_system ⇒ Object
Execute a command in the user’s environment Outputs result of the command without capturing it
#### Parameters
-
‘*a`: A splat of arguments evaluated as a command. (e.g. `’rm’, folder` is equivalent to ‘rm #folder`)
-
sudo: If truthy, run this command with sudo. If String, pass tosudo_reason -
env: process environment with which to execute this command -
‘**kwargs`: additional keyword arguments to pass to Process.spawn
#### Returns
-
status: TheProcess:Statusresult for the command execution
#### Usage ‘stat = CLI::Kit::System.system(’ls’, ‘a_folder’)‘
: (String cmd, *String args, ?sudo: (String | bool), ?env: Hash[String, String?], ?stdin: (IO | String | Integer | Symbol)?, **untyped kwargs) ?{ (String out, String err) -> void } -> Process::Status
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/cli/kit/support/test_helper.rb', line 61 def system(cmd, *args, sudo: false, env: ENV.to_h, stdin: nil, **kwargs, &block) cmd, args = apply_sudo(cmd, args, sudo) out_r, out_w = IO.pipe err_r, err_w = IO.pipe in_stream = if stdin stdin elsif STDIN.closed? :close else STDIN end cmd, args = resolve_path(cmd, args, env) process = Process #: as untyped pid = process.spawn(env, cmd, *args, 0 => in_stream, :out => out_w, :err => err_w, **kwargs) out_w.close err_w.close handlers = if block_given? { out_r => ->(data) { yield(data.force_encoding(Encoding::UTF_8), '') }, err_r => ->(data) { yield('', data.force_encoding(Encoding::UTF_8)) }, } else { out_r => ->(data) { STDOUT.write(data) }, err_r => ->(data) { STDOUT.write(data) }, } end previous_trailing = Hash.new('') loop do break if Process.wait(pid, Process::WNOHANG) ios = [err_r, out_r].reject(&:closed?) next if ios.empty? readers, = IO.select(ios, [], [], 1) next if readers.nil? # If IO.select times out we iterate again so we can check if the process has exited readers.each do |io| data, trailing = split_partial_characters(io.readpartial(4096)) handlers[io].call(previous_trailing[io] + data) previous_trailing[io] = trailing rescue IOError io.close end end $CHILD_STATUS end |
.os ⇒ Object
: -> Symbol
226 227 228 229 230 231 232 |
# File 'lib/cli/kit/system.rb', line 226 def os return :mac if /darwin/.match(RUBY_PLATFORM) return :linux if /linux/.match(RUBY_PLATFORM) return :windows if /mingw/.match(RUBY_PLATFORM) raise "Could not determine OS from platform #{RUBY_PLATFORM}" end |
.popen2(cmd, *args, sudo: false, env: ENV.to_h, **kwargs, &block) ⇒ Object
: (String cmd, *String args, ?sudo: (String | bool), ?env: Hash[String, String?], **untyped kwargs) ?{ (IO stdin, IO stdout, Process::Waiter wait_thr) -> [IO, IO, Process::Waiter] } -> [IO, IO, Process::Waiter]
100 101 102 |
# File 'lib/cli/kit/system.rb', line 100 def popen2(cmd, *args, sudo: false, env: ENV.to_h, **kwargs, &block) delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :popen2, &block) end |
.popen2e(cmd, *args, sudo: false, env: ENV.to_h, **kwargs, &block) ⇒ Object
: (String cmd, *String args, ?sudo: (String | bool), ?env: Hash[String, String?], **untyped kwargs) ?{ (IO stdin, IO stdout, Process::Waiter wait_thr) -> [IO, IO, Process::Waiter] } -> [IO, IO, Process::Waiter]
105 106 107 |
# File 'lib/cli/kit/system.rb', line 105 def popen2e(cmd, *args, sudo: false, env: ENV.to_h, **kwargs, &block) delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :popen2e, &block) end |
.popen3(cmd, *args, sudo: false, env: ENV.to_h, **kwargs, &block) ⇒ Object
: (String cmd, *String args, ?sudo: (String | bool), ?env: Hash[String, String?], **untyped kwargs) ?{ (IO stdin, IO stdout, IO stderr, Process::Waiter wait_thr) -> [IO, IO, IO, Process::Waiter] } -> [IO, IO, IO, Process::Waiter]
110 111 112 |
# File 'lib/cli/kit/system.rb', line 110 def popen3(cmd, *args, sudo: false, env: ENV.to_h, **kwargs, &block) delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :popen3, &block) end |
.reset! ⇒ Object
Resets the faked commands
175 176 177 |
# File 'lib/cli/kit/support/test_helper.rb', line 175 def reset! @delegate_open3 = {} end |
.split_partial_characters(data) ⇒ Object
Split off trailing partial UTF-8 Characters. UTF-8 Multibyte characters start with a 11xxxxxx byte that tells how many following bytes are part of this character, followed by some number of 10xxxxxx bytes. This simple algorithm will split off a whole trailing multi-byte character. : (String data) -> [String, String]
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 |
# File 'lib/cli/kit/system.rb', line 186 def split_partial_characters(data) last_byte = data.getbyte(-1) #: as !nil return [data, ''] if (last_byte & 0b1000_0000).zero? # UTF-8 is up to 4 characters per rune, so we could never want to trim more than that, and we want to avoid # allocating an array for the whole of data with bytes min_bound = -[4, data.bytesize].min fb = data.byteslice(min_bound..-1) #: as !nil final_bytes = fb.bytes partial_character_sub_index = final_bytes.rindex { |byte| byte & 0b1100_0000 == 0b1100_0000 } # Bail out for non UTF-8 return [data, ''] unless partial_character_sub_index start_byte = final_bytes[partial_character_sub_index] full_size = if start_byte & 0b1111_1000 == 0b1111_0000 4 elsif start_byte & 0b1111_0000 == 0b1110_0000 3 elsif start_byte & 0b1110_0000 == 0b110_00000 2 else nil # Not a valid UTF-8 character end return [data, ''] if full_size.nil? # Bail out for non UTF-8 if final_bytes.size - partial_character_sub_index == full_size # We have a full UTF-8 character, so we can just return the data return [data, ''] end partial_character_index = min_bound + partial_character_sub_index [ data.byteslice(0...partial_character_index), #: as !nil data.byteslice(partial_character_index..-1), #: as !nil ] end |
.sudo_reason(msg) ⇒ Object
Ask for sudo access with a message explaning the need for it Will make subsequent commands capable of running with sudo for a period of time
#### Parameters
-
msg: A message telling the user why sudo is needed
#### Usage ‘ctx.sudo_reason(“We need to do a thing”)`
: (String msg) -> void
22 23 24 25 26 27 28 29 30 |
# File 'lib/cli/kit/system.rb', line 22 def sudo_reason(msg) # See if sudo has a cached password %x(env SUDO_ASKPASS=/usr/bin/false sudo -A true > /dev/null 2>&1) return if $CHILD_STATUS.success? CLI::UI.with_frame_color(:blue) do puts(CLI::UI.fmt("{{i}} #{msg}")) end end |
.system(cmd, *a, sudo: false, env: {}, stdin: nil, **kwargs) ⇒ Object
Execute a command in the user’s environment Outputs result of the command without capturing it
#### Parameters
-
‘*a`: A splat of arguments evaluated as a command. (e.g. `’rm’, folder` is equivalent to ‘rm #folder`)
-
sudo: If truthy, run this command with sudo. If String, pass tosudo_reason -
env: process environment with which to execute this command -
‘**kwargs`: additional keyword arguments to pass to Process.spawn
#### Returns
-
status: TheProcess:Statusresult for the command execution
#### Usage ‘stat = CLI::Kit::System.system(’ls’, ‘a_folder’)‘
: (String cmd, *String args, ?sudo: (String | bool), ?env: Hash[String, String?], ?stdin: (IO | String | Integer | Symbol)?, **untyped kwargs) ?{ (String out, String err) -> void } -> Process::Status
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 |
# File 'lib/cli/kit/system.rb', line 130 def system(cmd, *args, sudo: false, env: ENV.to_h, stdin: nil, **kwargs, &block) cmd, args = apply_sudo(cmd, args, sudo) out_r, out_w = IO.pipe err_r, err_w = IO.pipe in_stream = if stdin stdin elsif STDIN.closed? :close else STDIN end cmd, args = resolve_path(cmd, args, env) process = Process #: as untyped pid = process.spawn(env, cmd, *args, 0 => in_stream, :out => out_w, :err => err_w, **kwargs) out_w.close err_w.close handlers = if block_given? { out_r => ->(data) { yield(data.force_encoding(Encoding::UTF_8), '') }, err_r => ->(data) { yield('', data.force_encoding(Encoding::UTF_8)) }, } else { out_r => ->(data) { STDOUT.write(data) }, err_r => ->(data) { STDOUT.write(data) }, } end previous_trailing = Hash.new('') loop do break if Process.wait(pid, Process::WNOHANG) ios = [err_r, out_r].reject(&:closed?) next if ios.empty? readers, = IO.select(ios, [], [], 1) next if readers.nil? # If IO.select times out we iterate again so we can check if the process has exited readers.each do |io| data, trailing = split_partial_characters(io.readpartial(4096)) handlers[io].call(previous_trailing[io] + data) previous_trailing[io] = trailing rescue IOError io.close end end $CHILD_STATUS end |
.which(cmd, env) ⇒ Object
: (String cmd, Hash[String, String?] env) -> String?
235 236 237 238 239 240 241 242 243 244 245 |
# File 'lib/cli/kit/system.rb', line 235 def which(cmd, env) exts = os == :windows ? (env['PATHEXT'] || 'exe').split(';') : [''] (env['PATH'] || '').split(File::PATH_SEPARATOR).each do |path| exts.each do |ext| exe = File.join(path, "#{cmd}#{ext}") return exe if File.executable?(exe) && !File.directory?(exe) end end nil end |