Class: GDKBox::Shell

Inherits:
Object
  • Object
show all
Defined in:
lib/gdkbox/shell.rb

Overview

Thin wrapper around shelling out to external commands.

All arguments are passed as an explicit argv array (never a single interpolated string) so user-supplied values cannot be interpreted by a shell. Tests inject a fake that records the commands instead of running them.

Defined Under Namespace

Classes: Result

Instance Method Summary collapse

Instance Method Details

#capture(*args, input: nil) ⇒ Object

Run a command and return its stdout, raising on failure.



47
48
49
# File 'lib/gdkbox/shell.rb', line 47

def capture(*args, input: nil)
  run!(*args, input: input).stdout
end

#run(*args, input: nil) ⇒ Object

Run a command, returning a Result. Never raises on a non-zero exit.



32
33
34
35
36
# File 'lib/gdkbox/shell.rb', line 32

def run(*args, input: nil)
  argv = args.map(&:to_s)
  stdout, stderr, status = Open3.capture3(*argv, stdin_data: input)
  Result.new(stdout, stderr, status.exitstatus || 1)
end

#run!(*args, input: nil) ⇒ Object

Run a command, raising CommandError on a non-zero exit.

Raises:



39
40
41
42
43
44
# File 'lib/gdkbox/shell.rb', line 39

def run!(*args, input: nil)
  result = run(*args, input: input)
  raise CommandError.new(args, result.status, result.stderr) unless result.success?

  result
end

#stream_tty(*args, input: nil) {|trailing| ... } ⇒ Object

Run a command attached to a pseudo-terminal, yielding each line of its output as it appears so callers can render live progress. A PTY is used (rather than a pipe) because some programs adapt their output to whether they are interactive: docker pull, for instance, only prints per-layer byte counts and progress bars when it believes it is on a terminal. ANSI escape sequences are stripped and carriage-return repaints are split into separate lines before yielding. Returns a Result with empty stdout/stderr (the output was streamed, not captured) and the child's exit status; never raises on a non-zero exit.

Yields:

  • (trailing)


60
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
# File 'lib/gdkbox/shell.rb', line 60

def stream_tty(*args, input: nil)
  require "pty"
  argv = args.map(&:to_s)
  reader, writer, pid = PTY.spawn(*argv)
  writer.write(input) if input
  writer.close

  buffer = +""
  begin
    loop do
      buffer << reader.readpartial(4096)
      while (idx = buffer.index(/[\r\n]/))
        line = clean_tty(buffer.slice!(0..idx))
        yield line if block_given? && line
      end
    end
  rescue Errno::EIO, EOFError
    # The child exited and closed its end of the PTY.
  ensure
    reader.close unless reader.closed?
  end

  trailing = clean_tty(buffer)
  yield trailing if block_given? && trailing
  _, status = Process.wait2(pid)
  Result.new("", "", status&.exitstatus || 1)
end

#which(command) ⇒ Object

Resolve an executable on PATH, returning its path or nil.



89
90
91
92
# File 'lib/gdkbox/shell.rb', line 89

def which(command)
  result = run("sh", "-c", "command -v #{command}")
  result.success? ? result.stdout.strip : nil
end