InfiniteRed

art.through(code)

A simple Ruby command-line application skeleton

To write a command-line application in Ruby is very simple, the following two-line application converts everything in the standard input to upper case and then outputs it:

#!/usr/bin/env ruby 
puts STDIN.read.upcase

Although complete, this is hardly a proper application, which should include options, arguments, help, input error trapping, etc. I've created a skeleton for such a command-line application.

Hoe and Gems

I intentionally put the entire application in one file, to make it easy for you to distribute. With this setup, your users only need the file in their path, and Ruby installed; no gems or anything else are required

Although a single file is convenient, it is proper to package your Ruby application as a gem, and distribute it via RubyForge. This way, someone can install your application with gem install YourApp.

If your users will have gems installed and you prefer to distribute your application that way, I recommend using the Hoe project to setup your directory structure:

sudo gem install hoe
sow your_app

You can also distribute your application in a variety of other ways, such as apt-get in Debian/Ubuntu or Ports in OS X/BSD. I chose the simplest route to start, which is one file, in your path.

Helping your users

There are many ways to provide help to your users, including simply puts "Your help...". The one I prefer is to use rdoc/usage , which uses the RDoc documentation you should already have in your code. This way, you only have to document your options once. RDoc documentation is very readable in the actual code, as well as when it's rendered to HTML.

In the following example, I list all the sections of my help using RDoc notation:

# == Synopsis 
#   This is a sample description of the application.
#   Blah blah blah.
#
# == Examples
#   This command does blah blah blah.
#     ruby_cl_skeleton foo.txt
#
#   Other examples:
#     ruby_cl_skeleton -q bar.doc
#     ruby_cl_skeleton --verbose foo.html
#
# == Usage 
#   ruby_cl_skeleton [options] source_file
#
#   For help use: ruby_cl_skeleton -h
#
# == Options
#   -h, --help          Displays help message
#   -v, --version       Display the version, then exit
#   -q, --quiet         Output as little as possible, overrides verbose
#   -V, --verbose       Verbose output
#   TO DO - add additional options
#
# == Author
#   YourName
#
# == Copyright
#   Copyright (c) 2007 YourName. Licensed under the MIT License:
#   http://www.opensource.org/licenses/mit-license.php

To create RDoc documentation from these comments use the following command:

rdoc my_file

The documentation that is produced can be seen here.

You can output help, to your users at runtime, using the RDoc documentation, with the following method:

RDoc::usage() #exits app

This will format your RDoc comments as plain text, output it, then close the application.

If you just want to output the usage section, you can do so with the following method call:

RDoc::usage('usage')

Many options for options

There are many ways to parse options (also known as a switch, flag, or parameter). I chose to use OptionParser. To parse basic options is as simple as the following:

opts = OptionParser.new 
opts.on('-v', '--version')    { output_version ; exit 0 }
opts.on('-h', '--help')       { output_help }
opts.on('-V', '--verbose')    { @options.verbose = true }  
opts.on('-q', '--quiet')      { @options.quiet = true }
# TO DO - add additional options
            
opts.parse!(@arguments)

You can use OptionParser to parse complex options also, including mandatory options, a list of values {start | stop | restart}, boolean switches --[no-]verbose, etc.

I chose to store my options using an OpenStruct object, which is a very convenient and clever alternative to a hash. I initialize them like so:

# Set defaults
@options = OpenStruct.new
@options.verbose = false
@options.quiet = false
# TO DO - add additional defaults

Creating a command-line application

To create your application, follow these steps:

  1. Make a copy of the skeleton code
  2. Make your file executable ( chmod +x your_file )
  3. Replace all instances of ruby_cl_skeleton with your application name
  4. Locate all the "# TO DO"s and make your changes
  5. Insert your functionality
  6. Distribute your application by placing the file anywhere in the user's path ( ~/bin /usr/bin )

A nice addition would be to make a rails type build command that creates the file for you, inserting the application name, etc.

Grab the skeleton code below. Happy coding.

Plain text file here.

#!/usr/bin/env ruby 

# == Synopsis 
#   This is a sample description of the application.
#   Blah blah blah.
#
# == Examples
#   This command does blah blah blah.
#     ruby_cl_skeleton foo.txt
#
#   Other examples:
#     ruby_cl_skeleton -q bar.doc
#     ruby_cl_skeleton --verbose foo.html
#
# == Usage 
#   ruby_cl_skeleton [options] source_file
#
#   For help use: ruby_cl_skeleton -h
#
# == Options
#   -h, --help          Displays help message
#   -v, --version       Display the version, then exit
#   -q, --quiet         Output as little as possible, overrides verbose
#   -V, --verbose       Verbose output
#   TO DO - add additional options
#
# == Author
#   YourName
#
# == Copyright
#   Copyright (c) 2007 YourName. Licensed under the MIT License:
#   http://www.opensource.org/licenses/mit-license.php


# TO DO - replace all ruby_cl_skeleton with your app name
# TO DO - replace all YourName with your actual name
# TO DO - update Synopsis, Examples, etc
# TO DO - change license if necessary



require 'optparse' 
require 'rdoc/usage'
require 'ostruct'
require 'date'


class App
  VERSION = '0.0.1'
  
  attr_reader :options

  def initialize(arguments, stdin)
    @arguments = arguments
    @stdin = stdin
    
    # Set defaults
    @options = OpenStruct.new
    @options.verbose = false
    @options.quiet = false
    # TO DO - add additional defaults
  end

  # Parse options, check arguments, then process the command
  def run
        
    if parsed_options? && arguments_valid? 
      
      puts "Start at #{DateTime.now}\\n\\n" if @options.verbose
      
      output_options if @options.verbose # [Optional]
            
      process_arguments            
      process_command
      
      puts "\\nFinished at #{DateTime.now}" if @options.verbose
      
    else
      output_usage
    end
      
  end
  
  protected
  
    def parsed_options?
      
      # Specify options
      opts = OptionParser.new 
      opts.on('-v', '--version')    { output_version ; exit 0 }
      opts.on('-h', '--help')       { output_help }
      opts.on('-V', '--verbose')    { @options.verbose = true }  
      opts.on('-q', '--quiet')      { @options.quiet = true }
      # TO DO - add additional options
            
      opts.parse!(@arguments) rescue return false
      
      process_options
      true      
    end

    # Performs post-parse processing on options
    def process_options
      @options.verbose = false if @options.quiet
    end
    
    def output_options
      puts "Options:\\n"
      
      @options.marshal_dump.each do |name, val|        
        puts "  #{name} = #{val}"
      end
    end

    # True if required arguments were provided
    def arguments_valid?
      # TO DO - implement your real logic here
      true if @arguments.length == 1 
    end
    
    # Setup the arguments
    def process_arguments
      # TO DO - place in local vars, etc
    end
    
    def output_help
      output_version
      RDoc::usage() #exits app
    end
    
    def output_usage
      RDoc::usage('usage') # gets usage from comments above
    end
    
    def output_version
      puts "#{File.basename(__FILE__)} version #{VERSION}"
    end
    
    def process_command
      # TO DO - do whatever this app does
      
      #process_standard_input # [Optional]
    end

    def process_standard_input
      input = @stdin.read      
      # TO DO - process input
      
      # [Optional]
      # @stdin.each do |line| 
      #  # TO DO - process each line
      #end
    end
end


# TO DO - Add your Modules, Classes, etc


# Create and run the application
app = App.new(ARGV, STDIN)
app.run

Share:
dzone del.icio.us digg it reddit spurl simpy blinklist furl blogmarks magnolia

  1. Jan Aerts  December 13, 2007 05:23

    Hi,

    I've written something similar some time back about one-off parser scripts: see saaientist.blogspot.com/2007/07/documenting-one-off-parsers.html for another example of a skeleton script. It also uses OpenStruct and RDoc::usage. It's interesting to see you using an App class, though. I might use that in the future as well.

    jan.

  2. jc  December 13, 2007 20:42

    The cmdparse gem is pretty awesome. I'd recommend trying that out.

  3. garryoldman  January 19, 2008 00:03

    I might use that in the future as well.

  4. matt  March 22, 2008 11:54

    To make your script more platform independent it would be good to add:

    require 'date'

    Ruby on Ubuntu Linux doesn't automatically load that library so any DateTime.now call will fail. Well, I should say my ubuntu installation doesn't automatically do it.

  5. matt  March 22, 2008 11:56

    Oh, I almost completely forgot:

    Thanks for taking the time to write this! It saved me tons of time and got me off in the right direction, instead of the dirty script direction.

  6. Todd Werth  March 22, 2008 15:07

    Thanks Matt, for noting the "require 'date'", I've updated the code.

    You're welcome, I'm glad you found it useful.

  7. David Jones  April 06, 2008 21:11

    Hi Todd, thanks for this article. I've added credits to your name in the README of a quick little command line app I wrote. See here: github.com/djones/pound-append/tree/master

  8. Todd Werth  April 08, 2008 09:25

    David, you're welcome. Very cool, I appreciate the credits, it's always nice to see people actually getting some use out of what you create.

  9. David Madden  May 13, 2008 13:43

    Thanks, this is really useful.

  10. edward  June 09, 2008 06:16

    hi. i am tryin to interface ruby with xfst that operates on the command line. however, the system command executes without any visible effect on the text files created to store the output. how can i remedy this

  11. Matt  May 29, 2008 12:29

    Todd,

    I tried using your code in a script I'm writing, but nothing except '-h' works. Everything else just prints out the usage statement. I thought maybe I'd messed something up in copying your code, so just created a new file with your code above in it and that's it...and I get the same results.

    Using '-V' doesn't return what I think it should, which is the timestamps and list of options used...it just returns the "Usage" message.

  12. Todd Werth  May 29, 2008 17:35

    Matt,

    Check your arguments_valid?

  13. Ben Emson  June 23, 2008 13:26

    Hi Todd
    Thanks so much for this. Its really useful, especially for rapid prototyping ideas.
    If I ever manage to build a decent app I will be sure to credit you to.
    Many thanks,
    Ben...

  14. Jason Holden  July 07, 2008 12:00

    So if you do use gems in our script what is the proper way to package/deploy a simple script with dependend gems? Can I create a single install package? Do I have to manually install the gems on each workstation?

  15. Todd Werth  July 21, 2008 11:07

    Jason,

    The proper way is to make a gem, which has dependency management. You can install a gem from disk, rather than a repository like RubyForge, and RubyGems handles installing all required gems. Usually if a user has ruby installed, they have RubyGems installed also (in OS X, for example, both of these are installed by default in Leopard).

    Another simple option is simply copy the code from your required gems into your project (license permitting). You won't get updates for those gems, and if you have multiple projects, each will have their own copy, but this way you can simply copy the folder to all your user's machines.

Leave a new comment:    * = Required

*
 (will not be shown)

*
*