lib/rake-commander/options/arguments.rb in rake-commander-0.1.4 vs lib/rake-commander/options/arguments.rb in rake-commander-0.2.0
- old
+ new
@@ -1,114 +1,226 @@
class RakeCommander
module Options
# Offers helpers to treat `ARGV`
module Arguments
- include RakeCommander::Options::Name
+ RAKE_COMMAND_EXTENDED_OPTIONS_START = '--'.freeze
+ NAME_ARGUMENT = /^--(?<option>[\w_-]*).*?$/.freeze
+ BOOLEAN_ARGUMENT = /(?:^|--)no-(?<option>[\w_-]*).*?$/.freeze
- # Options with arguments should not take another option as value.
- # `OptionParser` can do this even if the the argument is optional.
- # This method re-arranges the arguments based on options that receive parameters,
- # provided that an option is not taken as a value of a previous option that accepts arguments.
- # If an option with argument is missing the argument, but has a `default` value,
- # that `default` value will be inserted after the option in the array
- # to prevent the `OptionParser::MissingArgument` error to stop the parsing process.
- # @note
- # 1. Any word or letter with _hypen_ -`` or _double hypen_ `--` is interpreted as option(s)
- # 2. To overcome this limitation, you may enclose in double quotes and argument with
- # that start (i,e, `"--argument"`).
- # @example
- # 1. `-abc ARGUMENT` where only `c` receives the argument becomes `-ab -c ARGUMENT`
- # 3. `-abc ARGUMENT` where `b` and `c` are argument receivers becomes `-a -b nil -c ARGUMENT`
- # 2. `-acb ARGUMENT` where only `c` receives the argument becomes `-a -c nil -b ARGUMENT`
- # 4. `-c --some-option ARGUMENT` where both options receive argument, becomes `-c nil --some-option ARGUMENT`
- # 5. `-c --some-option -d ARGUMENT` where both options receive argument, becomes `-c nil --some-option nil -d ARGUMENT`
- # 6. `-cd ARGUMENT` where `c` default is `"yeah"`, becomes `-c yeah -d ARGUMENT`
- # @param argv [Array<String>]
- # @param options [Hash] the defined `RakeCommander::Option` to re-arrange `argv` with.
- # @return [Array<String>] the re-arranged `argv`
- def pre_parse_arguments(argv = ARGV, options:)
- pre_parsed = explicit_argument_options(argv, options)
- compact_short = ''
- pre_parsed.each_with_object([]) do |(opt_ref, args), out|
- next out.push(*args) unless opt_ref.is_a?(Symbol)
- is_short = opt_ref.to_s.length == 1
- next compact_short << opt_ref.to_s if is_short && args.empty?
- out.push("-#{compact_short}") unless compact_short.empty?
- compact_short = ''
- opt_str = is_short ? "-#{opt_ref}" : name_hyphen(opt_ref)
- out.push(opt_str, *args)
- end.tap do |out|
- out.push("-#{compact_short}") unless compact_short.empty?
+ class << self
+ def included(base)
+ super(base)
+ base.extend ClassMethods
end
end
- private
+ module ClassMethods
+ include RakeCommander::Options::Name
- # @example the output is actually a Hash, keyed by the Symbol of the option (short or name)
- # 1. `-abc ARGUMENT` where only `c` receives the argument becomes `:a :b :c ARGUMENT`
- # 3. `-abc ARGUMENT` where `b` and `c` are argument receivers becomes `:a :b nil :c ARGUMENT`
- # 2. `-acb ARGUMENT` where only `c` receives the argument becomes `:a :c nil :b ARGUMENT`
- # 4. `-c --some-option ARGUMENT` where both options receive argument, becomes `:c nil :some_option ARGUMENT`
- # 5. `-c --some-option -d ARGUMENT` where first two options receive argument, becomes `:c nil :some_option nil :d ARGUMENT`
- # 6. `-cd ARGUMENT` where `c` default is `"yeah"`, becomes `:c yeah :d ARGUMENT`
- # @return [Hash<Symbol, Array>]
- def explicit_argument_options(argv, options)
- decoupled = decluster_shorts_n_names_to_sym(argv)
- grouped = group_symbols_with_strings(decoupled)
- normalized = insert_missing_argument_to_groups(grouped, options)
- normalized.each_with_object({}) do |group, pre_parsed|
- opt_ref = group.first.is_a?(Symbol)? group.shift : nil
- pre_parsed[opt_ref] = group
+ # @note it assumes `ARGV` has been left unaltered.
+ # @return [Boolean] whether enhanced parsing should be switched on or off.
+ def argv_with_enhanced_syntax?(argv = ARGV)
+ return false unless argv.is_a?(Array)
+ argv.include?(RAKE_COMMAND_EXTENDED_OPTIONS_START)
end
- end
- # It adds the missing argument to options that expect it.
- # @note it uses `default` if present, and `nil` otherwise.
- # @param groups [@see #pair_symbols_with_strings]
- def insert_missing_argument_to_groups(groups, options)
- groups.each do |group|
- args = group.dup
- opt_ref = args.shift
- next unless args.empty?
- next unless opt_ref.is_a?(Symbol)
- next unless opt = options[opt_ref]
- next unless opt.argument?
- next group.push(opt.default) if opt.default?
- group.push(nil)
+ # Configuration setting
+ # Whether the additional arguments (extended options) managed by this gem
+ # should be removed/consumed from `ARGV` before `Rake` processes option arguments.
+ # @note
+ # 1. When `true` it **will enable**
+ # * A **patch** on `Rake::Application`**, provided that `ARGV` is cropped
+ # before `Rake` identifies **tasks** and rake native **options**.
+ # Note that this specific patch only works if rake commander was loaded
+ # BEFORE `Rake::Application#run` is invoked.
+ # 2. When `false`, an implicit `exit(0)` is added at the end of a rake task
+ # defined via `RakeCommander`, as a work-around that prevents `Rake` from
+ # chaining option arguments as if they were actual tasks.
+ # @note
+ # 1. This only refers to what comes after `RAKE_COMMAND_EXTENDED_OPTIONS_START` (`--`)
+ # @return [Boolean]
+ def argv_cropping_for_rake(value = :not_used)
+ @argv_cropping_for_rake = true if @argv_cropping_for_rake.nil?
+ return @argv_cropping_for_rake if value == :not_used
+ @argv_cropping_for_rake = !!value
end
- end
- # @return [Array<Array>] where the first element of each `Array` is a symbol
- # followed by one or more `String`.
- def group_symbols_with_strings(argv)
- [].tap do |out|
- curr_ary = nil
- argv.each do |arg|
- if arg.is_a?(Symbol)
- out << (curr_ary = [arg])
- else # must be `String`
- out << (curr_ary = []) unless curr_ary
- curr_ary << arg
+ # It returns the part of `ARGV` that are arguments of `RakeCommander::Options` parsing.
+ # @note please observe that `Rake` has it's own options. For this reason using
+ # a delimiter (`RAKE_COMMAND_EXTENDED_OPTIONS_START`) shows up to be necessary to
+ # create some sort of command line argument namespacing.
+ # @param argv [Array<String>] the command line arguments array.
+ # @return [Array<String>] the target arguments to be parsed by `RakeCommander::Options`
+ def argv_extended_options(argv = ARGV.dup)
+ if idx = argv.index(RAKE_COMMAND_EXTENDED_OPTIONS_START)
+ argv[idx+1..-1]
+ else
+ []
+ end
+ end
+
+ # It slices from the original `ARGV` the extended_options of this gem.
+ # @note this is necessary to prevent `Rake` to interpret them.
+ # @return [Array<String>] the argv without the extended options of this gem.
+ def argv_rake_native_arguments(argv = ARGV.dup)
+ return argv unless argv_cropping_for_rake
+ if idx = argv.index(RAKE_COMMAND_EXTENDED_OPTIONS_START)
+ argv = argv[0..idx]
+ end
+ argv
+ end
+
+ # **Re-open** `parse_options` method, provided that we slice `ARGV`
+ # to only include the extended options of this gem, which start at
+ # `RAKE_COMMAND_EXTENDED_OPTIONS_START`.
+ # @note
+ # 1. Without this `ARGV` cut, it will throw `OptionParser::InvalidOption` error
+ # - So some tidy up is necessary and the head of the command (i.e. `rake some:task --`)
+ # should be excluded from arguments to input to the options parser.
+ # @see `RakeCommander::Options#parse_options`
+ def parse_options(argv = ARGV, *args, **kargs, &block)
+ argv = argv_extended_options(argv)
+ argv = argv_pre_parsed(argv, options: options_hash(with_implicit: true))
+ super(argv, *args, **kargs, &block)
+ end
+
+ # Options with arguments should not take another option as value.
+ # `OptionParser` can do this even if the the argument is optional.
+ # This method re-arranges the arguments based on options that receive parameters,
+ # provided that an option is not taken as a value of a previous option that accepts arguments.
+ # If an option with argument is missing the argument, but has a `default` value,
+ # that `default` value will be inserted after the option in the array
+ # to prevent the `OptionParser::MissingArgument` error to stop the parsing process.
+ # @note
+ # 1. Any word or letter with _hypen_ -`` or _double hypen_ `--` is interpreted as option(s)
+ # 2. To overcome this limitation, you may enclose in double quotes and argument with
+ # that start (i,e, `"--argument"`).
+ # @example
+ # 1. `-abc ARGUMENT` where only `c` receives the argument becomes `-ab -c ARGUMENT`
+ # 3. `-abc ARGUMENT` where `b` and `c` are argument receivers becomes `-a -b nil -c ARGUMENT`
+ # 2. `-acb ARGUMENT` where only `c` receives the argument becomes `-a -c nil -b ARGUMENT`
+ # 4. `-c --some-option ARGUMENT` where both options receive argument, becomes `-c nil --some-option ARGUMENT`
+ # 5. `-c --some-option -d ARGUMENT` where both options receive argument, becomes `-c nil --some-option nil -d ARGUMENT`
+ # 6. `-cd ARGUMENT` where `c` default is `"yeah"`, becomes `-c yeah -d ARGUMENT`
+ # @param argv [Array<String>]
+ # @param options [Hash] the defined `RakeCommander::Option` to re-arrange `argv` with.
+ # @return [Array<String>] the re-arranged `argv`
+ def argv_pre_parsed(argv = ARGV, options:)
+ pre_parsed = explicit_argument_options(argv, options)
+ compact_short = ''
+ pre_parsed.each_with_object([]) do |(opt_ref, args), out|
+ next out.push(*args) unless opt_ref.is_a?(Symbol)
+ is_short = opt_ref.to_s.length == 1
+ next compact_short << opt_ref.to_s if is_short && args.empty?
+ out.push("-#{compact_short}") unless compact_short.empty?
+ compact_short = ''
+ opt_str = is_short ? "-#{opt_ref}" : name_hyphen(opt_ref)
+ out.push(opt_str, *args)
+ end.tap do |out|
+ out.push("-#{compact_short}") unless compact_short.empty?
+ end
+ end
+
+ protected
+
+ # It wraps the `task_method` to check if the patch to crop `ARGV` is active.
+ # If it's not active it will call `exit(0)` at the end of the task run, to prevent
+ # `Rake` from interpreting option arguments as rake tasks.
+ # @note **reopens** `RakeCommander::RakeTask` method
+ # * If `argv_cropping_for_rake` is `false` it calls `exit(0)` right at the end of the task.
+ # * This relates on whether the patch to `Rake::Application` has been applied.
+ # @return [Proc] the wrapped block.
+ def task_context(&task_method)
+ proc do |*task_args|
+ super(&task_method).call(*task_args)
+ exit(0) unless argv_cropping_for_rake
+ end
+ end
+
+ private
+
+ # @example the output is actually a Hash, keyed by the Symbol of the option (short or name)
+ # 1. `-abc ARGUMENT` where only `c` receives the argument becomes `:a :b :c ARGUMENT`
+ # 3. `-abc ARGUMENT` where `b` and `c` are argument receivers becomes `:a :b nil :c ARGUMENT`
+ # 2. `-acb ARGUMENT` where only `c` receives the argument becomes `:a :c nil :b ARGUMENT`
+ # 4. `-c --some-option ARGUMENT` where both options receive argument, becomes `:c nil :some_option ARGUMENT`
+ # 5. `-c --some-option -d ARGUMENT` where first two options receive argument, becomes `:c nil :some_option nil :d ARGUMENT`
+ # 6. `-cd ARGUMENT` where `c` default is `"yeah"`, becomes `:c yeah :d ARGUMENT`
+ # @return [Hash<Symbol, Array>]
+ def explicit_argument_options(argv, options)
+ decoupled = decluster_shorts_n_names_to_sym(argv)
+ grouped = group_symbols_with_strings(decoupled)
+ normalized = insert_missing_argument_to_groups(grouped, options)
+ normalized.each_with_object({}) do |group, pre_parsed|
+ opt_ref = group.first.is_a?(Symbol)? group.shift : nil
+ pre_parsed[opt_ref] = group
+ end
+ end
+
+ # It ADDS the missing argument to options that expect it.
+ # @note
+ # 1. It uses `default` if present
+ # 2. Otherwise it uses `nil`, but only if required (not when optional).
+ # @param groups [@see #pair_symbols_with_strings]
+ def insert_missing_argument_to_groups(groups, options)
+ groups.each do |group|
+ args = group.dup
+ opt_ref = args.shift
+ next unless args.empty?
+ next unless opt_ref.is_a?(Symbol)
+ next unless opt = _retrieve_option_ref(opt_ref, options)
+ next unless opt.argument?
+ next group.push(opt.default) if opt.default?
+ next unless opt.argument_required?
+ group.push(nil)
+ end
+ end
+
+ # Retrieve the option based on `ref`
+ # @note It might be `--no-option-name`
+ # @return [RakeCommander::Option, NilClass]
+ def _retrieve_option_ref(opt_ref, options)
+ opt = options[opt_ref]
+ return opt if opt
+ return nil unless match = opt_ref.to_s.match(BOOLEAN_ARGUMENT)
+ return nil unless opt_ref = match[:option]
+ return nil unless opt = options[opt_ref.to_sym]
+ return nil unless opt.boolean_name?
+ opt
+ end
+
+ # @return [Array<Array>] where the first element of each `Array` is a symbol
+ # followed by one or more `String`.
+ def group_symbols_with_strings(argv)
+ [].tap do |out|
+ curr_ary = nil
+ argv.each do |arg|
+ if arg.is_a?(Symbol)
+ out << (curr_ary = [arg])
+ else # must be `String`
+ out << (curr_ary = []) unless curr_ary
+ curr_ary << arg
+ end
end
end
end
- end
- # It splits `argv` compacted shorts into their `Symbol` version.
- # Symbolizes option `names` (long version).
- # @return [Array<String, Symbol>] where symbols are options and strings arguments.
- def decluster_shorts_n_names_to_sym(argv)
- argv.each_with_object([]) do |arg, out|
- if single_hyphen?(arg) # short option(s)
- options = arg.match(SINGLE_HYPHEN_REGEX)[:options]
- options.split('').each do |short|
- out << short_sym(short)
+ # It splits `argv` compacted shorts into their `Symbol` version.
+ # Symbolizes option `names` (long version).
+ # @return [Array<String, Symbol>] where symbols are options and strings arguments.
+ def decluster_shorts_n_names_to_sym(argv)
+ argv.each_with_object([]) do |arg, out|
+ if single_hyphen?(arg) # short option(s)
+ options = arg.match(SINGLE_HYPHEN_REGEX)[:options]
+ options.split('').each do |short|
+ out << short_sym(short)
+ end
+ elsif double_hyphen?(arg) # name option
+ name = arg.match(NAME_ARGUMENT)[:option]
+ out << name_sym(name)
+ else # argument
+ out << arg
end
- elsif double_hyphen?(arg) # name option
- name = arg.match(DOUBLE_HYPHEN_REGEX)[:option]
- out << name_sym(name)
- else # argument
- out << arg
end
end
end
end
end