if RUBY_PLATFORM == 'java'
require 'java'
require 'ostruct'
elsif RUBY_VERSION =~ /^1.8/
require 'open4'
else
require 'open3'
end
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).
# If you don't, you must provide a logger via #set_sh_logger.
#
# == Examples
#
# include Methadone::SH
#
# sh 'cp foo.txt /tmp'
# # => logs the command to DEBUG, executes the command, logs its output to DEBUG and its
# # error output to WARN, returns 0
#
# sh 'cp non_existent_file.txt /nowhere_good'
# # => logs the command to DEBUG, executes the command, logs its output to INFO and
# # 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 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
# 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
#
# 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.
#
# == More complex execution and subprocess management
#
# This is not intended to be a complete replacement for Open3 or an enhanced means of managing subprocesses.
# This is to make it easy for you to shell-out to external commands and have your app be robust and
# easy to maintain.
module SH
# Run a shell command, capturing and logging its output.
# 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,
# 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
#
# Example
#
# sh "cp foo /tmp"
# sh "ls /tmp" do |stdout|
# # stdout contains the output of ls /tmp
# end
# 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)
sh_logger.debug("Executing '#{command}'")
stdout,stderr,status = execution_strategy.run_command(command)
sh_logger.warn("Error output of '#{command}': #{stderr}") unless stderr.strip.length == 0
if status.exitstatus != 0
sh_logger.info("Output of '#{command}': #{stdout}")
sh_logger.warn("Error running '#{command}'")
else
sh_logger.debug("Output of '#{command}': #{stdout}")
call_block(block,stdout,stderr) unless block.nil?
end
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.
#
# Raises Methadone::FailedCommandError if the command exited nonzero.
def sh!(command,&block)
sh(command,&block).tap do |exitstatus|
raise Methadone::FailedCommandError.new(exitstatus,command) if exitstatus != 0
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
# CLILogging.
#
# Note that this method is *not* called sh_logger= to avoid annoying situations
# where Ruby thinks you are setting a local variable
def set_sh_logger(logger)
@sh_logger = logger
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
# 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.
def set_execution_strategy(strategy)
@execution_strategy = strategy
end
private
def exception_meaning_command_not_found
execution_strategy.exception_meaning_command_not_found
end
def self.default_execution_strategy_class
if RUBY_PLATFORM == 'java'
Methadone::ExecutionStrategy::JVM
elsif defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx'
Methadone::ExecutionStrategy::RBXOpen_4
elsif RUBY_VERSION =~ /^1.8/
Methadone::ExecutionStrategy::Open_4
else
Methadone::ExecutionStrategy::Open_3
end
end
def execution_strategy
@execution_strategy ||= SH.default_execution_strategy_class.new
end
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)
# blocks that take no arguments have arity -1. Or 0. Ugh.
if block.arity > 0
case block.arity
when 1
block.call(stdout)
else
# Let it fail for lambdas
block.call(stdout,stderr)
end
else
block.call
end
end
end
end