diff --git a/lib/getoptlong.rb b/lib/getoptlong.rb index d681a42..aaac5cd 100644 --- a/lib/getoptlong.rb +++ b/lib/getoptlong.rb @@ -395,6 +395,8 @@ class NeedlessArgument < Error; end class MissingArgument < Error; end class InvalidOption < Error; end + attr_accessor :argv, :env + # # Returns a new \GetoptLong object based on the given +arguments+. # See {Options}[#class-GetoptLong-label-Options]. @@ -409,11 +411,11 @@ class InvalidOption < Error; end # - Any option name or alias is not a string. # - Any option type is invalid. # - def initialize(*arguments) + def initialize(*arguments, argv: ARGV, env: ENV) # # Current ordering. # - if ENV.include?('POSIXLY_CORRECT') + if env.include?('POSIXLY_CORRECT') @ordering = REQUIRE_ORDER else @ordering = PERMUTE @@ -467,6 +469,9 @@ def initialize(*arguments) if 0 < arguments.length set_options(*arguments) end + + self.argv = argv + self.env = env end # Sets the ordering; see {Ordering}[#class-GetoptLong-label-Ordering]; @@ -502,7 +507,7 @@ def ordering=(ordering) if !ORDERINGS.include?(ordering) raise ArgumentError, "invalid ordering `#{ordering}'" end - if ordering == PERMUTE && ENV.include?('POSIXLY_CORRECT') + if ordering == PERMUTE && env.include?('POSIXLY_CORRECT') @ordering = REQUIRE_ORDER else @ordering = ordering @@ -614,9 +619,7 @@ def terminate raise RuntimeError, "an error has occurred" if @error != nil @status = STATUS_TERMINATED - @non_option_arguments.reverse_each do |argument| - ARGV.unshift(argument) - end + argv.unshift(*@non_option_arguments) @canonical_names = nil @argument_flags = nil @@ -690,26 +693,26 @@ def get # if 0 < @rest_singles.length argument = '-' + @rest_singles - elsif (ARGV.length == 0) + elsif argv.empty? terminate return nil elsif @ordering == PERMUTE - while 0 < ARGV.length && ARGV[0] !~ /\A-./ - @non_option_arguments.push(ARGV.shift) + while !(argv.empty? || /\A-./.match?(argv.first)) + @non_option_arguments.push(argv.shift) end - if ARGV.length == 0 + if argv.empty? terminate return nil end - argument = ARGV.shift + argument = argv.shift elsif @ordering == REQUIRE_ORDER - if (ARGV[0] !~ /\A-./) + unless /\A-./.match?(argv.first) terminate return nil end - argument = ARGV.shift + argument = argv.shift else - argument = ARGV.shift + argument = argv.shift end # @@ -756,8 +759,8 @@ def get if @argument_flags[option_name] == REQUIRED_ARGUMENT if argument =~ /=(.*)/m option_argument = $1 - elsif 0 < ARGV.length - option_argument = ARGV.shift + elsif !argv.empty? + option_argument = argv.shift else set_error(MissingArgument, "option `#{argument}' requires an argument") @@ -765,8 +768,8 @@ def get elsif @argument_flags[option_name] == OPTIONAL_ARGUMENT if argument =~ /=(.*)/m option_argument = $1 - elsif 0 < ARGV.length && ARGV[0] !~ /\A-./ - option_argument = ARGV.shift + elsif !(argv.empty? || /\A-./.match?(argv.first)) + option_argument = argv.shift else option_argument = '' end @@ -792,8 +795,8 @@ def get if 0 < @rest_singles.length option_argument = @rest_singles @rest_singles = '' - elsif 0 < ARGV.length - option_argument = ARGV.shift + elsif !argv.empty? + option_argument = argv.shift else # 1003.2 specifies the format of this message. set_error(MissingArgument, "option requires an argument -- #{ch}") @@ -802,8 +805,8 @@ def get if 0 < @rest_singles.length option_argument = @rest_singles @rest_singles = '' - elsif 0 < ARGV.length && ARGV[0] !~ /\A-./ - option_argument = ARGV.shift + elsif !(argv.empty? || /\A-./.match?(argv.first)) + option_argument = argv.shift else option_argument = '' end diff --git a/test/test_getoptlong.rb b/test/test_getoptlong.rb index 0cd370b..5828585 100644 --- a/test/test_getoptlong.rb +++ b/test/test_getoptlong.rb @@ -3,27 +3,31 @@ class TestGetoptLong < Test::Unit::TestCase - def verify(test_argv, expected_remaining_argv, expected_options) - # Save ARGV and replace it with a test ARGV. - argv_saved = ARGV.dup - ARGV.replace(test_argv) - # Define options. - opts = GetoptLong.new( + def getoptlong_new(argv: [], env: nil, options: nil) + options ||= [ ['--xxx', '-x', '--aaa', '-a', GetoptLong::REQUIRED_ARGUMENT], ['--yyy', '-y', '--bbb', '-b', GetoptLong::OPTIONAL_ARGUMENT], ['--zzz', '-z', '--ccc', '-c', GetoptLong::NO_ARGUMENT] - ) - opts.quiet = true + ] + env ||= ENV + + GetoptLong.new(*options, argv:, env:) + .tap { |opts| opts.quiet = true } + end + + def verify(test_argv, expected_remaining_argv, expected_options, env = nil) + # Define options. + opts = getoptlong_new(argv: test_argv, env:) + yield opts if block_given? + # Gather options. actual_options = [] opts.each do |opt, arg| actual_options << "#{opt}: #{arg}" end - # Save remaining test ARGV and restore original ARGV. - actual_remaining_argv = ARGV.dup - ARGV.replace(argv_saved) + # Assert. - assert_equal(expected_remaining_argv, actual_remaining_argv, 'ARGV') + assert_equal(expected_remaining_argv, test_argv, 'ARGV') assert_equal(expected_options, actual_options, 'Options') end @@ -46,6 +50,18 @@ def test_required_argument end end + def test_required_argument_assign + expected_options = [ + '--xxx: arg' + ] + expected_argv = %w[foo bar] + options = %w[--xxx --xx --x --aaa --aa --a] + options.each do |option| + argv = ['foo', "#{option}=arg", 'bar'] + verify(argv, expected_argv, expected_options) + end + end + def test_required_argument_missing options = %w[--xxx --xx --x -x --aaa --aa --a -a] options.each do |option| @@ -69,6 +85,18 @@ def test_optional_argument end end + def test_optional_argument_assign + expected_options = [ + '--yyy: arg' + ] + expected_argv = %w[foo bar] + options = %w[--yyy --y --y --bbb --bb --b] + options.each do |option| + argv = ['foo', 'bar', "#{option}=arg"] + verify(argv, expected_argv, expected_options) + end + end + def test_optional_argument_missing expected_options = [ '--yyy: ' @@ -160,4 +188,120 @@ def test_new_with_invalid_flag end end + def test_raise_ambiguous_option + e = assert_raise(GetoptLong::AmbiguousOption) do + options = [ + ['--xxx', GetoptLong::REQUIRED_ARGUMENT], + ['--xxy', GetoptLong::NO_ARGUMENT] + ] + getoptlong_new(argv: ['--xx'], options:).each { nil } + end + assert_match(/ambiguous/, e.message) + end + + def test_option_prefix + assert_nothing_raised do + options = [ + ['--xxx', GetoptLong::NO_ARGUMENT], + ['--xxx-y', GetoptLong::NO_ARGUMENT], + ['--xx', GetoptLong::NO_ARGUMENT] + ] + getoptlong_new(argv: ['--xx', '--xxx'], options:).each { nil } + end + end + + def test_raise_needless_argument + e = assert_raise(GetoptLong::NeedlessArgument) do + options = [['--x', GetoptLong::NO_ARGUMENT]] + getoptlong_new(argv: ['--x=z'], options:).each { nil } + end + assert_match(/doesn't allow an argument/, e.message) + end + + def test_raise_too_many_flags + e = assert_raise(ArgumentError) do + options = [ + ['-y', GetoptLong::REQUIRED_ARGUMENT, GetoptLong::NO_ARGUMENT] + ] + getoptlong_new(options:) + end + assert_match(/too many/, e.message) + end + + def test_raise_option_redefined + e = assert_raise(ArgumentError) do + options = [ + ['--xxx', '-x', GetoptLong::REQUIRED_ARGUMENT], + ['--exclude', '-x', GetoptLong::NO_ARGUMENT] + ] + getoptlong_new(options:) + end + assert_match(/redefined/, e.message) + end + + def test_set_ordering_raise + e = assert_raise(ArgumentError) do + getoptlong_new.ordering = 42 + end + assert_match(/invalid ordering/, e.message) + end + + def test_raise_unrecognized_option + e = assert_raise(GetoptLong::InvalidOption) do + getoptlong_new(argv: ['--asdf']).each { nil } + end + assert_match(/unrecognized option/, e.message) + end + + def test_ordering + GetoptLong::ORDERINGS.each do |order| + argv = ['foo', '--xxx', 'arg', 'bar'] + + expected_options, expected_argv = + case order + when GetoptLong::REQUIRE_ORDER + [[], %w[foo --xxx arg bar]] + when GetoptLong::PERMUTE + [['--xxx: arg'], %w[foo bar]] + when GetoptLong::RETURN_IN_ORDER + [[': foo', '--xxx: arg', ': bar'], []] + end + + verify(argv, expected_argv, expected_options) do |opts| + opts.ordering = order + end + end + end + + def test_env_posixly_correct + [{}, { 'POSIXLY_CORRECT' => '1' }].each do |order| + argv = ['foo', '--xxx', 'arg', 'bar'] + + expected_options, expected_argv = + case order + when {} + [['--xxx: arg'], %w[foo bar]] + else + [[], %w[foo --xxx arg bar]] + end + + verify(argv, expected_argv, expected_options, order) + end + end + + def test_raise_invalid_option_with_single_hyphen + options = [ + ['-x', GetoptLong::NO_ARGUMENT], + ['--x', GetoptLong::NO_ARGUMENT] + ] + argvs = [%w[-x-x foo], %w[-x- foo]] + + argvs.each do |argv| + e = assert_raise(GetoptLong::InvalidOption) do + getoptlong_new(argv:, options:).each { nil } + end + assert_match(/invalid option/, e.message) + end + end + end