Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

More parser refactoring.

  • Loading branch information...
commit 5c87fd4d7c67920fe33833429a7477d3a53218ae 1 parent ed97da6
@josevalim josevalim authored
View
7 lib/thor/base.rb
@@ -31,8 +31,8 @@ module Base
# config<Hash>:: Configuration for this Thor class.
#
def initialize(args=[], options={}, config={})
- opts = Thor::Options.new
- opts.parse_arguments(self.class.arguments, args).each do |key, value|
+ opts = Thor::Arguments.new(self.class.arguments)
+ opts.parse(args).each do |key, value|
send("#{key}=", value)
end
@@ -40,8 +40,7 @@ def initialize(args=[], options={}, config={})
parse_options = self.class.class_options
parse_options = parse_options.merge(config[:extra_options]) if config[:extra_options]
opts = Thor::Options.new(parse_options)
- opts.parse(options)
- options = opts.options
+ options = opts.parse(options)
self.class.merge_with_thor_options(options, config[:extra_options]) if config[:extra_options]
end
View
62 lib/thor/parser.rb
@@ -62,53 +62,10 @@ def unshift(arg)
end
end
- def current_is_switch?
- case peek
- when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM
- switch?($1)
- when SHORT_SQ_RE
- $1.split('').any? { |f| switch?("-#{f}") }
- end
- end
-
def current_is_value?
peek && peek.to_s !~ /^-/
end
- def switch?(arg)
- switch_option(arg) || @shorts.key?(arg)
- end
-
- def switch_option(arg)
- if arg =~ /^--(no|skip)-([-\w]+)$/
- @switches[arg] || @switches["--#{$2}"]
- else
- @switches[arg]
- end
- end
-
- # Check if the given argument is actually a shortcut.
- #
- def normalize_switch(arg)
- @shorts.key?(arg) ? @shorts[arg] : arg
- end
-
- # Receives switch, option and the current values hash and assign the next
- # value to it. At the end, remove the option from the array where non
- # assigned requireds are kept.
- #
- def parse_option(switch, option)
- @non_assigned_required.delete(option)
-
- type = if option.type == :default
- current_is_value? ? :string : :boolean
- else
- option.type
- end
-
- send(:"parse_#{type}", switch)
- end
-
# Runs through the argument array getting strings that contains ":" and
# mark it as a hash:
#
@@ -177,7 +134,23 @@ def parse_boolean(switch)
end
end
- # Raises an error if @required array is not empty after parsing.
+ # Receives switch, option and the current values hash and assign the next
+ # value to it. Also removes the option from the array where non assigned
+ # required are kept.
+ #
+ def parse_peek(switch, option)
+ @non_assigned_required.delete(option)
+
+ type = if option.type == :default
+ current_is_value? ? :string : :boolean
+ else
+ option.type
+ end
+
+ send(:"parse_#{type}", switch)
+ end
+
+ # Raises an error if @non_assigned_required array is not empty.
#
def check_requirement!
unless @non_assigned_required.empty?
@@ -191,5 +164,6 @@ def check_requirement!
end
end
+
require 'thor/parser/options'
require 'thor/parser/arguments'
View
25 lib/thor/parser/argument.rb
@@ -0,0 +1,25 @@
+require 'thor/parser/option'
+
+class Thor
+ # Argument is a subset of option. It does not support :boolean and :default
+ # as types.
+ #
+ class Argument < Option
+ VALID_TYPES = [:numeric, :hash, :array, :string]
+
+ def initialize(name, description=nil, required=true, type=:string, default=nil, banner=nil)
+ raise ArgumentError, "Argument name can't be nil." if name.nil?
+ raise ArgumentError, "Type :#{type} is not valid for arguments." if type && !VALID_TYPES.include?(type.to_sym)
+
+ super(name, description, required, type || :string, default, banner)
+ end
+
+ def argument?
+ true
+ end
+
+ def usage
+ required? ? banner : "[#{banner}]"
+ end
+ end
+end
View
177 lib/thor/parser/arguments.rb
@@ -1,174 +1,27 @@
-class Thor
- class Option
- attr_reader :name, :description, :required, :type, :default, :aliases, :group, :banner
-
- VALID_TYPES = [:boolean, :numeric, :hash, :array, :string, :default]
-
- def initialize(name, description=nil, required=nil, type=nil, default=nil, banner=nil, group=nil, aliases=nil)
- raise ArgumentError, "Option name can't be nil." if name.nil?
- raise ArgumentError, "Option cannot be required and have default values." if required && !default.nil?
- raise ArgumentError, "Type :#{type} is not valid for options." if type && !VALID_TYPES.include?(type.to_sym)
-
- @name = name.to_s
- @description = description
- @required = required || false
- @type = (type || :default).to_sym
- @default = default
- @aliases = [*aliases].compact
- @banner = banner || default_banner
- @group = group.to_s.capitalize if group
- end
-
- # This parse quick options given as method_options. It makes several
- # assumptions, but you can be more specific using the option method.
- #
- # parse :foo => "bar"
- # #=> Option foo with default value bar
- #
- # parse [:foo, :baz] => "bar"
- # #=> Option foo with default value bar and alias :baz
- #
- # parse :foo => :required
- # #=> Required option foo without default value
- #
- # parse :foo => :optional
- # #=> Optional foo without default value
- #
- # parse :foo => 2
- # #=> Option foo with default value 2 and type numeric
- #
- # parse :foo => :numeric
- # #=> Option foo without default value and type numeric
- #
- # parse :foo => true
- # #=> Option foo with default value true and type boolean
- #
- # The valid types are :boolean, :numeric, :hash, :array and :string. If none
- # is given a default type is assumed. This default type accepts arguments as
- # string (--foo=value) or booleans (just --foo).
- #
- # By default all options are optional, unless :required is given.
- #
- def self.parse(key, value)
- if key.is_a?(Array)
- name, *aliases = key
- else
- name, aliases = key, []
- end
-
- name = name.to_s
- default = value
-
- type = case value
- when Symbol
- default = nil
-
- if VALID_TYPES.include?(value)
- value
- elsif required = (value == :required)
- :string
- end
- when TrueClass, FalseClass
- :boolean
- when Numeric
- :numeric
- when Hash, Array, String
- value.class.name.downcase.to_sym
- end
-
- self.new(name.to_s, nil, required, type, default, nil, nil, aliases)
- end
-
- def argument?
- false
- end
-
- def required?
- required
- end
-
- def input_required?
- [ :numeric, :hash, :array, :string ].include?(type)
- end
-
- def switch_name
- @switch_name ||= dasherized? ? name : dasherize(name)
- end
+require 'thor/parser/argument'
- def human_name
- @human_name ||= dasherized? ? undasherize(name) : name
+class Thor
+ class Arguments < Parser
+
+ def initialize(arguments=[])
+ @arguments = arguments
+ @non_assigned_required = @arguments.select { |a| a.required? }
end
- def usage
- sample = if banner
- "#{switch_name}=#{banner}"
- else
- switch_name
- end
+ def parse(args)
+ @pile, assigns = args.dup, {}
- sample = "[#{sample}]" unless required?
- sample = "#{aliases.join(', ')}, #{sample}" unless aliases.empty?
- sample
- end
-
- def show_default?
- case default
- when Array, String, Hash
- !default.empty?
+ @arguments.each do |argument|
+ assigns[argument.human_name] = if peek
+ parse_peek(argument.switch_name, argument)
else
- default
- end
- end
-
- protected
-
- def dasherized?
- name.index('-') == 0
- end
-
- def undasherize(str)
- str.sub(/^-{1,2}/, '')
- end
-
- def dasherize(str)
- (str.length > 1 ? "--" : "-") + str.gsub('_', '-')
- end
-
- def default_banner
- case type
- when :boolean
- nil
- when :string, :default
- human_name.upcase
- when :numeric
- "N"
- when :hash
- "key:value"
- when :array
- "one two three"
+ argument.default
end
end
- end
- # Argument is a subset of option. It does not support :boolean and :default
- # as types.
- #
- class Argument < Option
- VALID_TYPES = [:numeric, :hash, :array, :string]
-
- def initialize(name, description=nil, required=true, type=:string, default=nil, banner=nil)
- raise ArgumentError, "Argument name can't be nil." if name.nil?
- raise ArgumentError, "Type :#{type} is not valid for arguments." if type && !VALID_TYPES.include?(type.to_sym)
-
- super(name, description, required, type || :string, default, banner)
+ check_requirement!
+ assigns
end
- def argument?
- true
- end
-
- def usage
- required? ? banner : "[#{banner}]"
- end
end
end
View
152 lib/thor/parser/option.rb
@@ -0,0 +1,152 @@
+class Thor
+ class Option
+ attr_reader :name, :description, :required, :type, :default, :aliases, :group, :banner
+
+ VALID_TYPES = [:boolean, :numeric, :hash, :array, :string, :default]
+
+ def initialize(name, description=nil, required=nil, type=nil, default=nil, banner=nil, group=nil, aliases=nil)
+ raise ArgumentError, "Option name can't be nil." if name.nil?
+ raise ArgumentError, "Option cannot be required and have default values." if required && !default.nil?
+ raise ArgumentError, "Type :#{type} is not valid for options." if type && !VALID_TYPES.include?(type.to_sym)
+
+ @name = name.to_s
+ @description = description
+ @required = required || false
+ @type = (type || :default).to_sym
+ @default = default
+ @aliases = [*aliases].compact
+ @banner = banner || default_banner
+ @group = group.to_s.capitalize if group
+ end
+
+ # This parse quick options given as method_options. It makes several
+ # assumptions, but you can be more specific using the option method.
+ #
+ # parse :foo => "bar"
+ # #=> Option foo with default value bar
+ #
+ # parse [:foo, :baz] => "bar"
+ # #=> Option foo with default value bar and alias :baz
+ #
+ # parse :foo => :required
+ # #=> Required option foo without default value
+ #
+ # parse :foo => :optional
+ # #=> Optional foo without default value
+ #
+ # parse :foo => 2
+ # #=> Option foo with default value 2 and type numeric
+ #
+ # parse :foo => :numeric
+ # #=> Option foo without default value and type numeric
+ #
+ # parse :foo => true
+ # #=> Option foo with default value true and type boolean
+ #
+ # The valid types are :boolean, :numeric, :hash, :array and :string. If none
+ # is given a default type is assumed. This default type accepts arguments as
+ # string (--foo=value) or booleans (just --foo).
+ #
+ # By default all options are optional, unless :required is given.
+ #
+ def self.parse(key, value)
+ if key.is_a?(Array)
+ name, *aliases = key
+ else
+ name, aliases = key, []
+ end
+
+ name = name.to_s
+ default = value
+
+ type = case value
+ when Symbol
+ default = nil
+
+ if VALID_TYPES.include?(value)
+ value
+ elsif required = (value == :required)
+ :string
+ end
+ when TrueClass, FalseClass
+ :boolean
+ when Numeric
+ :numeric
+ when Hash, Array, String
+ value.class.name.downcase.to_sym
+ end
+
+ self.new(name.to_s, nil, required, type, default, nil, nil, aliases)
+ end
+
+ def argument?
+ false
+ end
+
+ def required?
+ required
+ end
+
+ def input_required?
+ [ :numeric, :hash, :array, :string ].include?(type)
+ end
+
+ def switch_name
+ @switch_name ||= dasherized? ? name : dasherize(name)
+ end
+
+ def human_name
+ @human_name ||= dasherized? ? undasherize(name) : name
+ end
+
+ def usage
+ sample = if banner
+ "#{switch_name}=#{banner}"
+ else
+ switch_name
+ end
+
+ sample = "[#{sample}]" unless required?
+ sample = "#{aliases.join(', ')}, #{sample}" unless aliases.empty?
+ sample
+ end
+
+ def show_default?
+ case default
+ when Array, String, Hash
+ !default.empty?
+ else
+ default
+ end
+ end
+
+ protected
+
+ def dasherized?
+ name.index('-') == 0
+ end
+
+ def undasherize(str)
+ str.sub(/^-{1,2}/, '')
+ end
+
+ def dasherize(str)
+ (str.length > 1 ? "--" : "-") + str.gsub('_', '-')
+ end
+
+ def default_banner
+ case type
+ when :boolean
+ nil
+ when :string, :default
+ human_name.upcase
+ when :numeric
+ "N"
+ when :hash
+ "key:value"
+ when :array
+ "one two three"
+ end
+ end
+ end
+end
View
54 lib/thor/parser/options.rb
@@ -1,11 +1,7 @@
-class Thor
+require 'thor/parser/option'
- # This is a modified version of Daniel Berger's Getopt::Long class, licensed
- # under Ruby's license.
- #
+class Thor
class Options < Parser
- attr_reader :arguments, :options
-
# Takes an array of switches. Each array consists of up to three
# elements that indicate the name and type of switch. Returns a hash
# containing each switch name, minus the '-', as a key. The value
@@ -24,11 +20,10 @@ class Options < Parser
# ).parse(args)
#
def initialize(switches={})
- @arguments, @shorts, @options = [], {}, {}
- @non_assigned_required = []
+ @shorts, @non_assigned_required = {}, []
@switches = switches.values.inject({}) do |mem, option|
- @non_assigned_required << option if option.required?
+ @non_assigned_required << option if option.required?
option.aliases.each do |short|
@shorts[short.to_s] ||= option.switch_name
@@ -40,7 +35,7 @@ def initialize(switches={})
end
def parse(args)
- @pile = args.dup
+ @pile, options = args.dup, {}
while peek
if current_is_switch?
@@ -63,31 +58,46 @@ def parse(args)
raise MalformattedArgumentError, "cannot pass switch '#{peek}' as an argument" unless current_is_value?
end
- @options[option.human_name] = parse_option(switch, option)
+ options[option.human_name] = parse_peek(switch, option)
else
shift
end
end
check_requirement!
- @options
+ options
end
- def parse_arguments(arguments, args)
- @pile = args.dup
- assigns = {}
+ protected
+
+ # Returns true if the current value in peek is a registered switch.
+ #
+ def current_is_switch?
+ case peek
+ when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM
+ switch?($1)
+ when SHORT_SQ_RE
+ $1.split('').any? { |f| switch?("-#{f}") }
+ end
+ end
- # TODO Check validity!
- arguments.each do |argument|
- assigns[argument.human_name] = if peek
- send(:"parse_#{argument.type}", argument.switch_name)
+ def switch?(arg)
+ switch_option(arg) || @shorts.key?(arg)
+ end
+
+ def switch_option(arg)
+ if arg =~ /^--(no|skip)-([-\w]+)$/
+ @switches[arg] || @switches["--#{$2}"]
else
- argument.default
+ @switches[arg]
end
end
- assigns
- end
+ # Check if the given argument is actually a shortcut.
+ #
+ def normalize_switch(arg)
+ @shorts.key?(arg) ? @shorts[arg] : arg
+ end
end
end
View
4 spec/actions/copy_file_spec.rb
@@ -7,11 +7,11 @@
end
def invoker
- @invoker ||= MyCounter.new([], {}, { :root => destination_root })
+ @invoker ||= MyCounter.new([1,2], {}, { :root => destination_root })
end
def revoker
- @revoker ||= MyCounter.new([], {}, { :root => destination_root, :behavior => :revoke })
+ @revoker ||= MyCounter.new([1,2], {}, { :root => destination_root, :behavior => :revoke })
end
def exists_and_identical?(source, destination)
View
4 spec/actions/create_file_spec.rb
@@ -7,11 +7,11 @@
end
def invoker
- @invoker ||= MyCounter.new([], {}, { :root => destination_root })
+ @invoker ||= MyCounter.new([1,2], {}, { :root => destination_root })
end
def revoker
- @revoker ||= MyCounter.new([], {}, { :root => destination_root, :behavior => :revoke })
+ @revoker ||= MyCounter.new([1,2], {}, { :root => destination_root, :behavior => :revoke })
end
def file
View
4 spec/actions/directory_spec.rb
@@ -8,11 +8,11 @@
end
def invoker
- @invoker ||= MyCounter.new([], {}, { :root => destination_root })
+ @invoker ||= MyCounter.new([1,2], {}, { :root => destination_root })
end
def revoker
- @revoker ||= MyCounter.new([], {}, { :root => destination_root, :behavior => :revoke })
+ @revoker ||= MyCounter.new([1,2], {}, { :root => destination_root, :behavior => :revoke })
end
def exists_and_identical?(source_path, destination_path)
View
4 spec/actions/get_spec.rb
@@ -7,11 +7,11 @@
end
def invoker
- @invoker ||= MyCounter.new([], {}, { :root => destination_root })
+ @invoker ||= MyCounter.new([1,2], {}, { :root => destination_root })
end
def revoker
- @revoker ||= MyCounter.new([], {}, { :root => destination_root, :behavior => :revoke })
+ @revoker ||= MyCounter.new([1,2], {}, { :root => destination_root, :behavior => :revoke })
end
def exists_and_identical?(source, destination)
View
4 spec/actions/inject_into_file_spec.rb
@@ -8,11 +8,11 @@
end
def invoker(options={})
- @invoker ||= MyCounter.new([], options, { :root => destination_root })
+ @invoker ||= MyCounter.new([1,2], options, { :root => destination_root })
end
def revoker
- @revoker ||= MyCounter.new([], {}, { :root => destination_root, :behavior => :revoke })
+ @revoker ||= MyCounter.new([1,2], {}, { :root => destination_root, :behavior => :revoke })
end
def file
View
4 spec/actions/template_spec.rb
@@ -7,11 +7,11 @@
end
def invoker
- @invoker ||= MyCounter.new([], {}, { :root => destination_root })
+ @invoker ||= MyCounter.new([1,2], {}, { :root => destination_root })
end
def revoker
- @revoker ||= MyCounter.new([], {}, { :root => destination_root, :behavior => :revoke })
+ @revoker ||= MyCounter.new([1,2], {}, { :root => destination_root, :behavior => :revoke })
end
describe "#invoke!" do
View
17 spec/actions_spec.rb
@@ -2,7 +2,7 @@
describe Thor::Actions do
def runner(options={})
- @runner ||= MyCounter.new([], options, { :root => destination_root })
+ @runner ||= MyCounter.new([1], options, { :root => destination_root })
end
def action(*args, &block)
@@ -28,18 +28,18 @@ def file
end
it "can have behavior revoke" do
- MyCounter.new([], {}, :behavior => :revoke).behavior.must == :revoke
+ MyCounter.new([1], {}, :behavior => :revoke).behavior.must == :revoke
end
it "when behavior is set to force, overwrite options" do
- runner = MyCounter.new([], { :force => false, :skip => true }, :behavior => :force)
+ runner = MyCounter.new([1], { :force => false, :skip => true }, :behavior => :force)
runner.behavior.must == :invoke
runner.options.force.must be_true
runner.options.skip.must_not be_true
end
it "when behavior is set to skip, overwrite options" do
- runner = MyCounter.new([], { :force => true, :skip => false }, :behavior => :skip)
+ runner = MyCounter.new([1], { :force => true, :skip => false }, :behavior => :skip)
runner.behavior.must == :invoke
runner.options.force.must_not be_true
runner.options.skip.must be_true
@@ -49,19 +49,20 @@ def file
describe "accessors" do
describe "#root=" do
it "gets the current directory and expands the path to set the root" do
- base = MyCounter.new
+ base = MyCounter.new([1])
base.root = "here"
base.root.must == File.expand_path(File.join(File.dirname(__FILE__), "..", "here"))
end
it "does not use the current directory if one is given" do
- base = MyCounter.new
+ base = MyCounter.new([1])
base.root = "/"
base.root.must == "/"
end
it "uses the current directory if none is given" do
- MyCounter.new.root.must == File.expand_path(File.join(File.dirname(__FILE__), ".."))
+ base = MyCounter.new([1])
+ base.root.must == File.expand_path(File.join(File.dirname(__FILE__), ".."))
end
end
@@ -244,7 +245,7 @@ def file
end
def runner(options={})
- @runner ||= MyCounter.new([], options, { :root => destination_root })
+ @runner ||= MyCounter.new([1], options, { :root => destination_root })
end
def file
View
6 spec/base_spec.rb
@@ -1,5 +1,5 @@
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
-require 'thor/options'
+require 'thor/base'
class Amazing
desc "hello", "say hello"
@@ -17,7 +17,7 @@ def hello
end
it "sets arguments default values" do
- base = MyCounter.new []
+ base = MyCounter.new [1]
base.second.must == 2
end
@@ -27,7 +27,7 @@ def hello
end
it "sets options default values" do
- base = MyCounter.new []
+ base = MyCounter.new [1, 2]
base.options[:third].must == 3
end
View
2  spec/invocation_spec.rb
@@ -1,5 +1,5 @@
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
-require 'thor/options'
+require 'thor/base'
describe Thor::Invocation do
describe "#invoke" do
View
2  spec/option_spec.rb
@@ -1,5 +1,5 @@
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
-require 'thor/option'
+require 'thor/parser'
describe Thor::Option do
def parse(key, value)
View
2  spec/options_spec.rb
@@ -1,5 +1,5 @@
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
-require 'thor/options'
+require 'thor/parser'
describe Thor::Options do
def create(opts)
View
2  spec/shell/basic_spec.rb
@@ -61,7 +61,7 @@ def shell
end
it "does not print a message if base is set to quiet" do
- base = MyCounter.new
+ base = MyCounter.new [1,2]
mock(base).options { Hash.new(:quiet => true) }
dont_allow($stdout).puts
View
2  spec/shell_spec.rb
@@ -17,7 +17,7 @@
describe "#shell" do
it "returns the shell in use" do
- MyCounter.new.shell.must be_kind_of(Thor::Shell::Basic)
+ MyCounter.new([1,2]).shell.must be_kind_of(Thor::Shell::Basic)
end
end
end
Please sign in to comment.
Something went wrong with that request. Please try again.