diff options
author | Lee Jarvis <ljjarvis@gmail.com> | 2013-08-29 14:56:36 +0100 |
---|---|---|
committer | Lee Jarvis <ljjarvis@gmail.com> | 2013-08-29 14:56:36 +0100 |
commit | a35dbf55ba7b69aeb2dc5c21e72fa35be262ca63 (patch) | |
tree | c6ce67f5cc45c0b6af16bf5abd3e9c1bf1e0a58b /lib/slop.rb | |
parent | 8e7dca4c0f6ecba4eb21f8ca44dff8293a8bbbd6 (diff) | |
download | slop-a35dbf55ba7b69aeb2dc5c21e72fa35be262ca63.tar.gz |
Beginning of Slop 4.0 rewrite
Diffstat (limited to 'lib/slop.rb')
-rw-r--r-- | lib/slop.rb | 614 |
1 files changed, 28 insertions, 586 deletions
diff --git a/lib/slop.rb b/lib/slop.rb index 3c77334..10b9f69 100644 --- a/lib/slop.rb +++ b/lib/slop.rb @@ -1,609 +1,51 @@ +require 'slop/commands' +require 'slop/command' +require 'slop/option_builder' +require 'slop/processor' +require 'slop/options' require 'slop/option' -require 'slop/errors' +require 'slop/error' class Slop - VERSION = '3.4.5' + VERSION = "4.0.0" - # Returns a default Hash of configuration options this Slop instance uses. - DEFAULT_CONFIG = { - strict: true, - help: true, - banner: nil, - autocreate: false, - arguments: false, - optional_arguments: false, - ignore_case: false, - multiple_switches: true, - longest_flag: 0 - } - - # items - The Array of items to extract options from (default: ARGV). - # config - The Hash of configuration options to send to Slop.new(). - # block - An optional block used to add options. - # - # Examples: - # - # Slop.parse(ARGV, :help => true) do - # on '-n', '--name', 'Your username', :argument => true - # end - # - # Returns a new instance of Slop. - def self.parse(items = ARGV, config = {}, &block) - parse!(items.dup, config, &block) - end - - # items - The Array of items to extract options from (default: ARGV). - # config - The Hash of configuration options to send to Slop.new(). - # block - An optional block used to add options. - # - # Returns a new instance of Slop. - def self.parse!(items = ARGV, config = {}, &block) - config, items = items, ARGV if items.is_a?(Hash) && config.empty? - slop = new(config, &block) - slop.parse!(items) - slop - end - - # The Hash of configuration options for this Slop instance. - attr_reader :config - - # The Array of Slop::Option objects tied to this Slop instance. - attr_reader :options - - # The Hash of sub-commands for this Slop instance. - attr_reader :commands - - # Create a new instance of Slop and optionally build options via a block. - # - # config - A Hash of configuration options. - # block - An optional block used to specify options. - def initialize(config = {}, &block) - @config = DEFAULT_CONFIG.merge(config) - @options = [] - @commands = {} - @trash = [] - @triggered_options = [] - @unknown_options = [] - @callbacks = {} - @separators = {} - @runner = nil - @command = config.delete(:command) - - if block_given? - block.arity == 1 ? yield(self) : instance_eval(&block) - end - - if config[:help] - on('-h', '--help', 'Display this help message.', :tail => true) do - puts help - exit - end - end - end - - # Is strict mode enabled? - # - # Returns true if strict mode is enabled, false otherwise. - def strict? - config[:strict] - end - - # Set the banner. - # - # banner - The String to set the banner. - def banner=(banner) - config[:banner] = banner - end - - # Get or set the banner. - # - # banner - The String to set the banner. - # - # Returns the banner String. - def banner(banner = nil) - config[:banner] = banner if banner - config[:banner] - end - - # Set the description (used for commands). - # - # desc - The String to set the description. - def description=(desc) - config[:description] = desc - end - - # Get or set the description (used for commands). - # - # desc - The String to set the description. - # - # Returns the description String. - def description(desc = nil) - config[:description] = desc if desc - config[:description] - end - - # Add a new command. - # - # command - The Symbol or String used to identify this command. - # options - A Hash of configuration options (see Slop::new) - # - # Returns a new instance of Slop mapped to this command. - def command(command, options = {}, &block) - options = @config.merge(options) - @commands[command.to_s] = Slop.new(options.merge(:command => command.to_s), &block) - end - - # Parse a list of items, executing and gathering options along the way. - # - # items - The Array of items to extract options from (default: ARGV). - # block - An optional block which when used will yield non options. - # - # Returns an Array of original items. - def parse(items = ARGV, &block) - parse! items.dup, &block - items - end - - # Parse a list of items, executing and gathering options along the way. - # unlike parse() this method will remove any options and option arguments - # from the original Array. - # - # items - The Array of items to extract options from (default: ARGV). - # block - An optional block which when used will yield non options. - # - # Returns an Array of original items with options removed. - def parse!(items = ARGV, &block) - if items.empty? && @callbacks[:empty] - @callbacks[:empty].each { |cb| cb.call(self) } - return items - end - - if cmd = @commands[items[0]] - items.shift - return cmd.parse! items - end - - items.each_with_index do |item, index| - @trash << index && break if item == '--' - autocreate(items, index) if config[:autocreate] - process_item(items, index, &block) unless @trash.include?(index) - end - items.reject!.with_index { |item, index| @trash.include?(index) } - - missing_options = options.select { |opt| opt.required? && opt.count < 1 } - if missing_options.any? - raise MissingOptionError.new(self, "Missing required option(s): #{missing_options.map(&:key).join(', ')}") - end - - if @unknown_options.any? - raise InvalidOptionError.new(self, "Unknown options #{@unknown_options.join(', ')}") - end - - if @triggered_options.empty? && @callbacks[:no_options] - @callbacks[:no_options].each { |cb| cb.call(self) } - end - - if @runner.respond_to?(:call) - @runner.call(self, items) unless config[:help] and present?(:help) - end - - items - end - - # Add an Option. - # - # objects - An Array with an optional Hash as the last element. - # - # Examples: - # - # on '-u', '--username=', 'Your username' - # on :v, :verbose, 'Enable verbose mode' - # - # Returns the created instance of Slop::Option. - def on(*objects, &block) - option = build_option(objects, &block) - original = options.find do |o| - (o.long && o.long == option.long) || (o.short && o.short == option.short) - end - options.delete(original) if original - options << option - option - end - alias_method :option, :on - alias_method :opt, :on - - # Fetch an options argument value. - # - # key - The Symbol or String option short or long flag. - # - # Returns the Object value for this option, or nil. - def [](key) - option = fetch_option(key) - option.value if option - end - alias_method :get, :[] - - # Returns a new Hash with option flags as keys and option values as values. - # - # include_commands - If true, merge options from all sub-commands. - def to_hash(include_commands = false) - hash = Hash[options.map { |opt| [opt.key.to_sym, opt.value] }] - if include_commands - @commands.each { |cmd, opts| hash.merge!(cmd.to_sym => opts.to_hash) } - end - hash - end - alias_method :to_h, :to_hash - - # Specify code to be executed when these options are parsed. - # - # callable - An object responding to a call method. - # - # yields - The instance of Slop parsing these options - # An Array of unparsed arguments - # - # Example: - # - # Slop.parse do - # on :v, :verbose - # - # run do |opts, args| - # puts "Arguments: #{args.inspect}" if opts.verbose? - # end - # end - def run(callable = nil, &block) - @runner = callable || block - unless @runner.respond_to?(:call) - raise ArgumentError, "You must specify a callable object or a block to #run" - end - end - - # Check for an options presence. - # - # Examples: - # - # opts.parse %w( --foo ) - # opts.present?(:foo) #=> true - # opts.present?(:bar) #=> false - # - # Returns true if all of the keys are present in the parsed arguments. - def present?(*keys) - keys.all? { |key| (opt = fetch_option(key)) && opt.count > 0 } - end - - # Override this method so we can check if an option? method exists. - # - # Returns true if this option key exists in our list of options. - def respond_to_missing?(method_name, include_private = false) - options.any? { |o| o.key == method_name.to_s.chop } || super + class << self + attr_accessor :config end - # Fetch a list of options which were missing from the parsed list. - # - # Examples: - # - # opts = Slop.new do - # on :n, :name= - # on :p, :password= - # end - # - # opts.parse %w[ --name Lee ] - # opts.missing #=> ['password'] - # - # Returns an Array of Strings representing missing options. - def missing - (options - @triggered_options).map(&:key) - end + self.config = { + strict: true, + help: true, + ignore_case: false, + multiple_switches: true + } - # Fetch a Slop::Option object. - # - # key - The Symbol or String option key. - # - # Examples: - # - # opts.on(:foo, 'Something fooey', :argument => :optional) - # opt = opts.fetch_option(:foo) - # opt.class #=> Slop::Option - # opt.accepts_optional_argument? #=> true - # - # Returns an Option or nil if none were found. - def fetch_option(key) - options.find { |option| [option.long, option.short].include?(clean(key)) } + def self.parse!(items = ARGV, config = {}, &block) + Slop.new(config, &block).parse!(items) end - # Fetch a Slop object associated with this command. - # - # command - The String or Symbol name of the command. - # - # Examples: - # - # opts.command :foo do - # on :v, :verbose, 'Enable verbose mode' - # end - # - # # ruby run.rb foo -v - # opts.fetch_command(:foo).verbose? #=> true - def fetch_command(command) - @commands[command.to_s] + def self.parse(items = ARGV, config = {}, &block) + parse!(items.dup, config, &block) end - # Add a callback. - # - # label - The Symbol identifier to attach this callback. - # - # Returns nothing. - def add_callback(label, &block) - (@callbacks[label] ||= []) << block - end + attr_reader :command - # Add string separators between options. - # - # text - The String text to print. - def separator(text) - if @separators[options.size] - @separators[options.size] << "\n#{text}" - else - @separators[options.size] = text - end + def initialize(config = {}, &block) + @command = Command.new(:_global_, config, &block) end - # Print a handy Slop help string. - # - # Returns the banner followed by available option help strings. - def to_s - heads = options.reject(&:tail?) - tails = (options - heads) - opts = (heads + tails).select(&:help).map(&:to_s) - optstr = opts.each_with_index.map { |o, i| - (str = @separators[i + 1]) ? [o, str].join("\n") : o - }.join("\n") - - if @commands.any? - optstr << "\n" if !optstr.empty? - optstr << "\nAvailable commands:\n\n" - optstr << commands_to_help - optstr << "\n\nSee `<command> --help` for more information on a specific command." - end + # Delegate methods to command object - banner = config[:banner] - if banner.nil? - banner = "Usage: #{File.basename($0, '.*')}" - banner << " #{@command}" if @command - banner << " [command]" if @commands.any? - banner << " [options]" - end - if banner - "#{banner}\n#{@separators[0] ? "#{@separators[0]}\n" : ''}#{optstr}" - else - optstr - end + def respond_to_missing?(meth, include_private = false) + command.respond_to_missing?(meth) || super end - alias_method :help, :to_s - - private - # Convenience method for present?(:option). - # - # Examples: - # - # opts.parse %( --verbose ) - # opts.verbose? #=> true - # opts.other? #=> false - # - # Returns true if this option is present. If this method does not end - # with a ? character it will instead call super(). - def method_missing(method, *args, &block) - meth = method.to_s - if meth.end_with?('?') - meth.chop! - present?(meth) || present?(meth.gsub('_', '-')) + def method_missing(meth, *args, &block) + if command.respond_to?(meth) + command.send(meth, *args, &block) else super end end - # Process a list item, figure out if it's an option, execute any - # callbacks, assign any option arguments, and do some sanity checks. - # - # items - The Array of items to process. - # index - The current Integer index of the item we want to process. - # block - An optional block which when passed will yield non options. - # - # Returns nothing. - def process_item(items, index, &block) - return unless item = items[index] - option, argument = extract_option(item) if item.start_with?('-') - - if option - option.count += 1 unless item.start_with?('--no-') - option.count += 1 if option.key[0, 3] == "no-" - @trash << index - @triggered_options << option - - if option.expects_argument? - argument ||= items.at(index + 1) - - if !argument || argument =~ /\A--?[a-zA-Z][a-zA-Z0-9_-]*\z/ - raise MissingArgumentError.new(self, "#{option.key} expects an argument") - end - - execute_option(option, argument, index, item) - elsif option.accepts_optional_argument? - argument ||= items.at(index + 1) - - if argument && argument =~ /\A([^\-?]|-\d)+/ - execute_option(option, argument, index, item) - else - option.call(nil) - end - elsif config[:multiple_switches] && argument - execute_multiple_switches(option, argument, items, index) - else - option.value = option.count > 0 - option.call(nil) - end - else - @unknown_options << item if strict? && item =~ /\A--?/ - block.call(item) if block && !@trash.include?(index) - end - end - - # Execute an option, firing off callbacks and assigning arguments. - # - # option - The Slop::Option object found by #process_item. - # argument - The argument Object to assign to this option. - # index - The current Integer index of the object we're processing. - # item - The optional String item we're processing. - # - # Returns nothing. - def execute_option(option, argument, index, item = nil) - if !option - if config[:multiple_switches] && strict? - raise InvalidOptionError.new(self, "Unknown option -#{item}") - end - return - end - - if argument - unless item && item.end_with?("=#{argument}") - @trash << index + 1 unless option.argument_in_value - end - option.value = argument - else - option.value = option.count > 0 - end - - if option.match? && !argument.match(option.config[:match]) - raise InvalidArgumentError.new(self, "#{argument} is an invalid argument") - end - - option.call(option.value) - end - - # Execute a `-abc` type option where a, b and c are all options. This - # method is only executed if the multiple_switches argument is true. - # - # option - The first Option object. - # argument - The argument to this option. (Split into multiple Options). - # items - The Array of items currently being parsed. - # index - The index of the current item being processed. - # - # Returns nothing. - def execute_multiple_switches(option, argument, items, index) - execute_option(option, nil, index) - flags = argument.split('') - flags.each do |key| - if opt = fetch_option(key) - opt.count += 1 - if (opt.expects_argument? || opt.accepts_optional_argument?) && - (flags[-1] == opt.key) && (val = items[index+1]) - execute_option(opt, val, index, key) - else - execute_option(opt, nil, index, key) - end - else - raise InvalidOptionError, "Unknown option -#{key}" if strict? - end - end - end - - # Extract an option from a flag. - # - # flag - The flag key used to extract an option. - # - # Returns an Array of [option, argument]. - def extract_option(flag) - option = fetch_option(flag) - option ||= fetch_option(flag.downcase) if config[:ignore_case] - option ||= fetch_option(flag.gsub(/([^-])-/, '\1_')) - - unless option - case flag - when /\A--?([^=]+)=(.+)\z/, /\A-([a-zA-Z])(.+)\z/, /\A--no-(.+)\z/ - option, argument = fetch_option($1), ($2 || false) - option.argument_in_value = true if option - end - end - - [option, argument] - end - - # Autocreate an option on the fly. See the :autocreate Slop config option. - # - # items - The Array of items we're parsing. - # index - The current Integer index for the item we're processing. - # - # Returns nothing. - def autocreate(items, index) - flag = items[index] - if !fetch_option(flag) && !@trash.include?(index) - option = build_option(Array(flag)) - argument = items[index + 1] - option.config[:argument] = (argument && argument !~ /\A--?/) - option.config[:autocreated] = true - options << option - end - end - - # Build an option from a list of objects. - # - # objects - An Array of objects used to build this option. - # - # Returns a new instance of Slop::Option. - def build_option(objects, &block) - config = {} - config[:argument] = true if @config[:arguments] - config[:optional_argument] = true if @config[:optional_arguments] - - if objects.last.is_a?(Hash) - config.merge!(objects.pop) - end - - short = extract_short_flag(objects, config) - long = extract_long_flag(objects, config) - desc = objects.shift if objects[0].respond_to?(:to_str) - - Option.new(self, short, long, desc, config, &block) - end - - def extract_short_flag(objects, config) - flag = objects[0].to_s - if flag =~ /\A-?\w=?\z/ - config[:argument] ||= flag.end_with?('=') - objects.shift - flag.delete('-=') - end - end - - # Extract the long flag from an item. - # - # objects - The Array of objects passed from #build_option. - # config - The Hash of configuration options built in #build_option. - def extract_long_flag(objects, config) - flag = objects.first.to_s - if flag =~ /\A(?:--?)?[a-zA-Z0-9][a-zA-Z0-9_.-]+\=?\??\z/ - config[:argument] ||= true if flag.end_with?('=') - config[:optional_argument] = true if flag.end_with?('=?') - objects.shift - clean(flag).sub(/\=\??\z/, '') - end - end - - # Remove any leading -- characters from a string. - # - # object - The Object we want to cast to a String and clean. - # - # Returns the newly cleaned String with leading -- characters removed. - def clean(object) - object.to_s.sub(/\A--?/, '') - end - - def commands_to_help - padding = 0 - @commands.each { |c, _| padding = c.size if c.size > padding } - @commands.map { |cmd, opts| - " #{cmd}#{' ' * (padding - cmd.size)} #{opts.description}" - }.join("\n") - end - end |