NAME main.rb SYNOPSIS a class factory and dsl for generating command line programs real quick URI http://rubyforge.org/projects/codeforpeople/ http://codeforpeople.com/lib/ruby/ INSTALL gem install main HISTORY 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 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 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 you 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 ~ > ruby samples/a.rb --help NAME a.rb SYNOPSIS a.rb foo [options]+ PARAMETERS foo (1 -> int(foo)) the foo param --help, -h <========< 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] ~ > ruby samples/b.rb --help NAME b.rb SYNOPSIS b.rb foo [options]+ PARAMETERS foo (3 -> int(foo)) the foo param --help, -h <========< 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 ~ > ruby samples/c.rb --help NAME c.rb SYNOPSIS c.rb foo=foo [bar=bar] [options]+ PARAMETERS foo=foo (2 -> float(foo)) bar=bar (1 ~> bool(bar)) --help, -h <========< 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 ~ > ruby samples/d.rb --help NAME d.rb SYNOPSIS d.rb --foo=foo [options]+ PARAMETERS --foo=foo, -f (2 -> float(foo)) --bar=[bar], -b (1 ~> bool(bar=false)) --help, -h DOCS test/main.rb vim -o lib/main.rb lib/main/* 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 }