main.rbfeaturesthefollowing:-unificationofoption,argument,keyword,andenvironmentparameterparsing-autogenerationofusageandhelpmessages-supportformode/sub-commands-ioredirectionsupport-logginghooksusingruby'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'sformorecomplexapplicationswheremain.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 'thefoobaroptionisveryhandy'
}
environment('BARFOO'){
cast :list_of_bool
synopsis 'exportbarfoo=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) ]
'oneargumentwillgetprocessedviaint(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) ]
'twokeywordargumentsmightbeprocessedviafloat(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 ]
'oneoptionwithoptionalargumentmaybegivendirectly'
* --help, -h
no synopsis, simple switch takes no args and is not required
* export barfoo=value
a user defined synopsis

SAMPLES

<========<samples/a.rb>========>~>catsamples/a.rbrequire'main'ARGV.replace%w( 42 )ifARGV.empty?Main{argument('foo'){required# this is the defaultcast:int# value cast to Fixnumvalidate{|foo|foo==42}# raises error in failure case description'the foo param'# shown in --help}defrunpparams['foo'].given?pparams['foo'].valueend}~>rubysamples/a.rbtrue42<========<samples/b.rb>========>~>catsamples/b.rbrequire'main'ARGV.replace%w( 40 1 1 )ifARGV.empty?Main{argument('foo'){arity3# foo will given three timescast:int# value cast to Fixnumvalidate{|foo|[40,1].include?foo}# raises error in failure case description'the foo param'# shown in --help}defrunpparams['foo'].given?pparams['foo'].valuesend}~>rubysamples/b.rbtrue[40,1,1]<========<samples/c.rb>========>~>catsamples/c.rbrequire'main'ARGV.replace%w( foo=40 foo=2 bar=false )ifARGV.empty?Main{keyword('foo'){required# by default keywords are not requiredarity2cast:float}keyword('bar'){cast:bool}defrunpparams['foo'].given?pparams['foo'].valuespparams['bar'].given?pparams['bar'].valueend}~>rubysamples/c.rbtrue[40.0,2.0]truefalse<========<samples/d.rb>========>~>catsamples/d.rbrequire'main'ARGV.replace%w( --foo=40 -f2 )ifARGV.empty?Main{option('foo','f'){required# by default options are not required, we could use 'foo=foo'# above as a shortcutargument_requiredarity2cast:float}option('bar=[bar]','b'){# note shortcut syntax for optional args# argument_optional # we could also use this methodcast:booldefaultfalse}defrunpparams['foo'].given?pparams['foo'].valuespparams['bar'].given?pparams['bar'].valueend}~>rubysamples/d.rbtrue[40.0,2.0]nilfalse<========<samples/e.rb>========>~>catsamples/e.rbrequire'main'ARGV.replace%w( x y argument )Main{argument'argument'option'option'defrun()puts'run'endmode'a'dooption'a-option'defrun()puts'a-run'endendmode'x'dooption'x-option'defrun()puts'x-run'endmode'y'dooption'y-option'defrun()puts'y-run'endendend}~>rubysamples/e.rby-run<========<samples/f.rb>========>~>catsamples/f.rbrequire'main'ARGV.replace%W( compress /data )Main{argument('directory'){description'the directory to operate on'}option('force'){description'use a bigger hammer'}defrunputs'this is how we run when no mode is specified'endmode'compress'dooption('bzip'){description'use bzip compression'}defrunputs'this is how we run in compress mode'endendmode'uncompress'dooption('delete-after'){description'delete orginal file after uncompressing'}defrunputs'this is how we run in un-compress mode'endend}~>rubysamples/f.rbthisishowwerunincompressmode<========<samples/g.rb>========>~>catsamples/g.rbrequire'main'ARGV.replace%w( 42 )ifARGV.empty?Main{argument('foo')option('bar')run{puts"This is what to_options produces: #{params.to_options.inspect}"}}~>rubysamples/g.rbThisiswhatto_optionsproduces:{"help"=>nil,"foo"=>"42","bar"=>nil}<========<samples/h.rb>========>~>catsamples/h.rbrequire'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)dodefault{default_for_foobar}endmixin:foodofattr:default_for_foobar=>42endmixin:bardofattr:default_for_foobar=>42.0endrun{pparams[:foobar].value}mode:adomixin:fooendmode:bdomixin:barend}~>rubysamples/h.rb"forty-two"<========<samples/j.rb>========>~>catsamples/j.rb#!/usr/bin/env rubyrequire'open-uri'require'main'require'digest/sha2'# you have access to a sequel/amalgalite/sqlite db for free#Main{name:i_can_haz_dbdb{create_table(:mp3s)doprimary_key:idString:urlString:shaendunlesstable_exists?(:mp3s)}defrunurl='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)pdb[:mp3s].allpdbend}~>rubysamples/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.rbvim-plib/main.rblib/main/*rbAPIsectionbelow

HISTORY

5.1.0-supportforSTATE_DIRNAMEandSTATE_BASENAMEenvironmentvariables5.0.0?
4.4.0-supportforautomaticsequel/sqlite/amalgalitedbsforpersistentstateacrossinvocationsMain{db{create_table:foodoStringkeyStringvalendunlesstable_exists?:foo}defrundb[:foo].create(:key=>'using',:val=>'amalgalite')end}-supportforautomaticconfigfileswithautopopulatedtemplatedataMain{config:email=>'[email protected]',:password=>'pa$$word'defrunemail=config[:email]end}-newparamtertypes:pathname,:path,:slug,:input,and:output-input/outputparameters.canbefilenamesor'-'tosupplystdin/stdoutrespectivelyMain{input:ioutput:odefruni=params[:i].valueo=params[:o].valueline=i.getso.putslineend}-cleanupwarningsrunningwith'ruby -w'-fixafailingtest-abilitytoignoreparametersinsubmodesMain{argument:fooargument:bardefrunpparam[:bar].valueendmode:ignoringdoparams[:foo].ignore!end}4.0.0-avoiddupingios.newmethodsMain.push_ios!andMain.pop_ios!areutilizedfortesting.thiswasdonetomakeitsimpletowrapdaemon/servoluxprogramsaroundmain,althoughtnotstrictlyrequired.nottheversionbump-thereisnotreasontoexpectexistingmainprogramstobreak,butit*is*andinterfacechangewhichrequiresamajorversionbump.3.0.0-majorrefactortosupportmodesviamodule/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/attrarguments,options,egargument(:foo){arity-1}defrun# ARGV == %w( a b c )pfoo#=> %w( a b c )endthanksnathan2.8.1-movefromattributes.rbtofattr.rb2.8.0-added'to_options'methodforParameter::Table.thisallowsyoutoconvertalltheparameterstoasimplehash.forexampleMain{option'foo'argument'baz'run{putsparams.to_options.inspect}}2.7.0-removedbundledarrayfieldsandattributes.thesearenowdependanciesmanangedbyrubygems.a.k.a.youmusthaverubygemsinstalledformaintowork.2.6.0-added'mixin'feaatureforstoring,andlaterevaluatingablockofcode.thepurposeofthisisforusewithmodeswhereyouwanttokeepyourcodedry,butmaynotwanttodefinesomethinginthebaseclassforalltoinherit.'mixin'allowsyoutodefinethecodetoinheritonceandtheselectivelydropitinchildclasses(modes)ondemand.forexampleMain{mixin:foobardooption'foo'option'bar'endmode:installdomixin:foobarendmode:uninstalldomixin:foobarendmode:cleandoend}-modedefinitionsarenowdeferredtotheendoftheMainblock,soyoucandothisMain{mode'a'domixin:fooendmode'b'domixin:fooenddefinherited_method42endmixin'foo'dodefanother_inherited_method'forty-two'endend}-addedsanitycheckatendofparamtercontruction-improvedautousagegenerationwhenarityisusedwitharguments-removed'p'shortcutinparamerterdslbecauseitcollidedwithKernel.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'] = 'custommessage'
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*endablockcanbepassedtospecifyhowtoextractthevaluefromtheparameterargument('foo'){optionaldefault21cast:intattr{|param|param.value*2}}defrunpfoo#=> 42 end-fixedbugwhere'abort("message")'wouldprint"message"twiceonexitifrunningunderanestedmode(yesagain-thefixin2.4.0wasn'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'tdothattxtapi%(
foobar string, hash
barfoo hash, string
)}resultsinausagemessagewithsectionslike...EXAMPLESfoobarexamplebarfooexampleSAMPLESdothisdon'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'sfinetodothisMain{definitialize@a=42enddefrunp@aendmode'foo'dodefrunp@aendend}theclient#initialize will be called *after* main has done it's normalinitializationsothingslike@argv,@env,and@stdinwillallbethereininitialize.ofcourseyoucouldhavedonethisbeforebutyou'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 'thisfires*before*normalerrorhandlingusing#instance_eval...'enderrordoputs'this fires *instead of* normal error handling using #instance_eval...'enderror:afterdoputs'this fires *after* normal error handling using #instance_eval...'endendrun(){pparam['x'].given?}}-addedabilitytoexitatanytimebypassing*all*errorhandlingusing'throw :exit, 42'where42isthedesiredexitstatus.throwwithoutastatussimplyexitswith0.-added'help!'methodwhichsimplydumpsoutusageandexits2.0.0-removedneedforproxy.rbviaMain::Base.wrap_run!-addederrorhandlinghooksforparameterparsing-bundledarrayfields,attributes,andpervasivesalthoughgemsaretriedfirst-softenederrormessagesforparameterparsingerrors:certainclassesoferrorsarenow'softspoken'andprintonlythemessage,nottheentirestacktrace,tostderr.muchnicerforusers.thisisconfigurable.-addedsubcommand/modesupport-addedsupportforuserdefinedexceptionhandlingontoplevelexceptions/exits-addedsupportfornegativearity.thisusersruby'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 'zeroormore', 'oneormore' ...
'normore' 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(geninstallalib)Alib.scriptmainprogramgeneratorandalsosomeofjim's
freeze'sexcellentCommandLine::Aplicationintowhatihopeisasimplerandmoreunifiedinterface

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"this text will automatically be indented to the right level.\n\nit should describe how the program works in detail\n"## 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'stderropen('/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_levelLogger::DEBUG## you can configure exit codes. the defaults are shown#exit_success# 0exit_failure# 1exit_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"my own usage message\n"############################################################################ 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'dooption'force'dodescription'clobber existing installation'enddefruninherited_method()puts'installing...'endmode'docs'dodescription'installs the docs'defrunputs'installing docs...'endendendmode'un-install'dooption'force'dodescription'remove even if dependancies exist'enddefruninherited_method()puts'un-installing...'endenddefrunputs'no mode yo?'enddefinherited_methodputs'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.#arity2## 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#defaults40,2## you can add custom per-parameter error handlers using the following#error:beforedoputs'this fires *before* normal error handling using #instance_eval...'enderrordoputs'this fires *instead of* normal error handling using #instance_eval...'enderror:afterdoputs'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.#defrun## 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.#pfoo.values## the __first__ value can be retrieved via 'value'. note that this# never an array.#pfoo.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_statusexit_failure## a few shortcuts both set the exit_status and exit the program.#exit_success!exit_failure!exit_warn!end}