lib/clio/commandline.rb in clio-0.0.1 vs lib/clio/commandline.rb in clio-0.2.0

- old
+ new

@@ -1,275 +1,474 @@ -require 'shellwords' -require 'facets/kernel/object_class' -require 'facets/array/indexable' +require 'clio/facets/kernel' # for deep_copy +require 'clio/usage' +#require 'shellwords' module Clio - ### = Commandline - ### - ### What a strange thing is the Clio Commandline. - ### An entity unknown until put upon. - ### - ### cmd = Clio::Commandline.new("--force copy --file try.rb") - ### cmd.option_alias(:f?, :force?) - ### cmd.option_alias(:o, :file) - ### - ### cmd.file #=> 'try.rb' - ### cmd.force? #=> true - ### cmd.o #=> 'try.rb' - ### cmd.f? #=> true - ### - ### TODO: Allow option setter methods (?) - ### TODO: Allow a hash as argument to initialize (?) + + # = Commandline + # + # Clio's Commandline class is a very versitile command line parser. + # A Command can be used either declaritively, defining usage + # and help information upfront; or lazily, whereby information + # about usage is built-up as the commandline actually gets use in + # one's program; or you can use a mixture of the two. + # + # = Underlying Notation + # + # As you might expect the fluent notation can be broken down into + # block notation. + # + # cli = Clio::Command.new + # cli.usage do + # option(:verbose, :v) do + # help('verbose output') + # end + # option(:quiet, :q) do + # help('run silently') + # xor(:V) + # end + # command(:document) do + # help('generate documentation') + # option(:output, :o) do + # type('FILE') + # help('output directory') + # end + # argument('files') do + # multiple + # end + # end + # end + # + # Clearly block notation is DRY and easier to read, but fluent + # notation is important to have because it allows the Commandline + # object to be passed around as an argument and modified easily. + # + # == Method Notation + # + # This notation is very elegant, but slightly more limited in scope. + # For instance, subcommands that use non-letter characters, such as ':', + # can not be described with this notation. + # + # cli.usage.document('*files', '--output=FILE -o') + # cli.usage('--verbose -V','--quiet -q') + # + # cli.usage.help( + # 'document' , 'generate documentation', + # 'validate' , 'run tests or specifications', + # '--verbose' , 'verbose output', + # '--quiet' , 'run siltently' + # ) + # + # cli.usage.document.help( + # '--output', 'output directory' + # 'file*', 'files to document' + # ) + # + # This notation is slightly more limited in scope... so... + # + # cli.usage.command(:document, '--output=FILE -o', 'files*') + # + # == Bracket Shorthand Notation + # + # The core notation can be somewhat verbose. As a further convenience + # commandline usage can be defined with a brief <i>bracket shorthand</i>. + # This is especailly useful when the usage is simple and statically defined. + # + # cli.usage['document']['--output=FILE -o']['FILE*'] + # + # Using a little creativity to improve readabilty we can convert the + # whole example from above using this notation. + # + # cli.usage['--verbose -V', 'verbose output' ] \ + # ['--quiet -q', 'run silently' ] \ + # ['document', 'generate documention' ] \ + # [ '--output=FILE -o', 'output directory' ] \ + # [ 'FILE*', 'files to document' ] + # + # Alternately the help information can be left out and defined in + # a seprate set of usage calls. + # + # cli.usage['--verbose -V']['--quiet -q'] \ + # ['document']['--output=FILE -o']['FILE*'] + # + # cli.usage.help( + # 'document' , 'generate documentation', + # 'validate' , 'run tests or specifications', + # '--verbose' , 'verbose output', + # '--quiet' , 'run siltently' + # ) + # + # cli.usage['document'].help( + # '--output', 'output directory' + # 'FILE', 'files to docment' + # ) + # + # A little more verbose, but a bit more intutive. + # + # == Combining Notations + # + # Since the various notations all translate to same underlying + # structures, they can be mixed and matched as suites ones taste. + # For example we could mix Method Notation and Bracket Notation. + # + # cli.usage.document['--output=FILE -o']['file*'] + # cli.usage['--verbose -V']['--quiet -q'] + # + # The important thing to keep in mind when doing this is what is + # returned by each type of usage call. + # + # == Commandline Parsing + # + # With usage in place, call the +parse+ method to process the + # actual commandline. + # + # cli.parse + # + # If no command arguments are passed to +parse+, ARGV is used. + # + #-- + # == Passive Parsing + # + # The Command class allows you to declare as little or as + # much of the commandline interface upfront as is suitable to + # your application. When using the commandline object, if not + # already defined, options will be lazily created. For example: + # + # cli = Clio::Commandline.new('--force') + # cli.force? #=> true + # + # Commandline sees that you expect a '--force' flag to be an + # acceptable option. So it will call cli.usage.option('force') + # behind the scenes before trying to determine the actual value + # per the content of the command line. You can add aliases as + # parameters to this call as well. + # + # cli = Clio::Commandline.new('-f') + # cli.force?(:f) #=> true + # + # Once set, you do not need to specify the alias again: + # + # cli.force? #=> true + # + # With the exception of help information, this means you can + # generally just use a commandline as needed without having + # to declare anything upfront. + #++ + # + # == Usage Cache + # + # Lastly, Commandline provides a simple means to cache usage + # information to a configuration file, which then can be used + # again the next time the same command is used. This allows + # Commandline to provide high-performane tab completion. + # + #-- + # == Coming Soon + # + # In the future Commandline will be able to generate Manpage + # templates. + # + # TODO: Allow option setter methods (?) + # TODO: Allow a hash as argument to initialize (?) + #++ + class Commandline - instance_methods.each{ |m| private m if m !~ /^(__|instance_|object_|send$|inspect$)/ } - ### Splits a raw command line into two smaller - ### ones. The first including only options - ### upto the first non-option argument. This - ### makes quick work of separating a subcommand - ### from the options for a main command. - # TODO: Rename this method. - def self.gerrymander(argv=ARGV) - if String===argv - argv = Shellwords.shellwords(argv) - end - sub = argv.find{ |x| x !~ /^[-]/ } - idx = argv.index(sub) - opts = argv[0...idx] - scmd = argv[idx..-1] - return opts, scmd + # + instance_methods.each do |m| + private m if m !~ /^(__|instance_|object_|send$|class$|inspect$|respond_to\?$)/ end - ### Define an option attibute. - ### While commandline can be used without - ### pre-declartion of support options - ### doding so allows for creating option - ### aliases. Eg. --quiet and -q. - def self.attr(name, *aliases) - (@predefined_options ||= []) << [name, *aliases] + class << self - name = name.to_s - if name =~ /\?$/ - key = name.chomp('?') - #attr_writer name - module_eval "def #{key}?; @#{key} ; end" - aliases.each do |alt| - alt = alt.to_s.chomp('?') - alias_method("#{alt}?", "#{key}?") - #alias_method("#{alt}=", "#{name}=") - end - else - attr_reader name - #module_eval "def #{name}; self[:#{name}] ; end" - aliases.each do |alt| - #alt = alt.to_s.chomp('?') # TODO: raise error ? - alias_method("#{alt}" , "#{name}") - #alias_method("#{alt}=", "#{name}=") - end + #def inherited(subclass) +#p usage.to_s +#p subclass.usage.to_s +# subclass.usage = self.usage.clone #deep_copy +#p subclass.usage.to_s +# end + + # Command usage. + def usage + @usage ||= ( + if ancestors[1] < Commandline + ancestors[1].usage.dup + else + Usage.new + end + ) end - end - ### Returns a list of all pre-defined options. - ### It does this by seaching class ancestry - ### for instance_methods until it reaches the - ### Commandline base class. - ### TODO: Rename #runmodes method. - ### TODO: Robust enough? Use an Inheritor instead? - def self.predefined_options - @predefined_options ||= [] - ancestor = ancestors[1] - if ancestor > ::Clio::Commandline - @predefined_options - else - @predefined_options | ancestor.predefined_options + def usage=(u) + raise ArgumentError unless u <= Usage + @usage = u end - end - public + # if ancestors[1] < Command + # @usage = ancestors[0].usage.deep_copy + # else + # @usage = Usage.new + # end - ### This method provides the centralized means - ### of accessing the options and arguments on - ### the commandline. - def [](index) - case index - when Integer - @arguments[index] ||= ( - args = @argv.select{ |e| e !~ /^-/ } - val = args[index] - @argv.delete(args[index]) - val - ) - else - return send(index) if respond_to?(index) - key = index.to_s.chomp('?') - val = option_parse(index) - instance_variable_set("@#{key}", val) - (class << self; self; end).class_eval %{ - def #{index}; @#{key}; end - } - return val + # + def subcommand(name, help=nil, &block) + usage.subcommand(name, help, &block) end + alias_method :command, :subcommand + alias_method :cmd, :subcommand + + # + def option(name, *aliases, &block) + usage.option(name, *aliases, &block) + end + alias_method :switch, :option + + # + def opt(label, help, &block) + usage.opt(label, help, &block) + end + alias_method :swt, :opt + + # + def argument(*n_type, &block) + usage.argument(*n_type, &block) + end + + # + def help(string=nil) + usage.help(string) + end + + # + #def arg(label, help, &block) + # usage.arg(label, help, &block) + #end + end - def shift! - args = @argv.select{ |e| e !~ /^-/ } - val = args.first - @argv.delete(val) - val + # New Command. + def initialize(argv=nil, opts={}, &block) + argv_set(argv || ARGV) + #if opts[:usage] + # @usage = opts[:usage] + #else + # #@usage = load_cache + #end + if self.class == Commandline + @usage = Usage.new + else + @usage = self.class.usage #|| Usage.new #.deep_copy + end + @usage.instance_eval(&block) if block end - ### Define an option alias. This adds en entry to - ### the aliases hash, pointing new to a list of - ### all aliases and the first entry on th list - ### being the master key. - def option_alias(new, old) - self[old] - key = old.to_s.chomp('?') - val = option_parse(new) - instance_variable_set("@#{key}", val) if val - (class << self; self; end).class_eval do - alias_method new, old + # + def argv_set(argv) + # reset parser + @parser = nil + # convert to array if string + if String===argv + argv = Shellwords.shellwords(argv) end + # remove anything subsequent to '--' + if index = argv.index('--') + argv = argv[0...index] + end + @argv = argv end - ### Access to the underlying commandline "ARGV". - ### This will show what is yet to be processed. - def instance_delegate ; @argv ; end + # + def cli + #parse unless @cli + @cli + end - ### Returns a hash of all options parsed. - def instance_options - h = {} - ivs = instance_variables - ['@arguments','@argv'] - ivs.each do |iv| - val = instance_variable_get(iv) - h[iv.sub('@','').to_sym] = val if val - end - h + # + #def usage(name=nil, &block) + # @usage ||= Usage.new(name) + # @usage.instance_eval(&block) if block + # @usage + #end + + def usage + @usage end - ### Returns a list of all arguments parsed. - def instance_arguments - @arguments + # + def to_s + usage.to_s end - private + # + def to_s_help + usage.to_s_help + end - ### New Commandline. Takse a single argument - ### which can be a "shell" string, or an array - ### of shell arguments, like ARGV. If none - ### is given it defaults to ARGV. - def initialize(argv=ARGV) - case argv - when String - @argv = Shellwords.shellwords(argv) - #when Hash - # argv.each{ |k,v| send("#{k}=", v) } - else - @argv = argv.dup - end - @arguments = [] + # + def parse(argv=nil) + argv_set(argv) if argv + @cli = parser.parse + end - # parse predefined options attributes. - object_class.predefined_options.each do |modes| - key = modes.first.to_s.chomp('?') - modes.reverse.each do |i| - val = option_parse(i) - instance_variable_set("@#{key}", val) if val - end - end + # + def parser + @parser ||= Usage::Parser.new(usage, @argv) end - ### Routes to #[]. - def method_missing(name, *args) - super unless args.empty? - case name.to_s - when /\=$/ - super - else - self[name] - end + # + def [](i) + @cli[i] end - def option_parse(index) - index = index.to_s - name = index.chomp('?') - key = name.to_sym + # + def command ; cli.command ; end - kind = name.size == 1 ? 'letter' : 'word' - flag = index =~ /\?$/ ? 'flag' : 'value' + # + def commands ; cli.commands ; end - send("option_#{kind}_#{flag}", key) + # + def arguments ; cli.arguments ; end + + # + def switches ; cli.options ; end + + # + alias_method :options, :switches + + # Parameters + # + def parameters ; cli.parameters ; end + + # + def to_a + cli.to_a end - ### Parse a flag option. - def option_word_flag(name) - o = "--#{name}" - i = @argv.index_of{ |e| e =~ /^#{o}[=]?/ } - return false unless i - raise ArgumentError if @argv[i] =~ /=/ - @argv.delete_at(i) - return true + # Commandline fully valid? + # + def valid? + @cli.valid? end - ### Parse a value option. - def option_word_value(name) - o = "--#{name}" - i = @argv.index_of{ |e| e =~ /^#{o}[=]?/ } - return false unless i + # TODO: adding '-' is best idea? + # + def completion(argv=nil) + argv_set(argv) if argv + @argv << "\t" + parse + @argv.pop + parser.errors[0][1].completion.collect{ |s| s.to_s } + #@argv.pop if @argv.last == '?' + #load_cache + #parse + end - if @argv[i] =~ /=/ - key, val = *@argv[i].split('=') - argv[i] = nil - else - case @argv[i+1] - when nil, /^-/ - raise ArgumentError + # + #def load_cache + # if usage = Usage.load_cache + # @usage = usage + # end + #end + + # Method missing provide passive usage and parsing. + # + # TODO: This reparses the commandline after every query. + # Need only parse if usage has change. + def method_missing(s, *a) + begin + s = s.to_s + case s + when /[=]$/ + n = s.chomp('=') + usage.option(n).type(*a) + parse + res = @cli.options[n.to_sym] + when /[!]$/ + n = s.chomp('!') + cmd = usage.commands[n.to_sym] || usage.command(n, *a) + res = parse + when /[?]$/ + n = s.chomp('?') + u = usage.option(n, *a) + parse + res = @cli.options[u.key] else - key = @argv[i] - val = @argv[i+1] - @argv.delete_at(i) # do it twice - @argv.delete_at(i) + usage.option(s, *a) + parse + res = @cli.options[s.to_sym] end + rescue Usage::ParseError => e + res = nil end - return val + return res end - ### Parse a single letter flag option. - def option_letter_flag(letter) - o = letter - i = @argv.index_of{ |e| e =~ /[-][^-]\w*(#{o})\w*$/ } - if i - @argv[i] = @argv[i].gsub(o.to_s,'') - true - end - false - end + end # class Commandline - ### Parse a single letter value option. - def option_letter_value(letter) - o = letter - i = @argv.index_of{ |e| e =~ /[-]\w*#{o}(\=|$)/ } - return nil unless i - if @argv[i] =~ /=/ - rest, val = argv[i].split('=') - @argv[i] = rest - else - case @argv[i+1] - when nil, /^-/ - raise ArgumentError - else - val = @argv[i+1] - new = @argv[i].gsub(o.to_s,'') - if new == '-' - @argv.delete_at(i) - else - @argv[i] = new - end - @argv.delete_at(i+1) - end +end # module Clio + + + +=begin demo 1 + + cli = Clio::Commandline.new + + cli.usage do + command(:document) do + help('generate documentation') + option(:output, :o) do + type('FILE') + help('output directory') end - return val end + option(:verbose, :V) do + help('verbose output') + end + option(:quiet, :q) do + help('run silently') + xor(:verbose) + end + end + #p cli + puts + puts cli.to_s_help + +=end + +=begin demo 2 + + cli = Clio::Commandline.new('--verbose') + + cli.usage do + cmd(:document, 'generate documentation') do + opt('--output=FILE -o', 'output directory') + end + opt('--verbose -V', 'verbose output') + opt('--quiet -q', 'run silently') end -end + +=end + +=begin demo 3 + +# cli.usage %{ +# document generate documentation +# -o --output=FILE output directory +# -V --verbose verbose output +# -q --quiet run silently +# } + + #p cline.verbose?(:V) + #p cline.force?(:f) + #p cline.document.output='FILE' + + p cli + puts + puts cli.to_s_help + +=end + +#cli[['--verbose', '-V'],['--quiet', '-q']] \ +# ['--force'] \ +# ['document']['--output=FILE', '-o']