Module: Cinnabar::Command

Defined in:
lib/cinnabar/cmd_runner.rb,
lib/cinnabar/cmd_runner.rb,
lib/cinnabar/cmd_runner.rb

Overview

typed: false frozen_string_literal: true

Defined Under Namespace

Modules: ArrExt, ArrMixin, ArrRefin, TaskArrExt, TaskArrMixin, TaskArrRefin

Class Method Summary collapse

Class Method Details

.async_run(cmd_arr, env_hash = nil, opts: {}) ⇒ Array(IO, Process::Waiter)

Launch a command asynchronously (non-blocking) and return its stdout stream and process waiter.

This is a sugar over Open3.popen2, intended to start a subprocess and immediately hand back:

  1. an IO for reading the command's stdout, and
  2. a Process::Waiter (a thread-like object) that can be awaited later.
  • If :stdin_data is provided, the data will be written to the child's stdin and the stdin will be closed.
  • When :stdin_data is absent, stdin is simply closed and the method returns immediately without blocking on output.

Examples:

start a process and later wait for it


Cmd = Cinnabar::Command
stdout_fd, waiter = Cmd.async_run(['sh', '-c', 'echo hello; sleep 1; echo done'])

output, status = Cmd.wait_with_output(stdout_fd, waiter)

pass stdin data


Cmd = Cinnabar::Command
opts = {stdin_data: "Run in the background" }
io_and_waiter = Cmd.async_run(%w[wc -m], opts:)

output, status = Cmd.wait_with_output(*io_and_waiter)
status.success?   #=> true
output.to_i == 21 #=> true

Parameters:

  • cmd_arr (Array<String>)

    The command and its arguments (e.g., %w[printf hello]).

  • env_hash (#to_h) (defaults to: nil)

    Optional environment variables; keys/values will be normalized by #normalize_env before being passed to the child.

  • opts (Hash) (defaults to: {})

    Additional options.

    • Only the following keys are extracted and handled explicitly;
      • :stdin_data
      • :binmode
      • :stdin_binmode
      • :stdout_binmode

    all other keys are passed through to Open3.popen2 unchanged.

Options Hash (opts:):

  • :stdin_data (String, #readpartial)

    Data to write to the child's stdin. If it responds to #readpartial, it will be streamed via IO.copy_stream;

  • :binmode (Boolean)

    When true, set both stdin and stdout to binary mode (useful for binary data).

  • :stdin_binmode (Boolean)

    Sets only stdin to binary mode.

  • :stdout_binmode (Boolean)

    Sets only stdout to binary mode.

Returns:

  • (Array(IO, Process::Waiter))

    A pair [stdout_io, waiter]:

    • stdout_io is an IO for reading stdout
    • waiter is a Process::Waiter;
      • call waiter.value to get Process::Status;
      • or waiter.join to block until the process exits.

Raises:

  • (StandardError)

    Reraises any non-Errno::EPIPE exception encountered while writing to stdin. Errno::EPIPE is logged and swallowed.

See Also:



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
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/cinnabar/cmd_runner.rb', line 206

def async_run(cmd_arr, env_hash = nil, opts: {}) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/PerceivedComplexity,Metrics/CyclomaticComplexity
  "Asynchronously executing system command: #{cmd_arr}".log_dbg
  "opts: #{opts}".log_dbg

  stdin_data = opts.delete(:stdin_data)
  binmode = opts.delete(:binmode)
  stdin_binmode = opts.delete(:stdin_binmode)
  stdout_binmode = opts.delete(:stdout_binmode)

  'async_run() does not support the :allow_failure option.'.log_warn if opts.delete(:allow_failure)

  final_env = normalize_env(env_hash)

  stdin, stdout, waiter =
    if final_env.nil?
      Open3.popen2(*cmd_arr, opts)
    else
      Open3.popen2(final_env, *cmd_arr, opts)
    end

  if binmode
    stdin.binmode
    stdout.binmode
  else
    stdin.binmode if stdin_binmode
    stdout.binmode if stdout_binmode
  end

  # Non-blocking: no stdin to write; return immediately
  unless stdin_data
    stdin.close
    return [stdout, waiter]
  end

  begin
    if stdin_data.respond_to? :readpartial
      IO.copy_stream(stdin_data, stdin)
    else
      stdin.write stdin_data
    end
  rescue Errno::EPIPE => e
    e.log_err
  rescue StandardError => e
    "Failed to write stdin data: #{e}".log_err
    Kernel.raise e
  ensure
    stdin.close
  end
  [stdout, waiter]
end

.normalize_env(hash) ⇒ Hash{String => String}?

Returns a hash where both keys and values are strings.

Parameters:

  • hash (#to_h)

Returns:

  • (Hash{String => String}, nil)

    a hash where both keys and values are strings



129
130
131
132
133
134
135
# File 'lib/cinnabar/cmd_runner.rb', line 129

def normalize_env(hash)
  return nil if hash.nil?
  return nil if hash.respond_to?(:empty?) && hash.empty?

  hash.to_h { |k, v| [k.to_s, v.to_s] }
    .tap { "normalized_env:#{_1}".log_dbg }
end

.run(cmd_arr, env_hash = nil, opts: {}) ⇒ String?

Executes the command synchronously (blocking) and returns its standard output.

Examples:

pass env


Cmd = Cinnabar::Command
cmd_arr = %w[sh -c] << 'printf $WW'
env_hash = {WW: 2}
opts = {allow_failure: true}
output = Cmd.run(cmd_arr, env_hash, opts:)
output.to_i == 2 #=> true

pass stdin data


opts = {stdin_data: "Hello\nWorld\n"}
output = Cinnabar::Command.run(%w[wc -l], opts:)
output.to_i == 2 #=> true

Parameters:

  • cmd_arr (Array<String>)

    The command and its arguments (e.g., %w[printf hello]).

  • env_hash (#to_h) (defaults to: nil)

    Environment variables to pass to the command.

  • opts (Hash) (defaults to: {})
    • Only the :allow_failure is extracted and handled explicitly;
    • all other keys are passed through to Open3.capture2 unchanged.

Options Hash (opts:):

  • :allow_failure (Boolean)

    Indicates whether the command is allowed to fail.

Returns:

  • (String, nil)

    the standard output of the command.

Raises:

  • (RuntimeError)

    when allow_failure: false and the process exits with non-zero status

See Also:



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
# File 'lib/cinnabar/cmd_runner.rb', line 42

def run(cmd_arr, env_hash = nil, opts: {}) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
  'Running and capturing the output of a system command.'.log_dbg
  cmd_arr.log_info
  "opts: #{opts}".log_dbg

  allow_failure = opts.delete(:allow_failure) || false

  final_env = normalize_env(env_hash)

  begin
    stdout, status =
      if final_env.nil?
        Open3.capture2(*cmd_arr, opts)
      else
        Open3.capture2(final_env, *cmd_arr, opts)
      end
  rescue StandardError => e
    Kernel.raise e unless allow_failure
    e.log_err
    return stdout
  end

  return stdout if status.success?

  err_msg = "Command failed: #{cmd_arr.join(' ')}"
  Kernel.raise err_msg unless allow_failure

  err_msg.log_err
  stdout
end

.run_cmd(cmd_arr, env_hash = nil, opts: {}) ⇒ Boolean

Executes a system command using Ruby's Kernel.system.

It runs the command synchronously, blocks until completion, and does not capture stdout or stderr.

Examples:

pwd


Cmd = Cinnabar::Command
opts = {chdir: '/tmp', allow_failure: true}
Cmd.run_cmd(%w[pwd], opts:)

pass env


Cmd = Cinnabar::Command
cmd_arr = %w[sh -c] << 'printf $WW'
env_hash = {WW: 2}
Cmd.run_cmd(cmd_arr, env_hash)

Parameters:

  • cmd_arr (Array<String>)

    The command and its arguments as an array, e.g., %w[ls -lh].

  • env_hash (#to_h) (defaults to: nil)

    Environment variables to pass to the command.

  • opts (Hash) (defaults to: {})
    • Only the :allow_failure is extracted and handled explicitly;
    • all other keys are passed through to Kernel.system unchanged.

Options Hash (opts:):

  • :allow_failure (Boolean)

    Indicates whether the command is allowed to fail. If true, the method will return false instead of raising an exception when the command exits with a non-zero status.

Returns:

  • (Boolean)

    Returns true if the command succeeds (exit status 0), or false if it fails and allow_failure is true.

Raises:

  • (RuntimeError)

    Raises an error if the command fails and allow_failure is false.

See Also:



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/cinnabar/cmd_runner.rb', line 109

def run_cmd(cmd_arr, env_hash = nil, opts: {})
  'Running system command'.log_dbg
  cmd_arr.log_info
  "opts: #{opts}".log_dbg

  allow_failure = opts.delete(:allow_failure) || false
  exception = !allow_failure
  "exception: #{exception}".log_dbg
  options = opts.merge({ exception: })

  final_env = normalize_env(env_hash)
  if final_env.nil?
    Kernel.system(*cmd_arr, options)
  else
    Kernel.system(final_env, *cmd_arr, options)
  end
end

.wait_with_output(io_fd, waiter) ⇒ Array(String, Process::Status)

Note:

This method blocks until the process exits and all output is read.

Waits for a process to finish and reads all remaining output from its stdout.

Examples:

Wait for process and capture output


require 'sinlog'
using Sinlog::Refin

Cmd = Cinnabar::Command

fd, waiter = %w[ruby -e].push('sleep 2; puts "OK"')
              .then { Cmd.async_run(_1) }

"You can now do other things without waiting for the process to complete.".log_dbg

"blocking wait".log_info
output, status = Cmd.wait_with_output(fd, waiter)

"Exit code: #{status.exitstatus}".log_warn unless status.success?
"Output:\n#{output}".log_info

Parameters:

  • io_fd (IO)

    The IO object connected to the process's stdout (or combined stdout & stderr).

  • waiter (Process::Waiter)

    The waiter thread returned by Open3.popen2 or Open3.popen2e.

Returns:

  • (Array(String, Process::Status))

    A two-element array:

    • The full output read from io_fd.
    • The Process::Status object representing the process's exit status.

See Also:



287
288
289
290
291
292
# File 'lib/cinnabar/cmd_runner.rb', line 287

def wait_with_output(io_fd, waiter)
  status = waiter.value
  output = io_fd.read
  io_fd.close
  [output, status]
end