Module: Methadone::SH

Defined in:
lib/methadone/sh.rb

Overview

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.

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.included(k) ⇒ Object



66
67
68
# File 'lib/methadone/sh.rb', line 66

def self.included(k)
  k.extend(self)
end

Instance Method Details

#set_execution_strategy(strategy) ⇒ Object

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.



172
173
174
# File 'lib/methadone/sh.rb', line 172

def set_execution_strategy(strategy)
  @execution_strategy = strategy
end

#set_sh_logger(logger) ⇒ Object

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



154
155
156
# File 'lib/methadone/sh.rb', line 154

def set_sh_logger(logger)
  @sh_logger = logger
end

#sh(command, options = {}, &block) ⇒ Object

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.



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# 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) ⇒ Object

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


139
140
141
142
143
144
145
146
# 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