From: Jeremy McAnally Date: 2007-10-19T23:53:44+09:00 Subject: Re: [ANN] main-2.0.0 Fantastic! I always thought there should be a more Ruby-ish way of handling things like this. This probably won't make it into any of my current projects, but I'll definitely be using it in the future. Thanks! Jeremy On 10/18/07, ara.t.howard wrote: > > 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 > > 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 section below > > HISTORY > 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 > > 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 > } > > > ######################################################################## > ### > # 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 > > } > > > enjoy. > > a @ http://codeforpeople.com/ > -- > we can deny everything, except that we have the possibility of being > better. simply reflect on that. > h.h. the 14th dalai lama > > > > > -- http://www.jeremymcanally.com/ My books: Ruby in Practice http://www.manning.com/mcanally/ My free Ruby e-book http://www.humblelittlerubybook.com/ My blogs: http://www.mrneighborly.com/ http://www.rubyinpractice.com/