[ANN] main-4.0.0 (for avdi)

A

ara.t.howard

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 :eek:ptional
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 '(e-mail address removed)'
#
# 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 :eek:ption }
#
# 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 :eek:ptional
#
# 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

}
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Similar Threads

ANN main-4.4.0 0
[ANN] main-2.8.3 2
[ANN] main-3.0.1 0
[ANN] main-2.6.0 0
[ANN] main-2.1.0 6
[ANN] main-0.0.2 5
[ANN] main-2.5.0 4
[ANN] main-2.2.0 2

Members online

No members online now.

Forum statistics

Threads
474,056
Messages
2,570,443
Members
47,089
Latest member
Bobby2025b

Latest Threads

Top