Module: Kernel
- Defined in:
- lib/qbash.rb
Overview
Execute one bash command.
- Author
-
Yegor Bugayenko ([email protected])
- Copyright
-
Copyright © 2024-2025 Yegor Bugayenko
- License
-
MIT
Instance Method Summary collapse
-
#qbash(cmd, opts: [], stdin: '', env: {}, log: Loog::NULL, accept: [0], both: false, level: Logger::DEBUG) ⇒ String
Execute a single bash command safely with proper error handling.
Instance Method Details
#qbash(cmd, opts: [], stdin: '', env: {}, log: Loog::NULL, accept: [0], both: false, level: Logger::DEBUG) ⇒ String
Execute a single bash command safely with proper error handling.
QBash provides a safe way to execute shell commands with proper error handling, logging, and stdin/stdout management. It’s designed to be simple for basic use cases while offering powerful options for advanced scenarios.
Basic Usage
# Execute a command and get its output
year = qbash('date +%Y')
puts "Current year: #{year}"
# Execute a command that might fail
files = qbash('find /tmp -name "*.log"')
Working with Exit Codes
# Get both output and exit code
output, code = qbash('grep "error" /var/log/system.log', both: true)
puts "Command succeeded" if code.zero?
# Accept multiple exit codes as valid
result = qbash('grep "pattern" file.txt', accept: [0, 1])
Providing Input via STDIN
# Pass data to command's stdin
result = qbash('wc -l', stdin: "line 1\nline 2\nline 3")
Environment Variables
# Set environment variables for the command
output = qbash('echo $NAME', env: { 'NAME' => 'Ruby' })
Logging
# Enable detailed logging to stdout
qbash('ls -la', log: $stdout)
# Use custom logger with specific level
logger = Logger.new($stdout)
qbash('make all', log: logger, level: Logger::INFO)
Process Control
# Get control over long-running process
qbash('sleep 30') do |pid|
puts "Process #{pid} is running..."
# Do something while process is running
# Process will be terminated when block exits
end
For command with multiple arguments, you can use Shellwords.escape() to properly escape each argument. Stderr automatically merges with stdout.
Read this <a href=“github.com/yegor256/qbash”>README</a> file for more details.
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 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 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 |
# File 'lib/qbash.rb', line 84 def qbash(cmd, opts: [], stdin: '', env: {}, log: Loog::NULL, accept: [0], both: false, level: Logger::DEBUG) env.each { |k, v| raise "env[#{k}] is nil" if v.nil? } cmd = cmd.reject { |a| a.nil? || (a.is_a?(String) && a.empty?) }.join(' ') if cmd.is_a?(Array) logit = lambda do |msg| msg = msg.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?').gsub(/\n$/, '') mtd = case level when Logger::DEBUG :debug when Logger::INFO :info when Logger::WARN :warn when Logger::ERROR :error else raise "Unknown log level #{level}" end if log.nil? # nothing to print elsif log.respond_to?(mtd) log.__send__(mtd, msg) else log.print("#{msg}\n") end end buf = '' e = 1 start = Time.now bash = ['/bin/bash'] + opts + ['-c', cmd] Open3.popen2e(env, *bash) do |sin, sout, ctrl| pid = ctrl.pid logit["+ #{cmd} /##{pid}"] consume = lambda do loop do sleep 0.001 break if sout.closed? || sout.eof? ln = sout.gets # together with the \n at the end next if ln.nil? next if ln.empty? buf += ln ln = "##{ctrl.pid}: #{ln}" logit[ln] rescue IOError => e logit[e.] break end end sin.write(stdin) sin.close if block_given? watch = Thread.new { consume.call } watch.abort_on_exception = true begin yield pid ensure sout.close watch.join(0.01) watch.kill if watch.alive? attempt = 1 since = Time.now loop do Process.kill(0, pid) # should be dead already (raising Errno::ESRCH) Process.kill('TERM', pid) # let's try to kill it logit["Tried to stop ##{pid} with SIGTERM (attempt no.#{attempt}, #{since.ago}): #{cmd}"] sleep(0.1) attempt += 1 rescue Errno::ESRCH logit["Process ##{pid} reacted to SIGTERM, after #{attempt} attempts and #{since.ago}"] if attempt > 1 break end end else consume.call end e = ctrl.value.exitstatus if !accept.nil? && !accept.include?(e) raise "The command '#{cmd}' failed with exit code ##{e} in #{start.ago}\n#{buf}" end end return [buf, e] if both buf end |