NAME

main.rb

SYNOPSIS

a class factory and dsl for generating command line programs real quick

URI

http://codeforpeople.com/lib/ruby/
http://rubyforge.org/projects/codeforpeople/
http://github.com/ahoward/main

INSTALL

gem install main

DESCRIPTION

main.rb features the following:

  - unification of option, argument, keyword, and environment parameter
    parsing
  - auto generation of usage and help messages
  - support for mode/sub-commands
  - io redirection support
  - logging hooks using ruby's built-in logging mechanism
  - intelligent error handling and exit codes 
  - use as dsl or library for building Main objects
  - parsing user defined ARGV and ENV
  - zero requirements for understanding the obtuse apis of *any* command
    line option parsers
  - leather pants

in short main.rb aims to drastically lower the barrier to writing uniform
command line applications.

for instance, this program

  require 'main'

  Main {
    argument 'foo'
    option 'bar'

    def run
      p params['foo']
      p params['bar']
      exit_success!
    end
  }

sets up a program which requires one argument, 'bar', and which may accept one
command line switch, '--foo' in addition to the single option/mode which is always
accepted and handled appropriately: 'help', '--help', '-h'.  for the most
part main.rb stays out of your command line namespace but insists that your
application has at least a help mode/option.

main.rb supports sub-commands in a very simple way

  require 'main'

  Main {
    mode 'install' do
      def run() puts 'installing...' end
    end

    mode 'uninstall' do
      def run() puts 'uninstalling...' end
    end
  }

which allows a program, called 'a.rb', to be invoked as

  ruby a.rb install

and

  ruby a.rb uninstall

for simple programs main.rb is a real time saver but it's for more complex
applications where main.rb's unification of parameter parsing, class
configuration dsl, and auto-generation of usage messages can really streamline
command line application development.  for example the following 'a.rb'
program:

  require 'main'

  Main {
    argument('foo'){
      cast :int
    }
    keyword('bar'){
      arity 2
      cast :float
      defaults 0.0, 1.0
    }
    option('foobar'){
      argument :optional
      description 'the foobar option is very handy'
    }
    environment('BARFOO'){
      cast :list_of_bool
      synopsis 'export barfoo=value'
    }

    def run
      p params['foo'].value
      p params['bar'].values
      p params['foobar'].value
      p params['BARFOO'].value
    end
  }

when run with a command line of

  BARFOO=true,false,false ruby a.rb 42 bar=40 bar=2 --foobar=a

will produce

  42
  [40.0, 2.0]
  "a"
  [true, false, false]

while a command line of

  ruby a.rb --help

will produce

  NAME
    a.rb

  SYNOPSIS
    a.rb foo [bar=bar] [options]+

  PARAMETERS
    * foo [ 1 -> int(foo) ]

    * bar=bar [ 2 ~> float(bar=0.0,1.0) ]

    * --foobar=[foobar] [ 1 ~> foobar ]
        the foobar option is very handy

    * --help, -h

    * export barfoo=value

and this shows how all of argument, keyword, option, and environment parsing
can be declartively dealt with in a unified fashion - the dsl for all
parameter types is the same - and how auto synopsis and usage generation saves
keystrokes.  the parameter synopsis is compact and can be read as

    * foo [ 1 -> int(foo) ]

      'one argument will get processed via int(argument_name)'

        1        : one argument
        ->       : will get processed (the argument is required)
        int(foo) : the cast is int, the arg name is foo

    * bar=bar [ 2 ~> float(bar=0.0,1.0) ]

      'two keyword arguments might be processed via float(bar=0.0,1.0)'

        2                  : two arguments
        ~>                 : might be processed (the argument is optional)
        float(bar=0.0,1.0) : the cast will be float, the default values are
                             0.0 and 1.0

    * --foobar=[foobar] [ 1 ~> foobar ]

      'one option with optional argument may be given directly'

    * --help, -h

      no synopsis, simple switch takes no args and is not required

    * export barfoo=value

      a user defined synopsis

SAMPLES

<========< samples/a.rb >========>

~ > cat samples/a.rb

  require 'main'

  ARGV.replace %w( 42 ) if ARGV.empty?

  Main {
    argument('foo'){
      required                    # this is the default
      cast :int                   # value cast to Fixnum
      validate{|foo| foo == 42}   # raises error in failure case 
      description 'the foo param' # shown in --help
    }

    def run
      p params['foo'].given?
      p params['foo'].value
    end
  }

~ > ruby samples/a.rb

  true
  42

<========< samples/b.rb >========>

~ > cat samples/b.rb

  require 'main'

  ARGV.replace %w( 40 1 1 ) if ARGV.empty?

  Main {
    argument('foo'){
      arity 3                             # foo will given three times
      cast :int                           # value cast to Fixnum
      validate{|foo| [40,1].include? foo} # raises error in failure case 
      description 'the foo param'         # shown in --help
    }

    def run
      p params['foo'].given?
      p params['foo'].values
    end
  }

~ > ruby samples/b.rb

  true
  [40, 1, 1]

<========< samples/c.rb >========>

~ > cat samples/c.rb

  require 'main'

  ARGV.replace %w( foo=40 foo=2 bar=false ) if ARGV.empty?

  Main {
    keyword('foo'){
      required  # by default keywords are not required
      arity 2
      cast :float
    }
    keyword('bar'){
      cast :bool
    }

    def run
      p params['foo'].given?
      p params['foo'].values
      p params['bar'].given?
      p params['bar'].value
    end
  }

~ > ruby samples/c.rb

  true
  [40.0, 2.0]
  true
  false

<========< samples/d.rb >========>

~ > cat samples/d.rb

  require 'main'

  ARGV.replace %w( --foo=40 -f2 ) if ARGV.empty?

  Main {
    option('foo', 'f'){
      required  # by default options are not required, we could use 'foo=foo'
                # above as a shortcut
      argument_required
      arity 2
      cast :float
    }

    option('bar=[bar]', 'b'){  # note shortcut syntax for optional args
      # argument_optional      # we could also use this method
      cast :bool
      default false
    }

    def run
      p params['foo'].given?
      p params['foo'].values
      p params['bar'].given?
      p params['bar'].value
    end
  }

~ > ruby samples/d.rb

  true
  [40.0, 2.0]
  nil
  false

<========< samples/e.rb >========>

~ > cat samples/e.rb

  require 'main'

  ARGV.replace %w( x y argument )

  Main {
    argument 'argument'
    option 'option'

    def run() puts 'run' end

    mode 'a' do
      option 'a-option'
      def run() puts 'a-run' end
    end

    mode 'x' do
      option 'x-option'

      def run() puts 'x-run' end

        mode 'y' do
          option 'y-option'

          def run() puts 'y-run' end
        end
    end
  }

~ > ruby samples/e.rb

  y-run

<========< samples/f.rb >========>

~ > cat samples/f.rb

  require 'main'

  ARGV.replace %W( compress /data )

  Main {
    argument('directory'){ description 'the directory to operate on' }

    option('force'){ description 'use a bigger hammer' }

    def run
      puts 'this is how we run when no mode is specified'
    end

    mode 'compress' do
      option('bzip'){ description 'use bzip compression' }

      def run
        puts 'this is how we run in compress mode' 
      end
    end

    mode 'uncompress' do
      option('delete-after'){ description 'delete orginal file after uncompressing' }

      def run
        puts 'this is how we run in un-compress mode' 
      end
    end
  }

~ > ruby samples/f.rb

  this is how we run in compress mode

<========< samples/g.rb >========>

~ > cat samples/g.rb

  require 'main'

  ARGV.replace %w( 42 ) if ARGV.empty?

  Main {
    argument( 'foo' )
    option( 'bar' )

    run { puts "This is what to_options produces: #{params.to_options.inspect}" }
  }

~ > ruby samples/g.rb

  This is what to_options produces: {"help"=>nil, "foo"=>"42", "bar"=>nil}

<========< samples/h.rb >========>

~ > cat samples/h.rb

  require 'main'

  # block-defaults are instance_eval'd in the main instance and be combined with
  # mixins
  #
  # ./h.rb   #=> forty-two
  # ./h.rb a #=> 42 
  # ./h.rb b #=> 42.0 
  #

  Main {
    fattr :default_for_foobar => 'forty-two' 

    option(:foobar) do
      default{ default_for_foobar }
    end

    mixin :foo do
      fattr :default_for_foobar => 42
    end

    mixin :bar do
      fattr :default_for_foobar => 42.0
    end

    run{ p params[:foobar].value }

    mode :a do
      mixin :foo
    end

    mode :b do
      mixin :bar
    end
  }

~ > ruby samples/h.rb

  "forty-two"

<========< samples/j.rb >========>

~ > cat samples/j.rb

  #!/usr/bin/env ruby

  require 'open-uri'

  require 'main'
  require 'digest/sha2'

  # you have access to a sequel/amalgalite/sqlite db for free
  #

  Main {
    name :i_can_haz_db

    db {
      create_table(:mp3s) do
        primary_key :id
        String :url
        String :sha
      end unless table_exists?(:mp3s)
    }

    def run
      url = 'http://s3.amazonaws.com/drawohara.com.mp3/ween-voodoo_lady.mp3'
      mp3 = open(url){|fd| fd.read}
      sha = Digest::SHA2.hexdigest(mp3)

      db[:mp3s].insert(:url => url, :sha => sha)
      p db[:mp3s].all
      p db
    end
  }

~ > ruby samples/j.rb

  [{:url=>"http://s3.amazonaws.com/drawohara.com.mp3/ween-voodoo_lady.mp3", :sha=>"54c99ac7588dcfce1e70540b734805e9c69ff98dcca001e6f2bdec140fb0f9dc", :id=>1}, {:url=>"http://s3.amazonaws.com/drawohara.com.mp3/ween-voodoo_lady.mp3", :sha=>"54c99ac7588dcfce1e70540b734805e9c69ff98dcca001e6f2bdec140fb0f9dc", :id=>2}]
  #<Sequel::Amalgalite::Database: "amalgalite://Users/ahoward/.i_can_haz_db/db.sqlite">

DOCS

test/main.rb
vim -p lib/main.rb lib/main/*rb
API section below

HISTORY

4.4.0
  - support for automatic sequel/sqlite/amalgalite dbs for persistent state
    across invocations

      Main {
        db {
          create_table :foo do
            String key
            String val
          end unless table_exists? :foo
        }

        def run
          db[:foo].create(:key => 'using', :val => 'amalgalite')
        end
      }

    - support for automatic config files with auto populated template data

        Main {
          config :email => '[email protected]', :password => 'pa$$word'

          def run
            email = config[:email]
          end
        }

    - new paramter types :pathname, :path, :slug, :input, and :output 

    - input/output parameters.  can be filenames or '-' to supply
      stdin/stdout respectively

        Main {
          input :i
          output :o

          def run
            i = params[:i].value
            o = params[:o].value

            line = i.gets
            o.puts line
          end
        }

     - clean up warnings running with 'ruby -w'

     - fix a failing test

     - ability to ignore parameters in sub modes

        Main {
          argument :foo
          argument :bar

          def run
            p param[:bar].value
          end

          mode :ignoring do
            params[:foo].ignore!
          end
        }
4.0.0
  - avoid duping ios.  new methods Main.push_ios! and Main.pop_ios! are
  utilized for testing.  this was done to make it simple to wrap
  daemon/servolux programs around main, althought not strictly required.
  not the version bump - there is not reason to expect existing main
  programs to break, but it *is* and interface change which requires a major
  version bump.

3.0.0
  - major refactor to support modes via module/extend vs. subclassing.
  MIGHT NOT be backward compatible, though no known issues thus far.

2.9.0
  - support ruby 1.9

2.8.3
  - support for block defaults

2.8.2
  - fixes and tests for negative arity/attr arguments, options, eg

      argument(:foo){
        arity -1  
      }

      def run  # ARGV == %w( a b c )
        p foo  #=> %w( a b c )
      end

    thanks nathan

2.8.1
  - move from attributes.rb to fattr.rb

2.8.0 
  - added 'to_options' method for Parameter::Table.  this allows you to convert
    all the parameters to a simple hash.  
    for example

      Main {
        option 'foo'
        argument 'baz'

        run { puts params.to_options.inspect } 

      }

2.7.0
  - removed bundled arrayfields and attributes.  these are now dependancies
    mananged by rubygems.  a.k.a. you must have rubygems installed for main
    to work.

2.6.0
  - added 'mixin' feaature for storing, and later evaluating a block of
    code.  the purpose of this is for use with modes where you want to keep
    your code dry, but may not want to define something in the base class
    for all to inherit.  'mixin' allows you to define the code to inherit
    once and the selectively drop it in child classes (modes) on demand.
    for example

      Main {
        mixin :foobar do
          option 'foo'
          option 'bar'
        end

        mode :install do
          mixin :foobar
        end

        mode :uninstall do
          mixin :foobar
        end

        mode :clean do
        end
      }

  - mode definitions are now deferred to the end of the Main block, so you
    can do this

      Main {
        mode 'a' do
          mixin :foo
        end

        mode 'b' do
          mixin :foo
        end

        def inherited_method
          42
        end

        mixin 'foo' do
          def another_inherited_method
            'forty-two'
          end
        end
      }

  - added sanity check at end of paramter contruction

  - improved auto usage generation when arity is used with arguments

  - removed 'p' shortcut in paramerter dsl because it collided with
    Kernel.p.  it's now called 'param'.  this method is availble *inside* a
    parameter definition

      option('foo', 'f'){
        synopsis "arity = #{ param.arity }"
      }

  - fixed bug where '--' did not signal the end of parameter parsing in a
    getoptlong compliant way

  - added (before/after)_parse_parameters, (before/after)_initialize, and
    (before/after)_run hooks

  - fixed bug where adding to usage via

      usage['my_section'] = 'custom message'

    totally horked the default auto generated usage message

  - updated dependancies in gemspec.rb for attributes (~> 5.0.0) and
    arrayfields (~> 4.3.0)

  - check that client code defined run, iff not wrap_run! is called.  this is
    so mains with a mode, but no run defined, still function correctly when 
    passed a mode

  - added new shortcut for creating accessors for parameters.  for example

      option('foo'){
        argument :required
        cast :int
        attr
      }

      def run
        p foo ### this attr will return the parameter's *value*
      end

    a block can be passed to specify how to extract the value from the
    parameter

      argument('foo'){
        optional
        default 21 
        cast :int
        attr{|param| param.value * 2}
      }

      def run
        p foo #=> 42 
      end

  - fixed bug where 'abort("message")' would print "message" twice on exit
    if running under a nested mode (yes again - the fix in 2.4.0 wasn't
    complete)

  - added a time cast, which uses Time.parse

      argument('login_time'){ cast :time }

  - added a date cast, which uses Date.parse

      argument('login_date'){ cast :date }

2.5.0
  - added 'examples', 'samples', and 'api' kewords to main dsl.  each
    keyword takes a list of strings which will be included in the help
    message

      Main {
        examples "foobar example", "barfoo example"

        samples <<-txt
          do this

          don't do that
        txt

        api %(
          foobar string, hash

          barfoo hash, string
        ) 
      }

    results in a usage message with sections like

    ...

    EXAMPLES
      foobar example
      barfoo example

    SAMPLES
      do this

      don't do that

    API
      foobar string, hash

      barfoo hash, string

    ...

2.4.0
  - fixed bug where 'abort("message")' would print "message" twice on exit
    if running under a nested mode.

  - allowed parameters to be overridden completely in subclasses (modes)

2.3.0
  - re-worked Main.new such that client code may define an #initialize
    methods and the class will continue to work.  that is to say it's fine
    to do this

      Main {
        def initialize
          @a = 42
        end

        def run
          p @a
        end

        mode 'foo' do
          def run
            p @a
          end
        end
      }

    the client #initialize will be called *after* main has done it's normal
    initialization so things like @argv, @env, and @stdin will all be there
    in initialize.  of course you could have done this before but you'd have
    to both call super and call it with the correct arguments - now you can
    simply ignore it.

2.2.0
  - added ability for parameter dsl error handlers to accept an argument,
    this will be passed the current error.  for example

      argument(:x) do
        arity 42

        error do |e|
          case e
            when Parameter::Arity
          ...
        end
      end  

  - refined the mode parsing a bit: modes can now be abbreviated to uniqness
    and, when the mode is ambiuous, a nice error message is printed, for
    example:

      ambiguous mode: in = (inflate or install)?

2.1.0
  - added custom error handling dsl for parameters, this includes the ability
    to prepend, append, or replace the standard error handlers:

      require 'main'

      Main {
        argument 'x' do
          error :before do
            puts 'this fires *before* normal error handling using #instance_eval...'
          end

          error do
            puts 'this fires *instead of* normal error handling using #instance_eval...'
          end

          error :after do
            puts 'this fires *after* normal error handling using #instance_eval...'
          end
        end

        run(){ p param['x'].given? }
      }

  - added ability to exit at any time bypassing *all* error handling using
    'throw :exit, 42' where 42 is the desired exit status.  throw without a
    status simply exits with 0.

  - added 'help!' method which simply dumps out usage and exits

2.0.0
  - removed need for proxy.rb via Main::Base.wrap_run!
  - added error handling hooks for parameter parsing
  - bundled arrayfields, attributes, and pervasives although gems are tried
    first
  - softened error messages for parameter parsing errors: certain classes of
    errors are now 'softspoken' and print only the message, not the entire
    stacktrace, to stderr.  much nicer for users.  this is configurable.
  - added subcommand/mode support
  - added support for user defined exception handling on top level
    exceptions/exits
  - added support for negative arity.  this users ruby's own arity
    semantics, for example:

      lambda{|*a|}.arity     == -1
      lambda{|a,*b|}.arity   == -2
      lambda{|a,b,*c|}.arity == -3
      ...

    in otherwords parameters now support 'zero or more', 'one or more' ...
    'n or more' argument semantics

1.0.0
  - some improved usage messages from jeremy hinegardner

0.0.2
  - removed dependancy on attributes/arrayfields.  main now has zero gem
    dependancies.

  - added support for io redirection.  redirection of stdin, stdout, and
    stderr can be done to any io like object or object that can be
    inerpreted as a pathname (object.to_s)

  - main objects can now easily be created and run on demand, which makes
    testing a breeze

      def test_unit_goodness!
        main = 
          Main.new{
            stdout StringIO.new 
            stderr '/dev/null'

            def run
              puts 42
            end
          }

        main.run
        main.stdout.rewind

        assert main.stdout.read == "42\n"
      end

  - added API section to readme and called it 'docs'

  - wrote a bunch more tests.  there are now 42 of them.

0.0.1

  initial version.  this version extracts much of the functionality of alib's
  (gen install alib) Alib.script main program generator and also some of jim's
  freeze's excellent CommandLine::Aplication into what i hope is a simpler and
  more unified interface

API

Main {

###########################################################################
#                       CLASS LEVEL API                                   #
###########################################################################
#
# the name of the program, auto-set and used in usage 
#
  program 'foo.rb'
#
# a short description of program functionality, auto-set and used in usage
#
  synopsis "foo.rb arg [options]+"
#
# long description of program functionality, used in usage iff set
#
  description <<-hdoc
    this text will automatically be indented to the right level.

    it should describe how the program works in detail
  hdoc
#
# used in usage iff set
#
  author '[email protected]'
#
# used in usage
#
  version '0.0.42'
#
# stdin/out/err can be anthing which responds to read/write or a string
# which will be opened as in the appropriate mode 
#
  stdin '/dev/null'
  stdout '/dev/null'
  stderr open('/dev/null', 'w')
#
# the logger should be a Logger object, something 'write'-able, or a string
# which will be used to open the logger.  the logger_level specifies the
# initalize verbosity setting, the default is Logger::INFO
#
  logger(( program + '.log' ))
  logger_level Logger::DEBUG
#
# you can configure exit codes.  the defaults are shown
#
  exit_success # 0
  exit_failure # 1
  exit_warn    # 42
#
# the usage object is rather complex.  by default it's an object which can
# be built up in sections using the 
#
#   usage["BUGS"] = "something about bugs'
#
# syntax to append sections onto the already pre-built usage message which
# contains program, synopsis, parameter descriptions and the like
#
# however, you always replace the usage object wholesale with one of your
# chosing like so
#
  usage <<-txt
    my own usage message
  txt

###########################################################################
#                         MODE API                                        #
###########################################################################
#
# modes are class factories that inherit from their parent class.  they can
# be nested *arbitrarily* deep.  usage messages are tailored for each mode.
# modes are, for the most part, independant classes but parameters are
# always a superset of the parent class - a mode accepts all of it's parents
# paramters *plus* and additional ones
# 
  option 'inherited-option'
  argument 'inherited-argument'

  mode 'install' do
    option 'force' do
      description 'clobber existing installation'
    end

    def run
      inherited_method()
      puts 'installing...'
    end

    mode 'docs' do
      description 'installs the docs'

      def run
        puts 'installing docs...'
      end
    end
  end

  mode 'un-install' do
    option 'force' do
      description 'remove even if dependancies exist'
    end

    def run
      inherited_method()
      puts 'un-installing...'
    end
  end

  def run
    puts 'no mode yo?'
  end

  def inherited_method
    puts 'superclass_method...'
  end

###########################################################################
#                         PARAMETER API                                   #
###########################################################################
#
# all the parameter types of argument|keyword|option|environment share this
# api.  you must specify the type when the parameter method is used.
# alternatively used one of the shortcut methods
# argument|keyword|option|environment.  in otherwords
#
#   parameter('foo'){ type :option } 
#
# is synonymous with
#
#   option('foo'){ } 
#
  option 'foo' {
  #
  # required - whether this paramter must by supplied on the command line.
  # note that you can create 'required' options with this keyword
  #
    required # or required true
  #
  # argument_required - applies only to options.
  #
    argument_required # argument :required
  #
  # argument_optional - applies only to options.
  #
    argument_optional # argument :optional
  #
  # cast - should be either a lambda taking one argument, or a symbol
  # designation one of the built in casts defined in Main::Cast.  supported
  # types are :boolean|:integer|:float|:numeric|:string|:uri.  built-in
  # casts can be abbreviated
  #
    cast :int
  #
  # validate - should be a lambda taking one argument and returning
  # true|false
  #
    validate{|int| int == 42}
  #
  # synopsis - should be a concise characterization of the paramter.  a
  # default synopsis is built automatically from the parameter.  this
  # information is displayed in the usage message
  #
    synopsis '--foo'
  #
  # description - a longer description of the paramter.  it appears in the
  # usage also.
  #
    description 'a long description of foo'
  #
  # arity - indicates how many times the parameter should appear on the
  # command line.  the default is one.  negative arities are supported and
  # follow the same rules as ruby methods/procs.
  #
    arity 2
  #
  # default - you can provide a default value in case none is given.  the
  # alias 'defaults' reads a bit nicer when you are giving a list of
  # defaults for paramters of > 1 arity
  #
    defaults 40, 2
  #
  # you can add custom per-parameter error handlers using the following
  #
    error :before do
      puts 'this fires *before* normal error handling using #instance_eval...'
    end

    error do
      puts 'this fires *instead of* normal error handling using #instance_eval...'
    end

    error :after do
      puts 'this fires *after* normal error handling using #instance_eval...'
    end
  }

###########################################################################
#                       INSTANCE LEVEL API                                #
###########################################################################
#
# you must define a run method.  it is the only method you must define.
#
  def run
    #
    # all parameters are available in the 'params' hash and via the alias
    # 'param'.  it can be indexed via string or symbol.  the values are all
    # Main::Parameter objects
    #
      foo = params['foo']
    #
    # the given? method indicates whether or not the parameter was given on
    # the commandline/environment, etc.  in particular this will not be true
    # when a default value was specified but no parameter was given 
    #
      foo.given?
    #
    # the list of all values can be retrieved via 'values'.  note that this
    # is always an array.
    #
      p foo.values
    #
    # the __first__ value can be retrieved via 'value'.  note that this
    # never an array.
    #
      p foo.value
    #
    # the methods debug|info|warn|error|fatal are delegated to the logger
    # object
    #
      info{ "this goes to the log" }
    #
    # you can set the exit_status at anytime.  this status is used when
    # exiting the program.  exceptions cause this to be ext_failure if, and
    # only if, the current value was exit_success.  in otherwords an
    # un-caught exception always results in a failing exit_status
    #
      exit_status exit_failure
    #
    # a few shortcuts both set the exit_status and exit the program.
    #
      exit_success!
      exit_failure!
      exit_warn!
  end

}