summaryrefslogtreecommitdiff
path: root/lib/slop.rb
diff options
context:
space:
mode:
authorLee Jarvis <ljjarvis@gmail.com>2013-08-29 14:56:36 +0100
committerLee Jarvis <ljjarvis@gmail.com>2013-08-29 14:56:36 +0100
commita35dbf55ba7b69aeb2dc5c21e72fa35be262ca63 (patch)
treec6ce67f5cc45c0b6af16bf5abd3e9c1bf1e0a58b /lib/slop.rb
parent8e7dca4c0f6ecba4eb21f8ca44dff8293a8bbbd6 (diff)
downloadslop-a35dbf55ba7b69aeb2dc5c21e72fa35be262ca63.tar.gz
Beginning of Slop 4.0 rewrite
Diffstat (limited to 'lib/slop.rb')
-rw-r--r--lib/slop.rb614
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