Skip to content

Conversation

@Qard
Copy link

@Qard Qard commented Jan 10, 2026

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), bundling: true) do |parser|
  parser.on("-r", "") { removed = true }
  parser.on("-f", "") { forced = true }
end

{removed, forced} # => {true, true}

Fixes #10981

@Qard Qard force-pushed the short-option-grouping branch from 4eaf449 to 7ddb07f Compare January 10, 2026 13:22
@lasso
Copy link
Contributor

lasso commented Jan 12, 2026

Does the gnu_ prefix add any meaning to the parameter name? I would prefer just strict_bundling, I think that will be enough, unless we have a similar flag with that name already...

@kojix2
Copy link
Contributor

kojix2 commented Jan 12, 2026

Related to #10981

@Sija
Copy link
Contributor

Sija commented Jan 12, 2026

Refs #11537

@Qard
Copy link
Author

Qard commented Jan 13, 2026

Does the gnu_ prefix add any meaning to the parameter name? I would prefer just strict_bundling, I think that will be enough, unless we have a similar flag with that name already...

I would not have used it if not for the fact there is already another gnu_ prefixed parameter. 🤷🏻

@kojix2
Copy link
Contributor

kojix2 commented Jan 13, 2026

It would be nice if gnu_strict_bundling: true could be the default.

@Qard
Copy link
Author

Qard commented Jan 13, 2026

Agreed, but technically a breaking change as it currently will pass the rest of the characters in the sequence through as the value which someone could be depending on. I went for putting it behind a flag so as to not break anyone, but the default could be swapped in a future release if we wanted.

require "option_parser"

# current behaviour:
OptionParser.parse(%w(-vvvv)) do |parser|
  parser.on("-v", "") { |input| puts input } # logs "vvv", the remainder of characters after first v
end

# with bundling: true set:
level = 0
OptionParser.parse(%w(-vvvv), bundling: true) do |parser|
  parser.on("-v", "") { |input| level += 1 } # increments level four times
end

@ysbaddaden
Copy link
Collaborator

I don't think we need either gnu or strict in the argument name. Option bundling (or chaining) of short options is a POSIX feature:

How does your implementation behave with the following case?

Multiple options can be chained together, as long as the non-last ones are not argument taking. If a and b take no arguments while e takes an optional argument, -abe is the same as -a -b -e, but -bea is not the same as -b -e a due to the preceding rule.

require "option_parser"

OptionParser.parse(%w[-aeb], gnu_strict_bundling: true) do |parser|
  parser.on("-a", "") { p! a }
  parser.on("-b", "") { p! b }
  parser.on("-e VALUE", "") { |e| p! e }
end

@Qard
Copy link
Author

Qard commented Jan 15, 2026

It consumes b as the value to e. I've added another test to demonstrate that more explicitly. I also renamed the parameter to just bundling, but happy to call it whatever you think is most reasonable. 🙂

@Qard Qard force-pushed the short-option-grouping branch from 7ddb07f to 4a0a48a Compare January 15, 2026 13:26
@Qard Qard changed the title Support GNU strict short option bundling Support short option bundling Jan 15, 2026
@ysbaddaden
Copy link
Collaborator

I think the bundling limitation doesn't come from nowhere. If a short option takes a value then it's confusing to have it appear inside a bundle: is it followed by a value or by another short option? IMO it makes sense to fail to parse -bea and to require an explicit -be a instead (no ambiguity).

@Qard
Copy link
Author

Qard commented Jan 15, 2026

I don't disagree that a short option which takes a parameter being used in the bundle consumes the rest of the string is confusing, but that is how getopt is specified. Any option with a short flag can be bundled and, if it takes a value, it will consume the rest of the characters. We could deviate on that particular note, or add another option to fail in such cases. Not too sure how to proceed there. 🤔

@ysbaddaden
Copy link
Collaborator

ysbaddaden commented Jan 15, 2026

Are you sure? It's from the Wikipedia page, and I can't find the details in POSIX (yet), but GNU definitely has the rule:

Multiple options may follow a hyphen delimiter in a single token if the options do not take arguments. Thus, ‘-abc’ is equivalent to ‘-a -b -c’.

https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html

It means that glibc getopt shall parse -ofoo as -o foo and -ao foo as -a -o foo but fail to parse -aofoo.

EDIT: I found the guidelines in POSIX that state the same:

Guideline 4: All options should be preceded by the '-' delimiter character.

Guideline 5: One or more options without option-arguments, followed by at most one option that takes an option-argument, should be accepted when grouped behind one '-' delimiter.

https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html

I'll verify how it is in practice, but need to work out the C API 😅

EDIT 2: I'm not requesting the change, let's investigate the usual behavior and discuss before we decide how to proceed.

@Qard
Copy link
Author

Qard commented Jan 15, 2026

The getopt reference example shows cmd -aoarg path path where -o is a short option which consumes the remainder of the string. Could be that this example is incorrect. That was what I used as reference for the behaviour.

@ysbaddaden
Copy link
Collaborator

ysbaddaden commented Jan 15, 2026

You're right. I tested and it indeed works like that. I guess the whole -ofoo is the thing that causes 🤷

@Qard
Copy link
Author

Qard commented Jan 15, 2026

Yeah. A bit weird ergonomics, but it is what it is. 🤷🏻

@Qard Qard force-pushed the short-option-grouping branch from 4a0a48a to 70f88e8 Compare January 15, 2026 18:36
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), bundling: true) do |parser|
  parser.on("-r", "") { removed = true }
  parser.on("-f", "") { forced = true }
end

{removed, forced} # => {true, true}
```
@Qard Qard force-pushed the short-option-grouping branch from 70f88e8 to e2279d2 Compare January 25, 2026 21:50
# parser.on("-f", "") { forced = true }
# end
#
# {removed, forced} # => {true, true}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Polish:

Suggested change
# {removed, forced} # => {true, true}
# p removed # => true
# p forced # => true

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p is not necessary :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, you'd see nothing if you were to run the example.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

# => denotes return value, not the output printed

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yeah, the return value is what I had in mind with that. Is explicit logging in examples a more standard pattern we try to follow?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK, examples tend to focus on return values and not the output - unless there's a reason to do so, that is.

Copy link
Collaborator

@ysbaddaden ysbaddaden Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Qard No, we should have a check around stdlib for that. It's probably just me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OptionParser support for stacked options

5 participants