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}\
\
" if @options.verbose
      
      output_options if @options.verbose # [Optional]
            
      process_arguments            
      process_command
      
      puts "\
Finished 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:\
"
      
      @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
  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.  March 22, 2008 15:07 Todd Werth

    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.  April 08, 2008 09:25 Todd Werth

    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. 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.

  11.  May 29, 2008 17:35 Todd Werth

    Matt,

    Check your arguments_valid?

  12. 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

  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.  July 21, 2008 11:07 Todd Werth

    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.

  16. Juan October 12, 2008 08:34 

    Great script!

    Just a quick correction on the page it read [puts "Options:\\\
    "] which should only be one slash. The plain text file is correct tho.

  17.  October 12, 2008 12:06 Todd Werth

    Juan,
    Thanks for spotting that, I corrected it. Before I wrote this site in Ruby I was using WordPress and I was having issues with those backslashes; I forgot to change them back when I moved over to my code.

  18. G. Gibson December 11, 2008 15:14 

    Is there a way to evaluate an --blah-option=<value> scenario?
    Thanks in advance :)

  19. TJ Holowaychuk January 16, 2009 08:54 

    There is a better way :D
    Check out the commander gem

    github.com/visionmedia/commander/tree/master

  20. srboisvert February 24, 2009 06:42 

    There is some strange behaviour if you have any command line arguments and then you go to use gets in your code. It thinks your first command line argument is the file handler for STDIN. So you end up with (Errno::EACCES) or file not found messages

  21. srboisvert February 24, 2009 07:28 

    Never mind. Expected behaviour of gets. Learn something new everyday. If you want to use gets on stdin you have to clear ARGV.

  22. Klaus June 11, 2009 04:28 

    Is it possible to add parameter to options in this skeleton?
    Like
    foo -p 123 file.dat
    Where 123 is a parameter to option p.

  23. pbelasco July 07, 2009 18:36 

    Hi. Nice job. Very useful.
    My question is just about the last one.
    I'm trying to add an parameter (name of a file) after an option --infile but couldn't figure out how.
    Thnksalot.

  24. pbelasco July 08, 2009 07:43 

    What a shame.
    Just read the documentation of the Option Parser (www.ruby-doc.org/stdlib/libdoc/optparse/rdoc/classes/OptionParser.html)

    My app is done :)

  25. Steeve McCauley December 04, 2009 06:28 

    Todd, I just started looking into programming with ruby and this is exactly the sort of skeleton I was looking for, nice, thanks for that.

    One comment: shouldn't the arguments be validated before they are parsed? That is,

    if arguments_valid? && parsed_options?
    ...
    end

  26. Eric Hodel April 13, 2010 20:33 

    rdoc/usage no longer exists in RDoc 2. Just use optparse.

  27. z0mbix April 20, 2010 12:41 

    Thanks very much for this. This has saved me a lot of time and effort with many sysadmin tasks.

    Cheers z0mbix

Comments are closed for this article.
Feel free to contact me on twitter if you have any questions or comments: @twerth.