Skip to content

Commit

Permalink
Documentation and better separation of classes
Browse files Browse the repository at this point in the history
  • Loading branch information
davetron5000 committed Jan 21, 2012
1 parent e9d13d5 commit 61eff0e
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 53 deletions.
47 changes: 47 additions & 0 deletions README.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Currently, this library is under development and has the following to offer:

* Bootstrapping a new CLI app
* Lightweight DSL to structure your bin file
* Simple wrapper for running external commands with good logging
* Utility Classes
* Methadone::CLILogger - a logger subclass that sends messages to standard error standard out as appropriate
* Methadone::CLILogging - a module that, when included in any class, provides easy access to a shared logger
Expand Down Expand Up @@ -112,6 +113,52 @@ don't accept options, <tt>[options]</tt> won't appear in the help. The names of
will appear in proper order and <tt>:optional</tt> ones will be in square brackets. You don't have to
touch a thing.

== Wrapper for running external commands with good logging

While backtick and <tt>%x[]</tt> are nice for compact, bash-like scripting, they have some failings:

* You have to check the return value via <tt>$?</tt>
* You have no access to the standard error
* You really want to log: the command, the output, and the error so that for cron-like tasks, you can sort out what happened

Enter Methadone::SH

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 thecommand, 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

With this, you can easily script external commands in *almost* as expedient a fashion as with +bash+, however you get sensible logging along the way. By default, this uses the logger provided by Methadone::CLILogging (which is *not* mixed in when you mix in Methadone::SH). If you want to use a different logger, or don't want to mix in Methadone::CLILogging, simply call +set_sh_logger+ with your preferred logger.

But that's not all! You can run code when the command succeed by passing a block:

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

The <tt>sh!</tt> form works this way as well. The block form is also how you can access the standard output or error of the command that ran. Simply have your block accept one or two aguments:

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

This isn't a replacement for Open3 or ChildProcess, but a way to easily "do the right thing" for most cases.

== Utility Classes

Expand Down
4 changes: 4 additions & 0 deletions lib/methadone.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@
require 'methadone/cli_logging'
require 'methadone/main'
require 'methadone/error'
require 'methadone/execution_strategy/mri'
require 'methadone/execution_strategy/open_3'
require 'methadone/execution_strategy/open_4'
require 'methadone/execution_strategy/jvm'
require 'methadone/sh'
# Note: DO NOT require cli.rb OR cucumber.rb here
30 changes: 30 additions & 0 deletions lib/methadone/execution_strategy/jvm.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
module Methadone
module ExecutionStrategy
# ExecutionStrategy for the JVM that uses JVM classes to run the command and get its results.
class JVM
def run_command(command)
process = java.lang.Runtime.get_runtime.exec(command)
process.get_output_stream.close
stdout = input_stream_to_string(process.get_input_stream)
stderr = input_stream_to_string(process.get_error_stream)
exitstatus = process.wait_for
[stdout.chomp,stderr.chomp,OpenStruct.new(:exitstatus => exitstatus)]
end

def exception_meaning_command_not_found
NativeException
end

private
def input_stream_to_string(is)
''.tap do |string|
ch = is.read
while ch != -1
string << ch
ch = is.read
end
end
end
end
end
end
26 changes: 26 additions & 0 deletions lib/methadone/execution_strategy/mri.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module Methadone
# Module to contain ExecutionStrategy implementations.
# To build your own simply implement two methods:
#
# * <tt>run_command(command)</tt> - takes the command to run, and runs it, returning an array of size 3. Index 0
# should be the standard output as a String (never nil), Index 1 should be the
# standard error output as a String (never nil) and Index 2 should be a
# Process::Status representing the results of running the command. Since it's
# not straightforward to create an instance of this class, the returned object
# in this slot need only respond to <tt>exitstatus</tt>, which returns the exit code.
# * <tt>exception_meaning_command_not_found</tt> - return the class that, if caught, means that the underlying command
# couldn't be found. This is needed because currently impelmentations
# throw an exception, but they don't all throw the same one.
module ExecutionStrategy
# Base strategy for MRI rubies.
class MRI
def run_command(command)
raise "subclass must implement"
end

def exception_meaning_command_not_found
Errno::ENOENT
end
end
end
end
15 changes: 15 additions & 0 deletions lib/methadone/execution_strategy/open_3.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module Methadone
module ExecutionStrategy
# Implementation for modern Rubies that uses the built-in Open3 library
class Open_3 < MRI
def run_command(command)
stdout,stderr,status = Open3.capture3(command)
[stdout.chomp,stderr.chomp,status]
end

def exception_meaning_command_not_found
Errno::ENOENT
end
end
end
end
20 changes: 20 additions & 0 deletions lib/methadone/execution_strategy/open_4.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module Methadone
module ExecutionStrategy
# ExecutionStrategy for non-modern Rubies that must rely on
# Open4 to get access to the standard output AND error.
class Open_4 < MRI
def run_command(command)
pid, stdin_io, stdout_io, stderr_io = Open4::popen4(command)
stdin_io.close
stdout = stdout_io.read
stderr = stderr_io.read
_ , status = Process::waitpid2(pid)
[stdout.chomp,stderr.chomp,status]
end

def exception_meaning_command_not_found
Errno::ENOENT
end
end
end
end
66 changes: 13 additions & 53 deletions lib/methadone/sh.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ module Methadone
# Methadone::CLILogging. If you *don't*, you must provide a logger
# via #set_sh_logger.
#
# In order to work on as many Rubies as possible, this class defers the actual execution
# to an execution strategy. See #set_exec_strategy if you think you'd like to override
# that, or just want to know how it works.
#
# This is not intended to be a complete replacement for Open3, but instead of make common cases
# and good practice easy to accomplish.
module SH
Expand Down Expand Up @@ -80,7 +84,12 @@ def set_sh_logger(logger)
@sh_logger = logger
end

# Set the strategy to use for executing commands.
# Public: 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.
# Currently, for 1.8-style Rubies, Open4 is used (see Methadone::ExecutionStrategy::Open_4).
# For JRuby, JVM Runtime calls are used (see Methadone::ExecutionStrategy::JVM). For all
# others, we use the built-in Open3 library (see Methadone::ExecutionStrategy::Open_3).
# See Methadone::ExecutionStrategy for how to implement your own.
def set_exec_strategy(strategy)
@execution_strategy = strategy
end
Expand All @@ -91,63 +100,14 @@ def exception_meaning_command_not_found
exec_strategy.exception_meaning_command_not_found
end

class MRIExceutionStrategy
def exception_meaning_command_not_found
Errno::ENOENT
end
end

class Open3ExecutionStrategy < MRIExceutionStrategy
def run_command(command)
stdout,stderr,status = Open3.capture3(command)
[stdout.chomp,stderr.chomp,status]
end
end

class Open4ExecutionStrategy < MRIExceutionStrategy
def run_command(command)
pid, stdin_io, stdout_io, stderr_io = Open4::popen4(command)
stdin_io.close
stdout = stdout_io.read
stderr = stderr_io.read
_ , status = Process::waitpid2(pid)
[stdout.chomp,stderr.chomp,status]
end
end

class JVMExecutionStrategy
def run_command(command)
process = java.lang.Runtime.get_runtime.exec(command)
process.get_output_stream.close
stdout = input_stream_to_string(process.get_input_stream)
stderr = input_stream_to_string(process.get_error_stream)
exitstatus = process.wait_for
[stdout.chomp,stderr.chomp,OpenStruct.new(:exitstatus => exitstatus)]
end

def exception_meaning_command_not_found
NativeException
end

private
def input_stream_to_string(is)
''.tap do |string|
ch = is.read
while ch != -1
string << ch
ch = is.read
end
end
end
end

def self.default_exec_strategy_class
if RUBY_PLATFORM == 'java'
JVMExecutionStrategy
Methadone::ExecutionStrategy::JVM
elsif RUBY_VERSION =~ /^1.8/
Open4ExecutionStrategy
Methadone::ExecutionStrategy::Open_4
else
Open3ExecutionStrategy
Methadone::ExecutionStrategy::Open_3
end
end

Expand Down

0 comments on commit 61eff0e

Please sign in to comment.