command-line-api
command-line-api copied to clipboard
Support for multiple commands and options not bound to a command
I have a scenario where a console application has a number of general options (for things like input and output files) and then a number of commands representing a pipeline of operations that it should perform, with each command having its own specific options. For example:
myapp --i infile --o outfile command1 -a "foo" -b 42 command2 -c "bar" -d 99 command3 -e "zzz" -f false
etc.
In building a test application it appears that:
- If the application finds a command on the command-line then the options not associated with the command (--i and --o in the fictitious example above) are ignored - i.e. the root command handler is not called.
- If multiple commands are specified on the command-line then only the handler for the first command is called.
- Only if no commands are specified is the root command handler invoked, allowing me to access the value of the --i and --o options.
My questions:
- Since I'm new to using this package - do my findings above reflect the way that System.CommandLine is intended to work?
- Is there a way to do what I want to achieve using System.CommandLine? (Without trying to force it beyond its design assumptions)
- If there are multiple way to do this, are there any pointers to documentation that helps me understand the trade-offs for each approach?
Thanks.
As far as i know, System.CommandLine does not support "chained" command sequences (only hierarchical/nested commands, but which is not what you seem to seek).
I assume the order of the operations can be variable, i.e., instead of command1 -> command2 -> command3, a user might also want to do: command1 -> command3 -> command2. Is this correct?
However, all is not lost, in my opinion. If the order of the commands/operations is supposed to be variable, you could perhaps split the args array into different arrays for each command. You would need to scan the args array for your command verbs, with the command verbs indicating the boundary for each of these different arrays. Each array would contain the command name as well as the options for this command (except the first array for the root command only containing the root command options). Then you could do RootCommand.Invoke on each of these arrays, thus executing the respective command handler in turn.
With respect to the CLI example you have given above, the args array should end up being split like this:
{ "--i", "infile", "--o", "outfile" }
{ "command1", "-a", "foo", "-b", "42" }
{ "command2", "-c", "bar", "-d", "99" }
{ "command3", "-e", "zzz", "-f", "false"}
The command handlers should probably only queue up the operations to execute and not directly execute the operations themselves. Execute the queued up operation only after finishing iterating over each of the split args arrays. Because, if there is a CLI parse error for one of the subsequent commands, you probably want to get the error early and not only after a few operations have already been executed.
You would then also need to take care of any help option being present in one of the arrays (as that should probably prevent execution of any operation or some such) and you would also need to customize the "usage" section of the help text to indicate the correct CLI syntax. But that is something to deal with only after my suggested approach proved to be feasible and effective.
Thanks for taking the time to describe that. Its an interesting approach to the problem and I can't see why it wouldn't work. The commands/options correspond to structures/fields that are passed as an array to a one-shot library function, so iteratively building a queue of them maps quite well onto that. I'll try it over the next few days. Thanks again.
Hi! I have created a multi command usage. now i can use: myapp --i infile --o outfile command1 -a "foo" -b 42 command2 -c "bar" -d 99 command3 -e "zzz" -f false with this coomand: myapp --i infile --o outfile command1 -a "foo" -b 42 --beginNewCommand command2 -c "bar" -d 99 --beginNewCommand command3 -e "zzz" -f false
`
public struct ArgExecution
{
public List<string> Args { get; set; } = new();
public bool ExecuteIfSuccess { get; set; }
}
private const string OptionBeginNewCommandName = "--beginNewCommand";
private const string OptionBeginNewCommandIfSuccessName = "--beginNewCommandIfSuccess";
public RootCommand BuildCommandLine()
{
var rootCommand = new RootCommand();
rootCommand.Name = "Charm Command Test";
rootCommand.AddCommand(GetCrypterCommand());
Option<bool> optionBeginNewCommand = new(name: OptionBeginNewCommandName, description: "Option for end current command, and execute next text as new command.");
rootCommand.AddGlobalOption(optionBeginNewCommand);
Option<bool> optionBeginNewCommandIfSuccess = new(name: OptionBeginNewCommandIfSuccessName, description: "Option for end current command, and execute next text as new command only if this command is success.");
rootCommand.AddGlobalOption(optionBeginNewCommandIfSuccess);
return rootCommand;
}
public async Task<int> Execute(string[] args)
{
var rootCommand = BuildCommandLine();
var executions = SplitArgsInMultipleCommands(args);
var res = await ExecuteMultipleCommands(executions, s => rootCommand.InvokeAsync(s));
return res;
}
public async Task<int> ExecuteMultipleCommands(List<ArgExecution> executions, Func<string[], Task<int>> executeFunc)
{
int? statusCode = null;
foreach (var execution in executions)
{
if (execution.ExecuteIfSuccess && statusCode.GetValueOrDefault() != 0)
continue;
var res = await executeFunc(execution.Args.ToArray());
if (statusCode == null || (res != 0 && statusCode == 0))
statusCode = res;
}
return statusCode.GetValueOrDefault();
}
public static List<ArgExecution> SplitArgsInMultipleCommands(string[] args)
{
var currArg = new ArgExecution();
var executions = new List<ArgExecution>();
foreach (var s in args)
{
if (s == OptionBeginNewCommandName || s == OptionBeginNewCommandIfSuccessName)
{
executions.Add(currArg);
currArg = new ArgExecution() { ExecuteIfSuccess = s == OptionBeginNewCommandIfSuccessName };
continue;
}
currArg.Args.Add(s);
}
if (currArg.Args.Any() || executions.Count == 0)
executions.Add(currArg);
return executions;
}
`