lib/methadone/sh.rb in methadone-1.0.0.rc5 vs lib/methadone/sh.rb in methadone-1.0.0.rc6
- old
+ new
@@ -1,14 +1,21 @@
if RUBY_PLATFORM == 'java'
require 'java'
require 'ostruct'
elsif RUBY_VERSION =~ /^1.8/
+ begin
require 'open4'
+ rescue LoadError
+ STDERR.puts "!! For Ruby #{RUBY_VERSION}, the open4 library must be installed"
+ raise
+ end
else
require 'open3'
end
+require 'methadone/process_status'
+
module Methadone
# Module with various helper methods for executing external commands.
# In most cases, you can use #sh to run commands and have decent logging
# done. You will likely use this in a class that also mixes-in
# Methadone::CLILogging (remembering that Methadone::Main mixes this in for you).
@@ -27,27 +34,27 @@
# # its error output to WARN, returns the nonzero exit status of the underlying command
#
# sh! 'cp non_existent_file.txt /nowhere_good'
# # => same as above, EXCEPT, raises a Methadone::FailedCommandError
#
- # sh 'cp foo.txt /tmp' do
- # # Behaves exactly as before, but this block is called after
- # end
+ # sh 'cp foo.txt /tmp' do
+ # # Behaves exactly as before, but this block is called after
+ # end
#
- # sh 'cp non_existent_file.txt /nowhere_good' do
- # # This block isn't called, since the command failed
- # end
+ # sh 'cp non_existent_file.txt /nowhere_good' do
+ # # This block isn't called, since the command failed
+ # end
#
- # sh 'ls -l /tmp/' do |stdout|
- # # stdout contains the output of the command
+ # sh 'ls -l /tmp/' do |stdout|
+ # # stdout contains the output of the command
+ # end
+ # sh 'ls -l /tmp/ /non_existent_dir' do |stdout,stderr|
+ # # stdout contains the output of the command,
+ # # stderr contains the standard error output.
# end
- # sh 'ls -l /tmp/ /non_existent_dir' do |stdout,stderr|
- # # stdout contains the output of the command,
- # # stderr contains the standard error output.
- # end
#
- # == Handling remote execution
+ # == Handling process execution
#
# In order to work on as many Rubies as possible, this class defers the actual execution
# to an execution strategy. See #set_execution_strategy if you think you'd like to override
# that, or just want to know how it works.
#
@@ -64,14 +71,19 @@
# If the command completed successfully, it's output is logged at DEBUG.
# If not, its output as logged at INFO. In either case, its
# error output is logged at WARN.
#
# command:: the command to run
- # block:: if provided, will be called if the command exited nonzero. The block may take 0, 1, or 2 arguments.
- # The arguments provided are the standard output as a string and the standard error as a string,
+ # options:: options to control the call. Currently responds to:
+ # +:expected+:: an Int or Array of Int representing error codes, <b>in addition to 0</b>, that are
+ # expected and therefore constitute success. Useful for commands that don't use
+ # exit codes the way you'd like
+ # block:: if provided, will be called if the command exited nonzero. The block may take 0, 1, 2, or 3 arguments.
+ # The arguments provided are the standard output as a string, standard error as a string, and
+ # the exitstatus as an Int.
# You should be safe to pass in a lambda instead of a block, as long as your
- # lambda doesn't take more than two arguments
+ # lambda doesn't take more than three arguments
#
# Example
#
# sh "cp foo /tmp"
# sh "ls /tmp" do |stdout|
@@ -80,35 +92,37 @@
# sh "ls -l /tmp foobar" do |stdout,stderr|
# # ...
# end
#
# Returns the exit status of the command. Note that if the command doesn't exist, this returns 127.
- def sh(command,&block)
+ def sh(command,options={},&block)
sh_logger.debug("Executing '#{command}'")
stdout,stderr,status = execution_strategy.run_command(command)
+ process_status = Methadone::ProcessStatus.new(status,options[:expected])
sh_logger.warn("Error output of '#{command}': #{stderr}") unless stderr.strip.length == 0
- if status.exitstatus != 0
+ if process_status.success?
+ sh_logger.debug("Output of '#{command}': #{stdout}") unless stdout.strip.length == 0
+ call_block(block,stdout,stderr,process_status.exitstatus) unless block.nil?
+ else
sh_logger.info("Output of '#{command}': #{stdout}") unless stdout.strip.length == 0
sh_logger.warn("Error running '#{command}'")
- else
- sh_logger.debug("Output of '#{command}': #{stdout}") unless stdout.strip.length == 0
- call_block(block,stdout,stderr) unless block.nil?
end
- status.exitstatus
+ process_status.exitstatus
rescue exception_meaning_command_not_found => ex
sh_logger.error("Error running '#{command}': #{ex.message}")
127
end
# Run a command, throwing an exception if the command exited nonzero.
# Otherwise, behaves exactly like #sh.
#
- # options - options hash, responding to:
+ # options:: options hash, responding to:
+ # <tt>:expected</tt>:: same as for #sh
# <tt>:on_fail</tt>:: a custom error message. This allows you to have your
# app exit on shell command failures, but customize the error
# message that they see.
#
# Raises Methadone::FailedCommandError if the command exited nonzero.
@@ -118,12 +132,15 @@
# sh!("rsync foo bar")
# # => if command fails, app exits and user sees: "error: Command 'rsync foo bar' exited 12"
# sh!("rsync foo bar", :on_fail => "Couldn't rsync, check log for details")
# # => if command fails, app exits and user sees: "error: Couldn't rsync, check log for details
def sh!(command,options={},&block)
- sh(command,&block).tap do |exitstatus|
- raise Methadone::FailedCommandError.new(exitstatus,command,options[:on_fail]) if exitstatus != 0
+ sh(command,options,&block).tap do |exitstatus|
+ process_status = Methadone::ProcessStatus.new(exitstatus,options[:expected])
+ unless process_status.success?
+ raise Methadone::FailedCommandError.new(exitstatus,command,options[:on_fail])
+ end
end
end
# Override the default logger (which is the one provided by CLILogging).
# You would do this if you want a custom logger or you aren't mixing-in
@@ -136,12 +153,16 @@
end
# Set the strategy to use for executing commands. In general, you don't need to set this
# since this module chooses an appropriate implementation based on your Ruby platform:
#
- # 1.8 Rubies, including 1.8, and REE:: Open4 is used via Methadone::ExecutionStrategy::Open_4
- # Rubinius:: Open4 is used, but we handle things a bit differently; see Methadone::ExecutionStrategy::RBXOpen_4
+ # 1.8 Rubies, including 1.8, and REE:: Open4 is used via Methadone::ExecutionStrategy::Open_4. <b><tt>open4</tt> will not be
+ # installed as a dependency</b>. RubyGems doesn't allow conditional dependencies,
+ # so make sure that your app declares it as a dependency if you think you'll be
+ # running on 1.8 or REE.
+ # Rubinius:: Open4 is used, but we handle things a bit differently; see Methadone::ExecutionStrategy::RBXOpen_4.
+ # Same warning on dependencies applies.
# JRuby:: Use JVM calls to +Runtime+ via Methadone::ExecutionStrategy::JVM
# Windows:: Currently no support for Windows
# All others:: we use Open3 from the standard library, via Methadone::ExecutionStrategy::Open_3
#
# See Methadone::ExecutionStrategy::Base for how to implement your own.
@@ -174,18 +195,20 @@
def sh_logger
@sh_logger ||= self.logger
end
# Safely call our block, even if the user passed in a lambda
- def call_block(block,stdout,stderr)
+ def call_block(block,stdout,stderr,exitstatus)
# blocks that take no arguments have arity -1. Or 0. Ugh.
if block.arity > 0
case block.arity
when 1
block.call(stdout)
+ when 2
+ block.call(stdout,stderr)
else
# Let it fail for lambdas
- block.call(stdout,stderr)
+ block.call(stdout,stderr,exitstatus)
end
else
block.call
end
end