Skip to content

Commit

Permalink
Add long option parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
piotrmurach committed Mar 29, 2020
1 parent 9912971 commit 9d9aded
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 3 deletions.
3 changes: 3 additions & 0 deletions lib/tty/option.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ module TTY
module Option
Error = Class.new(StandardError)

# Raised when an option matches more than one parameter option
AmbiguousOption = Class.new(Error)

# Raised when overriding already defined conversion
ConversionAlreadyDefined = Class.new(Error)

Expand Down
50 changes: 49 additions & 1 deletion lib/tty/option/parser/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ module TTY
module Option
class Parser
class Options
LONG_OPTION_RE = /^(--[^=]+)(\s+|=)?(.*)?$/.freeze

SHORT_OPTION_RE = /^(-.)(.*)$/.freeze

# Create a command line env variables parser
#
# @param [Array<Option>] options
Expand All @@ -21,6 +25,7 @@ def initialize(options, **config)
@errors = {}
@remaining = []
@shorts = {}
@longs = {}

setup_opts
end
Expand All @@ -31,6 +36,7 @@ def initialize(options, **config)
def setup_opts
@options.each do |opt|
@shorts[opt.short_name] = opt
@longs[opt.long_name] = opt

if opt.default?
case opt.default
Expand Down Expand Up @@ -81,7 +87,49 @@ def next_option

argument = @argv.shift

if (matched = argument.match(/^(-.)(.*)$/))
if (matched = argument.match(LONG_OPTION_RE))
long, _sep, rest = matched[1..-1]

if (opt = @longs[long])
if opt.argument_required?
if !rest.empty?
value = rest
elsif !@argv.empty?
value = @argv.shift
else
record_error(MissingArgument,
"option #{long} requires an argument",
opt)
end
elsif opt.argument_optional?
if !rest.empty?
value = rest
elsif !@argv.empty?
value = @argv.shift
end
else # boolean flag
value = true
end
else
# option stuck together with an argument or abbreviated
matching_options = 0
@longs.each_key do |key|
if key.to_s.start_with?(long) ||
long.to_s.start_with?(key)
opt = @longs[key]
matching_options += 1
end
end

if matching_options.zero?
record_error(InvalidOption, "invalid option #{long}")
elsif matching_options == 1
value = long[opt.long_name.size..-1]
else
record_error(AmbiguousOption, "option #{long} is ambiguous")
end
end
elsif (matched = argument.match(SHORT_OPTION_RE))
short, other_singles = *matched[1..-1]

if (opt = @shorts[short])
Expand Down
116 changes: 114 additions & 2 deletions spec/unit/parser/options_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,111 @@ def parse(argv, options, **config)
expect(rest).to eq(%w[arg1 arg2 arg3])
end

it "parses long flag" do
params, = parse(%w[--foo], option(:foo, long: "--foo"))

expect(params[:foo]).to eq(true)
end

it "parses long option with a separate argument defined separate" do
params, = parse(%w[--foo bar], option(:foo, long: "--foo string"))

expect(params[:foo]).to eq("bar")
end

it "parses long option separted with = from required argument" do
params, = parse(%w[--foo=bar], option(:foo, long: "--foo string"))

expect(params[:foo]).to eq("bar")
end

it "parses long option separted with = from argument and defined with =" do
params, = parse(%w[--foo=bar], option(:foo, long: "--foo=string"))

expect(params[:foo]).to eq("bar")
end

it "parses long option with multiple arguments as a single value" do
params, = parse(%w[--foo bar\ baz], option(:foo, long: "--foo string"))

expect(params[:foo]).to eq("bar baz")
end

it "raises if long option with no argument" do
expect {
parse(%w[--foo], option(:foo, long: "--foo string"))
}.to raise_error(TTY::Option::MissingArgument,
"option --foo requires an argument")
end

it "parses long option with an optional argument defined together" do
params, = parse(%w[--foo], option(:foo, long: "--foo[string]"))

expect(params[:foo]).to eq(nil)
end

it "parses long option with an optional argument defined separate" do
params, = parse(%w[--foo], option(:foo, long: "--foo [string]"))

expect(params[:foo]).to eq(nil)
end

it "parses long option with an argument when an optional arg defined separate" do
params, = parse(%w[--foo bar], option(:foo, long: "--foo [string]"))

expect(params[:foo]).to eq("bar")
end

it "parses long option with an argument when an optional arg defined together" do
params, = parse(%w[--foo bar], option(:foo, long: "--foo[string]"))

expect(params[:foo]).to eq("bar")
end

it "parses long option with a glued argument when an optional arg defined separate" do
params, = parse(%w[--foobar], option(:foo, long: "--foo [string]"))

expect(params[:foo]).to eq("bar")
end

it "raises if long option isn't defined" do
expect {
parse(%w[--foo --bar], option(:foo, long: "--foo"))
}.to raise_error(TTY::Option::InvalidOption, "invalid option --bar")
end

it "raises if long option isn't defined" do
options = []
options << option(:foo, long: "--foobar")
options << option(:foo, long: "--foobaz")

expect {
parse(%w[--foob ], options)
}.to raise_error(TTY::Option::AmbiguousOption, "option --foob is ambiguous")
end

it "consumes non-option arguments" do
options = []
options << option(:foo, long: "--foo string")
options << option(:bar, short: "-b")

params, rest = parse(%w[arg1 --foo baz arg2 --bar arg3 -b], options)

expect(params[:foo]).to eq("baz")
expect(params[:bar]).to eq(true)
expect(rest).to eq(%w[arg1 arg2 arg3])
end

it "parses option-like values and ignores arguments looking like options" do
options = []
options << option(:foo, short: "-f", long: "--foo string")

params, rest = parse(%w[some---arg --foo --some--weird---value], options)

expect(params[:foo]).to eq("--some--weird---value")
expect(rest).to eq(%w[some---arg])
end

context "when no arguments" do
it "defines no flags and returns empty hash" do
params, rest = parse([], [])
Expand Down Expand Up @@ -184,9 +289,16 @@ def parse(argv, options, **config)
end

it "parses short flag with required argument and keeps the last argument" do
opts, rest = parse(%w[-f 1 -f 2 -f 3], option(:foo, short: "-f int"))
params, rest = parse(%w[-f 1 -f 2 -f 3], option(:foo, short: "-f int"))

expect(params[:foo]).to eq("3")
expect(rest).to eq([])
end

it "parses long flag with required argument and keeps the last argument" do
params, rest = parse(%w[--f 1 --f 2 --f 3], option(:foo, long: "--f int"))

expect(opts[:foo]).to eq("3")
expect(params[:foo]).to eq("3")
expect(rest).to eq([])
end
end
Expand Down

0 comments on commit 9d9aded

Please sign in to comment.