Class: GDKBox::Shell
- Inherits:
-
Object
- Object
- GDKBox::Shell
- 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
-
#capture(*args, input: nil) ⇒ Object
Run a command and return its stdout, raising on failure.
-
#run(*args, input: nil) ⇒ Object
Run a command, returning a Result.
-
#run!(*args, input: nil) ⇒ Object
Run a command, raising CommandError on a non-zero exit.
-
#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.
-
#which(command) ⇒ Object
Resolve an executable on PATH, returning its path or nil.
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.
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.
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 |