Skip to content

Commit 7d6fc2a

Browse files
authored
Support short option bundling in OPtionParser (#16563)
This enables short option bundling such as with `-rf` which will with this option trigger separate `-r` and `-f` handlers. ``` require "option_parser" removed = false forced = false OptionParser.parse(%w(-rf)) do |parser| parser.on("-r", "") { removed = true } parser.on("-f", "") { forced = true } end {removed, forced} # => {true, true} ```
1 parent 23fd68c commit 7d6fc2a

File tree

2 files changed

+128
-5
lines changed

2 files changed

+128
-5
lines changed

spec/std/option_parser_spec.cr

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,79 @@ describe "OptionParser" do
248248
expect_doesnt_capture_option [] of String, "-f FLAG"
249249
end
250250

251+
describe "bundling" do
252+
it "parses bundled boolean short options" do
253+
args = %w(-rf)
254+
called = [] of String
255+
OptionParser.parse(args) do |opts|
256+
opts.on("-r", "") { called << "-r" }
257+
opts.on("-f", "") { called << "-f" }
258+
end
259+
called.should eq(%w(-r -f))
260+
args.size.should eq(0)
261+
end
262+
263+
it "re-triggers handlers for repeated short flags" do
264+
args = %w(-vvv)
265+
verbosity = 0
266+
OptionParser.parse(args) do |opts|
267+
opts.on("-v", "") { verbosity += 1 }
268+
end
269+
verbosity.should eq(3)
270+
args.size.should eq(0)
271+
end
272+
273+
it "uses rest of token as required value and stops bundling" do
274+
args = %w(-ovalue -r)
275+
value = nil
276+
r = false
277+
OptionParser.parse(args) do |opts|
278+
opts.on("-o VALUE", "") { |v| value = v }
279+
opts.on("-r", "") { r = true }
280+
end
281+
value.should eq("value")
282+
r.should be_true
283+
args.size.should eq(0)
284+
end
285+
286+
it "assigns remainder as value for later required option" do
287+
args = %w(-ab123)
288+
a = false
289+
b = nil
290+
OptionParser.parse(args) do |opts|
291+
opts.on("-a", "") { a = true }
292+
opts.on("-b VALUE", "") { |v| b = v }
293+
end
294+
a.should be_true
295+
b.should eq("123")
296+
args.size.should eq(0)
297+
end
298+
299+
it "raises on invalid option inside bundle" do
300+
expect_raises OptionParser::InvalidOption, "Invalid option: -j" do
301+
OptionParser.parse(["-rj"]) do |opts|
302+
opts.on("-r", "") { }
303+
end
304+
end
305+
end
306+
307+
it "consumes rest of bundle as argument value when middle option requires argument" do
308+
args = %w(-aeb)
309+
a = false
310+
b = false
311+
e = nil
312+
OptionParser.parse(args) do |opts|
313+
opts.on("-a", "") { a = true }
314+
opts.on("-b", "") { b = true }
315+
opts.on("-e VALUE", "") { |v| e = v }
316+
end
317+
a.should be_true
318+
b.should be_false
319+
e.should eq("b")
320+
args.size.should eq(0)
321+
end
322+
end
323+
251324
describe "gnu_optional_args" do
252325
it "doesn't get optional argument for short flag after space" do
253326
flag = nil
@@ -381,7 +454,7 @@ describe "OptionParser" do
381454
end
382455

383456
it "raises on invalid option if value is given to none value handler (short flag, #9553) " do
384-
expect_raises OptionParser::InvalidOption, "Invalid option: -foo" do
457+
expect_raises OptionParser::InvalidOption, "Invalid option: -o" do
385458
OptionParser.parse(["-foo"]) do |opts|
386459
opts.on("-f", "some flag") { }
387460
end

src/option_parser.cr

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -401,9 +401,12 @@ class OptionParser
401401
break
402402
end
403403

404-
flag, value = parse_arg_to_flag_and_value(arg)
405-
406-
arg_index = handle_flag(flag, value, arg_index, args, handled_args)
404+
if bundled_short_arg?(arg)
405+
arg_index = handle_bundled_short_options(arg, arg_index, args, handled_args)
406+
else
407+
flag, value = parse_arg_to_flag_and_value(arg)
408+
arg_index = handle_flag(flag, value, arg_index, args, handled_args)
409+
end
407410

408411
arg_index += 1
409412
end
@@ -444,19 +447,66 @@ class OptionParser
444447
end
445448
end
446449

450+
private def bundled_short_arg?(arg : String) : Bool
451+
arg.starts_with?('-') && !arg.starts_with?("--") && arg.size > 2
452+
end
453+
447454
# Parses a command-line argument into a flag and optional inline value.
448455
private def parse_arg_to_flag_and_value(arg : String) : {String, String?}
449456
if arg.starts_with?("--")
450457
name, separator, value = arg.partition("=")
451458
if separator == "="
452459
return {name, value}
453460
end
454-
elsif arg.starts_with?('-') && arg.size > 2
461+
elsif bundled_short_arg?(arg)
455462
return {arg[0..1], arg[2..]}
456463
end
457464
{arg, nil}
458465
end
459466

467+
private def handle_bundled_short_options(arg : String, arg_index : Int32, args : Array(String), handled_args : Array(Int32)) : Int32
468+
rest = arg[1..]
469+
rest.each_char_with_index do |char, index|
470+
flag = "-#{char}"
471+
472+
suffix = index + 1 < rest.bytesize ? rest[(index + 1)..] : nil
473+
474+
if handler = @handlers[flag]?
475+
case handler.value_type
476+
in FlagValue::None
477+
next_index = handle_flag(flag, nil, arg_index, args, handled_args)
478+
return next_index unless next_index == arg_index
479+
in FlagValue::Required
480+
value = suffix
481+
if value && !value.empty?
482+
handled_args << arg_index
483+
handler.block.call(value)
484+
else
485+
next_index = handle_flag(flag, nil, arg_index, args, handled_args)
486+
return next_index unless next_index == arg_index
487+
end
488+
return arg_index
489+
in FlagValue::Optional
490+
value = suffix
491+
if value && !value.empty? && gnu_optional_args?
492+
handled_args << arg_index
493+
handler.block.call(value)
494+
return arg_index
495+
else
496+
next_index = handle_flag(flag, nil, arg_index, args, handled_args)
497+
return next_index unless next_index == arg_index
498+
end
499+
end
500+
else
501+
@invalid_option.call(flag)
502+
return arg_index
503+
end
504+
end
505+
506+
handled_args << arg_index
507+
arg_index
508+
end
509+
460510
# Processes a single flag/subcommand. Matches original behaviour exactly.
461511
private def handle_flag(flag : String, value : String?, arg_index : Int32, args : Array(String), handled_args : Array(Int32)) : Int32
462512
return arg_index unless handler = @handlers[flag]?

0 commit comments

Comments
 (0)