module Main class Base class << self def wrap_run! const_set :RUN, instance_method(:run) class_eval do def run *a, &b argv.push "--#{ argv.shift }" if argv.first == 'help' return mode_run! if mode_given? status = catch :exit do begin parse_parameters if params['help'] and params['help'].given? print usage.to_s exit end pre_run before_run self.class.const_get(:RUN).bind(self).call(*a, &b) after_run post_run finalize rescue Exception => e handle_exception e end nil end handle_throw status end end end def method_added m return if @in_method_added super if defined? super @in_method_added = true begin wrap_run! if m.to_s == 'run' ensure @in_method_added = false end end def self.inheritable_fattr name, &block block ||= lambda{} fattr( name ){ catch :value do if parent? value = parent.send name value = begin Util.mcp value rescue value.clone rescue value.dup end throw :value, value end instance_eval &block end } end # fattrs fattr( 'name' ){ File.basename $0 } fattr( 'synopsis' ){ Usage.default_synopsis(self) } fattr( 'description' ) fattr( 'usage' ){ Usage.default_usage self } fattr( 'modes' ){ Mode.list } fattr( 'mode_definitions' ){ Array.new } fattr( 'mode_name' ){ 'main' } fattr( 'parent' ){ nil } fattr( 'children' ){ Set.new } fattr( 'program' ){ File.basename $0 } fattr( 'author' ) fattr( 'version' ) fattr( 'stdin' ){ $stdin } fattr( 'stdout' ){ $stdout } fattr( 'stderr' ){ $stderr } fattr( 'logger' ){ stderr } fattr( 'logger_level' ){ Logger::INFO } fattr( 'exit_status' ){ EXIT_SUCCESS } fattr( 'exit_success' ){ EXIT_SUCCESS } fattr( 'exit_failure' ){ EXIT_FAILURE } fattr( 'exit_warn' ){ EXIT_WARN } inheritable_fattr( 'parameters' ){ Parameter::List[] } inheritable_fattr( 'can_has_hash' ){ Hash.new } inheritable_fattr( 'mixin_table' ){ Hash.new } # override a few fattrs def mode_name=(value) @mode_name = Mode.new value end def usage *argv, &block usage! unless defined? @usage return @usage if argv.empty? and block.nil? key, value, *ignored = argv value = block.call if block @usage[key.to_s] = value.to_s end def create parent = Base, *a, &b Class.new parent do |child| child.parent = parent unless parent == Base parent.children.add child child.context do child.class_eval &b if b child.default_options! #child.wrap_run! unless child.const_defined?(:RUN) mode_definitions.each do |name, block| klass = create context do mode_name name.to_s module_eval &block if block end modes.add klass end end end end def context &block @@context ||= [] unless block @@context.last else begin @@context.push self block.call @@context.last ensure @@context.pop end end end module ::Main singleton_class{ def current ::Main::Base.context end } end def fully_qualified_mode list = [] ancestors.each do |ancestor| break unless ancestor < Base list << ancestor.mode_name end list.reverse[1..-1] end def run(&b) define_method(:run, &b) end def new(*a, &b) allocate.instance_eval do pre_initialize before_initialize main_initialize *a, &b initialize after_initialize post_initialize self end end end module DSL def parameter *a, &b (parameters << Parameter.create(:parameter, *a, &b)).last end def option *a, &b (parameters << Parameter.create(:option, *a, &b)).last end alias_method 'opt', 'option' alias_method 'switch', 'option' def default_options! option 'help', 'h' unless parameters.has_option?('help', 'h') end def argument *a, &b (parameters << Parameter.create(:argument, *a, &b)).last end alias_method 'arg', 'argument' def keyword *a, &b (parameters << Parameter.create(:keyword, *a, &b)).last end alias_method 'kw', 'keyword' def environment *a, &b (parameters << Parameter.create(:environment, *a, &b)).last end alias_method 'env', 'environment' =begin def mode name, &b klass = create context do mode_name name.to_s module_eval &b if b end modes.add klass end =end def mode name, &b mode_definitions << [name, b] end def can_has ptype, *a, &b key = a.map{|s| s.to_s}.sort_by{|s| -s.size }.first can_has_hash.update key => [ptype, a, b] key end def has key, *keys keys = [key, *keys].flatten.compact.map{|k| k.to_s} keys.map do |key| ptype, a, b = can_has_hash[key] abort "yo - can *not* has #{ key.inspect }!?" unless(ptype and a and b) send ptype, *a, &b key end end def mixin name, *names, &block names = [name, *names].flatten.compact.map{|name| name.to_s} if block names.each do |name| mixin_table[name] = block end else names.each do |name| module_eval &mixin_table[name] end end end ## TODO - for some reason these hork the usage! %w[ examples samples api ].each do |chunkname| module_eval <<-code def #{ chunkname } *a, &b txt = b ? b.call : a.join("\\n") usage['#{ chunkname }'] = txt end code end alias_method 'example', 'examples' alias_method 'sample', 'samples' end extend DSL fattr 'argv' fattr 'env' fattr 'params' fattr 'logger' fattr 'stdin' fattr 'stdout' fattr 'stderr' %w( program name synopsis description author version exit_status exit_success exit_failure exit_warn logger_level usage ).each{|a| fattr(a){ self.class.send a}} %w( parameters param ).each do |dst| alias_method "#{ dst }", "params" alias_method "#{ dst }=", "params=" alias_method "#{ dst }?", "params?" end %w( debug info warn fatal error ).each do |m| module_eval <<-code def #{ m } *a, &b logger.#{ m } *a, &b end code end =begin =end def pre_initialize() :hook end def before_initialize() :hook end def main_initialize argv = ARGV, env = ENV, opts = {} @argv, @env, @opts = argv, env, opts setup_finalizers setup_io_restoration setup_io_redirection setup_logging end def initialize() :hook end def after_initialize() :hook end def post_initialize() :hook end def setup_finalizers @finalizers = finalizers = [] ObjectSpace.define_finalizer(self) do while((f = finalizers.pop)); f.call; end end end def finalize while((f = @finalizers.pop)); f.call; end end def setup_io_redirection self.stdin = @opts['stdin'] || @opts[:stdin] || self.class.stdin self.stdout = @opts['stdout'] || @opts[:stdout] || self.class.stdout self.stderr = @opts['stderr'] || @opts[:stderr] || self.class.stderr end def setup_logging log = self.class.logger || stderr self.logger = log end def logger= log unless(defined?(@logger) and @logger == log) case log when ::Logger, Logger @logger = log when IO, StringIO @logger = Logger.new log @logger.level = logger_level else @logger = Logger.new *log @logger.level = logger_level end end @logger end def setup_io_restoration [STDIN, STDOUT, STDERR].each do |io| dup = io.dup and @finalizers.push lambda{ io.reopen dup } end end def stdin= io unless(defined?(@stdin) and (@stdin == io)) @stdin = if io.respond_to? 'read' io else fd = open io.to_s, 'r+' @finalizers.push lambda{ fd.close } fd end begin STDIN.reopen @stdin rescue $stdin = @stdin ::Object.const_set 'STDIN', @stdin end end end def stdout= io unless(defined?(@stdout) and (@stdout == io)) @stdout = if io.respond_to? 'write' io else fd = open io.to_s, 'w+' @finalizers.push lambda{ fd.close } fd end STDOUT.reopen @stdout rescue($stdout = @stdout) end end def stderr= io unless(defined?(@stderr) and (@stderr == io)) @stderr = if io.respond_to? 'write' io else fd = open io.to_s, 'w+' @finalizers.push lambda{ fd.close } fd end STDERR.reopen @stderr rescue($stderr = @stderr) end end def pre_parse_parameters() :hook end def before_parse_parameters() :hook end def parse_parameters pre_parse_parameters self.class.parameters.parse @argv, @env @params = Parameter::Table.new self.class.parameters.each{|p| @params[p.name.to_s] = p} post_parse_parameters end def after_parse_parameters() :hook end def post_parse_parameters() :hook end def pre_run() :hook end def before_run() :hook end def run raise NotImplementedError, 'run not defined' end def after_run() :hook end def post_run() :hook end def mode_given? begin modes.size > 0 and argv.size > 0 and modes.find_by_mode(argv.first) rescue Mode::Ambiguous true end end def mode_run! mode = modes.find_by_mode argv.shift klass = modes[mode] or abort "bad mode <#{ mode }>" main = klass.new @argv, @env, @opts main.mode = mode main.run end def modes self.class.modes end fattr 'mode' def help! status = 0 print usage.to_s exit(status) end def abort message = 'exit' raise SystemExit.new(message) end def handle_exception e if e.respond_to?(:error_handler_before) fcall(e, :error_handler_before, self) end if e.respond_to?(:error_handler_instead) fcall(e, :error_handler_instead, self) else if e.respond_to? :status exit_status(( e.status )) end if Softspoken === e or SystemExit === e quiet = ((SystemExit === e and e.message.respond_to?('abort')) or # see main/stdext.rb (SystemExit === e and e.message == 'exit')) stderr.puts e.message unless quiet else fatal{ e } end end if e.respond_to?(:error_handler_after) fcall(e, :error_handler_after, self) end exit_status(( exit_failure )) if exit_status == exit_success exit_status(( Integer(exit_status) rescue(exit_status ? 0 : 1) )) exit exit_status end def fcall obj, m, *argv, &block m = obj.method m arity = m.arity if arity >= 0 argv = argv[0, arity] else arity = arity.abs - 1 argv = argv[0, arity] + argv[arity .. -1] end m.call *argv, &block end def handle_throw status exit(( Integer(status) rescue 0 )) end %w[ before instead after ].each do |which| module_eval <<-code def error_handler_#{ which } *argv, &block block.call *argv end code end def instance_eval_block *argv, &block sc = class << self self end sc.module_eval{ define_method '__instance_eval_block', &block } fcall self, '__instance_eval_block', *argv, &block end end end