diff --git a/CHANGELOG.md b/CHANGELOG.md index a823662d..12f1d17c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ ### HEAD +* Remove Slop as a runtime dependency by vendoring v3.4 as Pry::Slop. + People can depend on Slop v4 and Pry at the same time without running into version conflicts. ([#1497](https://github.com/pry/pry/issues/1497)) * Fix auto-indentation of code that uses a single-line rescue ([#1450](https://github.com/pry/pry/issues/1450)) * Remove "Pry::Config#refresh", please use "Pry::Config#clear" instead. * Defining a method called "ls" no longer breaks the "ls" command ([#1407](https://github.com/pry/pry/issues/1407)) diff --git a/lib/pry.rb b/lib/pry.rb index 01231d91..0181cecd 100644 --- a/lib/pry.rb +++ b/lib/pry.rb @@ -126,7 +126,7 @@ require 'shellwords' require 'stringio' require 'strscan' require 'coderay' -require 'slop' +require 'pry/slop' require 'rbconfig' require 'tempfile' require 'pathname' diff --git a/lib/pry/cli.rb b/lib/pry/cli.rb index d034f080..c00b48c4 100644 --- a/lib/pry/cli.rb +++ b/lib/pry/cli.rb @@ -19,7 +19,7 @@ class Pry # as CLI options. attr_accessor :input_args - # Add another set of CLI options (a Slop block) + # Add another set of CLI options (a Pry::Slop block) def add_options(&block) if options old_options = options @@ -68,16 +68,16 @@ class Pry self.input_args = args begin - opts = Slop.parse!( + opts = Pry::Slop.parse!( args, :help => true, :multiple_switches => false, :strict => true, &options ) - rescue Slop::InvalidOptionError + rescue Pry::Slop::InvalidOptionError # Display help message on unknown switches and exit. - puts Slop.new(&options) + puts Pry::Slop.new(&options) exit end @@ -124,7 +124,7 @@ end # Bring in options defined by plugins -Slop.new do +Pry::Slop.new do on "no-plugins" do Pry.config.should_load_plugins = false end diff --git a/lib/pry/command.rb b/lib/pry/command.rb index 2971b484..01f6db54 100644 --- a/lib/pry/command.rb +++ b/lib/pry/command.rb @@ -531,7 +531,7 @@ class Pry # subclasses. # # Create subclasses using {Pry::CommandSet#create_command}, and override the - # `options(opt)` method to set up an instance of Slop, and the `process` + # `options(opt)` method to set up an instance of Pry::Slop, and the `process` # method to actually run the command. If necessary, you can also override # `setup` which will be called before `options`, for example to require any # gems your command needs to run, or to set up state. @@ -607,15 +607,15 @@ class Pry end end - # Return the help generated by Slop for this command. + # Return the help generated by Pry::Slop for this command. def help slop.help end - # Return an instance of Slop that can parse either subcommands or the + # Return an instance of Pry::Slop that can parse either subcommands or the # options that this command accepts. def slop - Slop.new do |opt| + Pry::Slop.new do |opt| opt.banner(unindent(self.class.banner)) subcommands(opt) options(opt) @@ -644,7 +644,7 @@ class Pry # end def setup; end - # A method to setup Slop commands so it can parse the subcommands your + # A method to setup Pry::Slop commands so it can parse the subcommands your # command expects. If you need to set up default values, use `setup` # instead. # @@ -679,7 +679,7 @@ class Pry # end def subcommands(cmd); end - # A method to setup Slop so it can parse the options your command expects. + # A method to setup Pry::Slop so it can parse the options your command expects. # # @note Please don't do anything side-effecty in the main part of this # method, as it may be called by Pry at any time for introspection reasons. @@ -696,7 +696,7 @@ class Pry # The actual body of your command should go here. # - # The `opts` mehod can be called to get the options that Slop has passed, + # The `opts` mehod can be called to get the options that Pry::Slop has passed, # and `args` gives the remaining, unparsed arguments. # # The return value of this method is discarded unless the command was diff --git a/lib/pry/helpers/options_helpers.rb b/lib/pry/helpers/options_helpers.rb index e566d3bc..09fd0262 100644 --- a/lib/pry/helpers/options_helpers.rb +++ b/lib/pry/helpers/options_helpers.rb @@ -3,7 +3,7 @@ class Pry module OptionsHelpers module_function - # Add method options to the Slop instance + # Add method options to the Pry::Slop instance def method_options(opt) @method_target = target opt.on :M, "instance-methods", "Operate on instance methods." diff --git a/lib/pry/pry_instance.rb b/lib/pry/pry_instance.rb index 90f63c89..134a64ba 100644 --- a/lib/pry/pry_instance.rb +++ b/lib/pry/pry_instance.rb @@ -435,7 +435,7 @@ class Pry # @return [Boolean] `true` if `val` is a command, `false` otherwise def process_command_safely(val) process_command(val) - rescue CommandError, Slop::InvalidOptionError, MethodSource::SourceNotFoundError => e + rescue CommandError, Pry::Slop::InvalidOptionError, MethodSource::SourceNotFoundError => e Pry.last_internal_error = e output.puts "Error: #{e.message}" true diff --git a/lib/pry/slop.rb b/lib/pry/slop.rb new file mode 100644 index 00000000..a7792bdf --- /dev/null +++ b/lib/pry/slop.rb @@ -0,0 +1,661 @@ +class Pry::Slop + require_relative 'slop/option' + require_relative 'slop/commands' + include Enumerable + VERSION = '3.4.0' + + # The main Error class, all Exception classes inherit from this class. + class Error < StandardError; end + + # Raised when an option argument is expected but none are given. + class MissingArgumentError < Error; end + + # Raised when an option is expected/required but not present. + class MissingOptionError < Error; end + + # Raised when an argument does not match its intended match constraint. + class InvalidArgumentError < Error; end + + # Raised when an invalid option is found and the strict flag is enabled. + class InvalidOptionError < Error; end + + # Raised when an invalid command is found and the strict flag is enabled. + class InvalidCommandError < Error; end + + # Returns a default Hash of configuration options this Slop instance uses. + DEFAULT_OPTIONS = { + :strict => false, + :help => false, + :banner => nil, + :ignore_case => false, + :autocreate => false, + :arguments => false, + :optional_arguments => false, + :multiple_switches => true, + :longest_flag => 0 + } + + class << self + + # 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 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 parse!(items = ARGV, config = {}, &block) + config, items = items, ARGV if items.is_a?(Hash) && config.empty? + slop = Pry::Slop.new config, &block + slop.parse! items + slop + end + + # Build a Slop object from a option specification. + # + # This allows you to design your options via a simple String rather + # than programatically. Do note though that with this method, you're + # unable to pass any advanced options to the on() method when creating + # options. + # + # string - The optspec String + # config - A Hash of configuration options to pass to Slop.new + # + # Examples: + # + # opts = Slop.optspec(<<-SPEC) + # ruby foo.rb [options] + # --- + # n,name= Your name + # a,age= Your age + # A,auth Sign in with auth + # p,passcode= Your secret pass code + # SPEC + # + # opts.fetch_option(:name).description #=> "Your name" + # + # Returns a new instance of Slop. + def optspec(string, config = {}) + config[:banner], optspec = string.split(/^--+$/, 2) if string[/^--+$/] + lines = optspec.split("\n").reject(&:empty?) + opts = Slop.new(config) + + lines.each do |line| + opt, description = line.split(' ', 2) + short, long = opt.split(',').map { |s| s.sub(/\A--?/, '') } + opt = opts.on(short, long, description) + + if long && long.end_with?('=') + long.sub!(/\=$/, '') + opt.config[:argument] = true + end + end + + opts + end + + 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 + + # 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_OPTIONS.merge(config) + @options = [] + @commands = {} + @trash = [] + @triggered_options = [] + @unknown_options = [] + @callbacks = {} + @separators = {} + @runner = nil + + 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 + $stderr.puts help + 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) + @commands[command.to_s] = Pry::Slop.new(options, &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]] + return cmd.parse! items[1..-1] + 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, + "Missing required option(s): #{missing_options.map(&:key).join(', ')}" + end + + if @unknown_options.any? + raise InvalidOptionError, "Unknown options #{@unknown_options.join(', ')}" + end + + if @triggered_options.empty? && @callbacks[:no_options] + @callbacks[:no_options].each { |cb| cb.call(self) } + end + + @runner.call(self, items) if @runner.respond_to?(:call) + + 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) + options << option + option + end + alias option on + alias 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 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 to_h to_hash + + # Enumerable interface. Yields each Slop::Option. + def each(&block) + options.each(&block) + end + + # 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 + 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 + + # 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)) } + 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] + end + + # Add a callback. + # + # label - The Symbol identifier to attach this callback. + # + # Returns nothing. + def add_callback(label, &block) + (@callbacks[label] ||= []) << block + end + + # 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 + 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 ` --help` for more information on a specific command." + end + + banner = config[:banner] + banner = "Usage: #{File.basename($0, '.*')}#{' [command]' if @commands.any?} [options]" if banner.nil? + if banner + "#{banner}\n#{@separators[0] ? "#{@separators[0]}\n" : ''}#{optstr}" + else + optstr + end + end + alias 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('_', '-')) + 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, "#{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, 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, "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, "#{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). + # index - The index of the current item being processed. + # + # Returns nothing. + def execute_multiple_switches(option, argument, index) + execute_option(option, nil, index) + argument.split('').each do |key| + next unless opt = fetch_option(key) + opt.count += 1 + execute_option(opt, nil, index, key) + 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.last) + objects.pop + end + short = extract_short_flag(objects, config) + long = extract_long_flag(objects, config) + desc = objects[0].respond_to?(:to_str) ? objects.shift : nil + + Option.new(self, short, long, desc, config, &block) + end + + # Extract the short 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_short_flag(objects, config) + flag = clean(objects.first) + + if flag.size == 2 && flag.end_with?('=') + config[:argument] ||= true + flag.chop! + end + + if flag.size == 1 + objects.shift + flag + 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-Z][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 do |cmd, opts| + " #{cmd}#{' ' * (padding - cmd.size)} #{opts.description}" + end.join("\n") + end + +end diff --git a/lib/pry/slop/LICENSE b/lib/pry/slop/LICENSE new file mode 100644 index 00000000..ab47fd75 --- /dev/null +++ b/lib/pry/slop/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2012 Lee Jarvis + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/pry/slop/commands.rb b/lib/pry/slop/commands.rb new file mode 100644 index 00000000..d8ac1cb2 --- /dev/null +++ b/lib/pry/slop/commands.rb @@ -0,0 +1,196 @@ +class Pry::Slop + class Commands + include Enumerable + + attr_reader :config, :commands, :arguments + attr_writer :banner + + # Create a new instance of Slop::Commands and optionally build + # Slop instances via a block. Any configuration options used in + # this method will be the default configuration options sent to + # each Slop object created. + # + # config - An optional configuration Hash. + # block - Optional block used to define commands. + # + # Examples: + # + # commands = Slop::Commands.new do + # on :new do + # on '-o', '--outdir=', 'The output directory' + # on '-v', '--verbose', 'Enable verbose mode' + # end + # + # on :generate do + # on '--assets', 'Generate assets', :default => true + # end + # + # global do + # on '-D', '--debug', 'Enable debug mode', :default => false + # end + # end + # + # commands[:new].class #=> Slop + # commands.parse + # + def initialize(config = {}, &block) + @config = config + @commands = {} + @banner = nil + @triggered_command = nil + + warn "[DEPRECATED] Slop::Commands is deprecated and will be removed in "\ + "Slop version 4. Check out http://injekt.github.com/slop/#commands for "\ + "a new implementation of commands." + + if block_given? + block.arity == 1 ? yield(self) : instance_eval(&block) + end + end + + # Optionally set the banner for this command help output. + # + # banner - The String text to set the banner. + # + # Returns the String banner if one is set. + def banner(banner = nil) + @banner = banner if banner + @banner + end + + # Add a Slop instance for a specific command. + # + # command - A String or Symbol key used to identify this command. + # config - A Hash of configuration options to pass to Slop. + # block - An optional block used to pass options to Slop. + # + # Returns the newly created Slop instance mapped to command. + def on(command, config = {}, &block) + commands[command.to_s] = Slop.new(@config.merge(config), &block) + end + + # Add a Slop instance used when no other commands exist. + # + # config - A Hash of configuration options to pass to Slop. + # block - An optional block used to pass options to Slop. + # + # Returns the newly created Slop instance mapped to default. + def default(config = {}, &block) + on('default', config, &block) + end + + # Add a global Slop instance. + # + # config - A Hash of configuration options to pass to Slop. + # block - An optional block used to pass options to Slop. + # + # Returns the newly created Slop instance mapped to global. + def global(config = {}, &block) + on('global', config, &block) + end + + # Fetch the instance of Slop tied to a command. + # + # key - The String or Symbol key used to locate this command. + # + # Returns the Slop instance if this key is found, nil otherwise. + def [](key) + commands[key.to_s] + end + alias get [] + + # Check for a command presence. + # + # Examples: + # + # cmds.parse %w( foo ) + # cmds.present?(:foo) #=> true + # cmds.present?(:bar) #=> false + # + # Returns true if the given key is present in the parsed arguments. + def present?(key) + key.to_s == @triggered_command + end + + # Enumerable interface. + def each(&block) + @commands.each(&block) + end + + # Parse a list of items. + # + # items - The Array of items to parse. + # + # Returns the original Array of items. + def parse(items = ARGV) + parse! items.dup + items + end + + # Parse a list of items, removing any options or option arguments found. + # + # items - The Array of items to parse. + # + # Returns the original Array of items with options removed. + def parse!(items = ARGV) + if opts = commands[items[0].to_s] + @triggered_command = items.shift + execute_arguments! items + opts.parse! items + execute_global_opts! items + else + if opts = commands['default'] + opts.parse! items + else + if config[:strict] && items[0] + raise InvalidCommandError, "Unknown command `#{items[0]}`" + end + end + execute_global_opts! items + end + items + end + + # Returns a nested Hash with Slop options and values. See Slop#to_hash. + def to_hash + Hash[commands.map { |k, v| [k.to_sym, v.to_hash] }] + end + + # Returns the help String. + def to_s + defaults = commands.delete('default') + globals = commands.delete('global') + helps = commands.reject { |_, v| v.options.none? } + if globals && globals.options.any? + helps.merge!('Global options' => globals.to_s) + end + if defaults && defaults.options.any? + helps.merge!('Other options' => defaults.to_s) + end + banner = @banner ? "#{@banner}\n" : "" + banner + helps.map { |key, opts| " #{key}\n#{opts}" }.join("\n\n") + end + alias help to_s + + # Returns the inspection String. + def inspect + "#" + end + + private + + # Returns nothing. + def execute_arguments!(items) + @arguments = items.take_while { |arg| !arg.start_with?('-') } + items.shift @arguments.size + end + + # Returns nothing. + def execute_global_opts!(items) + if global_opts = commands['global'] + global_opts.parse! items + end + end + + end +end diff --git a/lib/pry/slop/option.rb b/lib/pry/slop/option.rb new file mode 100644 index 00000000..236939de --- /dev/null +++ b/lib/pry/slop/option.rb @@ -0,0 +1,208 @@ +class Pry::Slop + class Option + + # The default Hash of configuration options this class uses. + DEFAULT_OPTIONS = { + :argument => false, + :optional_argument => false, + :tail => false, + :default => nil, + :callback => nil, + :delimiter => ',', + :limit => 0, + :match => nil, + :optional => true, + :required => false, + :as => String, + :autocreated => false + } + + attr_reader :short, :long, :description, :config, :types + attr_accessor :count, :argument_in_value + + # Incapsulate internal option information, mainly used to store + # option specific configuration data, most of the meat of this + # class is found in the #value method. + # + # slop - The instance of Slop tied to this Option. + # short - The String or Symbol short flag. + # long - The String or Symbol long flag. + # description - The String description text. + # config - A Hash of configuration options. + # block - An optional block used as a callback. + def initialize(slop, short, long, description, config = {}, &block) + @slop = slop + @short = short + @long = long + @description = description + @config = DEFAULT_OPTIONS.merge(config) + @count = 0 + @callback = block_given? ? block : config[:callback] + @value = nil + + @types = { + :string => proc { |v| v.to_s }, + :symbol => proc { |v| v.to_sym }, + :integer => proc { |v| value_to_integer(v) }, + :float => proc { |v| value_to_float(v) }, + :range => proc { |v| value_to_range(v) }, + :count => proc { |v| @count } + } + + if long && long.size > @slop.config[:longest_flag] + @slop.config[:longest_flag] = long.size + end + + @config.each_key do |key| + predicate = :"#{key}?" + unless self.class.method_defined? predicate + self.class.__send__(:define_method, predicate) { !!@config[key] } + end + end + end + + # Returns true if this option expects an argument. + def expects_argument? + config[:argument] && config[:argument] != :optional + end + + # Returns true if this option accepts an optional argument. + def accepts_optional_argument? + config[:optional_argument] || config[:argument] == :optional + end + + # Returns the String flag of this option. Preferring the long flag. + def key + long || short + end + + # Call this options callback if one exists, and it responds to call(). + # + # Returns nothing. + def call(*objects) + @callback.call(*objects) if @callback.respond_to?(:call) + end + + # Set the new argument value for this option. + # + # We use this setter method to handle concatenating lists. That is, + # when an array type is specified and used more than once, values from + # both options will be grouped together and flattened into a single array. + def value=(new_value) + if config[:as].to_s.downcase == 'array' + @value ||= [] + + if new_value.respond_to?(:split) + @value.concat new_value.split(config[:delimiter], config[:limit]) + end + else + @value = new_value + end + end + + # Fetch the argument value for this option. + # + # Returns the Object once any type conversions have taken place. + def value + value = @value.nil? ? config[:default] : @value + + if [true, false, nil].include?(value) && config[:as].to_s != 'count' + return value + end + + type = config[:as] + if type.respond_to?(:call) + type.call(value) + else + if callable = types[type.to_s.downcase.to_sym] + callable.call(value) + else + value + end + end + end + + # Returns the help String for this option. + def to_s + return config[:help] if config[:help].respond_to?(:to_str) + + out = " #{short ? "-#{short}, " : ' ' * 4}" + + if long + out << "--#{long}" + size = long.size + diff = @slop.config[:longest_flag] - size + out << (' ' * (diff + 6)) + else + out << (' ' * (@slop.config[:longest_flag] + 8)) + end + + "#{out}#{description}" + end + alias help to_s + + # Returns the String inspection text. + def inspect + "# 1.1.0' - s.add_dependency 'slop', '~> 3.4' s.add_dependency 'method_source', '~> 0.8.1' s.add_development_dependency 'bundler', '~> 1.0' end diff --git a/spec/commands/cat/file_formatter_spec.rb b/spec/commands/cat/file_formatter_spec.rb index 374b120c..ad1a3eec 100644 --- a/spec/commands/cat/file_formatter_spec.rb +++ b/spec/commands/cat/file_formatter_spec.rb @@ -3,7 +3,7 @@ require_relative '../../helper' describe Pry::Command::Cat::FileFormatter do before do @p = Pry.new - @opt = Slop.new + @opt = Pry::Slop.new end describe "#file_and_line" do diff --git a/spec/commands/gem_list_spec.rb b/spec/commands/gem_list_spec.rb index 4edbf590..06bd3444 100644 --- a/spec/commands/gem_list_spec.rb +++ b/spec/commands/gem_list_spec.rb @@ -7,13 +7,12 @@ describe "gem-list" do it 'should work arglessly' do list = pry_eval('gem-list') - expect(list).to match(/slop \(/) expect(list).to match(/rspec \(/) end it 'should find arg' do - prylist = pry_eval('gem-list slop') - expect(prylist).to match(/slop \(/) + prylist = pry_eval('gem-list method_source') + expect(prylist).to match(/method_source \(/) expect(prylist).not_to match(/rspec/) end diff --git a/spec/helper.rb b/spec/helper.rb index c87f30b2..2e39250f 100644 --- a/spec/helper.rb +++ b/spec/helper.rb @@ -26,7 +26,7 @@ if ENV["SET_TRACE_FUNC"] } end -puts "Ruby v#{RUBY_VERSION} (#{defined?(RUBY_ENGINE) ? RUBY_ENGINE : "ruby"}), Pry v#{Pry::VERSION}, method_source v#{MethodSource::VERSION}, CodeRay v#{CodeRay::VERSION}, Slop v#{Slop::VERSION}" +puts "Ruby v#{RUBY_VERSION} (#{defined?(RUBY_ENGINE) ? RUBY_ENGINE : "ruby"}), Pry v#{Pry::VERSION}, method_source v#{MethodSource::VERSION}, CodeRay v#{CodeRay::VERSION}, Pry::Slop v#{Pry::Slop::VERSION}" RSpec.configure do |config| config.expect_with :rspec do |c|