[ANN] main-4.0.0 (for avdi)

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"

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

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

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 'ara.t.howard@gmail.com'
  #
  # 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

  }

--
-a
--
be kind whenever possible... it is always possible - h.h. the 14th dalai lama