@@ -112,8 +112,8 @@ class OptionParser
112112 # and uses it to parse the passed *args* (defaults to `ARGV`).
113113 #
114114 # Refer to `#gnu_optional_args?` for the behaviour of the named parameter.
115- def self.parse (args = ARGV , * , gnu_optional_args : Bool = false , & ) : self
116- parser = OptionParser .new(gnu_optional_args: gnu_optional_args)
115+ def self.parse (args = ARGV , * , gnu_optional_args : Bool = false , bundling : Bool = false , & ) : self
116+ parser = OptionParser .new(gnu_optional_args: gnu_optional_args, bundling: bundling )
117117 yield parser
118118 parser.parse(args)
119119 parser
@@ -122,7 +122,7 @@ class OptionParser
122122 # Creates a new parser.
123123 #
124124 # Refer to `#gnu_optional_args?` for the behaviour of the named parameter.
125- def initialize (* , @gnu_optional_args : Bool = false )
125+ def initialize (* , @gnu_optional_args : Bool = false , @bundling : Bool = false )
126126 @flags = [] of String
127127 @handlers = Hash (String , Handler ).new
128128 @stop = false
@@ -133,8 +133,8 @@ class OptionParser
133133 # Creates a new parser, with its configuration specified in the block.
134134 #
135135 # Refer to `#gnu_optional_args?` for the behaviour of the named parameter.
136- def self.new (* , gnu_optional_args : Bool = false , & )
137- new(gnu_optional_args: gnu_optional_args).tap { |parser | yield parser }
136+ def self.new (* , gnu_optional_args : Bool = false , bundling : Bool = false , & )
137+ new(gnu_optional_args: gnu_optional_args, bundling: bundling ).tap { |parser | yield parser }
138138 end
139139
140140 # Returns whether the GNU convention is followed for optional arguments.
@@ -173,6 +173,27 @@ class OptionParser
173173 # ```
174174 property? gnu_optional_args : Bool
175175
176+ # Returns whether short option bundling is enabled.
177+ #
178+ # If true, short options can be grouped after a single dash:
179+ #
180+ # ```
181+ # require "option_parser"
182+ #
183+ # removed = false
184+ # forced = false
185+ # OptionParser.parse(%w(-rf), bundling: true) do |parser|
186+ # parser.on("-r", "") { removed = true }
187+ # parser.on("-f", "") { forced = true }
188+ # end
189+ #
190+ # {removed, forced} # => {true, true}
191+ # ```
192+ #
193+ # Without `bundling: true`, a token like `-rf` is interpreted as
194+ # the flag `-r` with an inline value `f`.
195+ property? bundling : Bool
196+
176197 # Establishes the initial message for the help printout.
177198 # Typically, you want to write here the name of your program,
178199 # and a one-line template of its invocation.
@@ -401,9 +422,12 @@ class OptionParser
401422 break
402423 end
403424
404- flag, value = parse_arg_to_flag_and_value(arg)
405-
406- arg_index = handle_flag(flag, value, arg_index, args, handled_args)
425+ if bundling? && bundled_short_arg?(arg)
426+ arg_index = handle_bundled_short_options(arg, arg_index, args, handled_args)
427+ else
428+ flag, value = parse_arg_to_flag_and_value(arg)
429+ arg_index = handle_flag(flag, value, arg_index, args, handled_args)
430+ end
407431
408432 arg_index += 1
409433 end
@@ -444,19 +468,66 @@ class OptionParser
444468 end
445469 end
446470
471+ private def bundled_short_arg? (arg : String ) : Bool
472+ arg.starts_with?('-' ) && ! arg.starts_with?(" --" ) && arg.size > 2
473+ end
474+
447475 # Parses a command-line argument into a flag and optional inline value.
448476 private def parse_arg_to_flag_and_value (arg : String ) : {String , String ?}
449477 if arg.starts_with?(" --" )
450478 name, separator, value = arg.partition(" =" )
451479 if separator == " ="
452480 return {name, value}
453481 end
454- elsif arg.starts_with?( '-' ) && arg.size > 2
482+ elsif bundled_short_arg?( arg)
455483 return {arg[0 ..1], arg[2 ..]}
456484 end
457485 {arg, nil }
458486 end
459487
488+ private def handle_bundled_short_options (arg : String , arg_index : Int32 , args : Array (String ), handled_args : Array (Int32 )) : Int32
489+ rest = arg[1 ..]
490+ rest.each_char_with_index do |char , index |
491+ flag = " -#{ char } "
492+
493+ suffix = index + 1 < rest.bytesize ? rest[(index + 1 )..] : nil
494+
495+ if handler = @handlers [flag]?
496+ case handler.value_type
497+ in FlagValue ::None
498+ next_index = handle_flag(flag, nil , arg_index, args, handled_args)
499+ return next_index unless next_index == arg_index
500+ in FlagValue ::Required
501+ value = suffix
502+ if value && ! value.empty?
503+ handled_args << arg_index
504+ handler.block.call(value)
505+ else
506+ next_index = handle_flag(flag, nil , arg_index, args, handled_args)
507+ return next_index unless next_index == arg_index
508+ end
509+ return arg_index
510+ in FlagValue ::Optional
511+ value = suffix
512+ if value && ! value.empty? && gnu_optional_args?
513+ handled_args << arg_index
514+ handler.block.call(value)
515+ return arg_index
516+ else
517+ next_index = handle_flag(flag, nil , arg_index, args, handled_args)
518+ return next_index unless next_index == arg_index
519+ end
520+ end
521+ else
522+ @invalid_option .call(flag)
523+ return arg_index
524+ end
525+ end
526+
527+ handled_args << arg_index
528+ arg_index
529+ end
530+
460531 # Processes a single flag/subcommand. Matches original behaviour exactly.
461532 private def handle_flag (flag : String , value : String ?, arg_index : Int32 , args : Array (String ), handled_args : Array (Int32 )) : Int32
462533 return arg_index unless handler = @handlers [flag]?
0 commit comments