- 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
-
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
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.
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 aHash
-
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.
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.
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
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
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
Methadone uses aruba for BDD-style testing with cucumber. This library has some awesome steps, and methadone provides additional, more opinionated, steps.
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|
-
Run
command_to_run --help
using arubaWhen 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
See the roadmap