/methadone

Kick the bash habit - start your command line scripts off right in Ruby

Primary LanguageRubyApache License 2.0Apache-2.0

Methadone - kick the bash habit and start your command line apps off right

Author

Dave Copeland (davetron5000 at g mail dot com)

Copyright

Copyright © 2011 by Dave Copeland

License

Distributes under the Apache License, see LICENSE.txt in the source distro

A smattering of tools to make your command-line apps easily awesome; kick the bash habit without sacrificing any of the power.

The overall goal of this project is to allow you to write a command-line app in Ruby that is as close as possible to the expedience and concisenes of bash, but with all the power of Ruby available when you need it.

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

  • Cucumber Steps

Platforms

  • Works completely on:

    • MRI Ruby 1.9.2

    • MRI Ruby 1.9.3

    • MRI Ruby Head

    • MRI Ruby 1.8.7

    • RBX

    • REE

  • For JRuby, everything works but aruba; aruba just doesn’t work on JRuby for some reason, though this library is being used in production under JRuby 1.6.6

Bootstrapping a new CLI App

The methadone command-line app will bootstrap a new command-line app, setting up a proper gem structure, unit tests, and cucumber-based tests with aruba:

$ methadone --help
Usage: methadone [options] app_name
        --force                      Overwrite files if they exist
$ methadone newgem
$ cd newgem
$ rake
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
1 scenario (1 passed)
3 steps (3 passed)
$ cat features/newgem.feature
Feature: My bootstrapped app kinda works
  In order to get going on coding my awesome app
  I want to have aruba and cucumber setup
  So I don't have to do it myself

  Scenario: App just runs
    When I run `newgem --help`
    Then the exit status should be 0
    And the output should contain:
    """
    Usage: newgem [options]
    """

Basically, this sets you up with all the boilerplate that you should be using to write a command-line app.

DSL for your ‘bin` file

A canonical ‘OptionParser` driven app has a few problems with it structurally that methadone can solve

  • Backwards organization - main logic is at the bottom of the file, not the top

  • Verbose to use opts.on just to set a value in a Hash

  • No exception handling

Methadone gives you a simple,lightweight DSL to help. It’s important to note that we’re taking a light touch here; this is all a thin wrapper around OptionParser and you still have complete access to it if you’d like. We’re basically wrapping up some canonical boilerplate into more expedient code

#!/usr/bin/env ruby

require 'optparse'
require 'methadone'

include Methadone::Main

main do |name,password|
  name # => guaranteed to be non-nil
  password # => nil if user omitted on command line
  options[:switch] # => true if user used --switch or -s
  options[:s]      # => ALSO true if user used --switch or -s
  options[:f]      # => value of FILE if used on command-line
  options[:flag]   # => ALSO value of FILE if used on command-line

  # If something goes wrong, you can just raise an exception
  # or call exit_now! if you want to control the exit status
  #
  # Note that if you set DEBUG in the environment, the exception
  # will leak through; this can be handy to figure out why
  # your app might be failing
end

description "One line summary of your awesome app"

on("--[no-]switch","-s","Some switch")
on("-f FILE","--flag","Some flag")
on("-x FOO") do |foo|
  # something more complex; this is exactly OptionParser opts.on
end

arg :name
arg :password, :optional

# If you want to avoid automatic exception catching/logging do:
#
# leak_exceptions true
#
# Note that Methadone::Error exceptions are always caught and logged

go!

go! runs the block you gave to main, passing it the unparsed ARGV as parameters to the block. It will also parse the command-line via OptionParser and do a check on the remaining arguments to see if there’s enough to satisfy the :required args you’ve specified.

Finally, the banner/help string will be full constructed based on the interface you’ve declared. If you don’t accept options, [options] won’t appear in the help. The names of your arguments will appear in proper order and :optional 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 %x[] are nice for compact, bash-like scripting, they have some failings:

  • You have to check the return value via $?

  • 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 sh! 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

Currently, there are classes the assist in directing output logger-style to the right place; basically ensuring that errors go to STDERR and everything else goes to STDOUT. All of this is, of course, configurable

Examples

Using STDOUT as a log, respecting STDERR

require 'methadone'

include Methadone::CLILogging

command = "rm -rf /tmp/*"
debug("About to run #{command}") # => goes only to STDOUT, no logging format
if system(command)
  info("Succesfully ran #{command}") # => goes only to STDOUT, no logging format
else
  error("There was a problem running #{command}") # => goes only to STDERR, no logging format
end

Using a log file, but respecting STDERR

Here, since we have a logfile, that logfile gets ALL messages and they have the default logger format.

require 'methadone'

include Methadone::CLILogging

self.logger = CLILogger.new("logfile.txt")
command = "rm -rf /tmp/*"
debug("About to run #{command}") # => goes only to logfile.txt, in the logger-style format
if system(command)
  info("Succesfully ran #{command}") # => goes only to logfile.txt, in the logger-style format
else
  error("There was a problem running #{command}") 
  # => goes to logfile.txt in the logger-style format, and
  #    to STDERR in a plain format
end

Cucumber Steps

Methadone uses aruba for BDD-style testing with cucumber. This library has some awesome steps, and methadone provides additional, more opinionated, steps.

Example

Here’s an example from methadone’s own tests:

Scenario: Help is properly documented
  When I get help for "methadone"
  Then the exit status should be 0
  And the following options should be documented:
    |--force|
  And the banner should be present
  And the banner should include the version
  And the banner should document that this app takes options
  And the banner should document that this app's arguments are:
    |app_name|which is required|
    |dir_name|which is optional|

Steps Provided

  • Run command_to_run --help using aruba

    When I get help for "command_to_run"
  • Make sure that each option shows up in the help and has some sort of documentation

    Then the following options should be documented:
      |--force|
      |-x     |
  • Check an individual option for documentation:

    Then the option "--force" should be documented
  • Checks that the help has a proper usage banner

    Then the banner should be present
    
  • Checks that the banner includes the version

    Then the banner should include the version
    
  • Checks that the usage banner indicates it takes options via [options]

    Then the banner should document that this app takes options
    
  • Do the opposite; check that you don’t indicate options are accepted

    Then the banner should document that this app takes no options
    
  • Checks that the app’s usage banner documents that its arguments are args

    Then the banner should document that this app's arguments are "args"
  • Do the opposite; check that your app doesn’t take any arguments

    Then the banner should document that this app takes no arguments
    
  • Check for a usage description which occurs after the banner and a blank line

    Then there should be a one-line summary of what the app does

What might be

See the roadmap