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 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.

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.

Methods
I
S
Class Public methods
included(k)
# File lib/methadone/sh.rb, line 66
def self.included(k)
  k.extend(self)
end
Instance Public methods
set_execution_strategy(strategy)

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. open4 will not be installed as a dependency. 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.

# File lib/methadone/sh.rb, line 172
def set_execution_strategy(strategy)
  @execution_strategy = strategy
end
set_sh_logger(logger)

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

# File lib/methadone/sh.rb, line 154
def set_sh_logger(logger)
  @sh_logger = logger
end
sh(command,options={},&block)

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 as a String or Array of String. The String form is simplest, but is open to injection. If you need to execute a command that is assembled from some portion of user input, consider using an Array of String. This form prevents tokenization that occurs in the String form. The first element is the command to execute, and the remainder are the arguments. See Methadone::ExecutionStrategy::Base for more info.

options

options to control the call. Currently responds to:

:expected

an Int or Array of Int representing error codes, in addition to 0, 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 three 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.

# File lib/methadone/sh.rb, line 100
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("stderr output of '#{command}': #{stderr}") unless stderr.strip.length == 0

  if process_status.success?
    sh_logger.debug("stdout output of '#{command}': #{stdout}") unless stdout.strip.length == 0
    call_block(block,stdout,stderr,process_status.exitstatus) unless block.nil?
  else
    sh_logger.info("stdout output of '#{command}': #{stdout}") unless stdout.strip.length == 0
    sh_logger.warn("Error running '#{command}'")
  end

  process_status.exitstatus
rescue *exception_meaning_command_not_found => ex
  sh_logger.error("Error running '#{command}': #{ex.message}")
  127
end
sh!(command,options={},&block)

Run a command, throwing an exception if the command exited nonzero. Otherwise, behaves exactly like sh.

options

options hash, responding to:

:expected

same as for sh

:on_fail

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.

Examples:

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
# File lib/methadone/sh.rb, line 139
def sh!(command,options={},&block)
  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