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
{