diff --git a/README.md b/README.md index c84f5a1..691237e 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ to [releases](https://github.com/andrey-zherikov/argparse/releases) for breaking - [Built-in reporting of error happened during argument parsing](#error-handling). - [Built-in help generation](#help-generation). + ## Getting started Here is the simple example showing the usage of `argparse` utility. It uses the basic approach when all members are @@ -211,128 +212,6 @@ Optional arguments: -h, --help Show this help message and exit ``` -## Argument declaration - -### Positional arguments - -Positional arguments are expected to be at a specific position within the command line. This argument can be declared -using `PositionalArgument` UDA: - -```d -struct Params -{ - @PositionalArgument(0) - string firstName; - - @PositionalArgument(0, "lastName") - string arg; -} -``` - -Parameters of `PositionalArgument` UDA: - -|#|Name|Type|Optional/
Required|Description| -|---|---|---|---|---| -|1|`position`|`uint`|required|Zero-based unsigned position of the argument.| -|2|`name`|`string`|optional|Name of this argument that is shown in help text.
If not provided then the name of data member is used.| - -### Named arguments - -As an opposite to positional there can be named arguments (they are also called as flags or options). They can be -declared using `NamedArgument` UDA: - -```d -struct Params -{ - @NamedArgument - string greeting; - - @NamedArgument(["name", "first-name", "n"]) - string name; - - @NamedArgument("family", "last-name") - string family; -} -``` - -Parameters of `NamedArgument` UDA: - -|#|Name|Type|Optional/
Required|Description| -|---|---|---|---|---| -|1|`name`|`string` or `string[]`|optional|Name(s) of this argument that can show up in command line.| - -Named arguments might have multiple names, so they should be specified either as an array of strings or as a list of -parameters in `NamedArgument` UDA. Argument names can be either single-letter (called as short options) -or multi-letter (called as long options). Both cases are fully supported with one caveat: -if a single-letter argument is used with a double-dash (e.g. `--n`) in command line then it behaves the same as a -multi-letter option. When an argument is used with a single dash then it is treated as a single-letter argument. - -The following usages of the argument in the command line are equivalent: -`--name John`, `--name=John`, `--n John`, `--n=John`, `-nJohn`, `-n John`. Note that any other character can be used -instead of `=` - see [Parser customization](#parser-customization) for details. - -### Trailing arguments - -A lone double-dash terminates argument parsing by default. It is used to separate program arguments from other -parameters (e.g., arguments to be passed to another program). To store trailing arguments simply add a data member of -type `string[]` with `TrailingArguments` UDA: - -```d -struct T -{ - string a; - string b; - - @TrailingArguments string[] args; -} - -assert(CLI!T.parseArgs!((T t) { assert(t == T("A","",["-b","B"])); })(["-a","A","--","-b","B"]) == 0); -``` - -Note that any other character sequence can be used instead of `--` - see [Parser customization](#parser-customization) for details. - -### Optional and required arguments - -Arguments can be marked as required or optional by adding `Required()` or `.Optional()` to UDA. If required argument is -not present parser will error out. Positional agruments are required by default. - -```d -struct T -{ - @(PositionalArgument(0, "a").Optional()) - string a = "not set"; - - @(NamedArgument.Required()) - int b; -} - -assert(CLI!T.parseArgs!((T t) { assert(t == T("not set", 4)); })(["-b", "4"]) == 0); -``` - -### Limit the allowed values - -In some cases an argument can receive one of the limited set of values so `AllowedValues` can be used here: - -```d -struct T -{ - @(NamedArgument.AllowedValues!(["apple","pear","banana"])) - string fruit; -} - -assert(CLI!T.parseArgs!((T t) { assert(t == T("apple")); })(["--fruit", "apple"]) == 0); -assert(CLI!T.parseArgs!((T t) { assert(false); })(["--fruit", "kiwi"]) != 0); // "kiwi" is not allowed -``` - -For the value that is not in the allowed list, this error will be printed: - -``` -Error: Invalid value 'kiwi' for argument '--fruit'. -Valid argument values are: apple,pear,banana -``` - -Note that if the type of destination variable is `enum` then the allowed values are automatically limited to those -listed in the `enum`. ## Calling the parser @@ -345,7 +224,7 @@ listed in the `enum`. - `alias CLI(COMMANDS...) = CLI!(Config.init, COMMANDS)` - alias provided for convenience that allows using default `Config`, i.e. `config = Config.init`. -### Wrappers for main function +### Wrapper for main function The recommended and most convenient way to use `argparse` is through `CLI!(...).main(alias newMain)` mixin template. It declares the standard `main` function that parses command line arguments and calls provided `newMain` function with @@ -452,9 +331,9 @@ that it does not produce an error when extra arguments are present. It has the f **Parameters:** - - `receiver` - the object that's populated with parsed values. - - `args` - raw command line arguments. - - `unrecognizedArgs` - raw command line arguments that were not parsed. + - `receiver` - the object that's populated with parsed values. + - `args` - raw command line arguments. + - `unrecognizedArgs` - raw command line arguments that were not parsed. **Return value:** @@ -464,8 +343,8 @@ that it does not produce an error when extra arguments are present. It has the f **Parameters:** - - `receiver` - the object that's populated with parsed values. - - `args` - raw command line arguments that are modified to have parsed arguments removed. + - `receiver` - the object that's populated with parsed values. + - `args` - raw command line arguments that are modified to have parsed arguments removed. **Return value:** @@ -488,7 +367,7 @@ assert(args == ["-c", "C"]); ``` -### Calling a function after parsing +### Calling another `main` function after parsing Sometimes it's useful to call some function with an object that has all command line arguments parsed. For this usage, `argparse` provides `CLI!(...).parseArgs` template function that has the following signature: @@ -523,6 +402,219 @@ int main(string[] args) } ``` +## Shell completion + +`argparse` supports tab completion of last argument for certain shells (see below). However this support is limited to the names of arguments and +subcommands. + +### Wrappers for main function + +If you are using `CLI!(...).main(alias newMain)` mixin template in your code then you can easily build a completer +(program that provides completion) by defining `argparse_completion` version (`-version=argparse_completion` option of +`dmd`). Don't forget to use different file name for completer than your main program (`-of` option in `dmd`). No other +changes are necessary to generate completer but you should consider minimizing the set of imported modules when +`argparse_completion` version is defined. For example, you can put all imports into your main function that is passed to +`CLI!(...).main(alias newMain)` - `newMain` parameter is not used in completer. + +If you prefer having separate main module for completer then you can use `CLI!(...).completeMain` mixin template: +```d +mixin CLI!(...).completeMain; +``` + +In case if you prefer to have your own `main` function and would like to call completer by yourself, you can use +`int CLI!(...).complete(string[] args)` function. This function executes the completer by parsing provided `args` (note +that you should remove the first argument from `argv` passed to `main` function). The returned value is meant to be +returned from `main` function having zero value in case of success. + +### Low level completion + +In case if none of the above methods is suitable, `argparse` provides `string[] CLI!(...).completeArgs(string[] args)` +function. It takes arguments that should be completed and returns all possible completions. + +`completeArgs` function expects to receive all command line arguments (excluding `argv[0]` - first command line argument in `main` +function) in order to provide completions correctly (set of available arguments depends on subcommand). This function +supports two workflows: +- If the last argument in `args` is empty and it's not supposed to be a value for a command line argument, then all + available arguments and subcommands (if any) are returned. +- If the last argument in `args` is not empty and it's not supposed to be a value for a command line argument, then only + those arguments and subcommands (if any) are returned that starts with the same text as the last argument in `args`. + +For example, if there are `--foo`, `--bar` and `--baz` arguments available, then: +- Completion for `args=[""]` will be `["--foo", "--bar", "--baz"]`. +- Completion for `args=["--b"]` will be `["--bar", "--baz"]`. + +### Using the completer + +Completer that is provided by `argparse` supports the following shells: +- bash +- zsh +- tcsh +- fish + +Its usage consists of two steps: completion setup and completing of the command line. Both are implemented as +subcommands (`init` and `complete` accordingly). + +#### Completion setup + +Before using completion, completer should be added to the shell. This can be achieved by using `init` subcommand. It +accepts the following arguments (you can get them by running ` init --help`): +- `--bash`: provide completion for bash. +- `--zsh`: provide completion for zsh. Note: zsh completion is done through bash completion so you should execute `bashcompinit` first. +- `--tcsh`: provide completion for tcsh. +- `--fish`: provide completion for fish. +- `--completerPath `: path to completer. By default, the path to itself is used. +- `--commandName `: command name that should be completed. By default, the first name of your main command is used. + +Either `--bash`, `--zsh`, `--tcsh` or `--fish` is expected. + +As a result, completer prints the script to setup completion for requested shell into standard output (`stdout`) +which should be executed. To make this more streamlined, you can execute the output inside the current shell or to do +this during shell initialization (e.g. in `.bashrc` for bash). To help doing so, completer also prints sourcing +recommendation to standard output as a comment. + +Example of completer output for ` init --bash --commandName mytool --completerPath /path/to/completer` arguments: +``` +# Add this source command into .bashrc: +# source <(/path/to/completer init --bash --commandName mytool) +complete -C 'eval /path/to/completer --bash -- $COMP_LINE ---' mytool +``` + +Recommended workflow is to install completer into a system according to your installation policy and update shell +initialization/config file to source the output of `init` command. + +#### Completing of the command line + +Argument completion is done by `complete` subcommand (it's default one). It accepts the following arguments (you can get them by running ` complete --help`): +- `--bash`: provide completion for bash. +- `--tcsh`: provide completion for tcsh. +- `--fish`: provide completion for fish. + +As a result, completer prints all available completions, one per line assuming that it's called according to the output +of `init` command. + +## Argument declaration + +### Positional arguments + +Positional arguments are expected to be at a specific position within the command line. This argument can be declared +using `PositionalArgument` UDA: + +```d +struct Params +{ + @PositionalArgument(0) + string firstName; + + @PositionalArgument(0, "lastName") + string arg; +} +``` + +Parameters of `PositionalArgument` UDA: + +|#|Name|Type|Optional/
Required|Description| +|---|---|---|---|---| +|1|`position`|`uint`|required|Zero-based unsigned position of the argument.| +|2|`name`|`string`|optional|Name of this argument that is shown in help text.
If not provided then the name of data member is used.| + +### Named arguments + +As an opposite to positional there can be named arguments (they are also called as flags or options). They can be +declared using `NamedArgument` UDA: + +```d +struct Params +{ + @NamedArgument + string greeting; + + @NamedArgument(["name", "first-name", "n"]) + string name; + + @NamedArgument("family", "last-name") + string family; +} +``` + +Parameters of `NamedArgument` UDA: + +|#|Name|Type|Optional/
Required|Description| +|---|---|---|---|---| +|1|`name`|`string` or `string[]`|optional|Name(s) of this argument that can show up in command line.| + +Named arguments might have multiple names, so they should be specified either as an array of strings or as a list of +parameters in `NamedArgument` UDA. Argument names can be either single-letter (called as short options) +or multi-letter (called as long options). Both cases are fully supported with one caveat: +if a single-letter argument is used with a double-dash (e.g. `--n`) in command line then it behaves the same as a +multi-letter option. When an argument is used with a single dash then it is treated as a single-letter argument. + +The following usages of the argument in the command line are equivalent: +`--name John`, `--name=John`, `--n John`, `--n=John`, `-nJohn`, `-n John`. Note that any other character can be used +instead of `=` - see [Parser customization](#parser-customization) for details. + +### Trailing arguments + +A lone double-dash terminates argument parsing by default. It is used to separate program arguments from other +parameters (e.g., arguments to be passed to another program). To store trailing arguments simply add a data member of +type `string[]` with `TrailingArguments` UDA: + +```d +struct T +{ + string a; + string b; + + @TrailingArguments string[] args; +} + +assert(CLI!T.parseArgs!((T t) { assert(t == T("A","",["-b","B"])); })(["-a","A","--","-b","B"]) == 0); +``` + +Note that any other character sequence can be used instead of `--` - see [Parser customization](#parser-customization) for details. + +### Optional and required arguments + +Arguments can be marked as required or optional by adding `Required()` or `.Optional()` to UDA. If required argument is +not present parser will error out. Positional agruments are required by default. + +```d +struct T +{ + @(PositionalArgument(0, "a").Optional()) + string a = "not set"; + + @(NamedArgument.Required()) + int b; +} + +assert(CLI!T.parseArgs!((T t) { assert(t == T("not set", 4)); })(["-b", "4"]) == 0); +``` + +### Limit the allowed values + +In some cases an argument can receive one of the limited set of values so `AllowedValues` can be used here: + +```d +struct T +{ + @(NamedArgument.AllowedValues!(["apple","pear","banana"])) + string fruit; +} + +assert(CLI!T.parseArgs!((T t) { assert(t == T("apple")); })(["--fruit", "apple"]) == 0); +assert(CLI!T.parseArgs!((T t) { assert(false); })(["--fruit", "kiwi"]) != 0); // "kiwi" is not allowed +``` + +For the value that is not in the allowed list, this error will be printed: + +``` +Error: Invalid value 'kiwi' for argument '--fruit'. +Valid argument values are: apple,pear,banana +``` + +Note that if the type of destination variable is `enum` then the allowed values are automatically limited to those +listed in the `enum`. + ## Argument dependencies diff --git a/examples/completion/separate_main/app/app.d b/examples/completion/separate_main/app/app.d new file mode 100644 index 0000000..61fee93 --- /dev/null +++ b/examples/completion/separate_main/app/app.d @@ -0,0 +1,10 @@ +// This example shows the usage of a separate main for completer + +import argparse; +import cli; + +// This mixin defines standard main function that parses command line and prints completion result to stdout +mixin CLI!Program.main!((prog) +{ + // do something +}); \ No newline at end of file diff --git a/examples/completion/separate_main/app/dub.json b/examples/completion/separate_main/app/dub.json new file mode 100644 index 0000000..6e4dfeb --- /dev/null +++ b/examples/completion/separate_main/app/dub.json @@ -0,0 +1,7 @@ +{ + "license": "BSL-1.0", + "name": "completion-separate_main-app", + "targetType":"executable", + "sourcePaths":[".", "../source"], + "dependencies":{ "all:argparse":"*" } +} \ No newline at end of file diff --git a/examples/completion/separate_main/completer/app.d b/examples/completion/separate_main/completer/app.d new file mode 100644 index 0000000..2762e00 --- /dev/null +++ b/examples/completion/separate_main/completer/app.d @@ -0,0 +1,7 @@ +// This example shows the usage of a separate main for completer + +import argparse; +import cli; + +// This mixin defines standard main function that parses command line and prints completion result to stdout +mixin CLI!Program.mainComplete; \ No newline at end of file diff --git a/examples/completion/separate_main/completer/dub.json b/examples/completion/separate_main/completer/dub.json new file mode 100644 index 0000000..5cdea5d --- /dev/null +++ b/examples/completion/separate_main/completer/dub.json @@ -0,0 +1,7 @@ +{ + "license": "BSL-1.0", + "name": "completion-separate_main-completer", + "targetType":"executable", + "sourcePaths":[".", "../source"], + "dependencies":{ "all:argparse":"*" } +} \ No newline at end of file diff --git a/examples/completion/separate_main/source/cli.d b/examples/completion/separate_main/source/cli.d new file mode 100644 index 0000000..37e5fc8 --- /dev/null +++ b/examples/completion/separate_main/source/cli.d @@ -0,0 +1,18 @@ +module cli; + +struct cmd1 +{ + string car; + string can; + string ban; +} +struct cmd2 {} + +struct Program +{ + import std.sumtype: SumType; + + string foo, bar, baz; + + SumType!(cmd1, cmd2) cmd; +} \ No newline at end of file diff --git a/examples/completion/single_main/app/dub.json b/examples/completion/single_main/app/dub.json new file mode 100644 index 0000000..26b22a9 --- /dev/null +++ b/examples/completion/single_main/app/dub.json @@ -0,0 +1,7 @@ +{ + "license": "BSL-1.0", + "name": "completion-single_main-app", + "targetType":"executable", + "sourcePaths":["../source"], + "dependencies":{ "all:argparse":"*" } +} \ No newline at end of file diff --git a/examples/completion/single_main/completer/dub.json b/examples/completion/single_main/completer/dub.json new file mode 100644 index 0000000..da05163 --- /dev/null +++ b/examples/completion/single_main/completer/dub.json @@ -0,0 +1,8 @@ +{ + "license": "BSL-1.0", + "name": "completion-single_main-completer", + "targetType":"executable", + "versions": ["argparse_completion"], + "sourcePaths":["../source"], + "dependencies":{ "all:argparse":"*" } +} \ No newline at end of file diff --git a/examples/completion/single_main/source/app.d b/examples/completion/single_main/source/app.d new file mode 100644 index 0000000..e2f7345 --- /dev/null +++ b/examples/completion/single_main/source/app.d @@ -0,0 +1,30 @@ +// This example shows the usage of a single entry for both a program and a completer + +import argparse; + +struct cmd1 +{ + string car; + string can; + string ban; +} +struct cmd2 {} + +struct Program +{ + import std.sumtype: SumType; + + string foo, bar, baz; + + SumType!(cmd1, cmd2) cmd; +} + +// This mixin defines standard main function that parses command line and prints completion result to stdout +mixin CLI!Program.main!((prog) +{ + version(argparse_completion) + { + // This function is never used when 'argparse_completion' version is defined + static assert(false); + } +}); \ No newline at end of file diff --git a/examples/dub.json b/examples/dub.json index 3463a76..323c765 100644 --- a/examples/dub.json +++ b/examples/dub.json @@ -3,6 +3,10 @@ "name": "all", "targetType":"none", "dependencies":{ + "all:completion-separate_main-app":"*", + "all:completion-separate_main-completer":"*", + "all:completion-single_main-app":"*", + "all:completion-single_main-completer":"*", "all:getting_started-advanced":"*", "all:getting_started-basic":"*", "all:sub_commands-advanced":"*", @@ -12,6 +16,10 @@ }, "subPackages": [ "..", + "completion/separate_main/app", + "completion/separate_main/completer", + "completion/single_main/app", + "completion/single_main/completer", "getting_started/advanced", "getting_started/basic", "sub_commands/advanced", diff --git a/source/argparse.d b/source/argparse.d index 03438b8..ee5d749 100644 --- a/source/argparse.d +++ b/source/argparse.d @@ -2,6 +2,7 @@ module argparse; import std.typecons: Nullable; +import std.sumtype: SumType, match; import std.traits; private enum DEFAULT_COMMAND = ""; @@ -427,7 +428,7 @@ private alias Restriction = bool delegate(in Config config, in bool[size_t] cliA // Have to do this magic because closures are not supported in CFTE // DMD v2.098.0 prints "Error: closures are not yet supported in CTFE" -auto partiallyApply(alias fun,C...)(C context) +private auto partiallyApply(alias fun,C...)(C context) { import std.traits: ParameterTypeTuple; import core.lifetime: move, forward; @@ -676,42 +677,50 @@ private struct Arguments } } -private alias ParsingArgument(alias symbol, alias uda, ArgumentInfo info, RECEIVER) = +private alias ParsingArgument(alias symbol, alias uda, ArgumentInfo info, RECEIVER, bool completionMode) = delegate(in Config config, string argName, ref RECEIVER receiver, string rawValue, ref string[] rawArgs) { - try + static if(completionMode) { - auto rawValues = rawValue !is null ? [ rawValue ] : consumeValuesFromCLI(rawArgs, info, config); + if(rawValue is null) + consumeValuesFromCLI(rawArgs, info, config); - auto res = info.checkValuesCount(argName, rawValues.length); - if(!res) + return Result.Success; + } + else + { + try { - config.onError(res.errorMsg); - return res; - } + auto rawValues = rawValue !is null ? [ rawValue ] : consumeValuesFromCLI(rawArgs, info, config); - auto param = RawParam(config, argName, rawValues); + auto res = info.checkValuesCount(argName, rawValues.length); + if(!res) + { + config.onError(res.errorMsg); + return res; + } - auto target = &__traits(getMember, receiver, symbol); + auto param = RawParam(config, argName, rawValues); - static if(is(typeof(target) == function) || is(typeof(target) == delegate)) - return uda.parsingFunc.parse(target, param) ? Result.Success : Result.Failure; - else - return uda.parsingFunc.parse(*target, param) ? Result.Success : Result.Failure; - } - catch(Exception e) - { - config.onError(argName, ": ", e.msg); - return Result.Failure; + auto target = &__traits(getMember, receiver, symbol); + + static if(is(typeof(target) == function) || is(typeof(target) == delegate)) + return uda.parsingFunc.parse(target, param) ? Result.Success : Result.Failure; + else + return uda.parsingFunc.parse(*target, param) ? Result.Success : Result.Failure; + } + catch(Exception e) + { + config.onError(argName, ": ", e.msg); + return Result.Failure; + } } }; -private auto ParsingSubCommand(COMMAND_TYPE, CommandInfo info, RECEIVER, alias symbol)(const CommandArguments!RECEIVER* parentArguments) +private auto ParsingSubCommand(COMMAND_TYPE, CommandInfo info, RECEIVER, alias symbol, bool completionMode)(const CommandArguments!RECEIVER* parentArguments) { return delegate(ref Parser parser, const ref Parser.Argument arg, ref RECEIVER receiver) { - import std.sumtype: match; - auto target = &__traits(getMember, receiver, symbol); alias parse = (ref COMMAND_TYPE cmdTarget) @@ -721,7 +730,7 @@ private auto ParsingSubCommand(COMMAND_TYPE, CommandInfo info, RECEIVER, alias s auto command = CommandArguments!TYPE(parser.config, info, parentArguments); - return arg.match!(_ => parser.parse(command, cmdTarget, _)); + return parser.parse!completionMode(command, cmdTarget, arg); }; @@ -919,6 +928,8 @@ struct Result private string errorMsg; + private const(string)[] suggestions; + bool opCast(type)() const if (is(type == bool)) { return status == Status.success; @@ -953,7 +964,6 @@ private struct Parser string value = null; // null when there is no value } - import std.sumtype: SumType; alias Argument = SumType!(Unknown, EndOfArgs, Positional, NamedShort, NamedLong); immutable Config config; @@ -964,8 +974,12 @@ private struct Parser bool[size_t] idxParsedArgs; size_t idxNextPositional = 0; - private alias CmdParser = Result delegate(const ref Argument); - + struct CmdParser + { + Result delegate(const ref Argument) parse; + Result delegate(const ref Argument) complete; + const(string)[] completeSuggestion; + } CmdParser[] cmdStack; Argument splitArgumentNameValue(string arg) @@ -996,7 +1010,7 @@ private struct Parser auto parseArgument(T, PARSE)(PARSE parse, ref T receiver, string value, string nameWithDash, size_t argIndex) { - immutable res = parse(config, nameWithDash, receiver, value, args); + auto res = parse(config, nameWithDash, receiver, value, args); if(!res) return res; @@ -1016,37 +1030,54 @@ private struct Parser if(found.level < cmdStack.length) cmdStack.length = found.level; - cmdStack ~= (const ref arg) => found.parse(this, arg, receiver); + cmdStack ~= CmdParser((const ref arg) => found.parse(this, arg, receiver), (const ref arg) => found.complete(this, arg, receiver)); args.popFront(); return Result.Success; } - auto parse(T)(const ref CommandArguments!T cmd, ref T receiver, Unknown) + auto parse(bool completionMode, T)(const ref CommandArguments!T cmd, ref T receiver, Unknown) { + static if(completionMode) + { + import std.range: front, popFront; + import std.algorithm: filter; + import std.string:startsWith; + import std.array:array; + if(args.length == 1) + { + // last arg means we need to provide args and subcommands + auto A = args[0] == "" ? cmd.completeSuggestion : cmd.completeSuggestion.filter!(_ => _.startsWith(args[0])).array; + return Result(0, Result.Status.success, "", A); + } + } + return Result.UnknownArgument; } - auto parse(T)(const ref CommandArguments!T cmd, ref T receiver, EndOfArgs) + auto parse(bool completionMode, T)(const ref CommandArguments!T cmd, ref T receiver, EndOfArgs) { - static if(is(typeof(cmd.setTrailingArgs))) - cmd.setTrailingArgs(receiver, args[1..$]); - else - unrecognizedArgs ~= args[1..$]; + static if(!completionMode) + { + static if (is(typeof(cmd.setTrailingArgs))) + cmd.setTrailingArgs(receiver, args[1..$]); + else + unrecognizedArgs ~= args[1..$]; + } args = []; return Result.Success; } - auto parse(T)(const ref CommandArguments!T cmd, ref T receiver, Positional) + auto parse(bool completionMode, T)(const ref CommandArguments!T cmd, ref T receiver, Positional) { auto foundArg = cmd.findPositionalArgument(idxNextPositional); if(foundArg.arg is null) return parseSubCommand(cmd, receiver); - immutable res = parseArgument(cmd.parseFunctions[foundArg.index], receiver, null, foundArg.arg.names[0], foundArg.index); + auto res = parseArgument(cmd.getParseFunction!completionMode(foundArg.index), receiver, null, foundArg.arg.names[0], foundArg.index); if(!res) return res; @@ -1055,7 +1086,7 @@ private struct Parser return Result.Success; } - auto parse(T)(const ref CommandArguments!T cmd, ref T receiver, NamedLong arg) + auto parse(bool completionMode, T)(const ref CommandArguments!T cmd, ref T receiver, NamedLong arg) { import std.algorithm : startsWith; import std.range: popFront; @@ -1075,10 +1106,10 @@ private struct Parser return Result.UnknownArgument; args.popFront(); - return parseArgument(cmd.parseFunctions[foundArg.index], receiver, arg.value, arg.nameWithDash, foundArg.index); + return parseArgument(cmd.getParseFunction!completionMode(foundArg.index), receiver, arg.value, arg.nameWithDash, foundArg.index); } - auto parse(T)(const ref CommandArguments!T cmd, ref T receiver, NamedShort arg) + auto parse(bool completionMode, T)(const ref CommandArguments!T cmd, ref T receiver, NamedShort arg) { import std.range: popFront; @@ -1086,7 +1117,7 @@ private struct Parser if(foundArg.arg !is null) { args.popFront(); - return parseArgument(cmd.parseFunctions[foundArg.index], receiver, arg.value, arg.nameWithDash, foundArg.index); + return parseArgument(cmd.getParseFunction!completionMode(foundArg.index), receiver, arg.value, arg.nameWithDash, foundArg.index); } // Try to parse "-ABC..." where "A","B","C" are different single-letter arguments @@ -1116,7 +1147,7 @@ private struct Parser arg.name = ""; } - immutable res = parseArgument(cmd.parseFunctions[foundArg.index], receiver, value, "-"~name, foundArg.index); + auto res = parseArgument(cmd.getParseFunction!completionMode(foundArg.index), receiver, value, "-"~name, foundArg.index); if(!res) return res; } @@ -1126,43 +1157,78 @@ private struct Parser return Result.Success; } - auto parse(Argument arg) + auto parse(bool completionMode, T)(const ref CommandArguments!T cmd, ref T receiver, Argument arg) + { + return arg.match!(_ => parse!completionMode(cmd, receiver, _)); + } + + auto parse(bool completionMode)(Argument arg) { import std.range: front, popFront; + auto result = Result.Success; + + const argsCount = args.length; + foreach_reverse(cmdParser; cmdStack) { - immutable res = cmdParser(arg); - if(res.status != Result.Status.unknownArgument) - return res; + static if(completionMode) + { + auto res = cmdParser.complete(arg); + if(res) + result.suggestions ~= res.suggestions; + } + else + { + auto res = cmdParser.parse(arg); + + if(res.status != Result.Status.unknownArgument) + return res; + } } - unrecognizedArgs ~= args.front; - args.popFront(); + if(args.length > 0 && argsCount == args.length) + { + unrecognizedArgs ~= args.front; + args.popFront(); + } - return Result.Success; + return result; } - auto parseAll(T)(const ref CommandArguments!T cmd, ref T receiver) + auto parseAll(bool completionMode, T)(const ref CommandArguments!T cmd, ref T receiver) { import std.range: empty, front; - cmdStack ~= (const ref arg) + cmdStack ~= CmdParser( + (const ref arg) { - import std.sumtype: match; - - return arg.match!(_ => parse(cmd, receiver, _)); - }; + return parse!completionMode(cmd, receiver, arg); + }, + (const ref arg) + { + return parse!completionMode(cmd, receiver, arg); + }, + cmd.completeSuggestion); auto found = cmd.findSubCommand(DEFAULT_COMMAND); if(found.parse !is null) - cmdStack ~= (const ref arg) => found.parse(this, arg, receiver); + { + cmdStack ~= CmdParser((const ref arg) => found.parse(this, arg, receiver)); + } while(!args.empty) { - immutable res = parse(splitArgumentNameValue(args.front)); + static if(completionMode) + auto res = parse!completionMode(args.length > 1 ? splitArgumentNameValue(args.front) : Argument.init); + else + auto res = parse!completionMode(splitArgumentNameValue(args.front)); if(!res) return res; + + static if(completionMode) + if(args.empty) + return res; } if(!cmd.checkRestrictions(idxParsedArgs, config)) @@ -1197,7 +1263,7 @@ private Result parseCLIKnownArgs(T)(ref T receiver, { auto parser = Parser(config, args); - immutable res = parser.parseAll(cmd, receiver); + auto res = parser.parseAll!false(cmd, receiver); if(!res) return res; @@ -1428,6 +1494,8 @@ unittest static assert(p.findNamedArgument("boo").arg !is null); static assert(p.findPositionalArgument(0).arg !is null); static assert(p.findPositionalArgument(1).arg is null); + static assert(p.getParseFunction!false(p.findNamedArgument("b").index) !is null); + static assert(p.getParseFunction!true(p.findNamedArgument("b").index) !is null); } unittest @@ -1643,8 +1711,6 @@ unittest unittest { - import std.sumtype: SumType, match; - struct T { struct cmd1 { string a; } @@ -1668,8 +1734,6 @@ unittest unittest { - import std.sumtype: SumType, match; - struct T { struct cmd1 { string a; } @@ -1698,6 +1762,157 @@ struct Main } } +private template defaultCommandName(COMMAND) +{ + static if(getUDAs!(COMMAND, CommandInfo).length > 0) + enum defaultCommandName = getUDAs!(COMMAND, CommandInfo)[0].names[0]; + else + enum defaultCommandName = COMMAND.stringof; +} + +private struct Complete(COMMAND) +{ + @(Command("init") + .Description("Print initialization script for shell completion.") + .ShortDescription("Print initialization script.") + ) + struct Init + { + @MutuallyExclusive + { + @(NamedArgument.Description("Provide completion for bash.")) + bool bash; + @(NamedArgument.Description("Provide completion for zsh.")) + bool zsh; + @(NamedArgument.Description("Provide completion for tcsh.")) + bool tcsh; + @(NamedArgument.Description("Provide completion for fish.")) + bool fish; + } + + @(NamedArgument.Description("Path to completer. Default value: path to this executable.")) + string completerPath; // path to this binary + + @(NamedArgument.Description("Command name. Default value: "~defaultCommandName!COMMAND~".")) + string commandName = defaultCommandName!COMMAND; // command to complete + + void execute(Config config)() + { + import std.stdio: writeln; + + if(completerPath.length == 0) + { + import std.file: thisExePath; + completerPath = thisExePath(); + } + + string commandNameArg; + if(commandName != defaultCommandName!COMMAND) + commandNameArg = " --commandName "~commandName; + + if(bash) + { + // According to bash documentation: + // When the function or command is invoked, the first argument ($1) is the name of the command whose + // arguments are being completed, the second` argument ($2) is the word being completed, and the third + // argument ($3) is the word preceding the word being completed on the current command line. + // + // So we add "---" argument to distinguish between the end of actual parameters and those that were added by bash + + writeln("# Add this source command into .bashrc:"); + writeln("# source <(", completerPath, " init --bash", commandNameArg, ")"); + // 'eval' is used to properly get arguments with spaces. For example, in case of "1 2" argument, + // we will get "1 2" as is, compare to "\"1", "2\"" without 'eval'. + writeln("complete -C 'eval ", completerPath, " --bash -- $COMP_LINE ---' ", commandName); + } + else if(zsh) + { + // We use bash completion for zsh + writeln("# Ensure that you called compinit and bashcompinit like below in your .zshrc:"); + writeln("# autoload -Uz compinit && compinit"); + writeln("# autoload -Uz bashcompinit && bashcompinit"); + writeln("# And then add this source command after them into your .zshrc:"); + writeln("# source <(", completerPath, " init --zsh", commandNameArg, ")"); + writeln("complete -C 'eval ", completerPath, " --bash -- $COMP_LINE ---' ", commandName); + } + else if(tcsh) + { + // Comments start with ":" in tsch + writeln(": Add this eval command into .tcshrc: ;"); + writeln(": eval `", completerPath, " init --tcsh", commandNameArg, "` ;"); + writeln("complete ", commandName, " 'p,*,`", completerPath, " --tcsh -- $COMMAND_LINE`,'"); + } + else if(fish) + { + writeln("# Add this source command into ~/.config/fish/config.fish:"); + writeln("# ", completerPath, " init --fish", commandNameArg, " | source"); + writeln("complete -c ", commandName, " -a '(COMMAND_LINE=(commandline -p) ", completerPath, " --fish -- (commandline -op))' --no-files"); + } + } + } + + @(Command("complete") + .Description("Print completion.") + ) + struct Complete + { + @MutuallyExclusive + { + @(NamedArgument.Description("Provide completion for bash.")) + bool bash; + @(NamedArgument.Description("Provide completion for tcsh.")) + bool tcsh; + @(NamedArgument.Description("Provide completion for fish.")) + bool fish; + } + + @TrailingArguments + string[] args; + + void execute(Config config)() + { + import std.process: environment; + import std.stdio: writeln; + import std.algorithm: each; + + if(bash) + { + // According to bash documentation: + // When the function or command is invoked, the first argument ($1) is the name of the command whose + // arguments are being completed, the second` argument ($2) is the word being completed, and the third + // argument ($3) is the word preceding the word being completed on the current command line. + // + // We don't use these arguments so we just remove those after "---" including itself + while(args.length > 0 && args[$-1] != "---") + args = args[0..$-1]; + + // Remove "---" + if(args.length > 0 && args[$-1] == "---") + args = args[0..$-1]; + + // COMP_LINE environment variable contains current command line so if it ends with space ' ' then we + // should provide all available arguments. To do so, we add an empty argument + auto cmdLine = environment.get("COMP_LINE", ""); + if(cmdLine.length > 0 && cmdLine[$-1] == ' ') + args ~= ""; + } + else if(tcsh || fish) + { + // COMMAND_LINE environment variable contains current command line so if it ends with space ' ' then we + // should provide all available arguments. To do so, we add an empty argument + auto cmdLine = environment.get("COMMAND_LINE", ""); + if(cmdLine.length > 0 && cmdLine[$-1] == ' ') + args ~= ""; + } + + CLI!(config, COMMAND).completeArgs(args).each!writeln; + } + } + + @SubCommands + SumType!(Init, Default!Complete) cmd; +} + template CLI(Config config, COMMANDS...) { mixin template main(alias newMain) @@ -1726,7 +1941,7 @@ template CLI(Config config, COMMAND) auto parser = Parser(config, args); auto command = CommandArguments!COMMAND(config); - auto res = parser.parseAll(command, receiver); + auto res = parser.parseAll!false(command, receiver); if(!res) return res; @@ -1794,11 +2009,67 @@ template CLI(Config config, COMMAND) } } - mixin template main(alias newMain) + string[] completeArgs(string[] args) + { + import std.algorithm: sort, uniq; + import std.array: array; + + auto command = CommandArguments!COMMAND(config); + + auto parser = Parser(config, args.length == 0 ? [""] : args); + + COMMAND dummy; + + auto res = parser.parseAll!true(command, dummy); + + return res ? res.suggestions.dup.sort.uniq.array : []; + } + + int complete(string[] args) + { + // dmd fails with core.exception.OutOfMemoryError@core\lifetime.d(137): Memory allocation failed + // if we call anything from CLI!(config, Complete!COMMAND) so we have to directly call parser here + + Complete!COMMAND receiver; + + auto parser = Parser(config, args); + + auto command = CommandArguments!(Complete!COMMAND)(config); + auto res = parser.parseAll!false(command, receiver); + if(!res) + return 1; + + if(res && parser.unrecognizedArgs.length > 0) + { + config.onError("Unrecognized arguments: ", parser.unrecognizedArgs); + return 1; + } + + receiver.cmd.match!(_ => _.execute!config()); + + return 0; + } + + mixin template mainComplete() { int main(string[] argv) { - return CLI!(config, COMMAND).parseArgs!(newMain)(argv[1..$]); + return CLI!(config, COMMAND).complete(argv[1..$]); + } + } + + mixin template main(alias newMain) + { + version(argparse_completion) + { + mixin CLI!(config, COMMAND).mainComplete; + } + else + { + int main(string[] argv) + { + return CLI!(config, COMMAND).parseArgs!(newMain)(argv[1..$]); + } } } } @@ -1815,6 +2086,69 @@ template CLI(Config config) alias CLI(COMMANDS...) = CLI!(Config.init, COMMANDS); +unittest +{ + struct T + { + struct cmd1 + { + string foo; + string bar; + string baz; + } + struct cmd2 + { + string cat,can,dog; + } + + @NamedArgument("apple","a") + string a = "dummyA"; + @NamedArgument + string s = "dummyS"; + @NamedArgument + string b = "dummyB"; + + @SubCommands + SumType!(cmd1, cmd2) cmd; + } + + assert(CLI!T.completeArgs([]) == ["--apple","--help","-a","-b","-h","-s","cmd1","cmd2"]); + assert(CLI!T.completeArgs([""]) == ["--apple","--help","-a","-b","-h","-s","cmd1","cmd2"]); + assert(CLI!T.completeArgs(["-a"]) == ["-a"]); + assert(CLI!T.completeArgs(["c"]) == ["cmd1","cmd2"]); + assert(CLI!T.completeArgs(["cmd1"]) == ["cmd1"]); + assert(CLI!T.completeArgs(["cmd1",""]) == ["--apple","--bar","--baz","--foo","--help","-a","-b","-h","-s","cmd1","cmd2"]); + assert(CLI!T.completeArgs(["-a","val-a",""]) == ["--apple","--help","-a","-b","-h","-s","cmd1","cmd2"]); + + assert(!CLI!T.complete(["init","--bash","--commandName","mytool"])); + assert(!CLI!T.complete(["init","--zsh"])); + assert(!CLI!T.complete(["init","--tcsh"])); + assert(!CLI!T.complete(["init","--fish"])); + + assert(CLI!T.complete(["init","--unknown"])); + + import std.process: environment; + { + environment["COMP_LINE"] = "mytool "; + assert(!CLI!T.complete(["--bash","--","---","foo","foo"])); + + environment["COMP_LINE"] = "mytool c"; + assert(!CLI!T.complete(["--bash","--","c","---"])); + + environment.remove("COMP_LINE"); + } + { + environment["COMMAND_LINE"] = "mytool "; + assert(!CLI!T.complete(["--tcsh","--"])); + + environment["COMMAND_LINE"] = "mytool c"; + assert(!CLI!T.complete(["--fish","--","c"])); + + environment.remove("COMMAND_LINE"); + } +} + + unittest { struct T @@ -3385,7 +3719,8 @@ private struct CommandArguments(RECEIVER) Arguments arguments; - ParseFunction!RECEIVER[] parseFunctions; + ParseFunction!RECEIVER[] parseArguments; + ParseFunction!RECEIVER[] completeArguments; uint level; // (sub-)command level, 0 = top level @@ -3393,6 +3728,11 @@ private struct CommandArguments(RECEIVER) size_t[string] subCommandsByName; CommandInfo[] subCommands; ParseSubCommandFunction!RECEIVER[] parseSubCommands; + ParseSubCommandFunction!RECEIVER[] completeSubCommands; + + // completion + string[] completeSuggestion; + mixin ForwardMemberFunction!"arguments.findPositionalArgument"; mixin ForwardMemberFunction!"arguments.findNamedArgument"; @@ -3433,7 +3773,7 @@ private struct CommandArguments(RECEIVER) if(config.addHelp) { arguments.addArgument!helpArgument; - parseFunctions ~= delegate (in Config config, string argName, ref RECEIVER receiver, string rawValue, ref string[] rawArgs) + parseArguments ~= delegate (in Config config, string argName, ref RECEIVER receiver, string rawValue, ref string[] rawArgs) { import std.stdio: stdout; @@ -3441,7 +3781,20 @@ private struct CommandArguments(RECEIVER) return Result(0); }; + completeArguments ~= delegate (in Config config, string argName, ref RECEIVER receiver, string rawValue, ref string[] rawArgs) + { + return Result.Success; + }; } + + + import std.algorithm: sort, map; + import std.range: join; + import std.array: array; + + completeSuggestion = arguments.argsNamed.keys.map!(_ => getArgumentName(_, config)).array; + completeSuggestion ~= subCommandsByName.keys.array; + completeSuggestion.sort; } private void fillArguments() @@ -3504,7 +3857,8 @@ private struct CommandArguments(RECEIVER) else arguments.addArgument!(info, restrictions); - parseFunctions ~= ParsingArgument!(symbol, uda, info, RECEIVER); + parseArguments ~= ParsingArgument!(symbol, uda, info, RECEIVER, false); + completeArguments ~= ParsingArgument!(symbol, uda, info, RECEIVER, true); } private void addSubCommands(alias symbol)() @@ -3557,20 +3911,30 @@ private struct CommandArguments(RECEIVER) subCommands ~= info; //group.arguments ~= index; - parseSubCommands ~= ParsingSubCommand!(TYPE, info, RECEIVER, symbol)(&this); + parseSubCommands ~= ParsingSubCommand!(TYPE, info, RECEIVER, symbol, false)(&this); + completeSubCommands ~= ParsingSubCommand!(TYPE, info, RECEIVER, symbol, true)(&this); }} } + auto getParseFunction(bool completionMode)(size_t index) const + { + static if(completionMode) + return completeArguments[index]; + else + return parseArguments[index]; + } + auto findSubCommand(string name) const { struct Result { uint level = uint.max; ParseSubCommandFunction!RECEIVER parse; + ParseSubCommandFunction!RECEIVER complete; } auto p = arguments.convertCase(name) in subCommandsByName; - return !p ? Result.init : Result(level+1, parseSubCommands[*p]); + return !p ? Result.init : Result(level+1, parseSubCommands[*p], completeSubCommands[*p]); } static if(getSymbolsByUDA!(RECEIVER, TrailingArguments).length == 1) @@ -3635,8 +3999,12 @@ unittest private string getArgumentName(string name, in Config config) { - name = config.namedArgChar ~ name; - return name.length > 2 ? config.namedArgChar ~ name : name; + import std.conv: to; + + immutable dash = config.namedArgChar.to!string; + + name = dash ~ name; + return name.length > 2 ? dash ~ name : name; } unittest @@ -4126,8 +4494,6 @@ unittest unittest { - import std.sumtype: SumType, match; - @Command("MYPROG") struct T {