Skip to content

Instantly share code, notes, and snippets.

@nsmmrs
Last active September 3, 2022 13:42
Show Gist options
  • Save nsmmrs/4e73b23850ed884527ddc3e01da4a066 to your computer and use it in GitHub Desktop.
Save nsmmrs/4e73b23850ed884527ddc3e01da4a066 to your computer and use it in GitHub Desktop.
A convenience wrapper around `Open3.capture3` for easily running commands, capturing the output, and handling failures.
require "open3"
require "json"
class ShellCommand
attr_reader :line, :stdin, :binmode, :stdout, :stderr, :status
def initialize(line, stdin, binmode, stdout, stderr, status)
@line = line
@stdin = stdin
@binmode = binmode
@stdout = stdout
@stderr = stderr
@status = status
end
private_class_method :new
def success?() status.success? end
def pid() status.pid end
def exit_code() status.exitstatus end
def self.run(*line, **options)
line = line.map(&:to_s)
stdin = options[:stdin_data] || ""
binmode = options[:binmode] || false
args = line + [options]
captures = begin
Open3.capture3(*args)
rescue
raise Invalid.new(args)
end
command = new(line, stdin, binmode, *captures)
case
when block_given?
yield command
when command.success?
command.stdout.rstrip
else
raise Failed.new(command)
end
end
class Failed < StandardError
attr_reader :command
def initialize(command)
@command = command
end
def message
"Shell command failed: #{command.to_json}"
end
end
class Invalid < StandardError
def initialize(line)
super "Shell command invalid (did not run): #{line.to_json}"
end
end
def as_json
{
line: line,
stdin: stdin,
binmode: binmode,
stdout: stdout,
stderr: stderr,
pid: pid,
exit_code: exit_code,
}
end
def to_json(*args)
as_json.to_json(*args)
end
end
@nsmmrs
Copy link
Author

nsmmrs commented Sep 3, 2022

I wrote a simpler version of this to clean up a bunch of code around system() calls in one of our Rails apps.

I liked how it turned out, so I cleaned it up and fleshed it out a little.

ShellCommand.run takes the same arguments as Open3.capture3, but it simply returns the output of a command, or raises an error with all the command info (both as an object and in the message).

You can also pass a block to do your own error handling, or ignore errors. For example, the following will return false rather than raise an exception in the failure case:

ShellCommand.run "ls", some_file_name, &:success?

@ShalokShalom
Copy link

Thanks a lot!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment