diff options
-rw-r--r-- | .github/workflows/ci.yml | 4 | ||||
-rw-r--r-- | CHANGELOG.md | 17 | ||||
-rw-r--r-- | README.md | 37 | ||||
-rw-r--r-- | lib/slop.rb | 2 | ||||
-rw-r--r-- | lib/slop/error.rb | 12 | ||||
-rw-r--r-- | lib/slop/option.rb | 20 | ||||
-rwxr-xr-x | lib/slop/options.rb | 4 | ||||
-rw-r--r-- | lib/slop/parser.rb | 2 | ||||
-rw-r--r-- | lib/slop/types.rb | 25 | ||||
-rw-r--r-- | test/error_test.rb | 15 | ||||
-rw-r--r-- | test/parser_test.rb | 27 | ||||
-rw-r--r-- | test/types_test.rb | 32 |
12 files changed, 173 insertions, 24 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9bac1d4..5b69200 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,10 +22,10 @@ jobs: strategy: fail-fast: false matrix: - ruby: [ "2.0", "2.1", "2.2", "2.3", "2.4", "2.5", "2.6", "2.7", "3.0", "3.1", head, jruby, truffleruby ] + ruby: [ "2.0", "2.1", "2.2", "2.3", "2.4", "2.5", "2.6", "2.7", "3.0", "3.1", "3.2", head, jruby, truffleruby ] name: Ruby ${{ matrix.ruby }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index fdd1e96..9af3f7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,23 @@ Changelog ========= +v4.10.0 (2023-02-15) + +Features: + * Add support for optional type validation [#278](https://github.com/leejarvis/slop/pull/278) (Victor Gama) + +v4.9.3 (2022-09-30) +------------------- + +Bug fixes: + * Fix explicitly false boolean options and allow for additional false arguments [#276](https://github.com/leejarvis/slop/pull/276) (Eugene Otto) + +v4.9.2 (2022-03-26) +------------------- + +Bug fixes: + * Handle flag arguments that contain equals character [#275](https://github.com/leejarvis/slop/pull/275) (ConnorWGarvey) + v4.9.1 (2021-05-28) ------------------- @@ -22,17 +22,19 @@ opts = Slop.parse do |o| o.bool '-v', '--verbose', 'enable verbose mode' o.bool '-q', '--quiet', 'suppress output (quiet mode)' o.bool '-c', '--check-ssl-certificate', 'check SSL certificate for host' + o.bool '-k', '--use-keychain', 'store passphrase in OS keychain' o.on '--version', 'print the version' do puts Slop::VERSION exit end end -ARGV #=> -v --login alice --host 192.168.0.1 -m post --check-ssl-certificate +ARGV #=> -v --login alice --host 192.168.0.1 -m post --check-ssl-certificate --use-keychain false opts[:host] #=> 192.168.0.1 opts[:login] #=> alice opts[:method] #=> :post +opts[:use_keychain] #=> false opts.verbose? #=> true opts.quiet? #=> false opts.check_ssl_certificate? #=> true @@ -53,7 +55,7 @@ Built in Option types are as follows: ```ruby o.string #=> Slop::StringOption, expects an argument -o.bool #=> Slop::BoolOption, no argument, aliased to BooleanOption +o.bool #=> Slop::BoolOption, argument optional, aliased to BooleanOption o.integer #=> Slop::IntegerOption, expects an argument, aliased to IntOption o.float #=> Slop::FloatOption, expects an argument o.array #=> Slop::ArrayOption, expects an argument @@ -202,6 +204,8 @@ Slop will raise errors for the following: * An option used without an argument when it expects one: `Slop::MissingArgument` * An option used that Slop doesn't know about: `Slop::UnknownOption` * An option marked as `required` when not provided: `Slop::MissingRequiredOption` +* An option marked as `validate_types`, with an argument that does not match its +type (i.e. `bla` for `integer`): `Slop::InvalidOptionValue` These errors inherit from `Slop::Error`, so you can rescue them all. Alternatively you can suppress these errors with the `suppress_errors` config @@ -220,6 +224,33 @@ opts = Slop.parse do end ``` +Validating Types +---------------- + +By default, Slop does not validate whether an argument is a valid value for a +given option; instead, if the option has a default value, it will be used over +the invalid argument provided. +In order to have types (such as `integer` and `float`) validate and indicate +that the provided value is invalid, an extra option can be either provided to +the argument itself, or its option set: + +```ruby +opts = Slop::Options.new +opts.int "-p", "--port", "a port", default: 80, validate_types: true + +parser = Slop::Parser.new(opts) +result = parser.parse(["--port", "bla"]) +# invalid value for -p, --port (Slop::InvalidOptionValue) + +# Or to the option set... +opts = Slop::Options.new(validate_types: true) +opts.int "-p", "--port", "a port", default: 80 + +parser = Slop::Parser.new(opts) +result = parser.parse(["--port", "bla"]) +# invalid value for -p, --port (Slop::InvalidOptionValue) +``` + Printing help ------------- @@ -277,4 +308,4 @@ end Commands -------- -Slop not longer has built in support for git-style subcommands. +Slop no longer has built in support for git-style subcommands. diff --git a/lib/slop.rb b/lib/slop.rb index 35da9cd..7b5ef54 100644 --- a/lib/slop.rb +++ b/lib/slop.rb @@ -6,7 +6,7 @@ require 'slop/types' require 'slop/error' module Slop - VERSION = '4.9.1' + VERSION = '4.10.0' # Parse an array of options (defaults to ARGV). Accepts an # optional hash of configuration options and block. diff --git a/lib/slop/error.rb b/lib/slop/error.rb index ddfa1a7..b83cab3 100644 --- a/lib/slop/error.rb +++ b/lib/slop/error.rb @@ -38,4 +38,16 @@ module Slop # Suppress with the `suppress_errors` config option. class MissingRequiredOption < Error end + + # Raised when a given option is provided by the user and does not + # match the expected format for that type. This is only raised if + # validate_types is set to true. + class InvalidOptionValue < Error + attr_reader :flag + + def initialize(msg, flag) + super(msg) + @flag = flag + end + end end diff --git a/lib/slop/option.rb b/lib/slop/option.rb index 093b37f..655b283 100644 --- a/lib/slop/option.rb +++ b/lib/slop/option.rb @@ -56,7 +56,11 @@ module Slop raise Slop::MissingArgument.new("missing argument for #{flag}", flags) end else - @value = call(value) + if validate_type? && !valid?(value) && !suppress_errors? + raise Slop::InvalidOptionValue.new("invalid value for #{flag}", flags) + end + + @value = valid?(value) && call(value) end block.call(@value) if block.respond_to?(:call) @@ -107,6 +111,13 @@ module Slop config[:required] end + # Returns true if an exception should be raised when this option value can't + # be parsed into the desired type or does not conform to the expected type's + # format + def validate_type? + config[:validate_type] || config[:validate_types] + end + # Returns all flags joined by a comma. Used by the help string. def flag flags.join(", ") @@ -119,6 +130,13 @@ module Slop key.to_sym end + # Override this if you want to provide a custom validator for a type. This + # method must return whether the provided value is valid for the current + # argument's type + def valid?(value) + true + end + # Returns true if this option should be displayed with dashes transformed into underscores. def underscore_flags? config[:underscore_flags] diff --git a/lib/slop/options.rb b/lib/slop/options.rb index 17e9501..5e71eac 100755 --- a/lib/slop/options.rb +++ b/lib/slop/options.rb @@ -7,6 +7,7 @@ module Slop type: "null", banner: true, underscore_flags: true, + validate_types: false, } # The Array of Option instances we've created. @@ -24,6 +25,9 @@ module Slop # The String banner prefixed to the help string. attr_accessor :banner + # Whether we should validate types of values provided by the user + attr_accessor :validate_types + def initialize(**config, &block) @options = [] @separators = [] diff --git a/lib/slop/parser.rb b/lib/slop/parser.rb index 49d0230..3c2c9c6 100644 --- a/lib/slop/parser.rb +++ b/lib/slop/parser.rb @@ -52,7 +52,7 @@ module Slop # support `foo=bar` orig_flag = flag.dup - if match = flag.match(/([^=]+)=([^=]*)/) + if match = flag.match(/([^=]+)=(.*)/) flag, arg = match.captures end diff --git a/lib/slop/types.rb b/lib/slop/types.rb index 4ccd42e..a08537d 100644 --- a/lib/slop/types.rb +++ b/lib/slop/types.rb @@ -21,6 +21,15 @@ module Slop class BoolOption < Option attr_accessor :explicit_value + FALSE_VALUES = [false, 'false', 'no', 'off', '0'].freeze + TRUE_VALUES = [true, 'true', 'yes', 'on', '1'].freeze + VALID_VALUES = (FALSE_VALUES + TRUE_VALUES).freeze + + def valid?(value) + return true if value.is_a?(String) && value.start_with?("--") + value.nil? || VALID_VALUES.include?(value) + end + def call(value) self.explicit_value = value !force_false? @@ -35,7 +44,7 @@ module Slop end def force_false? - explicit_value == false + FALSE_VALUES.include?(explicit_value) end def default_value @@ -50,8 +59,14 @@ module Slop # Cast the option argument to an Integer. class IntegerOption < Option + INT_STRING_REGEXP = /\A[+-]?\d+\z/.freeze + + def valid?(value) + value =~ INT_STRING_REGEXP + end + def call(value) - value =~ /\A[+-]?\d+\z/ && value.to_i + value.to_i end end IntOption = IntegerOption @@ -60,8 +75,12 @@ module Slop class FloatOption < Option FLOAT_STRING_REGEXP = /\A[+-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+-]?\d+)?\z/.freeze + def valid?(value) + value =~ FLOAT_STRING_REGEXP + end + def call(value) - value =~ FLOAT_STRING_REGEXP && value.to_f + value.to_f end end diff --git a/test/error_test.rb b/test/error_test.rb index d9c71ab..8ea9069 100644 --- a/test/error_test.rb +++ b/test/error_test.rb @@ -63,3 +63,18 @@ describe Slop::MissingRequiredOption do opts.parse [] end end + +describe Slop::InvalidOptionValue do + it "raises when an option has an invalid value" do + opts = Slop::Options.new(validate_types: true) + opts.integer "-n", "--number", default: 10 + assert_raises(Slop::InvalidOptionValue) { opts.parse %w(-n foo) } + end + + it "does not raise when errors are suppressed" do + opts = Slop::Options.new(suppress_errors: true) + opts.integer "-n", "--number", default: 10, validate_type: true + r = opts.parse %w(-n foo) + assert_equal(10, r[:n]) + end +end diff --git a/test/parser_test.rb b/test/parser_test.rb index b729ee8..bbdc4a9 100644 --- a/test/parser_test.rb +++ b/test/parser_test.rb @@ -17,16 +17,25 @@ describe Slop::Parser do assert_equal ["-v", "--name", "lee"], @parser.arguments end - it "parses flag=argument" do - @options.integer "-p", "--port" - @result.parser.parse %w(--name=bob -p=123) - assert_equal "bob", @result[:name] - assert_equal 123, @result[:port] + describe "for flag=argument" do + it "parses names and values" do + @options.integer "-p", "--port" + @result.parser.parse %w(--name=bob -p=123) + assert_equal "bob", @result[:name] + assert_equal 123, @result[:port] + end - @options.string "--foo" - @result.parser.parse %w(--foo = =) - assert_equal "=", @result[:foo] - assert_equal %w(=), @result.args + it "treats an = separated by a space as a value" do + @options.string "--foo" + @result.parser.parse %w(--foo = =) + assert_equal "=", @result[:foo] + assert_equal %w(=), @result.args + end + + it "includes = in strings" do + @result.parser.parse(%w(--name=b=b)) + assert_equal "b=b", @result[:name] + end end it "parses flag=''" do diff --git a/test/types_test.rb b/test/types_test.rb index a45302a..c6f0864 100644 --- a/test/types_test.rb +++ b/test/types_test.rb @@ -31,12 +31,14 @@ end describe Slop::BoolOption do before do @options = Slop::Options.new - @verbose = @options.bool "--verbose" + @verbose = @options.bool "--verbose", validate_type: true @quiet = @options.bool "--quiet" @inversed = @options.bool "--inversed", default: true + @explicit = @options.bool "--explicit", validate_type: true @bloc = @options.bool("--bloc"){|val| (@bloc_val ||= []) << val} @result = @options.parse %w(--verbose --no-inversed - --bloc --no-bloc) + --bloc --no-bloc + --explicit=false) end it "returns true if used" do @@ -54,13 +56,23 @@ describe Slop::BoolOption do it "will invert the value passed to &block via --no- prefix" do assert_equal [true, false], @bloc_val end + + it "returns false when explicitly false" do + assert_equal false, @result[:explicit] + end + + it "raises with invalid types" do + assert_raises(Slop::InvalidOptionValue) do + @result.parser.parse %w(--verbose foo) + end + end end describe Slop::IntegerOption do before do @options = Slop::Options.new @age = @options.integer "--age" - @minus = @options.integer "--minus" + @minus = @options.integer "--minus", validate_type: true @plus = @options.integer "--plus" @result = @options.parse %w(--age 20 --minus -10 --plus +30) end @@ -75,6 +87,12 @@ describe Slop::IntegerOption do @result.parser.parse %w(--age hello) assert_nil @result[:age] end + + it "raises with invalid types" do + assert_raises(Slop::InvalidOptionValue) do + @result.parser.parse %w(--minus foo) + end + end end describe Slop::FloatOption do @@ -82,7 +100,7 @@ describe Slop::FloatOption do @options = Slop::Options.new @apr = @options.float "--apr" @apr_value = 2.9 - @minus = @options.float "--minus" + @minus = @options.float "--minus", validate_type: true @plus = @options.float "--plus" @scientific_notation = @options.float "--scientific-notation" @scientific_notation_value = 4e21 @@ -119,6 +137,12 @@ describe Slop::FloatOption do @result.parser.parse %w(--apr hello) assert_nil @result[:apr] end + + it "raises with invalid types" do + assert_raises(Slop::InvalidOptionValue) do + @result.parser.parse %w(--minus foo) + end + end end describe Slop::ArrayOption do |