Skip to content

Argument Separator#

The argument separater -- has been adopted by various tools to serve two different strategies: end-of-options indicator and pass-thru arguments.

Use AppSettings.Parser.DefaultArgumentSeparatorStrategy to specify which strategy to use.

The default is EndOfOptions so that by default users have it as a fallback to enter any value as an operand.

The strategy can be changed for a command using the Command attribute: [Command(ArgumentSeparatorStrategy=ArgumentSeparatorStrategy.PassThru)].

We recommend leaving the default as EndOfOptions and overriding on the Command when you have a command that expects to receive arguments to pass thru to another process.

End of Options Indicator#

Posix guideline (#10)

The first -- argument that is not an option-argument should be accepted as a delimiter indicating the end of options. Any following arguments should be treated as operands, even if they begin with the '-' character.

This convention was created to handle cases where operand values begin with - or -- causing the parser to interpret them as options.

All operands after the first -- will be stored in CommandContext.ParseResult.SeparatedArguments regardless of whether they were expected or not.

Let's look at an example.

public void EndOfOptions(IConsole console, CommandContext ctx, string? arg1)
{
    var parserSettings = ctx.AppConfig.AppSettings.Parser;
    console.WriteLine("IgnoreUnexpectedOperands: " + 
                      parserSettings.IgnoreUnexpectedOperands);
    console.WriteLine("DefaultArgumentSeparatorStrategy: " + 
                      parserSettings.DefaultArgumentSeparatorStrategy);
    console.WriteLine();
    console.WriteLine($"arg1: {arg1}");
    console.WriteLine($"separated: {string.Join(',', ctx.ParseResult!.SeparatedArguments)}");
    console.WriteLine($"remaining: {string.Join(',', ctx.ParseResult!.RemainingOperands)}");
}
snippet source | anchor

This command expects a single operand, but if the operand value looks like an option, the parser will throw an exception

$ example.exe EndOfOptions --option-mask
Unrecognized option '--option-mask'

Usage: example.exe EndOfOptions [<arg1>]

Arguments:

  arg1  <TEXT>
snippet source | anchor

But if we put the separator before the operand value, the parser will interpret the value as an operand

$ example.exe EndOfOptions -- --option-mask
IgnoreUnexpectedOperands: False
DefaultArgumentSeparatorStrategy: EndOfOptions

arg1: --option-mask
separated: --option-mask
remaining:
snippet source | anchor

Separated operands are available in CommandContext.ParseResult.SeparatedArguments

Unexpected Operands#

Unexpected operands occur when there are no longer operands to assign values to. This will result in a parsing exception. Using the EndOfOptions command from above...

$ example.exe EndOfOptions expected unexpected
Unrecognized command or argument 'unexpected'

Usage: example.exe EndOfOptions [<arg1>]

Arguments:

  arg1  <TEXT>
snippet source | anchor

set AppSettings.Parser.IgnoreUnexpectedOperands = true or [Command(IgnoreUnexpectedOperands = true)] to ignore unexpected operands.

$ example.exe EndOfOptions expected unexpected
IgnoreUnexpectedOperands: True
DefaultArgumentSeparatorStrategy: EndOfOptions

arg1: expected
separated: 
remaining: unexpected
snippet source | anchor

Unexpected operands are available in CommandContext.ParseResult.RemainingOperands

Pass-thru arguments#

While the Posix guideline specifies the -- should be used as an end-of-options indicator, there's a common pattern to use -- to denote arguments to be passed to a sub-process.

For example, dotnet.exe has this discription for --:

Delimits arguments to dotnet run from arguments for the application being run. All arguments after this delimiter are passed to the application run.

Let's modify the EndOfOptions example using the [Command] attribute to set the ArgumentSeparatorStrategy and IgnoreUnexpectedOperands

[Command(ArgumentSeparatorStrategy = ArgumentSeparatorStrategy.PassThru)]
public void PassThru(IConsole console, CommandContext ctx, string? arg1)
{
    var parserSettings = ctx.AppConfig.AppSettings.Parser;
    console.WriteLine("IgnoreUnexpectedOperands: " +
                      parserSettings.IgnoreUnexpectedOperands);
    console.WriteLine("DefaultArgumentSeparatorStrategy: " +
                      parserSettings.DefaultArgumentSeparatorStrategy);
    console.WriteLine();
    console.WriteLine($"arg1: {arg1}");
    console.WriteLine($"separated: {string.Join(',', ctx.ParseResult!.SeparatedArguments)}");
    console.WriteLine($"remaining: {string.Join(',', ctx.ParseResult!.RemainingOperands)}");
}
snippet source | anchor

Help will append [[--] <arg>...] to the usage example when ArgumentSeparatorStrategy.PassThru is used.

$ example.exe PassThru -h
Usage: example.exe PassThru [<arg1>] [[--] <arg>...]

Arguments:

  arg1  <TEXT>
snippet source | anchor

Here is how an unexpected arg is processed with the separator

$ example.exe PassThru expected -- pass-thru
IgnoreUnexpectedOperands: False
DefaultArgumentSeparatorStrategy: EndOfOptions

arg1: expected
separated: pass-thru
remaining:
snippet source | anchor

How to support both?#

Explicit support for both concepts is complicated to provide generically because the framework cannot know

  • if a operands for a given command can be formatted like options or directives
  • if a command can expect pass-thru arguments
  • if a user entered -- to indicate end-of-options or pass-thru arguments

Due to this complexity, we'll give you the data and let you determine the best approach based on the requirements of the command.

Argument Parsing Diagram#

This diagram shows how the parser handles options and operands based on settings.

Argument Parse Behavior