| Subcribe via RSS

Getting Command Line Options in Ruby

Recently, after many, many years of serious coding in full OO Perl (none of this measly “admin scripting” you see in Perl that is called “Perl” — but real OO app level Perl!! ;-)), I decided to take the full dive into the deep end of the pool with Ruby for my scripting tool of choice.

I had been playing around with Ruby for over a decade having latched on to it right after it came out around the 2000 timeframe. I immediately saw it as an elegant and concise language that lived up to its billing; once I wrapped my head around the syntax and approach, I could write fairly good Ruby code in no time flat. It definitely had its advantages over Perl, in my opinion, being fully OO’d, and still retained from Perl what I liked most about it.

However, I needed to get real work done and my collection and apprehension of the Perl world (read “CPAN modules”) was much more extensive. For most of the early 2000’s, I was doing a lot more custom web development and mod_perl development (for Rodan & Fields as well as other smaller clients for TechnologEase) and was much too hooked on the templating benefits of Template::Toolkit, HTML::Template and Text::Template and well… Ruby just fell to the wayside. I had to get work done and I simply put Ruby back on the shelf and admired it from afar. My tribute to Ruby was to hijack elements of Ruby and bring them over to Perl. One of my favorites was a one-liner exported into every Perl script:

sub puts { for (@_) { print "$_\n" } } ## Print to stdout, Ruby-style

I also had built a small, but very concise framework in Perl for TechnologEase applications I developed while freelancing. This was nothing the size of anything like Catalyst or Moose but it did (and still does) the job, miraculously transforming everything instantly into objects that were easy to use and create. (If you know anything about OO Perl, you know that the OO Perl model is a bit… concocted, shall we say?!) And part of this framework, esp. for writing command line DSLs was a TechnologEase::Options class for… getting, parsing and creating custom class instances of command line options. It worked something like this:

my $options = TechnologEase::Options->new(
   sourcePath => 's:',
   destPath   => 'd:',
   testing    => 't',
   verbose    => 'v',
);


This would automagically parse the command line (using getopts() under the covers) and create read-only accessors for sourcePath, destPath, testing and verbose. So I could then later write something like:

## Command line example: $ myutil.pl -s /home/chris -d /tmp -v

puts(
   "Source Path.......: ${\ $options->sourcePath }",
   "Destination Path..: ${\ $options->destPath }",
) if $options->verbose;
my $cmd = "mv ${\ $options->sourcePath }/somefile ${\ $options->destPath }";
puts( shell( $cmd ) );

## Where:

sub shell {
   my @output;
   for (@_) { push @output, `$_` }
   chomp @output;
   return wantarray ? @output : \@output;
}

So here I am suddenly committed to Ruby and I have to learn all over again how to do some stuff that just flowed in Perl and that because of custom code that I wrote that objectfies everything for me. TechnologEase::Options wasn’t my only custom class built into my TechnologEase framework, as you can imagine. Now I have to port what I can over to Ruby. The one redeeming factor to this whole effort is… most of my framework provided for easy objects which don’t come that easy in Perl outside of a framework of some sort. (Hence the growth of frameworks like Moose, etc.) Ruby IS 100% OO, so… in theory, all I have to do is write in Ruby and port only a few other features built up from a base class (TechnologEase::BaseClass) that gave me auto-OO.

Now in Ruby, there is the OptionParser class. But I didn’t like this at all. It works almost counter-intuitively to what I was used to in Perl. In Perl, as you note above, I get a custom class instance with custom accessors that match whatever I plan to have on the command line. When I create the instance, I’m telling it what the instance should look like in one statement. Boom. Done. The OptionParser class in Ruby requires me to build the CLASS explicitly. I’m not used to doing that.

So I created my own TechnologEase#Options class in Ruby and it works great, just like it did in Perl — quick, fast and intuitive. Here it is:

module TechnologEase
   require "optparse"

   class Options
      attr_reader :args, :cmdline, :options_list
      alias :command_line :cmdline
      def initialize( options )
         @cmdline = ARGV.join( ' ' )
         @options_list = options.values.join
         params = ARGV.getopts( @options_list )
         options.each do |key,value|
            value.sub!( /:/, '' )
            self.class.send( :define_method, key ) { params[value] }
         end
         @args = Array.new( ARGV )
      end
   end
end

Now I can just require this and use it as I did in Perl:

require "TechnologEase/options"

options = TechnologEase::Options.new({
   :source_path => 's:',
   :dest_path   => 'd:',
   :verbose     => 'v',
})

puts "Source path.......: #{options.source_path}"
puts "Destination path..: #{options.dest_path}"
puts "Verbose is........: #{options.verbose ? 'ON' : 'OFF'}"
puts "Command line was..: #{options.cmdline}"
puts "Arguments.........: #{options.args.join( ' ' )}"
puts "getopts() list....: #{options.options_list}"

So from a command line, I get this:

pj@matrix-dhcp-191:~/Work/Clients/Nunya/Business> ./bkupct.rb -s dude -d yo purge them all split
Source path.......: dude
Destination path..: yo
Verbose is........: OFF
Command line was..: -s dude -d yo purge them all split
Arguments.........: purge them all split
getopts list......: s:d:v

It turns out, I wasn’t completely up on symbols in Ruby and I made a bit of a rookie mistake. Once that was cleared up, my first implementation actually worked, and it was using some advanced Ruby to boot — calling a private method :define_method to dynamically define a method, which is what the Perl version actually did. (This is a lot easier to do in Perl, actually, than Ruby!)

I posted this to a popular Ruby forum and got back some interesting answers. I do want to call out an even more elegant solution that was posted by one Robert Klemm who actually, very nicely extended the OptionParser class to provide a :quick solution. He posted his solution to GitHub here and it’s worth noting. I’ll post his code inline here for all to see. Very nice, Robert:


require 'optparse'

# Pass a Hash which maps Hash keys to option names
def OptionParser.quick!(argv, options)
  result = {}

  new do |opts|
    options.each do |key, op|
      # prepare key
      raise ArgumentError, 'Invalid option: %p' % op unless /\A([a-z])(:)?\z/i =~ op

      has_arg = $2
      opt_char = has_arg ? "-#$1 VALUE" : "-#$1"

      # add option
      opts.on opt_char, do |v|
        result[key] = v
      end
    end
  end.parse! argv

  result
end

options = OptionParser.quick!(ARGV,
  :source_path => 's:',
  :dest_path => 'd:',
  :verbose => 'v',
)

p options, ARGV

Any time you can extend an already existing class, that’s the way to fly. I stuck with my own implementation — it worked and I needed to get a script out the door, but Robert’s code is still recommended.

That concludes today’s dive into Ruby. This allowed me to finish off a DSL for a custom Internet-based offsite backup solution for a client, in Ruby instead of Perl, my old friend… Sorry buddy. Ruby is just too beautiful. :-)

Comments are closed.