# = arguments.rb # # == Copyright (c) 2006 Thomas Sawyer # # Ruby License # # This module is free software. You may use, modify, and/or # redistribute this software under the same terms as Ruby. # # This program is distributed in the hope that it will be # useful, but WITHOUT ANY WARRANTY; without even the implied # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR # PURPOSE. # # == Author(s) # # * Thomas Sawyer # # == Developer Notes # # TODO Move to console/arguments.rb, but I'm not sure yet # if adding subdirectories to more/ is a good idea. # CREDIT Thomas Sawyer # Author:: Thomas Sawyer # Copyright:: Copyright (c) 2005 Thomas Sawyer # License:: Ruby License #require 'facets/core/array/index' require 'shellwords' module Console; end # = Console Arguments # # Console Arguments provaide a simpel and convient measn # of parsing arguments passed to via commandline. # # Unlike other more complex libs (like Facets' own Console::Command) # Arguments provides only the basic standard parsing functionality. # In many casses that's all one really needs. class Console::Arguments attr :line attr :argv attr :opts attr :arity def initialize( line=nil, *opts_arity ) @line, @argv = parse_line(line) @opts, @arity = clean(*opts_arity) # caches #@words = Hash.new #{|h,k| h[k]=[]} #@flags = Hash.new #{|h,k| h[k]=[]} #@preflags = Hash.new #{|h,k| h[k]=[]} end private # First pass parser to split the command line into an # array using Shellwords, if not already so divided. def parse_line(line=nil) if line case line when String argv = Shellwords.shellwords(line) else argv = line.to_ary.dup line = argv.join(' ') end else argv = ARGV.dup line = argv.join(' ') end return line, argv end public # Parse off the front flags of a command line. # # line = "--trace stamp --file VERSION" # # argv,keys = *parse_front_flags(line) # # argv #=> ["stamp", "--file", "VERSION"] # keys #=> [["trace", true]] # # Provided an arity hash if any flags take free standing # parameters. # # line = "--level 4 stamp --file VERSION" # # argv,keys = *parse_front_flags(line, :trace => 1) # # argv #=> ["stamp", "--file", "VERSION"] # keys #=> [["level", 4]] # def preflags #( *opts_arity ) #opts, arity = clean(*opts_arity) @preflags ||= ( preflags, args = *parse_preflags(argv) #, *opts_arity) preflags ) end # Like preflags but will remove preflags from # the argumuments array (argv). def preflags! #( *opts_arity ) #opts, arity = clean(*opts_arity) @preflags ||= ( preflags, args = *parse_preflags(argv) #, *opts_arity) @argv = args preflags ) end # def words( *opts_arity ) #opts, arity = clean(*opts_arity) @words ||= ( parse #( opts, arity ) @words ) end # def flags( *opts_arity ) #opts, arity = clean(*opts_arity) @flags ||= ( parse #( opts, arity ) @flags ) end # Returns the parsed command line as an array + hash collection # of parameters, like what a Ruby method accepts. Because of the # use of the hash for flags and options parameters, repeat entries # can be used with this, in that case use #parse directly. def parameters args, keys = parse args.flatten << keys unless keys.empty? return *args end # Basic parser partitions the command line into flags and # arguments. Flags are converted to a associative array # and then the two parts are returned. # # line = "--trace stamp --file=VERSION" # # args,keys = *parse_command(line) # # args #=> ["stamp"] # keys #=> [["trace",true], ["file","VERSION"]] # def parse #( *opts_arity ) #opts, arity = clean(*opts_arity) args = associate_flags(argv) #, *opts_arity) flags, words = args.partition{ |a| Array === a } flags = format_flags(flags) unless opts.include?(:repeat) @words = words @flags = flags return words, flags end # Multi-command. # # Parses a chain of commands from a single command line. # This assumes commands take no free standing arguments, # and rather only utilize option flags. # # line = "--trace rubyforge --groupid=2014 publish --copy='**/*' # # This method does not support flag arity since each command in # the chain could have it's own arity settings. So '=' must be # used to set a flag parameter. def multi_command #( *opts_arity ) #opts, arity = clean(*opts_arity) pflags = preflags #(*opts_arity) args = argv.dup args = multi_flag(args) unless opts.include?(:simple) cmds = [] f = args.find{ |e| e !~ /^-/ } i = f ? args.index(f) : -1 until i < 0 args = args[i..-1] # chain command cmd = args.shift f = args.find{ |e| e !~ /^-/ } if f i = args.index(f) subopts = args[0...i] else i = -1 subopts = args[0..-1] end keys = format_flags(associate_flags(subopts)) #, *opts_arity)) cmds << [cmd, keys] end return cmds, pflags end =begin # Subcommand. # # Subcommand parsing assumes that the first non-flag # entry is a subcommand, followed by subcommand flags. # def parse_subcommand( line=nil, arity={} ) argv = parse_line(line) argv = multi_flag(argv) argv, glbl = parse_front_flags( argv, arity ) cmd = argv.shift args, keys = parse_command(argv, arity) return cmd, args, keys, glbl end =end private # Parse and normalize options and arity. def clean( *opts_arity ) case opts_arity.last when Hash opts = opts_arity[0...-1] arity = opts_arity.last else opts = opts_arity arity = {} end opts = clean_opts(opts) arity = clean_arity(arity) return opts, arity end # Ensure opts are a uniform. def clean_opts( opts ) opts2 = opts.collect{ |o| o.to_sym } opts2 = opts2 & [:simple, :repeat] # valid options only return opts2 end # Ensure arity is uniform. def clean_arity( arity ) arity2 = {} arity.each{ |k,v| arity2[k.to_s] = v.to_i } return arity2 end # Parse preflags. private def parse_preflags( args, *opts_arity ) opts, arity = clean(*opts_arity) args = args.dup args = multi_flag(args) unless opts.include?(:simple) flags = [] while args.first =~ /^-/ key = args.shift key.sub!(/^-{1,2}/,'') if key.index('=') key, val = key.split('=') elsif a = arity[key] val = args.slice!(0,a) else val = true end flags << [key, val] end flags = format_flags(flags) unless opts.include?(:repeat) return flags, args end # Parse flags takes the command line and # transforms it such that flags (eg. -x and --x) # are elemetnal associative arrays. # # line = "--foo hello --try=this" # # parse_flags(line) #=> [ [foo,true], hello, ["try","this"] ] # def associate_flags( args ) #, *opts_arity ) #opts, arity = clean(*opts_arity) args = args.dup args = multi_flag(args) unless opts.include?(:simple) i = 0 while i < args.size arg = args[i] case arg when /^-/ arg = arg.sub(/^-{1,2}/,'') if arg.index('=') key, val = arg.split('=') args[i] = [key, val||true] elsif arity.key?(arg) cnt = arity[arg] key = arg val = args[i+1,cnt] args[i,cnt+1] = [[key, *val]] i += cnt else key = arg args[i] = [key,true] end end i += 1 end return args end =begin # Parse out flags across the entire command line. # Flags parsed are the front flags and any specified in the # the +flags+ parameter. The +flags+ parameters can be either # a list of flag names or a name => arity hash. # # Returns the remaining split line and the global flags. def parse_global_flags( line=nil, flags={} ) argv = parse_line(line) argv = multi_flag(argv) keys = [] flags.each do |name, arity| while i = argv.index{ |e| e =~ /^-{1,2}#{name}/ } arg = argv[i] if arg.index('=') key, val = arg.split('=') keys << [name,val] elsif arity val = argv.slice!(i+1,arity) keys << [name,val] else keys << [name,true] end argv.delete(i) end end i = argv.index{ |e| e !~ /^-/ } glbl = argv[0...i] # main options argv = argv[i..-1] # rest of line glbl.each do |key| name = key.sub(/^-{1,2}/,'') keys.unshift [name, true] end keys = format_flags(keys) return argv, keys end =end # Split single letter option groupings into separate options. # ie. -xyz => -x -y -z def multi_flag(args=nil) args ||= argv args.collect { |arg| if md = /^-(\w{2,})/.match( arg ) md[1].split(//).collect { |c| "-#{c}" } else arg.dup end }.flatten end # Format flag options. This converts the associative array of flags into # hash. This prevents repate option flags. If you need repeat options flags # you can deacitvate this formatting by using the submodule RepeatFlag. def format_flags(assoc_array) Hash[*assoc_array.flatten] end end # _____ _ # |_ _|__ ___| |_ # | |/ _ \/ __| __| # | | __/\__ \ |_ # |_|\___||___/\__| # =begin test require 'test/unit' class TestArguments < Test::Unit::TestCase include Console def test_simple line = "-x baz --foo=8 bar" cargs = Arguments.new(line) args, keys = cargs.parse assert_equal(['baz','bar'],args) assert_equal({'foo'=>'8','x'=>true},keys) end def test_repeat line = "-x baz --foo=9 bar" cargs = Arguments.new(line, :repeat) args, keys = cargs.parse assert_equal(['baz','bar'],args) assert_equal([['x',true],['foo','9']],keys) end def test_preflags line = "-x --foo=7 baz bar" cargs = Arguments.new(line) flags = cargs.preflags assert_equal({'x'=>true,'foo'=>'7'},flags) end def test_preflags! line = "-x baz --foo=7 bar" cargs = Arguments.new(line) glbl = cargs.preflags! words, keys = cargs.parse cmd, *args = *words assert_equal({'x'=>true},glbl) assert_equal('baz',cmd) assert_equal(['bar'],args) assert_equal({'foo'=>'7'},keys) end def test_multi_command line = "-x baz --foo=6 bar --zoo='xx'" cargs = Arguments.new(line) cmds, preflags = cargs.multi_command assert_equal({'x'=>true},preflags) cmd, keys = *cmds[0] assert_equal('baz',cmd) assert_equal({'foo'=>'6'},keys) cmd, keys = *cmds[1] assert_equal('bar',cmd) assert_equal({'zoo'=>'xx'},keys) end def test_with_arity line = "-q baz --aq 5 bar" cargs = Arguments.new(line,'aq'=>1) words, flags = cargs.parse assert_equal(['baz','bar'],words) assert_equal({'q'=>true,'aq'=>'5'},flags) end end =end