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

Class Method Details

.capture2(*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 to `sudo_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’)‘



46
47
48
# File 'lib/cli/kit/system.rb', line 46

def capture2(*a, sudo: false, env: ENV, **kwargs)
  delegate_open3(*a, sudo: sudo, env: env, method: :capture2, **kwargs)
end

.capture2e(*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 to `sudo_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’)‘



67
68
69
# File 'lib/cli/kit/system.rb', line 67

def capture2e(*a, sudo: false, env: ENV, **kwargs)
  delegate_open3(*a, sudo: sudo, env: env, method: :capture2e, **kwargs)
end

.capture3(*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 to `sudo_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’)‘



89
90
91
# File 'lib/cli/kit/system.rb', line 89

def capture3(*a, sudo: false, env: ENV, **kwargs)
  delegate_open3(*a, sudo: sudo, env: env, method: :capture3, **kwargs)
end

.error_messageObject

Returns the errors associated to a test run

#### Returns ‘errors` (String) a string representing errors found on this run, nil if none



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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/cli/kit/support/test_helper.rb', line 150

def error_message
  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(<<~EOF)
    {{bold:Unexpected command invocations:}}
    {{command:#{errors[:unexpected].join("\n")}}}
    EOF
  end

  unless errors[:not_run].empty?
    final_error << CLI::UI.fmt(<<~EOF)
    {{bold:Expected commands were not run:}}
    {{command:#{errors[:not_run].join("\n")}}}
    EOF
  end

  unless errors[:other].empty?
    final_error << CLI::UI.fmt(<<~EOF)
    {{bold:Commands were not run as expected:}}
    #{errors[:other].map { |cmd, msg| "{{command:#{cmd}}}\n#{msg}" }.join("\n\n")}
    EOF
  end

  return nil if final_error.empty?
  "\n" + final_error.join("\n") # Initial new line for formatting reasons
end

.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

Raises:

  • (ArgumentError)


119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/cli/kit/support/test_helper.rb', line 119

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_capture2Object

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 to `sudo_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’)‘



51
52
53
# File 'lib/cli/kit/support/test_helper.rb', line 51

def capture2(*a, sudo: false, env: ENV, **kwargs)
  delegate_open3(*a, sudo: sudo, env: env, method: :capture2, **kwargs)
end

.original_capture2eObject

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 to `sudo_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’)‘



69
70
71
# File 'lib/cli/kit/support/test_helper.rb', line 69

def capture2e(*a, sudo: false, env: ENV, **kwargs)
  delegate_open3(*a, sudo: sudo, env: env, method: :capture2e, **kwargs)
end

.original_capture3Object

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 to `sudo_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’)‘



87
88
89
# File 'lib/cli/kit/support/test_helper.rb', line 87

def capture3(*a, sudo: false, env: ENV, **kwargs)
  delegate_open3(*a, sudo: sudo, env: env, method: :capture3, **kwargs)
end

.original_systemObject

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 to `sudo_reason`

  • ‘env`: process environment with which to execute this command

  • ‘**kwargs`: additional keyword arguments to pass to Process.spawn

#### Returns

  • ‘status`: boolean success status of the command execution

#### Usage ‘stat = CLI::Kit::System.system(’ls’, ‘a_folder’)‘



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/cli/kit/support/test_helper.rb', line 36

def system(*a, sudo: false, env: ENV, **kwargs)
  a = apply_sudo(*a, sudo)

  out_r, out_w = IO.pipe
  err_r, err_w = IO.pipe
  in_stream = STDIN.closed? ? :close : STDIN
  pid = Process.spawn(env, *resolve_path(a, env), 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
    ios = [err_r, out_r].reject(&:closed?)
    break if ios.empty?

    readers, = IO.select(ios)
    readers.each do |io|
      begin
        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
  end

  Process.wait(pid)
  $CHILD_STATUS
end

.reset!Object

Resets the faked commands



142
143
144
# File 'lib/cli/kit/support/test_helper.rb', line 142

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.



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/cli/kit/system.rb', line 150

def split_partial_characters(data)
  last_byte = data.getbyte(-1)
  return [data, ''] if (last_byte & 0b1000_0000).zero?

  # UTF-8 is up to 6 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 = -[6, data.bytesize].min
  final_bytes = data.byteslice(min_bound..-1).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
  partial_character_index = min_bound + partial_character_sub_index

  [data.byteslice(0...partial_character_index), data.byteslice(partial_character_index..-1)]
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”)`



20
21
22
23
24
25
26
27
# File 'lib/cli/kit/system.rb', line 20

def sudo_reason(msg)
  # See if sudo has a cached password
  `env SUDO_ASKPASS=/usr/bin/false sudo -A true`
  return if $CHILD_STATUS.success?
  CLI::UI.with_frame_color(:blue) do
    puts(CLI::UI.fmt("{{i}} #{msg}"))
  end
end

.system(*a, sudo: false, env: {}, **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 to `sudo_reason`

  • ‘env`: process environment with which to execute this command

  • ‘**kwargs`: additional keyword arguments to pass to Process.spawn

#### Returns

  • ‘status`: boolean success status of the command execution

#### Usage ‘stat = CLI::Kit::System.system(’ls’, ‘a_folder’)‘



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/cli/kit/system.rb', line 108

def system(*a, sudo: false, env: ENV, **kwargs)
  a = apply_sudo(*a, sudo)

  out_r, out_w = IO.pipe
  err_r, err_w = IO.pipe
  in_stream = STDIN.closed? ? :close : STDIN
  pid = Process.spawn(env, *resolve_path(a, env), 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
    ios = [err_r, out_r].reject(&:closed?)
    break if ios.empty?

    readers, = IO.select(ios)
    readers.each do |io|
      begin
        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
  end

  Process.wait(pid)
  $CHILD_STATUS
end