Skip to content

Commit

Permalink
Shell completion (#40)
Browse files Browse the repository at this point in the history
* Small changes

* Add completion mode to parser

* Add bash completion

* Add zsh completion

* Add tcsh completion

* Add fish completion

* Rename parseFunctions -> parseArguments

* Avoid parsing in completion mode

* Add CLI.mainComplete

* Add examples

* Fix description

* Update readme

* Fix the build

* Add unit tests
  • Loading branch information
andrey-zherikov authored May 25, 2022
1 parent 3f27b2c commit ce4ca59
Show file tree
Hide file tree
Showing 11 changed files with 763 additions and 203 deletions.
350 changes: 221 additions & 129 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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/<br/>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.<br/>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/<br/>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

Expand All @@ -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
Expand Down Expand Up @@ -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:**

Expand All @@ -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:**

Expand All @@ -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:
Expand Down Expand Up @@ -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 `<completer> 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>`: path to completer. By default, the path to itself is used.
- `--commandName <name>`: 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 `<completer> 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 `<completer> 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/<br/>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.<br/>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/<br/>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

Expand Down
Loading

0 comments on commit ce4ca59

Please sign in to comment.