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']