diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 0741c84..0273bc5 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -15,7 +15,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] - dc: [dmd-latest, ldc-latest] + dc: [dmd-latest, ldc-1.34.0] runs-on: ${{ matrix.os }} steps: @@ -27,25 +27,25 @@ jobs: compiler: ${{ matrix.dc }} - name: Run tests - run: dub test --build=unittest-cov + run: dub test --build=unittest-cov --verbose - name: Run tests (dip1000) env: DFLAGS: -dip1000 - run: dub test --build=unittest-cov + run: dub test --build=unittest-cov --verbose - name: Run tests (in) env: DFLAGS: -preview=in - run: dub test --build=unittest-cov + run: dub test --build=unittest-cov --verbose - name: Run tests (dip1000 & in) env: DFLAGS: -dip1000 -preview=in - run: dub test --build=unittest-cov + run: dub test --build=unittest-cov --verbose - name: Build examples - run: dub --root examples build --parallel + run: dub --root examples build --parallel --verbose - name: Upload coverage to Codecov uses: codecov/codecov-action@v2 diff --git a/README.md b/README.md index 2af4f2b..05cadb8 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # Parser for command-line arguments -`argparse` is a self-contained flexible utility to parse command line arguments. +`argparse` is a self-contained flexible utility to parse command-line arguments. ## Features @@ -11,14 +11,14 @@ - Automatic type conversion of the value. - Required by default, can be marked as optional. - [Named arguments](#named-arguments): - - Multiple names are supported including short (`-v`) and long (`--verbose`) ones. + - Multiple names are supported, including short (`-v`) and long (`--verbose`) ones. - [Case-sensitive/-insensitive parsing.](#case-sensitivity) - [Bundling of short names](#bundling-of-single-letter-arguments) (`-vvv` is same as `-v -v -v`). - [Equals sign is accepted](#assign-character) (`-v=debug`, `--verbose=debug`). - Automatic type conversion of the value. - Optional by default, can be marked as required. - [Support different types of destination data member](#supported-types): - - Scalar (e.g. `int`, `float`, `bool`). + - Scalar (e.g., `int`, `float`, `bool`). - String arguments. - Enum arguments. - Array arguments. @@ -28,17 +28,17 @@ - Mixin to inject standard `main` function. - Parsing of known arguments only (returning not recognized ones). - Enforcing that there are no unknown arguments provided. -- [Shell completion](#shell-completion) -- [Options terminator](#trailing-arguments) (e.g. parsing up to `--` leaving any argument specified after it). +- [Shell completion](#shell-completion). +- [Options terminator](#trailing-arguments) (e.g., parsing up to `--` leaving any argument specified after it). - [Arguments groups](#argument-dependencies). - [Subcommands](#commands). - [Fully customizable parsing](#argument-parsing-customization): - - Raw (`string`) data validation (i.e. before parsing). + - Raw (`string`) data validation (i.e., before parsing). - Custom conversion of argument value (`string` -> any `destination type`). - - Validation of parsed data (i.e. after conversion to `destination type`). + - Validation of parsed data (i.e., after conversion to `destination type`). - Custom action on parsed data (doing something different from storing the parsed value in a member of destination object). -- [ANSI colors and styles](#ansi-colors-and-styles) +- [ANSI colors and styles](#ansi-colors-and-styles). - [Built-in reporting of error happened during argument parsing](#error-handling). - [Built-in help generation](#help-generation). @@ -54,39 +54,39 @@ import argparse; struct Basic { // Basic data types are supported: - // --name argument + // '--name' argument string name; - - // --number argument + + // '--number' argument int number; - - // --boolean argument + + // '--boolean' argument bool boolean; // Argument can have default value if it's not specified in command line - // --unused argument + // '--unused' argument string unused = "some default value"; // Enums are also supported enum Enum { unset, foo, boo } - // --choice argument + // '--choice' argument Enum choice; // Use array to store multiple values - // --array argument + // '--array' argument int[] array; // Callback with no args (flag) - // --callback argument + // '--callback' argument void callback() {} // Callback with single value - // --callback1 argument + // '--callback1' argument void callback1(string value) { assert(value == "cb-value"); } // Callback with zero or more values - // --callback2 argument + // '--callback2' argument void callback2(string[] value) { assert(value == ["cb-v1","cb-v2"]); } } @@ -95,7 +95,7 @@ mixin CLI!Basic.main!((args) { // 'args' has 'Basic' type static assert(is(typeof(args) == Basic)); - + // do whatever you need import std.stdio: writeln; args.writeln; @@ -103,25 +103,22 @@ mixin CLI!Basic.main!((args) }); ``` -If you run the program above with `-h` argument then you'll see the following output: +If you run the program above with `-h` argument, then you’ll see the following output: ``` -usage: hello_world [--name NAME] [--number NUMBER] [--boolean [BOOLEAN]] [--unused UNUSED] [--choice {unset,foo,boo}] [--array ARRAY ...] [--callback] [--callback1 CALLBACK1] [--callback2 [CALLBACK2 ...]] [-h] +Usage: hello_world [--name NAME] [--number NUMBER] [--boolean] [--unused UNUSED] [--choice {unset,foo,boo}] [--array ARRAY ...] [--callback] [--callback1 CALLBACK1] [--callback2 [CALLBACK2 ...]] [-h] Optional arguments: --name NAME --number NUMBER - --boolean [BOOLEAN] + --boolean --unused UNUSED --choice {unset,foo,boo} - --array ARRAY ... --callback --callback1 CALLBACK1 - --callback2 [CALLBACK2 ...] - - -h, --help Show this help message and exit + -h, --help Show this help message and exit ``` For more sophisticated CLI usage, `argparse` provides few UDAs: @@ -144,7 +141,7 @@ struct Advanced // Named argument can have custom or multiple names @NamedArgument("apple","appl") int apple; - + @NamedArgument(["b","banana","ban"]) int banana; } @@ -166,10 +163,10 @@ mixin CLI!Advanced.main!((args, unparsed) }); ``` -If you run it with `-h` argument then you'll see the following: +If you run it with `-h` argument, then you’ll see the following: ``` -usage: hello_world name [--unused UNUSED] [--number NUMBER] [--boolean [BOOLEAN]] [--apple APPLE] [-b BANANA] [-h] +Usage: hello_world [--unused UNUSED] [--number NUMBER] [--boolean] [--apple APPLE] [-b BANANA] [-h] name Required arguments: name @@ -177,29 +174,28 @@ Required arguments: Optional arguments: --unused UNUSED --number NUMBER - --boolean [BOOLEAN] - --apple APPLE + --boolean + --apple APPLE, --appl APPLE -b BANANA, --banana BANANA, --ban BANANA - - -h, --help Show this help message and exit + -h, --help Show this help message and exit ``` ## Calling the parser `argparse` provides `CLI` template to call the parser covering different use cases. It has the following signatures: -- `template CLI(Config config, COMMAND)` - this is main template that provides multiple API (see below) for all +- `template CLI(Config config, COMMAND)` – this is main template that provides multiple API (see below) for all supported use cases. -- `template CLI(Config config, COMMANDS...)` - convenience wrapper of the previous template that provides `main` +- `template CLI(Config config, COMMANDS...)` – convenience wrapper of the previous template that provides `main` template mixin only for the simplest use case with subcommands. See corresponding [section](#commands) for details about subcommands. -- `alias CLI(COMMANDS...) = CLI!(Config.init, COMMANDS)` - alias provided for convenience that allows using default - `Config`, i.e. `config = Config.init`. +- `alias CLI(COMMANDS...) = CLI!(Config.init, COMMANDS)` – alias provided for convenience that allows using default + `Config`, i.e., `config = Config.init`. ### 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 +It declares the standard `main` function that parses command-line arguments and calls provided `newMain` function with an object that contains parsed arguments. `newMain` function must satisfy these requirements: @@ -210,7 +206,7 @@ an object that contains parsed arguments. compile-time checking of the type of the first parameter (see examples below for details). - Optionally `newMain` function can take a `string[]` parameter as a second argument. Providing such a function will mean that `argparse` will parse known arguments only and all unknown ones will be passed as a second parameter to - `newMain` function. If `newMain` function doesn't have such parameter then `argparse` will error out if there is an + `newMain` function. If `newMain` function doesn’t have such parameter, then `argparse` will error out if there is an unknown argument provided in command line. - Optionally `newMain` can return a result that can be cast to `int`. In this case, this result will be returned from standard `main` function. @@ -254,9 +250,9 @@ mixin CLI!(cmd1, cmd2).main!((args, unparsed) writeln("cmd1: ", args); else static if(is(typeof(args) == cmd2)) writeln("cmd2: ", args); - else + else static assert(false); // this would never happen - + // unparsed arguments has 'string[]' type static assert(is(typeof(unparsed) == string[])); @@ -266,23 +262,23 @@ mixin CLI!(cmd1, cmd2).main!((args, unparsed) ### Providing a new `main` function without wrapping standard `main` -If wrapping of standard `main` function doesn't fit your needs (e.g. you need to do some initialization before parsing -the command line) then you can use `CLI!(...).parseArgs` function: +If wrapping of standard `main` function doesn’t fit your needs (e.g., you need to do some initialization before parsing +the command line), then you can use `CLI!(...).parseArgs` function: `int parseArgs(alias newMain)(string[] args, COMMAND initialValue = COMMAND.init)` **Parameters:** -- `newMain` - function that's called with object of type `COMMAND` as a first parameter filled with the data parsed from +- `newMain` – function that’s called with object of type `COMMAND` as a first parameter filled with the data parsed from command line; optionally it can take `string[]` as a second parameter which will contain unknown arguments (see [Wrapper for main function](#wrapper-for-main-function) section for details). -- `args` - raw command line arguments (excluding `argv[0]` - first command line argument in `main` function). -- `initialValue` - initial value for the object passed to `newMain` function. +- `args` – raw command-line arguments (excluding `argv[0]` – first command-line argument in `main` function). +- `initialValue` – initial value for the object passed to `newMain` function. **Return value:** -If there is an error happened during the parsing then non-zero value is returned. In case of no error, if `newMain` -function returns a value that can be cast to `int` then this value is returned or `0` otherwise. +If there is an error happened during the parsing, then non-zero value is returned. In case of no error, if `newMain` +function returns a value that can be cast to `int`, then this value is returned, or `0` otherwise. **Usage example:** @@ -313,12 +309,12 @@ int main(string[] args) For the cases when providing `newMain` function is not possible or feasible, `parseArgs` function can accept a reference to an object that receives the values of command line arguments: -`Result parseArgs(ref COMMAND receiver, string[] args))` +`Result parseArgs(ref COMMAND receiver, string[] args)` **Parameters:** -- `receiver` - object that is populated with parsed values. -- `args` - raw command line arguments (excluding `argv[0]` - first command line argument in `main` function). +- `receiver` – object that is populated with parsed values. +- `args` – raw command-line arguments (excluding `argv[0]` – first command-line argument in `main` function). **Return value:** @@ -340,9 +336,9 @@ int main(string[] argv) if(!CLI!COMMAND.parseArgs(cmd, argv[1..$])) return 1; // parsing failure - + // Do whatever is needed - + return 0; } ``` @@ -358,9 +354,9 @@ that it does not produce an error when unknown arguments are present. It has the **Parameters:** - - `receiver` - the object that's populated with parsed values. - - `args` - raw command line arguments (excluding `argv[0]` - first command line argument in `main` function). - - `unrecognizedArgs` - raw command line arguments that were not parsed. + - `receiver` – the object that’s populated with parsed values. + - `args` – raw command-line arguments (excluding `argv[0]` – first command-line argument in `main` function). + - `unrecognizedArgs` – raw command-line arguments that were not parsed. **Return value:** @@ -370,9 +366,9 @@ that it does not produce an error when unknown arguments are present. It has the **Parameters:** - - `receiver` - the object that's populated with parsed values. - - `args` - raw command line arguments that are modified to have parsed arguments removed (excluding `argv[0]` - first - command line argument in `main` function). + - `receiver` – the object that’s populated with parsed values. + - `args` – raw command-line arguments that are modified to have parsed arguments removed (excluding `argv[0]` – first + command-line argument in `main` function). **Return value:** @@ -389,7 +385,7 @@ struct T auto args = [ "-a", "A", "-c", "C" ]; T result; -assert(CLI!T.parseKnownArgs!(result, args)); +assert(CLI!T.parseKnownArgs(result, args)); assert(result == T("A")); assert(args == ["-c", "C"]); ``` @@ -397,40 +393,40 @@ assert(args == ["-c", "C"]); ## 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. +`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 +`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. +`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: +If you prefer having separate main module for completer, then you can use `CLI!(...).mainComplete` mixin template: ```d -mixin CLI!(...).completeMain; +mixin CLI!(...).mainComplete; ``` 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. +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 +`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`. +- 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 start 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"]`. @@ -462,7 +458,7 @@ 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 +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: @@ -477,12 +473,12 @@ 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`): +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 +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 @@ -508,11 +504,11 @@ 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. | +| 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 +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 @@ -538,17 +534,17 @@ Parameters of `NamedArgument` UDA: 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. +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. +`--name John`, `--name=John`, `--n John`, `--n=John`, `-nJohn`, `-n John`, `-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 +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 @@ -563,12 +559,13 @@ struct T 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. +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 arguments are required by default. +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 arguments are required by default. ```d struct T @@ -605,7 +602,7 @@ 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 +Note that if the type of destination variable is `enum`, then the allowed values are automatically limited to those listed in the `enum`. @@ -613,7 +610,7 @@ listed in the `enum`. ### Mutually exclusive arguments -Mutually exclusive arguments (i.e. those that can't be used together) can be declared using `MutuallyExclusive()` UDA: +Mutually exclusive arguments (i.e., those that can’t be used together) can be declared using `MutuallyExclusive()` UDA: ```d struct T @@ -634,7 +631,7 @@ assert(CLI!T.parseArgs!((T t) {})([]) == 0); assert(CLI!T.parseArgs!((T t) { assert(false); })(["-a","a","-b","b"]) != 0); ``` -**Note that parenthesis are required in this UDA to work correctly.** +**Note that parentheses are required in this UDA to work correctly.** Set of mutually exclusive arguments can be marked as required in order to require exactly one of the arguments: @@ -659,7 +656,7 @@ assert(CLI!T.parseArgs!((T t) { assert(false); })(["-a","a","-b","b"]) != 0); ### Mutually required arguments -Mutually required arguments (i.e. those that require other arguments) can be declared using `RequiredTogether()` UDA: +Mutually required arguments (i.e., those that require other arguments) can be declared using `RequiredTogether()` UDA: ```d struct T @@ -680,7 +677,7 @@ assert(CLI!T.parseArgs!((T t) { assert(false); })(["-a","a"]) != 0); assert(CLI!T.parseArgs!((T t) { assert(false); })(["-b","b"]) != 0); ``` -**Note that parenthesis are required in this UDA to work correctly.** +**Note that parentheses are required in this UDA to work correctly.** Set of mutually required arguments can be marked as required in order to require all arguments: @@ -705,8 +702,8 @@ assert(CLI!T.parseArgs!((T t) { assert(false); })([]) != 0); ## Commands -Sophisticated command-line tools, like `git`, have many subcommands (e.g., `commit`, `push` etc.), each with its own set -of arguments. There are few ways to declare subcommands with `argparse`. +Sophisticated command-line tools, like `git`, have many subcommands (e.g., `commit`, `push`, etc.), each with its own +set of arguments. There are few ways to declare subcommands with `argparse`. ### Subcommands without UDA @@ -812,7 +809,7 @@ mixin CLI!Program.main!((prog) ### Subcommand name and aliases -To define a command name that is not the same as the type that represents this command, one should use `Command` UDA - +To define a command name that is not the same as the type that represents this command, one should use `Command` UDA – it accepts a name and list of name aliases. All these names are recognized by the parser and are displayed in the help text. For example: @@ -832,14 +829,14 @@ Would result in this help fragment: maximum,max Print the maximum ``` -If `Command` has no names listed then the name of the type is used as a command name: +If `Command` has no names listed, then the name of the type is used as a command name: ``` MaxCmd Print the maximum ``` ### Default subcommand -The default command is a command that is ran when user doesn't specify any command in the command line. +The default command is a command that is ran when user doesn’t specify any command in the command line. To mark a command as default, one should use `Default` template: ```d @@ -852,29 +849,35 @@ SumType!(sum, min, Default!max) cmd; `Command` UDA provides few customizations that affect help text. It can be used for **top-level command** and **subcommands**. -- Program name (i.e. the name of top-level command) and subcommand name can be provided to `Command` UDA as a parameter. - If program name is not provided then `Runtime.args[0]` (a.k.a. `argv[0]` from `main` function) is used. If subcommand name is not provided then the name of +- Program name (i.e., the name of top-level command) and subcommand name can be provided to `Command` UDA as a parameter. + If program name is not provided, then `Runtime.args[0]` (a.k.a. `argv[0]` from `main` function) is used. If subcommand name is not provided, then the name of the type that represents the command is used. -- `Usage` - allows custom usage text. By default, the parser calculates the usage message from the arguments it contains +- `Usage` – allows custom usage text. By default, the parser calculates the usage message from the arguments it contains but this can be overridden with `Usage` call. If the custom text contains `%(PROG)` then it will be replaced by the command/program name. -- `Description` - used to provide a description of what the command/program does and how it works. In help messages, the +- `Description` – used to provide a description of what the command/program does and how it works. In help messages, the description is displayed between the usage string and the list of the command arguments. -- `ShortDescription` - used to provide a brief description of what the subcommand does. It is applicable to subcommands only - and is displayed in "Available commands" section on help screen of the parent command. -- `Epilog` - custom text that is printed after the list of the arguments. +- `ShortDescription` – used to provide a brief description of what the subcommand does. It is applicable to subcommands + only and is displayed in *Available commands* section on help screen of the parent command. +- `Epilog` – custom text that is printed after the list of the arguments. -`Usage`, `Description`, `ShortDescription` and `Epilog` modifiers take either `string` or `string delegate()` value - -the latter can be used to return a value that is not known at compile time. +`Usage`, `Description`, `ShortDescription` and `Epilog` modifiers take either `string` or `string delegate()` +value – the latter can be used to return a value that is not known at compile time. ### Argument There are some customizations supported on argument level for both `PositionalArgument` and `NamedArgument` UDAs: -- `Description` - provides brief description of the argument. This text is printed next to the argument in the argument - list section of a help message. `Description` takes either `string` or `string delegate()` value - the latter can be used to return a value that is not known at compile time. -- `HideFromHelp` - can be used to indicate that the argument shouldn't be printed in help message. -- `Placeholder` - provides custom text that it used to indicate the value of the argument in help message. +- `Description` – provides brief description of the argument. This text is printed next to the argument + in the argument-list section of a help message. `Description` takes either `string` or `string delegate()` + value – the latter can be used to return a value that is not known at compile time. +- `HideFromHelp` – can be used to indicate that the argument shouldn’t be printed in help message. +- `Placeholder` – provides custom text that is used to indicate the value of the argument in help message. + +### Help text styling + +`argparse` uses `Config.styling` to determine what style should be applied to different parts of the help text. +Please refer to [Styling scheme](#styling-scheme) for available settings. ### Example @@ -909,25 +912,25 @@ CLI!T.parseArgs!((T t) {})(["-h"]); This example will print the following help message: ``` -usage: MYPROG [-s S] [-p VALUE] -f {apple,pear} [-i {1,4,16,8}] [-h] param0 {q,a} +Usage: MYPROG [-s S] [-p VALUE] -f {apple,pear} [-i {1,4,16,8}] [-h] param0 {q,a} custom description Required arguments: -f {apple,pear}, --fruit {apple,pear} - This is a help text for fruit. Very very very very - very very very very very very very very very very - very very very very very long text - param0 This is a help text for param0. Very very very very - very very very very very very very very very very - very very very very very long text + This is a help text for fruit. Very very very very very very + very very very very very very very very very very very very + very long text + param0 This is a help text for param0. Very very very very very very + very very very very very very very very very very very very + very long text {q,a} Optional arguments: -s S -p VALUE -i {1,4,16,8} - -h, --help Show this help message and exit + -h, --help Show this help message and exit custom epilog ``` @@ -940,8 +943,9 @@ created using `ArgumentGroup` UDA. This UDA has some customization for displaying text: -- `Description` - provides brief description of the group. This text is printed right after group name. - It takes either `string` or `string delegate()` value - the latter can be used to return a value that is not known at compile time. +- `Description` – provides brief description of the group. This text is printed right after group name. + It takes either `string` or `string delegate()` value – the latter can be used to return a value that is not known + at compile time. Example: @@ -972,23 +976,23 @@ When an argument is attributed with a group, the parser treats it just like a no in a separate group for help messages: ``` -usage: MYPROG [-a A] [-b B] [-c C] [-d D] [-h] p q +Usage: MYPROG [-a A] [-b B] [-c C] [-d D] [-h] p q group1: group1 description - -a A - -b B - p + -a A + -b B + p group2: group2 description - -c C - -d D + -c C + -d D Required arguments: - q + q Optional arguments: -h, --help Show this help message and exit @@ -1049,7 +1053,7 @@ The `argparse.ansi` submodule provides supported styles and colors. You can use - `onLightCyan` - `onWhite` -There is also a "virtual" style `noStyle` that means no styling is applied. It's useful in ternary operations as a fallback +There is also a “virtual” style `noStyle` that means no styling is applied. It’s useful in ternary operations as a fallback for the case when styling is disabled. See below example for details. All styles above can be combined using `.` and even be used in regular output: @@ -1060,9 +1064,9 @@ void printText(bool enableStyle) { // style is enabled at runtime when `enableStyle` is true auto myStyle = enableStyle ? bold.italic.cyan.onRed : noStyle; - + // "Hello" is always printed in green; - // "world!" is printed in bold, italic, cyan and on red when `enableStyle` is true, "as is" otherwise + // "world!" is printed in bold, italic, cyan and on red when `enableStyle` is true, or "as is" otherwise writeln(green("Hello "), myStyle("world!")); } ``` @@ -1076,16 +1080,16 @@ This example shows how styling can be used in custom help text (`Usage`, `Descri ### Styling mode -By default `argparse` will try to detect whether ANSI styling is supported and if so it will apply styling to the help text. -In some cases this behavior should be adjusted or overridden. To do so you can use `Config.stylingMode`: +By default `argparse` will try to detect whether ANSI styling is supported, and if so, it will apply styling to the help text. +In some cases this behavior should be adjusted or overridden. To do so, you can use `Config.stylingMode`. Argparse provides the following setting to control the styling: -- If it's set to `Config.StylingMode.on` then styling is **always enabled**. -- If it's set to `Config.StylingMode.off` then styling is **always disabled**. -- If it's set to `Config.StylingMode.autodetect` then [heuristics](#heuristics-for-enabling-styling) are used to determine +- If it’s set to `Config.StylingMode.on`, then styling is **always enabled**. +- If it’s set to `Config.StylingMode.off`, then styling is **always disabled**. +- If it’s set to `Config.StylingMode.autodetect`, then [heuristics](#heuristics-for-enabling-styling) are used to determine whether styling will be applied. -In some cases styling control should be exposed to a user as a command line argument (similar to `--color` argument in `ls` and `grep` commands). -Argparse supports this use case - just add an argument to your command (you can customize it with `@NamedArgument` UDA): +In some cases styling control should be exposed to a user as a command-line argument (similar to `--color` argument in `ls` and `grep` commands). +Argparse supports this use case – just add an argument to your command (you can customize it with `@NamedArgument` UDA): ```d static auto color = ansiStylingArgument; @@ -1109,51 +1113,39 @@ struct Arguments mixin CLI!Arguments.main!((args) { - // 'autodetect' is converted to either 'on' or 'off' - if(args.color == Config.StylingMode.on) + // 'autodetect' is converted to either 'on' or 'off' + if(args.color) writeln("Colors are enabled"); else writeln("Colors are disabled"); }); ``` -### Help text styling scheme - -`argparse` uses `Config.helpStyle` to determine what style should be applied to different parts of help text. -This parameter has the following members that can be tuned: - -- `programName`: style for the program name. Default is `bold`. -- `subcommandName`: style for the subcommand name. Default is `bold`. -- `argumentGroupTitle`: style for the title of argument group. Default is `bold.underline`. -- `namedArgumentName`: style for the name of named argument. Default is `lightYellow`. -- `namedArgumentValue`: style for the value of named argument. Default is `italic`. -- `positionalArgumentValue`: style for the value of positional argument. Default is `lightYellow`. - ### Heuristics for enabling styling -Below is the exact sequence of steps argparse uses to determine whether or not to emit ANSI escape codes +Below is the exact sequence of steps `argparse` uses to determine whether or not to emit ANSI escape codes (see detectSupport() function [here](https://github.com/andrey-zherikov/argparse/blob/master/source/argparse/ansi.d) for details): -1. If environment variable `NO_COLOR != ""` then styling is **disabled**. See [here](https://no-color.org/) for details. -2. If environment variable `CLICOLOR_FORCE != "0"` then styling is **enabled**. See [here](https://bixense.com/clicolors/) for details. -3. If environment variable `CLICOLOR == "0"` then styling is **disabled**. See [here](https://bixense.com/clicolors/) for details. -4. If environment variable `ConEmuANSI == "OFF"` then styling is **disabled**. See [here](https://conemu.github.io/en/AnsiEscapeCodes.html#Environment_variable) for details. -5. If environment variable `ConEmuANSI == "ON"` then styling is **enabled**. See [here](https://conemu.github.io/en/AnsiEscapeCodes.html#Environment_variable) for details. -6. If environment variable `ANSICON` is defined (regardless of its value) then styling is **enabled**. See [here](https://github.com/adoxa/ansicon/blob/master/readme.txt) for details. +1. If environment variable `NO_COLOR != ""`, then styling is **disabled**. See [here](https://no-color.org/) for details. +2. If environment variable `CLICOLOR_FORCE != "0"`, then styling is **enabled**. See [here](https://bixense.com/clicolors/) for details. +3. If environment variable `CLICOLOR == "0"`, then styling is **disabled**. See [here](https://bixense.com/clicolors/) for details. +4. If environment variable `ConEmuANSI == "OFF"`, then styling is **disabled**. See [here](https://conemu.github.io/en/AnsiEscapeCodes.html#Environment_variable) for details. +5. If environment variable `ConEmuANSI == "ON"`, then styling is **enabled**. See [here](https://conemu.github.io/en/AnsiEscapeCodes.html#Environment_variable) for details. +6. If environment variable `ANSICON` is defined (regardless of its value), then styling is **enabled**. See [here](https://github.com/adoxa/ansicon/blob/master/readme.txt) for details. 7. **Windows only** (`version(Windows)`): - 1. If environment variable `TERM` contains `"cygwin"` or starts with `"xterm"` then styling is **enabled**. - 2. If `GetConsoleMode` call for `STD_OUTPUT_HANDLE` returns a mode that has `ENABLE_VIRTUAL_TERMINAL_PROCESSING` set then styling is **enabled**. - 3. If `SetConsoleMode` call for `STD_OUTPUT_HANDLE` with `ENABLE_VIRTUAL_TERMINAL_PROCESSING` mode was successful then styling is **enabled**. + 1. If environment variable `TERM` contains `"cygwin"` or starts with `"xterm"`, then styling is **enabled**. + 2. If `GetConsoleMode` call for `STD_OUTPUT_HANDLE` returns a mode that has `ENABLE_VIRTUAL_TERMINAL_PROCESSING` set, then styling is **enabled**. + 3. If `SetConsoleMode` call for `STD_OUTPUT_HANDLE` with `ENABLE_VIRTUAL_TERMINAL_PROCESSING` mode was successful, then styling is **enabled**. 8. **Posix only** (`version(Posix)`): - 1. If `STDOUT` is **not** redirected then styling is **enabled**. -9. If none of the above applies then styling is **disabled**. + 1. If `STDOUT` is **not** redirected, then styling is **enabled**. +9. If none of the above applies, then styling is **disabled**. ## Supported types ### Boolean -Boolean types usually represent command line flags. `argparse` supports multiple ways of providing flag value: +Boolean types usually represent command-line flags. `argparse` supports multiple ways of providing flag value: ```d struct T @@ -1162,10 +1154,10 @@ struct T } assert(CLI!T.parseArgs!((T t) { assert(t == T(true)); })(["-b"]) == 0); -assert(CLI!T.parseArgs!((T t) { assert(t == T(true)); })(["-b","true"]) == 0); assert(CLI!T.parseArgs!((T t) { assert(t == T(true)); })(["-b=true"]) == 0); -assert(CLI!T.parseArgs!((T t) { assert(t == T(false)); })(["-b","false"]) == 0); assert(CLI!T.parseArgs!((T t) { assert(t == T(false)); })(["-b=false"]) == 0); +assert(CLI!T.parseArgs!((T t) { assert(false); })(["-b","true"]) == 1); +assert(CLI!T.parseArgs!((T t) { assert(false); })(["-b","false"]) == 1); ``` ### Numeric @@ -1185,7 +1177,7 @@ assert(CLI!T.parseArgs!((T t) { assert(t == T(-5,8,12.345)); })(["-i","-5","-u", ### String -`argparse` supports string arguments as pass trough: +`argparse` supports string arguments as pass through: ```d struct T @@ -1199,7 +1191,7 @@ assert(CLI!T.parseArgs!((T t) { assert(t == T("foo")); })(["-a","foo"]) == 0); ### Enum If an argument is bound to an enum, an enum symbol as a string is expected as a value, or right within the argument -separated with an "=" sign: +separated with an “=” sign: ```d struct T @@ -1213,7 +1205,7 @@ assert(CLI!T.parseArgs!((T t) { assert(t == T(T.Fruit.apple)); })(["-a","apple"] assert(CLI!T.parseArgs!((T t) { assert(t == T(T.Fruit.pear)); })(["-a=pear"]) == 0); ``` -In some cases the value for command line argument might have characters that are not allowed in enum identifiers. +In some cases the value for command-line argument might have characters that are not allowed in enum identifiers. There is `ArgumentValue` UDA that can be used to adjust allowed values: ```d @@ -1286,10 +1278,10 @@ In case the argument is bound to static array then the maximum number of values dynamic array, the number of values is not limited. The minimum number of values is `1` in all cases. This behavior can be customized by calling the following functions: -- `NumberOfValues(ulong min, ulong max)` - sets both minimum and maximum number of values. -- `NumberOfValues(ulong num)` - sets both minimum and maximum number of values to the same value. -- `MinNumberOfValues(ulong min)` - sets minimum number of values. -- `MaxNumberOfValues(ulong max)` - sets maximum number of values. +- `NumberOfValues(ulong min, ulong max)` – sets both minimum and maximum number of values. +- `NumberOfValues(ulong num)` – sets both minimum and maximum number of values to the same value. +- `MinNumberOfValues(ulong min)` – sets minimum number of values. +- `MaxNumberOfValues(ulong max)` – sets maximum number of values. ```d struct T @@ -1306,8 +1298,8 @@ assert(CLI!T.parseArgs!((T t) { assert(t == T([1],[4,5])); })(["-a","1","-b","4" ### Associative array -If an argument is bound to an associative array, a string of the form "name=value" is expected as the next entry in -command line, or right within the option separated with an "=" sign: +If an argument is bound to an associative array, a string of the form “name=value” is expected as the next entry in +command line, or right within the option separated with an “=” sign: ```d struct T @@ -1376,7 +1368,7 @@ assert(CLI!T.parseArgs!((T t) { assert(t == T(4)); })(["-a","-a","-a","-a"]) == ### Custom types -Any arbitrary type can be used to receive command line argument values. `argparse` supports this use case - you just need +Any arbitrary type can be used to receive command-line-argument values. `argparse` supports this use case – you just need to provide parsing function: ```d @@ -1397,27 +1389,27 @@ assert(CLI!T.parseArgs!((T t) { assert(t == T(Value("foo"))); return 12345; })([ Some time the functionality provided out of the box is not enough and it needs to be tuned. -Parsing of a command line string values into some typed `receiver` member consists of multiple steps: +Parsing of command-line string values into some typed `receiver` member consists of multiple steps: -- **Pre-validation** - argument values are validated as raw strings. -- **Parsing** - raw argument values are converted to a different type (usually the type of the receiver). -- **Validation** - converted value is validated. -- **Action** - depending on a type of the `receiver`, it might be either assignment of converted value to a `receiver`, +- **Pre-validation** – argument values are validated as raw strings. +- **Parsing** – raw argument values are converted to a different type (usually the type of the receiver). +- **Validation** – converted value is validated. +- **Action** – depending on a type of the `receiver`, it might be either assignment of converted value to a `receiver`, appending value if `receiver` is an array or other operation. -In case if argument does not expect any value then the only one step is involved: +In case if argument does not expect any value, then the only one step is involved: -- **Action if no value** - similar to **Action** step above but without converted value. +- **Action if no value** – similar to **Action** step above but without converted value. -If any of the steps fails then the command line parsing fails as well. +If any of the steps fails, then the command-line parsing fails as well. -Each of the step above can be customized with UDA modifiers below. These modifiers take a function that might accept +Each of the steps above can be customized with UDA modifiers below. These modifiers take a function that might accept either argument value(s) or `Param` struct that has these fields (there is also an alias, `RawParam`, where the type of the `value` field is `string[]`): - `config`- Config object that is passed to parsing function. -- `name` - Argument name that is specified in command line. -- `value` - Array of argument values that are provided in command line. +- `name` – Argument name that is specified in command line. +- `value` – Array of argument values that are provided in command line. ### Pre-validation @@ -1526,19 +1518,19 @@ assert(CLI!T.parseArgs!((T t) { assert(t == T(4)); })(["-a","!4"]) == 0); ## Parser customization -`argparser` provides decent amount of settings to customize the parser. All customizations can be done by creating +`argparse` provides decent amount of settings to customize the parser. All customizations can be done by creating `Config` object with required settings (see below). ### Assign character -`Config.assignChar` - the assignment character used in arguments with value: `-a=5`, `-b=foo`. +`Config.assignChar` – the assignment character used in arguments with value: `-a=5`, `-b=foo`. Default is equal sign `=`. ### Array separator -`Config.arraySep` - when set to `char.init`, value to array and associative array receivers are treated as an individual -value. That is, only one argument is appended inserted per appearance of the argument. If `arraySep` is set to something +`Config.arraySep` – when set to `char.init`, values to array and associative-array receivers are treated as an individual +value. That is, only one argument is appended/inserted per appearance of the argument. If `arraySep` is set to something else, then each value is first split by the separator, and the individual pieces are treated as values to the same argument. @@ -1561,65 +1553,68 @@ enum cfg = { assert(CLI!(cfg, T).parseArgs!((T t) { assert(t == T(["1","2","3","4","5"])); })(["-a","1,2,3","-a","4","5"]) == 0); ``` -### Named argument character +### Named argument prefix character -`Config.namedArgChar` - the character that named arguments begin with. +`Config.namedArgPrefix` – the character that named arguments begin with. -Default is dash `-`. +Default is dash (`-`). ### End of arguments -`Config.endOfArgs` - the string that conventionally marks the end of all arguments. +`Config.endOfArgs` – the string that conventionally marks the end of all arguments. -Default is double-dash `--`. +Default is double dash (`--`). ### Case sensitivity -`Config.caseSensitive` - by default argument names are case-sensitive. You can change that behavior by setting thia +`Config.caseSensitive` – by default argument names are case-sensitive. You can change that behavior by setting this member to `false`. Default is `true`. ### Bundling of single-letter arguments -`Config.bundling` - when it is set to `true`, single-letter arguments can be bundled together, i.e. `-abc` is the same +`Config.bundling` – when it is set to `true`, single-letter arguments can be bundled together, i.e., `-abc` is the same as `-a -b -c`. Default is `false`. ### Adding help generation -`Config.addHelp` - when it is set to `true` then `-h` and `--help` arguments are added to the parser. In case if the -command line has one of these arguments then the corresponding help text is printed and the parsing will be stopped. -If `CLI!(...).parseArgs(alias newMain)` or `CLI!(...).main(alias newMain)` is used then provided `newMain` function will +`Config.addHelp` – when it is set to `true`, then `-h` and `--help` arguments are added to the parser. In case if the +command line has one of these arguments, then the corresponding help text is printed and the parsing will be stopped. +If `CLI!(...).parseArgs(alias newMain)` or `CLI!(...).main(alias newMain)` is used, then provided `newMain` function will not be called. Default is `true`. -### Help styling mode +### Styling mode -`Config.stylingMode` - styling mode that is used to print help text. It has the following type: `enum StylingMode { autodetect, on, off }`. +`Config.stylingMode` – styling mode for the text output (error messages and help screen). It has the following type: `enum StylingMode { autodetect, on, off }`. +If it's set to `Config.StylingMode.on` then styling is **always enabled**. +If it's set to `Config.StylingMode.off` then styling is **always disabled**. -Default value is `Config.StylingMode.autodetect`. +Default value is `Config.StylingMode.autodetect`, which means that styling will be enabled when possible. See [ANSI coloring and styling](#ansi-colors-and-styles) for details. -### Help styling scheme +### Styling scheme -`Config.helpStyle` - contains help text style. It has the following members: +`Config.styling` – contains style for the text output (error messages and help screen). It has the following members: -- `programName`: style for the program name. -- `subcommandName`: style for the subcommand name. -- `argumentGroupTitle`: style for the title of argument group. -- `namedArgumentName`: style for the name of named argument. -- `namedArgumentValue`: style for the value of named argument. -- `positionalArgumentValue`: style for the value of positional argument. +- `programName`: style for the program name. Default is `bold`. +- `subcommandName`: style for the subcommand name. Default is `bold`. +- `argumentGroupTitle`: style for the title of argument group. Default is `bold.underline`. +- `argumentName`: style for the argument name. Default is `lightYellow`. +- `namedArgumentValue`: style for the value of named argument. Default is `italic`. +- `positionalArgumentValue`: style for the value of positional argument. Default is `lightYellow`. +- `errorMessagePrefix`: style for *Error:* prefix in error messages. Default is `red`. See [ANSI coloring and styling](#ansi-colors-and-styles) for details. ### Error handling -`Config.errorHandler` - this is a handler function for all errors occurred during parsing the command line. It might be +`Config.errorHandler` – this is a handler function for all errors occurred during parsing the command line. It might be either a function or a delegate that takes `string` parameter which would be an error message. The default behavior is to print error message to `stderr`. diff --git a/examples/getting_started/advanced/app.d b/examples/getting_started/advanced/app.d index 12ff215..316dbd6 100644 --- a/examples/getting_started/advanced/app.d +++ b/examples/getting_started/advanced/app.d @@ -31,7 +31,7 @@ struct Advanced @NamedArgument Enum choice; - // Custom types can also be used with custon parsing function + // Custom types can also be used with custom parsing function struct CustomType { double d; } @@ -47,8 +47,8 @@ auto config() { Config cfg; - cfg.helpStyle.programName = blue.onYellow; - cfg.helpStyle.namedArgumentName = bold.italic.cyan.onRed; + cfg.styling.programName = blue.onYellow; + cfg.styling.argumentName = bold.italic.cyan.onRed; return cfg; } @@ -68,9 +68,8 @@ mixin CLI!(config(), Advanced).main!((args, unparsed) writeln("Unparsed args: ", unparsed); // use actual styling mode to print output - auto style = Advanced.color == Config.StylingMode.on ? red.onWhite : noStyle; - writeln(style("Styling mode: "), Advanced.color); - assert(Advanced.color == Config.StylingMode.on || Advanced.color == Config.StylingMode.off); + auto style = Advanced.color ? red.onWhite : noStyle; + writeln(style("Styling mode: "), Advanced.color ? "on" : "off"); return 0; }); \ No newline at end of file diff --git a/examples/getting_started/basic/app.d b/examples/getting_started/basic/app.d index 2509397..4131aab 100644 --- a/examples/getting_started/basic/app.d +++ b/examples/getting_started/basic/app.d @@ -4,39 +4,39 @@ import argparse; struct Basic { // Basic data types are supported: - // --name argument + // '--name' argument string name; - // --number argument + // '--number' argument int number; - // --boolean argument + // '--boolean' argument bool boolean; // Argument can have default value if it's not specified in command line - // --unused argument + // '--unused' argument string unused = "some default value"; // Enums are also supported enum Enum { unset, foo, boo } - // --choice argument + // '--choice' argument Enum choice; // Use array to store multiple values - // --array argument + // '--array' argument int[] array; // Callback with no args (flag) - // --callback argument + // '--callback' argument void callback() {} // Callback with single value - // --callback1 argument + // '--callback1' argument void callback1(string value) { assert(value == "cb-value"); } // Callback with zero or more values - // --callback2 argument + // '--callback2' argument void callback2(string[] value) { assert(value == ["cb-v1","cb-v2"]); } } diff --git a/source/argparse/ansi.d b/source/argparse/ansi.d index 1f7782e..37ade10 100644 --- a/source/argparse/ansi.d +++ b/source/argparse/ansi.d @@ -1,7 +1,5 @@ module argparse.ansi; -import std.regex: ctRegex; - // The string that starts an ANSI command sequence. private enum prefix = "\033["; @@ -14,9 +12,6 @@ private enum suffix = "m"; // The sequence used to reset all styling. private enum reset = prefix ~ suffix; -// Regex to match ANSI sequence -private enum sequenceRegex = ctRegex!(`\x1b\[(\d*(;\d*)*)?m`); - // Code offset between foreground and background private enum colorBgOffset = 10; @@ -52,38 +47,47 @@ private enum Color package struct TextStyle { - private ubyte[] style; + private string style = prefix; - private this(ubyte[] st) - { - style = st; - } - private this(ubyte st) + private this(const(ubyte)[] st...) scope inout nothrow pure @safe { - if(st != 0) - style = [st]; + import std.algorithm.iteration: joiner, map; + import std.array: appender; + import std.conv: toChars; + import std.utf: byCodeUnit; + + auto a = appender(prefix); + a ~= st.map!(_ => uint(_).toChars).joiner(separator.byCodeUnit); + style = a[]; } - private auto opBinary(string op)(ubyte other) if(op == "~") + private ref opOpAssign(string op : "~")(ubyte other) { - return other != 0 ? TextStyle(style ~ other) : this; + import std.array: appender; + import std.conv: toChars; + + if(other != 0) + { + auto a = appender(style); + if(style.length != prefix.length) + a ~= separator; + a ~= uint(other).toChars; + style = a[]; + } + return this; } public auto opCall(string str) const { - import std.conv: text, to; - import std.algorithm: joiner, map; - import std.range: chain; - - if(style.length == 0 || str.length == 0) + if(str.length == 0 || style.length == prefix.length) return str; - - return text(prefix, style.map!(to!string).joiner(separator), suffix, str, reset); + return style ~ suffix ~ str ~ reset; } } -unittest +nothrow pure @safe unittest { + assert(TextStyle.init("foo") == "foo"); assert(TextStyle([])("foo") == "foo"); assert(TextStyle([Font.bold])("foo") == "\033[1mfoo\033[m"); assert(TextStyle([Font.bold, Font.italic])("foo") == "\033[1;3mfoo\033[m"); @@ -97,7 +101,7 @@ package struct StyledText string text; - string toString() const + string toString() return scope const nothrow pure @safe { return style(text); } @@ -107,6 +111,7 @@ package struct StyledText { return toString() ~ rhs; } + // lhs ~ this string opBinaryRight(string op : "~")(string lhs) const { @@ -114,31 +119,41 @@ package struct StyledText } } -unittest +nothrow pure @safe unittest { auto s = TextStyle([Font.bold]); assert(StyledText(s, "foo").toString() == s("foo")); + + const ubyte[1] data = [Font.bold]; + scope c = const TextStyle(data); + assert((const StyledText(c, "foo")).toString() == c("foo")); + + immutable foo = StyledText(s, "foo"); + assert(foo ~ "bar" == s("foo") ~ "bar"); + assert("bar" ~ foo == "bar" ~ s("foo")); } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// package template StyleImpl(ubyte styleCode) { - public auto StyleImpl() + immutable style = TextStyle(styleCode); + + public TextStyle StyleImpl() { - return TextStyle(styleCode); + return style; } public auto StyleImpl(TextStyle otherStyle) { - return otherStyle ~ styleCode; + return otherStyle ~= styleCode; } public auto StyleImpl(string text) { - return StyledText(TextStyle(styleCode), text); + return StyledText(style, text); } public auto StyleImpl(TextStyle otherStyle, string text) { - return StyledText(otherStyle ~ styleCode, text); + return StyledText(otherStyle ~= styleCode, text); } } @@ -183,7 +198,7 @@ alias onLightCyan = StyleImpl!(colorBgOffset + Color.lightCyan); alias onWhite = StyleImpl!(colorBgOffset + Color.white); -unittest +nothrow pure @safe unittest { assert(bold == TextStyle([Font.bold])); assert(bold.italic == TextStyle([Font.bold, Font.italic])); @@ -195,13 +210,107 @@ unittest assert(bold.italic.red.onWhite("foo").toString() == "\033[1;3;31;107mfoo\033[m"); } +nothrow pure @safe @nogc unittest +{ + auto style = bold; + style = italic; // Should be able to reassign. +} + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -package auto getUnstyledText(string text) +/** + Split a string into two parts: `result[0]` (head) is the first textual chunk (i.e., with no command sequences) that + occurs in `text`; `result[1]` (tail) is everything that follows it, with one leading command sequence stripped. + */ +private inout(char)[][2] findNextTextChunk(return scope inout(char)[] text) nothrow pure @safe @nogc +{ + import std.ascii: isDigit; + import std.string: indexOf; + + static assert(separator.length == 1); + static assert(suffix.length == 1); + + size_t idx = 0; + + while(true) + { + immutable seqIdx = text.indexOf(prefix, idx); + + if(seqIdx < 0) + return [text, null]; + + idx = seqIdx + prefix.length; + while(idx < text.length && (text[idx] == separator[0] || isDigit(text[idx]))) + idx++; + + if(idx < text.length && text[idx] == suffix[0]) + { + idx++; + if(seqIdx > 0) // If the chunk is not empty + return [text[0 .. seqIdx], text[idx .. $]]; + + // Chunk is empty so we skip command sequence and continue + text = text[idx .. $]; + idx = 0; + } + } +} + +public auto getUnstyledText(C : char)(C[] text) { - import std.regex: splitter; + struct Unstyler + { + private C[] head, tail; + + @property bool empty() const { return head.length == 0; } + + @property inout(C)[] front() inout { return head; } + + void popFront() + { + auto a = findNextTextChunk(tail); + head = a[0]; + tail = a[1]; + } + + @property auto save() inout { return this; } + } + + auto a = findNextTextChunk(text); + return Unstyler(a[0], a[1]); +} + +nothrow pure @safe @nogc unittest +{ + import std.range.primitives: ElementType, isForwardRange; + + alias R = typeof(getUnstyledText("")); + assert(isForwardRange!R); + assert(is(ElementType!R == string)); +} + +nothrow pure @safe @nogc unittest +{ + bool eq(T)(T actual, const(char[])[] expected...) // This allows `expected` to be `@nogc` even without `-dip1000` + { + import std.algorithm.comparison: equal; + + return equal(actual, expected); + } + + assert(eq(getUnstyledText(""))); + assert(eq(getUnstyledText("\x1b[m"))); + assert(eq(getUnstyledText("a\x1b[m"), "a")); + assert(eq(getUnstyledText("a\x1b[0;1m\x1b[9mm\x1b[m\x1b["), "a", "m", "\x1b[")); + assert(eq(getUnstyledText("a\x1b[0:abc\x1b[m"), "a\x1b[0:abc")); + + char[2] m = "\x1b["; + const char[2] c = "\x1b["; + immutable char[2] i = "\x1b["; - return text.splitter(sequenceRegex); + assert(eq(getUnstyledText(m), "\x1b[")); + assert(eq(getUnstyledText(c), "\x1b[")); + assert(eq(getUnstyledText(i), "\x1b[")); } package size_t getUnstyledTextLength(string text) @@ -213,7 +322,7 @@ package size_t getUnstyledTextLength(string text) package size_t getUnstyledTextLength(StyledText text) { - return getUnstyledTextLength(text.toString()); + return getUnstyledTextLength(text.text); } unittest diff --git a/source/argparse/api/ansi.d b/source/argparse/api/ansi.d index 5784781..f812a4d 100644 --- a/source/argparse/api/ansi.d +++ b/source/argparse/api/ansi.d @@ -3,7 +3,6 @@ module argparse.api.ansi; import argparse.config; import argparse.param; import argparse.api.argument: NamedArgument, Description, NumberOfValues, AllowedValues, Parse, Action, ActionNoValue; -import argparse.internal.hooks: Hooks; import argparse.internal.parsehelpers: PassThrough; /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -18,79 +17,73 @@ import argparse.internal.parsehelpers: PassThrough; .Action!(AnsiStylingArgument.action) .ActionNoValue!(AnsiStylingArgument.action) ) -@(Hooks.onParsingDone!(AnsiStylingArgument.finalize)) private struct AnsiStylingArgument { - Config.StylingMode stylingMode = Config.StylingMode.autodetect; + package(argparse) static bool isEnabled; - alias stylingMode this; - - string toString() const + public bool opCast(T : bool)() const { - import std.conv: to; - return stylingMode.to!string; + return isEnabled; } - void set(const Config* config, Config.StylingMode mode) - { - config.setStylingMode(stylingMode = mode); - } - static void action(ref AnsiStylingArgument receiver, RawParam param) + private static void action(ref AnsiStylingArgument receiver, RawParam param) { switch(param.value[0]) { - case "always": receiver.set(param.config, Config.StylingMode.on); return; - case "auto": receiver.set(param.config, Config.StylingMode.autodetect); return; - case "never": receiver.set(param.config, Config.StylingMode.off); return; + case "auto": isEnabled = param.config.stylingMode == Config.StylingMode.on; return; + case "always": isEnabled = true; return; + case "never": isEnabled = false; return; default: } } - static void action(ref AnsiStylingArgument receiver, Param!void param) - { - receiver.set(param.config, Config.StylingMode.on); - } - static void finalize(ref AnsiStylingArgument receiver, const Config* config) + private static void action(ref AnsiStylingArgument receiver, Param!void param) { - receiver.set(config, config.stylingMode); + isEnabled = true; } } -auto ansiStylingArgument() +unittest { - return AnsiStylingArgument.init; + AnsiStylingArgument arg; + AnsiStylingArgument.action(arg, Param!void.init); + assert(arg); + + AnsiStylingArgument.action(arg, RawParam(null, "", [""])); } unittest { - import std.conv: to; + AnsiStylingArgument arg; - assert(ansiStylingArgument == AnsiStylingArgument.init); - assert(ansiStylingArgument.toString() == Config.StylingMode.autodetect.to!string); + AnsiStylingArgument.action(arg, RawParam(null, "", ["always"])); + assert(arg); - Config config; - config.setStylingModeHandlers ~= (Config.StylingMode mode) { config.stylingMode = mode; }; + AnsiStylingArgument.action(arg, RawParam(null, "", ["never"])); + assert(!arg); +} +unittest +{ + Config config; AnsiStylingArgument arg; - AnsiStylingArgument.action(arg, Param!void(&config)); - assert(config.stylingMode == Config.StylingMode.on); - assert(arg.toString() == Config.StylingMode.on.to!string); + config.stylingMode = Config.StylingMode.on; + AnsiStylingArgument.action(arg, RawParam(&config, "", ["auto"])); + assert(arg); + + config.stylingMode = Config.StylingMode.off; + AnsiStylingArgument.action(arg, RawParam(&config, "", ["auto"])); + assert(!arg); } -unittest -{ - auto test(string value) - { - Config config; - config.setStylingModeHandlers ~= (Config.StylingMode mode) { config.stylingMode = mode; }; +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - AnsiStylingArgument arg; - AnsiStylingArgument.action(arg, RawParam(&config, "", [value])); - return config.stylingMode; - } +auto ansiStylingArgument() +{ + return AnsiStylingArgument.init; +} - assert(test("always") == Config.StylingMode.on); - assert(test("auto") == Config.StylingMode.autodetect); - assert(test("never") == Config.StylingMode.off); - assert(test("") == Config.StylingMode.autodetect); +unittest +{ + assert(ansiStylingArgument == AnsiStylingArgument.init); } \ No newline at end of file diff --git a/source/argparse/api/argument.d b/source/argparse/api/argument.d index a036fd8..c05e1f8 100644 --- a/source/argparse/api/argument.d +++ b/source/argparse/api/argument.d @@ -124,7 +124,7 @@ unittest auto PositionalArgument(uint pos) { - auto arg = ArgumentUDA!(ValueParser!(void, void, void, void, void, void))(ArgumentInfo()).Required(); + auto arg = ArgumentUDA!(ValueParser!(void, void, void, void, void, void))(ArgumentInfo.init).Required(); arg.info.position = pos; return arg; } @@ -158,7 +158,7 @@ unittest auto arg = NamedArgument("foo"); assert(!arg.info.required); assert(!arg.info.positional); - assert(arg.info.names == ["foo"]); + assert(arg.info.shortNames == ["foo"]); } unittest @@ -166,7 +166,7 @@ unittest auto arg = NamedArgument(["foo","bar"]); assert(!arg.info.required); assert(!arg.info.positional); - assert(arg.info.names == ["foo","bar"]); + assert(arg.info.shortNames == ["foo","bar"]); } unittest @@ -174,7 +174,7 @@ unittest auto arg = NamedArgument("foo","bar"); assert(!arg.info.required); assert(!arg.info.positional); - assert(arg.info.names == ["foo","bar"]); + assert(arg.info.shortNames == ["foo","bar"]); } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -309,7 +309,7 @@ auto AllowedValues(alias values, ARG)(ARG arg) auto desc = arg.Validation!(ValueInList!(values, KeyType!(typeof(valuesAA)))); if(desc.info.placeholder.length == 0) - desc.info.placeholder = formatAllowedValues!values; + desc.info.placeholder = formatAllowedValues(values); return desc; } @@ -321,20 +321,20 @@ unittest /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -auto Counter(T)(auto ref ArgumentUDA!T uda) +private struct CounterParsingFunction { - struct CounterParsingFunction + static Result parse(T)(ref T receiver, const ref RawParam param) { - static Result parse(T)(ref T receiver, const ref RawParam param) - { - assert(param.value.length == 0); + assert(param.value.length == 0); - ++receiver; + ++receiver; - return Result.Success; - } + return Result.Success; } +} +auto Counter(T)(auto ref ArgumentUDA!T uda) +{ auto desc = ArgumentUDA!(CounterParsingFunction)(uda.tupleof); desc.info.minValuesCount = 0; desc.info.maxValuesCount = 0; diff --git a/source/argparse/api/cli.d b/source/argparse/api/cli.d index 21fa580..2fca043 100644 --- a/source/argparse/api/cli.d +++ b/source/argparse/api/cli.d @@ -2,18 +2,62 @@ module argparse.api.cli; import argparse.config; import argparse.result; - +import argparse.api.ansi: ansiStylingArgument; +import argparse.ansi: getUnstyledText; import argparse.internal.parser: callParser; import argparse.internal.completer: completeArgs, Complete; +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +/// Private helper for error output + +private void defaultErrorPrinter(T...)(T message) +{ + import std.stdio: stderr, writeln; + + stderr.writeln(message); +} + +private void onError(Config config, alias printer = defaultErrorPrinter)(string message) nothrow +{ + import std.algorithm.iteration: joiner; + + static if(config.errorHandlerFunc) + config.errorHandlerFunc(message); + else + try + { + if(ansiStylingArgument) + printer(config.styling.errorMessagePrefix("Error: "), message); + else + printer("Error: ", message.getUnstyledText.joiner); + } + catch(Exception e) + { + throw new Error(e.msg); + } +} + +unittest +{ + import std.exception; + + static void printer(T...)(T m) + { + throw new Exception("My Message."); + } + + assert(collectExceptionMsg!Error(onError!(Config.init, printer)("text")) == "My Message."); +} + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /// Public API for CLI wrapper /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// template CLI(Config config, COMMANDS...) { - mixin template main(alias newMain) + template main(alias newMain) { import std.sumtype: SumType, match; @@ -36,7 +80,12 @@ template CLI(Config config, COMMAND) { static Result parseKnownArgs(ref COMMAND receiver, string[] args, out string[] unrecognizedArgs) { - return callParser!(config, false)(receiver, args, unrecognizedArgs); + auto res = callParser!(config, false)(receiver, args, unrecognizedArgs); + + if(!res && res.errorMsg.length > 0) + onError!config(res.errorMsg); + + return res; } static Result parseKnownArgs(ref COMMAND receiver, ref string[] args) @@ -56,7 +105,7 @@ template CLI(Config config, COMMAND) if(res && args.length > 0) { res = Result.Error("Unrecognized arguments: ", args); - config.onError(res.errorMsg); + onError!config(res.errorMsg); } return res; @@ -98,38 +147,25 @@ template CLI(Config config, COMMAND) } } - string[] completeArgs(string[] args) + // This is a template to avoid compiling it unless it is actually used. + string[] completeArgs()(string[] args) { return .completeArgs!(config, COMMAND)(args); } - int complete(string[] args) + // This is a template to avoid compiling it unless it is actually used. + int complete()(string[] args) { import std.sumtype: match; - // 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; - string[] unrecognizedArgs; - - auto res = callParser!(config, false)(receiver, args, unrecognizedArgs); - if(!res) - return 1; - - if(res && unrecognizedArgs.length > 0) - { - import std.conv: to; - config.onError("Unrecognized arguments: "~unrecognizedArgs.to!string); - return 1; - } - - receiver.cmd.match!(_ => _.execute!config()); - - return 0; + // We are able to instantiate `CLI` with different arguments solely because we reside in a templated function. + // If we weren't, that would lead to infinite template recursion. + return CLI!(config, Complete!COMMAND).parseArgs!(comp => + comp.cmd.match!(_ => _.execute!(config, COMMAND)) + )(args); } - mixin template mainComplete() + template mainComplete() { int main(string[] argv) { @@ -137,7 +173,7 @@ template CLI(Config config, COMMAND) } } - mixin template main(alias newMain) + template main(alias newMain) { version(argparse_completion) { diff --git a/source/argparse/api/restriction.d b/source/argparse/api/restriction.d index 1c5a7f9..6db89b4 100644 --- a/source/argparse/api/restriction.d +++ b/source/argparse/api/restriction.d @@ -1,13 +1,15 @@ module argparse.api.restriction; -import argparse.internal.arguments: RestrictionGroup; +import argparse.internal.restriction: RestrictionGroup; + +import std.conv: to; /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /// Public API for restrictions /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Required group +/// Required group auto ref Required(T : RestrictionGroup)(auto ref T group) { @@ -21,11 +23,10 @@ unittest } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Arguments required together +/// Arguments required together -auto RequiredTogether(string file=__FILE__, uint line = __LINE__)() +auto RequiredTogether(string file=__FILE__, uint line = __LINE__) { - import std.conv: to; return RestrictionGroup(file~":"~line.to!string, RestrictionGroup.Type.together); } @@ -37,11 +38,10 @@ unittest } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Mutually exclusive arguments +/// Mutually exclusive arguments -auto MutuallyExclusive(string file=__FILE__, uint line = __LINE__)() +auto MutuallyExclusive(string file=__FILE__, uint line = __LINE__) { - import std.conv: to; return RestrictionGroup(file~":"~line.to!string, RestrictionGroup.Type.exclusive); } diff --git a/source/argparse/config.d b/source/argparse/config.d index 2a8b0c7..91fa40b 100644 --- a/source/argparse/config.d +++ b/source/argparse/config.d @@ -23,10 +23,10 @@ struct Config char arraySep = char.init; /** - The option character. + The prefix for argument name. Defaults to '-'. */ - char namedArgChar = '-'; + char namedArgPrefix = '-'; /** The string that conventionally marks the end of all options. @@ -61,23 +61,17 @@ struct Config bool addHelp = true; /** - Styling and coloring mode. + Styling. + */ + Style styling = Style.Default; + + /** + Styling mode. Defaults to auto-detectection of the capability. */ enum StylingMode { autodetect, on, off } StylingMode stylingMode = StylingMode.autodetect; - package void delegate(StylingMode)[] setStylingModeHandlers; - package void setStylingMode(StylingMode mode) const - { - foreach(dg; setStylingModeHandlers) - dg(mode); - } - - /** - Help style. - */ - Style helpStyle = Style.Default; /** Delegate that processes error messages if they happen during argument parsing. @@ -94,38 +88,24 @@ struct Config { return errorHandlerFunc = func; } +} +unittest +{ + auto f = function(string s) nothrow {}; - package void onError(string message) const nothrow - { - import std.stdio: stderr, writeln; - - try - { - if(errorHandlerFunc) - errorHandlerFunc(message); - else - stderr.writeln("Error: ", message); - } - catch(Exception e) - { - throw new Error(e.msg); - } - } + Config c; + assert(!c.errorHandlerFunc); + assert((c.errorHandler = f)); + assert(c.errorHandlerFunc); } unittest { - enum text = "--just testing error func--"; - - bool called = false; + auto f = delegate(string s) nothrow {}; Config c; - c.errorHandler = (string s) - { - assert(s == text); - called = true; - }; - c.onError(text); - assert(called); + assert(!c.errorHandlerFunc); + assert((c.errorHandler = f) == f); + assert(c.errorHandlerFunc == f); } \ No newline at end of file diff --git a/source/argparse/internal/argumentparser.d b/source/argparse/internal/argumentparser.d deleted file mode 100644 index 28f53ef..0000000 --- a/source/argparse/internal/argumentparser.d +++ /dev/null @@ -1,81 +0,0 @@ -module argparse.internal.argumentparser; - -import argparse.api.argument: NamedArgument, NumberOfValues; -import argparse.config; -import argparse.result; -import argparse.param; -import argparse.internal.argumentuda: getMemberArgumentUDA; - - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -private alias ParseFunction(COMMAND_STACK, RECEIVER) = Result delegate(const COMMAND_STACK cmdStack, Config* config, ref RECEIVER receiver, string argName, string[] rawValues); - -private alias ParsingArgument(COMMAND_STACK, RECEIVER, alias symbol, alias uda, bool completionMode) = - delegate(const COMMAND_STACK cmdStack, Config* config, ref RECEIVER receiver, string argName, string[] rawValues) - { - static if(completionMode) - { - return Result.Success; - } - else - { - try - { - auto res = uda.info.checkValuesCount(argName, rawValues.length); - if(!res) - return res; - - auto param = RawParam(config, argName, rawValues); - - auto target = &__traits(getMember, receiver, symbol); - - static if(is(typeof(target) == function) || is(typeof(target) == delegate)) - return uda.parsingFunc.parse(target, param); - else - return uda.parsingFunc.parse(*target, param); - } - catch(Exception e) - { - return Result.Error(argName, ": ", e.msg); - } - } - }; - -unittest -{ - struct T { string a; } - - auto test(TYPE)(string[] values) - { - Config config; - TYPE t; - - return ParsingArgument!(string[], TYPE, "a", NamedArgument("arg-name").NumberOfValues(1), false)([], &config, t, "arg-name", values); - } - - assert(test!T(["raw-value"])); - assert(!test!T(["value1","value2"])); -} - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -package auto getArgumentParsingFunctions(Config config, COMMAND_STACK, TYPE, symbols...)() -{ - ParseFunction!(COMMAND_STACK, TYPE)[] res; - - static foreach(symbol; symbols) - res ~= ParsingArgument!(COMMAND_STACK, TYPE, symbol, getMemberArgumentUDA!(config, TYPE, symbol, NamedArgument), false); - - return res; -} - -package auto getArgumentCompletionFunctions(Config config, COMMAND_STACK, TYPE, symbols...)() -{ - ParseFunction!(COMMAND_STACK, TYPE)[] res; - - static foreach(symbol; symbols) - res ~= ParsingArgument!(COMMAND_STACK, TYPE, symbol, getMemberArgumentUDA!(config, TYPE, symbol, NamedArgument), true); - - return res; -} diff --git a/source/argparse/internal/arguments.d b/source/argparse/internal/arguments.d index 9d52f72..cc500ae 100644 --- a/source/argparse/internal/arguments.d +++ b/source/argparse/internal/arguments.d @@ -1,20 +1,19 @@ module argparse.internal.arguments; -import argparse.internal.utils: partiallyApply; import argparse.internal.lazystring; import argparse.config; import argparse.result; import std.typecons: Nullable; -import std.traits: getUDAs; /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// package(argparse) struct ArgumentInfo { - string[] names; - string[] displayNames; // names prefixed with Config.namedArgChar + string[] shortNames; + string[] longNames; + string[] displayNames; // names prefixed with Config.namedArgPrefix string displayName() const { @@ -24,6 +23,8 @@ package(argparse) struct ArgumentInfo LazyString description; string placeholder; + string memberSymbol; + bool hideFromHelp = false; // if true then this argument is not printed on help page bool required; @@ -35,7 +36,7 @@ package(argparse) struct ArgumentInfo Nullable!ulong minValuesCount; Nullable!ulong maxValuesCount; - auto checkValuesCount(string argName, ulong count) const + auto checkValuesCount(const Config config, string argName, ulong count) const { immutable min = minValuesCount.get; immutable max = maxValuesCount.get; @@ -45,23 +46,18 @@ package(argparse) struct ArgumentInfo return Result.Success; if(min == max && count != min) - { - return Result.Error("argument ",argName,": expected ",min,min == 1 ? " value" : " values"); - } + return Result.Error("Argument '",config.styling.argumentName(argName),"': expected ",min,min == 1 ? " value" : " values"); + if(count < min) - { - return Result.Error("argument ",argName,": expected at least ",min,min == 1 ? " value" : " values"); - } + return Result.Error("Argument '",config.styling.argumentName(argName),"': expected at least ",min,min == 1 ? " value" : " values"); + if(count > max) - { - return Result.Error("argument ",argName,": expected at most ",max,max == 1 ? " value" : " values"); - } + return Result.Error("Argument '",config.styling.argumentName(argName),"': expected at most ",max,max == 1 ? " value" : " values"); return Result.Success; } bool allowBooleanNegation = true; - bool ignoreInDefaultCommand; } unittest @@ -75,176 +71,226 @@ unittest return info; } - assert(info(2,4).checkValuesCount("", 1).isError("expected at least 2 values")); - assert(info(2,4).checkValuesCount("", 2)); - assert(info(2,4).checkValuesCount("", 3)); - assert(info(2,4).checkValuesCount("", 4)); - assert(info(2,4).checkValuesCount("", 5).isError("expected at most 4 values")); + assert(info(2,4).checkValuesCount(Config.init, "", 1).isError("expected at least 2 values")); + assert(info(2,4).checkValuesCount(Config.init, "", 2)); + assert(info(2,4).checkValuesCount(Config.init, "", 3)); + assert(info(2,4).checkValuesCount(Config.init, "", 4)); + assert(info(2,4).checkValuesCount(Config.init, "", 5).isError("expected at most 4 values")); - assert(info(2,2).checkValuesCount("", 1).isError("expected 2 values")); - assert(info(2,2).checkValuesCount("", 2)); - assert(info(2,2).checkValuesCount("", 3).isError("expected 2 values")); + assert(info(2,2).checkValuesCount(Config.init, "", 1).isError("expected 2 values")); + assert(info(2,2).checkValuesCount(Config.init, "", 2)); + assert(info(2,2).checkValuesCount(Config.init, "", 3).isError("expected 2 values")); - assert(info(1,1).checkValuesCount("", 0).isError("expected 1 value")); - assert(info(1,1).checkValuesCount("", 1)); - assert(info(1,1).checkValuesCount("", 2).isError("expected 1 value")); + assert(info(1,1).checkValuesCount(Config.init, "", 0).isError("expected 1 value")); + assert(info(1,1).checkValuesCount(Config.init, "", 1)); + assert(info(1,1).checkValuesCount(Config.init, "", 2).isError("expected 1 value")); - assert(info(0,1).checkValuesCount("", 0)); - assert(info(0,1).checkValuesCount("", 1)); - assert(info(0,1).checkValuesCount("", 2).isError("expected at most 1 value")); + assert(info(0,1).checkValuesCount(Config.init, "", 0)); + assert(info(0,1).checkValuesCount(Config.init, "", 1)); + assert(info(0,1).checkValuesCount(Config.init, "", 2).isError("expected at most 1 value")); - assert(info(1,2).checkValuesCount("", 0).isError("expected at least 1 value")); - assert(info(1,2).checkValuesCount("", 1)); - assert(info(1,2).checkValuesCount("", 2)); - assert(info(1,2).checkValuesCount("", 3).isError("expected at most 2 values")); + assert(info(1,2).checkValuesCount(Config.init, "", 0).isError("expected at least 1 value")); + assert(info(1,2).checkValuesCount(Config.init, "", 1)); + assert(info(1,2).checkValuesCount(Config.init, "", 2)); + assert(info(1,2).checkValuesCount(Config.init, "", 3).isError("expected at most 2 values")); } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -package alias Restriction = Result delegate(Config* config, in bool[size_t] cliArgs, in ArgumentInfo[] allArgs); - -package struct Restrictions +package ArgumentInfo finalize(MEMBERTYPE)(ArgumentInfo info, const Config config, string symbol) { - static Restriction RequiredArg(ArgumentInfo info)(size_t index) + import std.algorithm: each, map; + import std.array: array; + import std.conv: text; + import std.range: chain; + import std.traits: isBoolean; + + static if(!isBoolean!MEMBERTYPE) + info.allowBooleanNegation = false; + + if(info.shortNames.length == 0 && info.longNames.length == 0) { - return partiallyApply!((size_t index, Config* config, in bool[size_t] cliArgs, in ArgumentInfo[] allArgs) - { - return (index in cliArgs) ? - Result.Success : - Result.Error("The following argument is required: ", info.displayName); - })(index); + if(symbol.length == 1) + info.shortNames = [ symbol ]; + else + info.longNames = [ symbol ]; } - static Result RequiredTogether(Config* config, - in bool[size_t] cliArgs, - in ArgumentInfo[] allArgs, - in size_t[] restrictionArgs) + if(info.placeholder.length == 0) { - size_t foundIndex = size_t.max; - size_t missedIndex = size_t.max; - - foreach(index; restrictionArgs) + static if(is(MEMBERTYPE == enum)) { - if(index in cliArgs) - { - if(foundIndex == size_t.max) - foundIndex = index; - } - else if(missedIndex == size_t.max) - missedIndex = index; + import argparse.internal.enumhelpers: getEnumValues; + import argparse.internal.utils: formatAllowedValues; - if(foundIndex != size_t.max && missedIndex != size_t.max) - return Result.Error("Missed argument '", allArgs[missedIndex].displayName, "' - it is required by argument '", allArgs[foundIndex].displayName); + info.placeholder = formatAllowedValues(getEnumValues!MEMBERTYPE); } + else + { + import std.uni: toUpper; - return Result.Success; + info.placeholder = info.positional ? symbol : symbol.toUpper; + } } - static Result MutuallyExclusive(Config* config, - in bool[size_t] cliArgs, - in ArgumentInfo[] allArgs, - in size_t[] restrictionArgs) - { - size_t foundIndex = size_t.max; + info.memberSymbol = symbol; - foreach(index; restrictionArgs) - if(index in cliArgs) - { - if(foundIndex == size_t.max) - foundIndex = index; - else - return Result.Error("Argument '", allArgs[foundIndex].displayName, "' is not allowed with argument '", allArgs[index].displayName,"'"); - } + if(info.positional) + info.displayNames = [ info.placeholder ]; + else + { + alias toDisplayName = _ => ( _.length == 1 ? config.namedArgPrefix ~ _ : text(config.namedArgPrefix, config.namedArgPrefix, _)); - return Result.Success; + info.displayNames = chain(info.shortNames, info.longNames).map!toDisplayName.array; } - static Result RequiredAnyOf(Config* config, - in bool[size_t] cliArgs, - in ArgumentInfo[] allArgs, - in size_t[] restrictionArgs) + if(!config.caseSensitive) { - import std.algorithm: map; - import std.array: join; + info.shortNames.each!((ref _) => _ = config.convertCase(_)); + info.longNames .each!((ref _) => _ = config.convertCase(_)); + } - foreach(index; restrictionArgs) - if(index in cliArgs) - return Result.Success; + // Note: `info.{minValuesCount,maxValuesCount}` are left unchanged - return Result.Error("One of the following arguments is required: '", restrictionArgs.map!(_ => allArgs[_].displayName).join("', '"), "'"); - } + return info; } -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -package(argparse) struct Group +unittest { - string name; - LazyString description; - size_t[] arguments; + auto createInfo(string placeholder = "") + { + ArgumentInfo info; + info.allowBooleanNegation = true; + info.position = 0; + info.placeholder = placeholder; + return info; + } + + auto res = createInfo().finalize!int(Config.init, "default_name"); + assert(!res.allowBooleanNegation); + assert(res.shortNames == []); + assert(res.longNames == ["default_name"]); + assert(res.displayNames == ["default_name"]); + assert(res.placeholder == "default_name"); + + res = createInfo().finalize!int(Config.init, "i"); + assert(!res.allowBooleanNegation); + assert(res.shortNames == ["i"]); + assert(res.longNames == []); + assert(res.displayNames == ["i"]); + assert(res.placeholder == "i"); + + res = createInfo("myvalue").finalize!int(Config.init, "default_name"); + assert(res.placeholder == "myvalue"); + assert(res.displayNames == ["myvalue"]); } -private template getMemberGroupUDA(TYPE, alias symbol) +unittest { - private enum udas = getUDAs!(__traits(getMember, TYPE, symbol), Group); + auto createInfo(string placeholder = "") + { + ArgumentInfo info; + info.allowBooleanNegation = true; + info.placeholder = placeholder; + return info; + } - static assert(udas.length <= 1, "Member "~TYPE.stringof~"."~symbol~" has multiple 'Group' UDAs"); - static if(udas.length > 0) - enum getMemberGroupUDA = udas[0]; + auto res = createInfo().finalize!bool(Config.init, "default_name"); + assert(res.allowBooleanNegation); + assert(res.shortNames == []); + assert(res.longNames == ["default_name"]); + assert(res.displayNames == ["--default_name"]); + assert(res.placeholder == "DEFAULT_NAME"); + + res = createInfo().finalize!bool(Config.init, "b"); + assert(res.allowBooleanNegation); + assert(res.shortNames == ["b"]); + assert(res.longNames == []); + assert(res.displayNames == ["-b"]); + assert(res.placeholder == "B"); + + res = createInfo("myvalue").finalize!bool(Config.init, "default_name"); + assert(res.placeholder == "myvalue"); + assert(res.displayNames == ["--default_name"]); } -private enum hasMemberGroupUDA(TYPE, alias symbol) = __traits(compiles, { enum group = getMemberGroupUDA!(TYPE, symbol); }); +unittest +{ + enum Config config = { caseSensitive: false }; + auto createInfo(string placeholder = "") + { + ArgumentInfo info; + info.allowBooleanNegation = true; + info.placeholder = placeholder; + return info; + } -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + auto res = createInfo().finalize!bool(config, "default_name"); + assert(res.allowBooleanNegation); + assert(res.shortNames == []); + assert(res.longNames == ["DEFAULT_NAME"]); + assert(res.displayNames == ["--default_name"]); + assert(res.placeholder == "DEFAULT_NAME"); + + res = createInfo().finalize!bool(config, "b"); + assert(res.allowBooleanNegation); + assert(res.shortNames == ["B"]); + assert(res.longNames == []); + assert(res.displayNames == ["-b"]); + assert(res.placeholder == "B"); + + res = createInfo("myvalue").finalize!bool(config, "default_name"); + assert(res.placeholder == "myvalue"); + assert(res.displayNames == ["--default_name"]); +} -package(argparse) struct RestrictionGroup +unittest { - string location; + enum E { a=1, b=1, c } - enum Type { together, exclusive } - Type type; + auto createInfo(string placeholder = "") + { + ArgumentInfo info; + info.placeholder = placeholder; + return info; + } - size_t[] arguments; + auto res = createInfo().finalize!E(Config.init, "default_name"); + assert(res.placeholder == "{a,b,c}"); - bool required; + res = createInfo("myvalue").finalize!E(Config.init, "default_name"); + assert(res.placeholder == "myvalue"); } -unittest +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +package(argparse) struct Group { - assert(!RestrictionGroup.init.required); + string name; + LazyString description; + size_t[] argIndex; } -private auto getRestrictionGroups(TYPE, alias symbol)() +private template getMemberGroupUDA(TYPE, string symbol) { - RestrictionGroup[] restrictions; + import std.traits: getUDAs; - static foreach(gr; getUDAs!(__traits(getMember, TYPE, symbol), RestrictionGroup)) - restrictions ~= gr; + private enum udas = getUDAs!(__traits(getMember, TYPE, symbol), Group); - return restrictions; + static assert(udas.length <= 1, "Member "~TYPE.stringof~"."~symbol~" has multiple 'Group' UDAs"); + static if(udas.length > 0) + enum getMemberGroupUDA = udas[0]; } -private enum getRestrictionGroups(TYPE, typeof(null) symbol) = RestrictionGroup[].init; - -unittest -{ - struct T - { - @(RestrictionGroup("1")) - @(RestrictionGroup("2")) - @(RestrictionGroup("3")) - int a; - } +private enum hasMemberGroupUDA(TYPE, alias symbol) = __traits(compiles, { enum group = getMemberGroupUDA!(TYPE, symbol); }); - assert(getRestrictionGroups!(T, "a") == [RestrictionGroup("1"), RestrictionGroup("2"), RestrictionGroup("3")]); -} /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// package struct Arguments { - ArgumentInfo[] arguments; + ArgumentInfo[] info; // named arguments size_t[string] argsNamed; @@ -253,115 +299,85 @@ package struct Arguments size_t[] argsPositional; Group[] userGroups; - size_t[string] groupsByName; + private size_t[string] groupsByName; - enum requiredGroupName = "Required arguments"; - enum optionalGroupName = "Optional arguments"; + private enum requiredGroupName = "Required arguments"; + private enum optionalGroupName = "Optional arguments"; Group requiredGroup = Group(requiredGroupName); Group optionalGroup = Group(optionalGroupName); - Restriction[] restrictions; - RestrictionGroup[] restrictionGroups; + auto namedArguments() const + { + import std.algorithm: filter; + + return info.filter!((ref _) => !_.positional); + } + + auto positionalArguments() const + { + import std.algorithm: map; + + return argsPositional.map!(ref (_) => info[_]); + } - @property auto positionalArguments() const { return argsPositional; } + void add(TYPE, ArgumentInfo[] infos)() + { + info = infos; + static foreach(index, info; infos) + add!(TYPE, info)(index); + } - void addArgument(TYPE, alias symbol, ArgumentInfo info)() + private void add(TYPE, ArgumentInfo info)(size_t argIndex) { - static if(hasMemberGroupUDA!(TYPE, symbol)) + static if(info.memberSymbol.length > 0 && hasMemberGroupUDA!(TYPE, info.memberSymbol)) { - enum group = getMemberGroupUDA!(TYPE, symbol); + enum group = getMemberGroupUDA!(TYPE, info.memberSymbol); auto index = (group.name in groupsByName); if(index !is null) - addArgumentImpl!(TYPE, symbol, info)(userGroups[*index]); + addArgumentImpl(userGroups[*index], info, argIndex); else { groupsByName[group.name] = userGroups.length; userGroups ~= group; - addArgumentImpl!(TYPE, symbol, info)(userGroups[$-1]); + addArgumentImpl(userGroups[$-1], info, argIndex); } } else static if(info.required) - addArgumentImpl!(TYPE, symbol, info)(requiredGroup); + addArgumentImpl(requiredGroup, info, argIndex); else - addArgumentImpl!(TYPE, symbol, info)(optionalGroup); + addArgumentImpl(optionalGroup, info, argIndex); } - private void addArgumentImpl(TYPE, alias symbol, ArgumentInfo info)(ref Group group) + private void addArgumentImpl(ref Group group, ArgumentInfo info, size_t argIndex) { - static assert(info.names.length > 0); + assert(info.shortNames.length + info.longNames.length > 0); - immutable index = arguments.length; - - static if(info.positional) + if(info.positional) { if(argsPositional.length <= info.position.get) argsPositional.length = info.position.get + 1; - argsPositional[info.position.get] = index; + argsPositional[info.position.get] = argIndex; } else - static foreach(name; info.names) + { + import std.range: chain; + + foreach(name; chain(info.shortNames, info.longNames)) { assert(!(name in argsNamed), "Duplicated argument name: "~name); - argsNamed[name] = index; + argsNamed[name] = argIndex; } - - arguments ~= info; - group.arguments ~= index; - - static if(info.required) - restrictions ~= Restrictions.RequiredArg!info(index); - - static foreach(restriction; getRestrictionGroups!(TYPE, symbol)) - addRestriction!(info, restriction)(index); - } - - void addRestriction(ArgumentInfo info, RestrictionGroup restriction)(size_t argIndex) - { - auto groupIndex = (restriction.location in groupsByName); - auto index = groupIndex !is null - ? *groupIndex - : { - auto index = groupsByName[restriction.location] = restrictionGroups.length; - restrictionGroups ~= restriction; - - static if(restriction.required) - restrictions ~= (a, in b, in c) => Restrictions.RequiredAnyOf(a, b, c, restrictionGroups[index].arguments); - - enum checkFunc = - { - final switch(restriction.type) - { - case RestrictionGroup.Type.together: return &Restrictions.RequiredTogether; - case RestrictionGroup.Type.exclusive: return &Restrictions.MutuallyExclusive; - } - }(); - - restrictions ~= (a, in b, in c) => checkFunc(a, b, c, restrictionGroups[index].arguments); - - return index; - }(); - - restrictionGroups[index].arguments ~= argIndex; - } - - - Result checkRestrictions(in bool[size_t] cliArgs, Config* config) const - { - foreach(restriction; restrictions) - { - auto res = restriction(config, cliArgs, arguments); - if(!res) - return res; } - return Result.Success; + group.argIndex ~= argIndex; } + struct FindResult { size_t index = size_t.max; @@ -370,7 +386,7 @@ package struct Arguments FindResult findArgumentImpl(const size_t* pIndex) const { - return pIndex ? FindResult(*pIndex, &arguments[*pIndex]) : FindResult.init; + return pIndex ? FindResult(*pIndex, &info[*pIndex]) : FindResult.init; } auto findPositionalArgument(size_t position) const diff --git a/source/argparse/internal/argumentuda.d b/source/argparse/internal/argumentuda.d index 81a5d7f..69d6f5c 100644 --- a/source/argparse/internal/argumentuda.d +++ b/source/argparse/internal/argumentuda.d @@ -1,11 +1,7 @@ module argparse.internal.argumentuda; import argparse.config; -import argparse.internal.arguments: ArgumentInfo; -import argparse.internal.utils: formatAllowedValues; -import argparse.internal.enumhelpers: getEnumValues; - -import std.traits; +import argparse.internal.arguments: ArgumentInfo, finalize; /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -19,7 +15,8 @@ package(argparse) struct ArgumentUDA(ValueParser) { auto newInfo = info; - if(newInfo.names.length == 0) newInfo.names = uda.info.names; + if(newInfo.shortNames.length == 0) newInfo.shortNames = uda.info.shortNames; + if(newInfo.longNames.length == 0) newInfo.longNames = uda.info.longNames; if(newInfo.placeholder.length == 0) newInfo.placeholder = uda.info.placeholder; if(!newInfo.description.isSet()) newInfo.description = uda.info.description; if(newInfo.position.isNull()) newInfo.position = uda.info.position; @@ -38,6 +35,8 @@ package(argparse) struct ArgumentUDA(ValueParser) private template defaultValuesCount(TYPE) if(!is(TYPE == void)) { + import std.traits; + static if(is(typeof(*TYPE) == function) || is(typeof(*TYPE) == delegate)) alias T = typeof(*TYPE); else @@ -55,7 +54,7 @@ if(!is(TYPE == void)) } else static if(isStaticArray!T) { - enum min = 1; + enum min = T.length; enum max = T.length; } else static if(isArray!T || isAssociativeArray!T) @@ -96,155 +95,134 @@ if(!is(TYPE == void)) static assert(false, "Type is not supported: " ~ T.stringof); } -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -private template getArgumentUDAImpl(Config config, MEMBERTYPE, string defaultName) +package auto getMemberArgumentUDA(TYPE, string symbol, T)(const Config config, ArgumentUDA!T defaultUDA) { - auto finalize(alias initUDA)() - { - import std.array: array; - import std.algorithm: map, each; - import std.conv: text; + import std.traits: getUDAs; - auto uda = initUDA; + alias member = __traits(getMember, TYPE, symbol); + alias MemberType = typeof(member); - static if(!isBoolean!MEMBERTYPE) - uda.info.allowBooleanNegation = false; + alias udas = getUDAs!(member, ArgumentUDA); + alias typeUDAs = getUDAs!(MemberType, ArgumentUDA); - static if(initUDA.info.names.length == 0) - uda.info.names = [ defaultName ]; + static assert(udas.length <= 1, "Member "~TYPE.stringof~"."~symbol~" has multiple '*Argument' UDAs"); + static assert(typeUDAs.length <= 1, "Type "~MemberType.stringof~" has multiple '*Argument' UDAs"); - static if(initUDA.info.placeholder.length == 0) - { - static if(is(MEMBERTYPE == enum)) - uda.info.placeholder = formatAllowedValues!(getEnumValues!MEMBERTYPE); - else static if(initUDA.info.positional) - uda.info.placeholder = defaultName; - else - { - import std.uni : toUpper; - uda.info.placeholder = defaultName.toUpper; - } - } + static if(udas.length > 0) + { + auto uda0 = udas[0]; + enum checkMinMax0 = udas[0].info.minValuesCount.isNull || udas[0].info.maxValuesCount.isNull; + } + else + { + alias uda0 = defaultUDA; + enum checkMinMax0 = true; // Passed `defaultUDA` always has undefined `minValuesCount`/`maxValuesCount` + } - static if(initUDA.info.positional) - uda.info.displayNames = [ uda.info.placeholder ]; - else - { - alias toDisplayName = _ => ( _.length == 1 ? text(config.namedArgChar, _) : text(config.namedArgChar, config.namedArgChar, _)); + static if(typeUDAs.length > 0) + { + auto uda1 = uda0.addDefaults(typeUDAs[0]); + enum checkMinMax1 = typeUDAs[0].info.minValuesCount.isNull || typeUDAs[0].info.maxValuesCount.isNull; + } + else + { + alias uda1 = uda0; + enum checkMinMax1 = true; + } - uda.info.displayNames = uda.info.names.map!toDisplayName.array; - } + uda1.info = uda1.info.finalize!MemberType(config, symbol); - static if(!config.caseSensitive) - uda.info.names.each!((ref _) => _ = config.convertCase(_)); + static if(checkMinMax0 && checkMinMax1) + { + // We must guard `defaultValuesCount!MemberType` by a `static if` to not instantiate it unless we need it + // because it produces a compilation error for unsupported types. + if(uda1.info.minValuesCount.isNull) uda1.info.minValuesCount = defaultValuesCount!MemberType.min; + if(uda1.info.maxValuesCount.isNull) uda1.info.maxValuesCount = defaultValuesCount!MemberType.max; + } - static if(initUDA.info.minValuesCount.isNull) uda.info.minValuesCount = defaultValuesCount!MEMBERTYPE.min; - static if(initUDA.info.maxValuesCount.isNull) uda.info.maxValuesCount = defaultValuesCount!MEMBERTYPE.max; + return uda1; +} +unittest +{ + enum defaultUDA = ArgumentUDA!void.init; + + auto createUDA(ulong min, ulong max) + { + auto uda = defaultUDA; + uda.info.minValuesCount = min; + uda.info.maxValuesCount = max; return uda; } -} -package template getArgumentUDA(Config config, MEMBERTYPE, string defaultName, alias initUDA) -{ - static if(__traits(compiles, getUDAs!(MEMBERTYPE, ArgumentUDA)) && getUDAs!(MEMBERTYPE, ArgumentUDA).length == 1) - enum uda = initUDA.addDefaults(getUDAs!(MEMBERTYPE, ArgumentUDA)[0]); - else - alias uda = initUDA; + @createUDA(5, 10) + struct FiveToTen {} - enum getArgumentUDA = getArgumentUDAImpl!(config, MEMBERTYPE, defaultName).finalize!uda; -} + @defaultUDA + struct UnspecifiedA {} + struct UnspecifiedB {} -unittest -{ - auto createUDA(string placeholder = "")() + @createUDA(5, 10) @createUDA(5, 10) + struct Multiple {} + + struct Args { - ArgumentInfo info; - info.allowBooleanNegation = true; - info.position = 0; - info.placeholder = placeholder; - return ArgumentUDA!void(info); + @defaultUDA bool flag; + @defaultUDA int count; + @defaultUDA @defaultUDA int incorrect; + + @defaultUDA FiveToTen fiveTen; + @defaultUDA UnspecifiedA ua; + @defaultUDA UnspecifiedB ub; + @defaultUDA Multiple mult; + + @createUDA(1, 2) FiveToTen fiveTen1; + @createUDA(1, 2) UnspecifiedA ua1; + @createUDA(1, 2) UnspecifiedB ub1; + @createUDA(1, 2) Multiple mult1; } - assert(createUDA().info.allowBooleanNegation); // make codecov happy - - auto res = getArgumentUDA!(Config.init, int, "default_name", createUDA()).info; - assert(res == getArgumentUDAImpl!(Config.init, int, "default_name").finalize!(createUDA()).info); - assert(!res.allowBooleanNegation); - assert(res.names == ["default_name"]); - assert(res.displayNames == ["default_name"]); - assert(res.minValuesCount == defaultValuesCount!int.min); - assert(res.maxValuesCount == defaultValuesCount!int.max); - assert(res.placeholder == "default_name"); - - res = getArgumentUDA!(Config.init, int, "default_name", createUDA!"myvalue"()).info; - assert(res == getArgumentUDAImpl!(Config.init, int, "default_name").finalize!(createUDA!"myvalue"()).info); - assert(res.placeholder == "myvalue"); - assert(res.displayNames == ["myvalue"]); -} -unittest -{ - auto createUDA(string placeholder = "")() + auto getInfo(string symbol)() { - ArgumentInfo info; - info.allowBooleanNegation = true; - info.placeholder = placeholder; - return ArgumentUDA!void(info); + return getMemberArgumentUDA!(Args, symbol)(Config.init, defaultUDA).info; } - assert(createUDA().info.allowBooleanNegation); // make codecov happy - - auto res = getArgumentUDA!(Config.init, bool, "default_name", createUDA()).info; - assert(res == getArgumentUDAImpl!(Config.init, bool, "default_name").finalize!(createUDA()).info); - assert(res.allowBooleanNegation); - assert(res.names == ["default_name"]); - assert(res.displayNames == ["--default_name"]); - assert(res.minValuesCount == defaultValuesCount!bool.min); - assert(res.maxValuesCount == defaultValuesCount!bool.max); - assert(res.placeholder == "DEFAULT_NAME"); - - res = getArgumentUDA!(Config.init, bool, "default_name", createUDA!"myvalue"()).info; - assert(res == getArgumentUDAImpl!(Config.init, bool, "default_name").finalize!(createUDA!"myvalue"()).info); - assert(res.placeholder == "myvalue"); - assert(res.displayNames == ["--default_name"]); -} -unittest -{ - enum E { a=1, b=1, c } + // Built-in types: - auto createUDA(string placeholder = "")() - { - ArgumentInfo info; - info.placeholder = placeholder; - return ArgumentUDA!void(info); - } - assert(createUDA().info.allowBooleanNegation); // make codecov happy + auto res = getInfo!"flag"; + assert(res.minValuesCount == 0); + assert(res.maxValuesCount == 0); - auto res = getArgumentUDA!(Config.init, E, "default_name", createUDA()).info; - assert(res == getArgumentUDAImpl!(Config.init, E, "default_name").finalize!(createUDA()).info); - assert(res.placeholder == "{a,b,c}"); + res = getInfo!"count"; + assert(res.minValuesCount == 1); + assert(res.maxValuesCount == 1); - res = getArgumentUDA!(Config.init, E, "default_name", createUDA!"myvalue"()).info; - assert(res == getArgumentUDAImpl!(Config.init, E, "default_name").finalize!(createUDA!"myvalue"()).info); - assert(res.placeholder == "myvalue"); -} + assert(!__traits(compiles, getInfo!"incorrect")); -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // With type-inherited quantifiers: -package template getMemberArgumentUDA(Config config, TYPE, alias symbol, alias defaultUDA) -{ - private alias member = __traits(getMember, TYPE, symbol); + res = getInfo!"fiveTen"; + assert(res.minValuesCount == 5); + assert(res.maxValuesCount == 10); - private enum udas = getUDAs!(member, ArgumentUDA); - static assert(udas.length <= 1, "Member "~TYPE.stringof~"."~symbol~" has multiple '*Argument' UDAs"); + assert(!__traits(compiles, getInfo!"ua")); + assert(!__traits(compiles, getInfo!"ub")); + assert(!__traits(compiles, getInfo!"mult")); - static if(udas.length > 0) - private enum uda = udas[0]; - else - private alias uda = defaultUDA; + // With explicit quantifiers: - enum getMemberArgumentUDA = getArgumentUDA!(config, typeof(member), symbol, uda); -} + res = getInfo!"fiveTen1"; + assert(res.minValuesCount == 1); + assert(res.maxValuesCount == 2); + res = getInfo!"ua1"; + assert(res.minValuesCount == 1); + assert(res.maxValuesCount == 2); + + res = getInfo!"ub1"; + assert(res.minValuesCount == 1); + assert(res.maxValuesCount == 2); + + assert(!__traits(compiles, getInfo!"mult1")); +} diff --git a/source/argparse/internal/command.d b/source/argparse/internal/command.d index 6f47f37..68abe44 100644 --- a/source/argparse/internal/command.d +++ b/source/argparse/internal/command.d @@ -1,16 +1,15 @@ module argparse.internal.command; import argparse.config; +import argparse.param; import argparse.result; -import argparse.api.argument: TrailingArguments, NamedArgument; +import argparse.api.argument: TrailingArguments, NamedArgument, NumberOfValues; import argparse.api.command: isDefaultCommand, RemoveDefaultAttribute, SubCommandsUDA = SubCommands; -import argparse.internal.arguments: Arguments; +import argparse.internal.arguments: Arguments, ArgumentInfo, finalize; +import argparse.internal.argumentuda: ArgumentUDA, getMemberArgumentUDA; import argparse.internal.commandinfo; -import argparse.internal.subcommands: SubCommands; -import argparse.internal.hooks: HookHandlers; -import argparse.internal.argumentparser; -import argparse.internal.argumentuda: ArgumentUDA, getArgumentUDA, getMemberArgumentUDA; import argparse.internal.help: HelpArgumentUDA; +import argparse.internal.restriction; import std.typecons: Nullable, nullable; import std.traits: getSymbolsByUDA, hasUDA, getUDAs; @@ -19,32 +18,145 @@ import std.sumtype: match, isSumType; /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -package struct Command +package struct SubCommands { - bool isDefault; + size_t[string] byName; + + CommandInfo[] info; + - Arguments.FindResult findNamedArgument(string name) const + void add(CommandInfo[] cmdInfo) { - return arguments.findNamedArgument(name); + info = cmdInfo; + + foreach(index, info; cmdInfo) + foreach(name; info.names) + { + assert(!(name in byName), "Duplicated name of subcommand: "~name); + byName[name] = index; + } } - Arguments.FindResult findPositionalArgument(size_t position) const +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +private Result ArgumentParsingFunction(Config config, alias uda, RECEIVER)(const Command[] cmdStack, + ref RECEIVER receiver, + string argName, + string[] rawValues) +{ + static if(is(typeof(uda.parse))) + return uda.parse!config(cmdStack, receiver, argName, rawValues); + else + try + { + auto res = uda.info.checkValuesCount(config, argName, rawValues.length); + if(!res) + return res; + + const cfg = config; + auto param = RawParam(&cfg, argName, rawValues); + + auto target = &__traits(getMember, receiver, uda.info.memberSymbol); + + static if(is(typeof(target) == function) || is(typeof(target) == delegate)) + return uda.parsingFunc.parse(target, param); + else + return uda.parsingFunc.parse(*target, param); + } + catch(Exception e) + { + return Result.Error("Argument '", config.styling.argumentName(argName), ": ", e.msg); + } +} + +private Result ArgumentCompleteFunction(Config config, alias uda, RECEIVER)(const Command[] cmdStack, + ref RECEIVER receiver, + string argName, + string[] rawValues) +{ + return Result.Success; +} + + +unittest +{ + struct T { string a; } + + auto test(string[] values) { - return arguments.findPositionalArgument(position); + T t; + + enum uda = getMemberArgumentUDA!(T, "a")(Config.init, NamedArgument("arg-name").NumberOfValues(1)); + + return ArgumentParsingFunction!(Config.init, uda)([], t, "arg-name", values); } - alias ParseFunction = Result delegate(const Command[] cmdStack, Config* config, string argName, string[] argValue); - ParseFunction[] parseFunctions; - ParseFunction[] completeFunctions; + assert(test(["raw-value"])); + assert(!test(["value1","value2"])); +} - Result parseArgument(const Command[] cmdStack, Config* config, size_t argIndex, string argName, string[] argValue) const +unittest +{ + struct T { - return parseFunctions[argIndex](cmdStack, config, argName, argValue); + void func() { throw new Exception("My Message."); } } - Result completeArgument(const Command[] cmdStack, Config* config, size_t argIndex, string argName, string[] argValue) const + + T t; + + enum uda = getMemberArgumentUDA!(T, "func")(Config.init, NamedArgument("arg-name").NumberOfValues(0)); + + auto res = ArgumentParsingFunction!(Config.init, uda)([], t, "arg-name", []); + + assert(res.isError(Config.init.styling.argumentName("arg-name")~": My Message.")); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +package struct Command +{ + alias Parse = Result delegate(const Command[] cmdStack, string argName, string[] argValue); + + struct Argument + { + size_t index = size_t.max; + + const(ArgumentInfo)* info; + + Parse parse; + Parse complete; + + + bool opCast(T : bool)() const + { + return info !is null; + } + } + + private Parse[] parseFuncs; + private Parse[] completeFuncs; + + auto findNamedArgument(string name) const { - return completeFunctions[argIndex](cmdStack, config, argName, argValue); + auto res = arguments.findNamedArgument(name); + if(!res.arg) + return Argument.init; + + return Argument(res.index, res.arg, parseFuncs[res.index], completeFuncs[res.index]); } + auto findPositionalArgument(size_t position) const + { + auto res = arguments.findPositionalArgument(position); + if(!res.arg) + return Argument.init; + + return Argument(res.index, res.arg, parseFuncs[res.index], completeFuncs[res.index]); + } + + void delegate(ref string[] args) setTrailingArgs; @@ -57,54 +169,45 @@ package struct Command import std.array: array, join; // suggestions are names of all arguments and subcommands - auto suggestions_ = chain(arguments.arguments.map!(_ => _.displayNames).join, subCommands.byName.keys); + auto suggestions_ = chain(arguments.namedArguments.map!(_ => _.displayNames).join, subCommands.byName.keys); // empty prefix means that we need to provide all suggestions, otherwise they are filtered return prefix == "" ? suggestions_.array : suggestions_.filter!(_ => _.startsWith(prefix)).array; } - string displayName() const { return info.displayNames[0]; } Arguments arguments; + Restrictions restrictions; + CommandInfo info; - SubCommands subCommands; - Command delegate() pure nothrow [] subCommandCreate; - Command delegate() pure nothrow defaultSubCommand; + string displayName() const { return info.displayNames[0]; } - auto getDefaultSubCommand(const Command[] cmdStack) const - { - return defaultSubCommand is null ? Nullable!Command.init : nullable(defaultSubCommand()); - } + SubCommands subCommands; + Command delegate() [] subCommandCreate; + Command delegate() defaultSubCommand; - auto getSubCommand(const Command[] cmdStack, string name) const + auto getSubCommand(string name) const { auto pIndex = name in subCommands.byName; if(pIndex is null) - return Nullable!Command.init; + return null; - return nullable(subCommandCreate[*pIndex]()); + return subCommandCreate[*pIndex]; } - HookHandlers hooks; - - void onParsingDone(const Config* config) const + Result checkRestrictions(in bool[size_t] cliArgs) const { - hooks.onParsingDone(config); - } - - Result checkRestrictions(in bool[size_t] cliArgs, Config* config) const - { - return arguments.checkRestrictions(cliArgs, config); + return restrictions.check(cliArgs); } } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -private enum hasNoMembersWithUDA(COMMAND) = getSymbolsByUDA!(COMMAND, ArgumentUDA ).length == 0 && - getSymbolsByUDA!(COMMAND, NamedArgument).length == 0 && - getSymbolsByUDA!(COMMAND, SubCommands ).length == 0; +private enum hasNoMembersWithUDA(COMMAND) = getSymbolsByUDA!(COMMAND, ArgumentUDA ).length == 0 && + getSymbolsByUDA!(COMMAND, NamedArgument ).length == 0 && + getSymbolsByUDA!(COMMAND, SubCommands).length == 0; private enum isOpFunction(alias mem) = is(typeof(mem) == function) && __traits(identifier, mem).length > 2 && __traits(identifier, mem)[0..2] == "op"; @@ -115,7 +218,7 @@ private template iterateArguments(TYPE) import std.meta: Filter; import std.sumtype: isSumType; - template filter(alias sym) + template filter(string sym) { alias mem = __traits(getMember, TYPE, sym); @@ -135,7 +238,7 @@ private template subCommandSymbol(TYPE) import std.meta: Filter, AliasSeq; import std.sumtype: isSumType; - template filter(alias sym) + template filter(string sym) { alias mem = __traits(getMember, TYPE, sym); @@ -157,109 +260,166 @@ private template subCommandSymbol(TYPE) /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -package(argparse) Command createCommand(Config config, COMMAND_TYPE, CommandInfo info = getCommandInfo!(config, COMMAND_TYPE))(ref COMMAND_TYPE receiver) +private struct SubCommand(TYPE) { - import std.algorithm: map; - import std.array: array; - - Command res; + alias Type = TYPE; - static foreach(symbol; iterateArguments!COMMAND_TYPE) - {{ - enum uda = getMemberArgumentUDA!(config, COMMAND_TYPE, symbol, NamedArgument); + CommandInfo info; +} - res.arguments.addArgument!(COMMAND_TYPE, symbol, uda.info); - }} +private template TypeTraits(Config config, TYPE) +{ + import std.meta: AliasSeq, Filter, staticMap, staticSort; + import std.range: chain; - res.parseFunctions = getArgumentParsingFunctions!(config, Command[], COMMAND_TYPE, iterateArguments!COMMAND_TYPE).map!(_ => - (const Command[] cmdStack, Config* config, string argName, string[] argValue) - => _(cmdStack, config, receiver, argName, argValue) - ).array; + ///////////////////////////////////////////////////////////////////// + /// Arguments - res.completeFunctions = getArgumentCompletionFunctions!(config, Command[], COMMAND_TYPE, iterateArguments!COMMAND_TYPE).map!(_ => - (const Command[] cmdStack, Config* config, string argName, string[] argValue) - => _(cmdStack, config, receiver, argName, argValue) - ).array; + private enum getArgumentUDA(string sym) = getMemberArgumentUDA!(TYPE, sym)(config, NamedArgument); + private enum getArgumentInfo(alias uda) = uda.info; + private enum positional(ArgumentInfo info) = info.positional; + private enum comparePosition(ArgumentInfo info1, ArgumentInfo info2) = info1.position.get - info2.position.get; static if(config.addHelp) - {{ - enum uda = getArgumentUDA!(Config.init, bool, null, HelpArgumentUDA()); + { + private enum helpUDA = HelpArgumentUDA(HelpArgumentUDA.init.info.finalize!bool(config, null)); + enum argumentUDAs = AliasSeq!(staticMap!(getArgumentUDA, iterateArguments!TYPE), helpUDA); + } + else + enum argumentUDAs = staticMap!(getArgumentUDA, iterateArguments!TYPE); - res.arguments.addArgument!(COMMAND_TYPE, null, uda.info); + enum argumentInfos = staticMap!(getArgumentInfo, argumentUDAs); - res.parseFunctions ~= - (const Command[] cmdStack, Config* config, string argName, string[] argValue) - => uda.parsingFunc.parse!COMMAND_TYPE(cmdStack, config, receiver, argName, argValue); - res.completeFunctions ~= - (const Command[] cmdStack, Config* config, string argName, string[] argValue) - => Result.Success; - }} + ///////////////////////////////////////////////////////////////////// + /// Subcommands - res.hooks.bind!(COMMAND_TYPE, iterateArguments!COMMAND_TYPE)(receiver); + private enum getCommandInfo(CMD) = .getCommandInfo!(RemoveDefaultAttribute!CMD)(config, RemoveDefaultAttribute!CMD.stringof); + private enum getSubcommand(CMD) = SubCommand!CMD(getCommandInfo!CMD); - res.setTrailingArgs = (ref string[] args) + static if(.subCommandSymbol!TYPE.length == 0) + private alias subCommandTypes = AliasSeq!(); + else { - .setTrailingArgs(receiver, args); - }; + enum subCommandSymbol = .subCommandSymbol!TYPE; + private alias subCommandTypes = AliasSeq!(typeof(__traits(getMember, TYPE, subCommandSymbol)).Types); + } + + enum subCommands = staticMap!(getSubcommand, subCommandTypes); + enum subCommandInfos = staticMap!(getCommandInfo, subCommandTypes); + + private alias defaultSubCommands = Filter!(isDefaultCommand, subCommandTypes); + + ///////////////////////////////////////////////////////////////////// + /// Static checks whether TYPE does not violate argparse requirements + + private enum positionalArgInfos = staticSort!(comparePosition, Filter!(positional, argumentInfos)); + + static foreach(info; argumentInfos) + static foreach (name; chain(info.shortNames, info.longNames)) + static assert(name[0] != config.namedArgPrefix, TYPE.stringof~": Argument name should not begin with '"~config.namedArgPrefix~"': "~name); + + static foreach(int i, info; positionalArgInfos) + static assert({ + enum int pos = info.position.get; + + static if(i < pos) + static assert(false, TYPE.stringof~": Positional argument with index "~i.stringof~" is missed"); + else static if(i > pos) + static assert(false, TYPE.stringof~": Positional argument with index "~pos.stringof~" is duplicated"); + + static if(pos < positionalArgInfos.length - 1) + static assert(info.minValuesCount.get == info.maxValuesCount.get, + TYPE.stringof~": Positional argument with index "~pos.stringof~" has variable number of values."); + + static if(i > 0 && info.required) + static assert(positionalArgInfos[i-1].required, TYPE.stringof~": Required positional argument with index "~pos.stringof~" comes after optional positional argument"); + + return true; + }()); + + static if(is(subCommandSymbol)) + { + static assert(defaultSubCommands.length <= 1, TYPE.stringof~": Multiple default subcommands in "~TYPE.stringof~"."~subCommandSymbol); + + static if(positionalArgInfos.length > 0 && defaultSubCommands.length > 0) + static assert(positionalArgInfos[$-1].required, TYPE.stringof~": Optional positional arguments and default subcommand are used together in one command"); + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +package(argparse) Command createCommand(Config config, COMMAND_TYPE)(ref COMMAND_TYPE receiver, CommandInfo info) +{ + import std.meta: staticMap; + import std.algorithm: map; + import std.array: array; + + alias typeTraits = TypeTraits!(config, COMMAND_TYPE); + + Command res; res.info = info; + res.arguments.add!(COMMAND_TYPE, [typeTraits.argumentInfos]); + res.restrictions.add!(COMMAND_TYPE, [typeTraits.argumentInfos])(config); + + enum getArgumentParsingFunction(alias uda) = + (const Command[] cmdStack, string argName, string[] argValue) + => ArgumentParsingFunction!(config, uda)(cmdStack, receiver, argName, argValue); + + res.parseFuncs = [staticMap!(getArgumentParsingFunction, typeTraits.argumentUDAs)]; + + enum getArgumentCompleteFunction(alias uda) = + (const Command[] cmdStack, string argName, string[] argValue) + => ArgumentCompleteFunction!(config, uda)(cmdStack, receiver, argName, argValue); - enum symbol = subCommandSymbol!COMMAND_TYPE; + res.completeFuncs = [staticMap!(getArgumentCompleteFunction, typeTraits.argumentUDAs)]; - static if(symbol.length > 0) + res.setTrailingArgs = (ref string[] args) { - static foreach(TYPE; typeof(__traits(getMember, COMMAND_TYPE, symbol)).Types) - {{ - enum cmdInfo = getCommandInfo!(config, RemoveDefaultAttribute!TYPE, RemoveDefaultAttribute!TYPE.stringof); + .setTrailingArgs(receiver, args); + }; - static if(isDefaultCommand!TYPE) - { - assert(res.defaultSubCommand is null, "Multiple default subcommands: "~COMMAND_TYPE.stringof~"."~symbol); - res.defaultSubCommand = () => ParsingSubCommandCreate!(config, TYPE, cmdInfo, COMMAND_TYPE, symbol)()(receiver); - } + res.subCommands.add([typeTraits.subCommandInfos]); - res.subCommands.add!cmdInfo; + static foreach(subcmd; typeTraits.subCommands) + {{ + auto createFunc = () => ParsingSubCommandCreate!(config, subcmd.Type)( + __traits(getMember, receiver, typeTraits.subCommandSymbol), + subcmd.info, + ); - res.subCommandCreate ~= () => ParsingSubCommandCreate!(config, TYPE, cmdInfo, COMMAND_TYPE, symbol)()(receiver); - }} - } + static if(isDefaultCommand!(subcmd.Type)) + res.defaultSubCommand = createFunc; + + res.subCommandCreate ~= createFunc; + }} return res; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -private auto ParsingSubCommandCreate(Config config, COMMAND_TYPE, CommandInfo info, RECEIVER, alias symbol)() +private auto ParsingSubCommandCreate(Config config, COMMAND_TYPE, TARGET)(ref TARGET target, CommandInfo info) { - return function (ref RECEIVER receiver) - { - auto target = &__traits(getMember, receiver, symbol); + alias create = (ref COMMAND_TYPE actualTarget) => + createCommand!(config, RemoveDefaultAttribute!COMMAND_TYPE)(actualTarget, info); - alias create = (ref COMMAND_TYPE actualTarget) - { - auto cmd = createCommand!(config, RemoveDefaultAttribute!COMMAND_TYPE, info)(actualTarget); - cmd.isDefault = isDefaultCommand!COMMAND_TYPE; - return cmd; - }; + static if(TARGET.Types.length == 1) + return target.match!create; + else + { + // Initialize if needed + if(target.match!((ref COMMAND_TYPE t) => false, _ => true)) + target = COMMAND_TYPE.init; - static if(typeof(*target).Types.length == 1) - return (*target).match!create; - else - { - // Initialize if needed - if((*target).match!((ref COMMAND_TYPE t) => false, _ => true)) - *target = COMMAND_TYPE.init; - - return (*target).match!(create, - (_) - { - assert(false, "This should never happen"); - return Command.init; - } - ); - } - }; + return target.match!(create, + function Command(_) + { + assert(false, "This should never happen"); + } + ); + } } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/source/argparse/internal/commandinfo.d b/source/argparse/internal/commandinfo.d index 4615774..d9d9867 100644 --- a/source/argparse/internal/commandinfo.d +++ b/source/argparse/internal/commandinfo.d @@ -17,13 +17,11 @@ package(argparse) struct CommandInfo /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -private auto finalize(Config config, alias initUDA)() +private auto finalize(const Config config, CommandInfo uda) { - auto uda = initUDA; - uda.displayNames = uda.names.dup; - static if(!config.caseSensitive) + if(!config.caseSensitive) { import std.algorithm: each; uda.names.each!((ref _) => _ = config.convertCase(_)); @@ -34,7 +32,7 @@ private auto finalize(Config config, alias initUDA)() unittest { - auto uda = finalize!(Config.init, CommandInfo(["cmd-Name"])); + auto uda = finalize(Config.init, CommandInfo(["cmd-Name"])); assert(uda.displayNames == ["cmd-Name"]); assert(uda.names == ["cmd-Name"]); } @@ -47,22 +45,23 @@ unittest return config; }(); - auto uda = finalize!(config, CommandInfo(["cmd-Name"])); + auto uda = finalize(config, CommandInfo(["cmd-Name"])); assert(uda.displayNames == ["cmd-Name"]); assert(uda.names == ["CMD-NAME"]); } -package template getCommandInfo(Config config, COMMAND, string name = "") +package(argparse) CommandInfo getCommandInfo(COMMAND)(const Config config, string name = "") { import std.traits: getUDAs; enum udas = getUDAs!(COMMAND, CommandInfo); - static assert(udas.length <= 1, COMMAND.stringof~" has more that one @Command UDA"); + static assert(udas.length <= 1, COMMAND.stringof~" has multiple @Command UDA"); static if(udas.length > 0) - enum getCommandInfo = finalize!(config, udas[0]); + CommandInfo info = finalize(config, udas[0]); else - enum getCommandInfo = finalize!(config, CommandInfo([name])); + CommandInfo info = finalize(config, CommandInfo([name])); - static assert(name == "" || getCommandInfo.names.length > 0 && getCommandInfo.names[0].length > 0, "Command "~COMMAND.stringof~" must have name"); + assert(name == "" || info.names.length > 0 && info.names[0].length > 0, "Command "~COMMAND.stringof~" must have name"); + return info; } \ No newline at end of file diff --git a/source/argparse/internal/completer.d b/source/argparse/internal/completer.d index a6aeb79..7bf7c82 100644 --- a/source/argparse/internal/completer.d +++ b/source/argparse/internal/completer.d @@ -62,6 +62,64 @@ package(argparse) string[] completeArgs(Config config, COMMAND)(string[] args) /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +@(Command("complete") +.Description("Print completion.") +) +private struct CompleteCmd +{ + @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, COMMAND)() + { + 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 ~= ""; + } + + completeArgs!(config, COMMAND)(args).each!writeln; + } +} + // DMD 2.100.2 fails with 'Illegal instruction' if delegate is put directly into NamedArgument.Description private alias Complete_Init_CommandName_Description(COMMAND) = delegate () { @@ -95,7 +153,7 @@ package(argparse) struct Complete(COMMAND) @(NamedArgument.Description(Complete_Init_CommandName_Description!COMMAND)) string commandName; // command to complete - void execute(Config config)() + void execute(Config config, COMMAND)() { import std.stdio: writeln; @@ -152,64 +210,6 @@ package(argparse) struct Complete(COMMAND) } } - @(Command("complete") - .Description("Print completion.") - ) - private struct CompleteCmd - { - @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 ~= ""; - } - - completeArgs!(config, COMMAND)(args).each!writeln; - } - } - @SubCommands SumType!(InitCmd, Default!CompleteCmd) cmd; } \ No newline at end of file diff --git a/source/argparse/internal/help.d b/source/argparse/internal/help.d index f3d57b8..84da6c0 100644 --- a/source/argparse/internal/help.d +++ b/source/argparse/internal/help.d @@ -3,6 +3,7 @@ module argparse.internal.help; import argparse.ansi; import argparse.config; import argparse.result; +import argparse.api.ansi: ansiStylingArgument; import argparse.internal.lazystring; import argparse.internal.arguments: ArgumentInfo, Arguments; import argparse.internal.commandinfo: CommandInfo; @@ -140,54 +141,57 @@ private void print(void delegate(string) sink, const ref Section section, string } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -package auto HelpArgumentUDA() +package struct HelpArgumentUDA { - struct HelpArgumentParsingFunction + ArgumentInfo info = { + ArgumentInfo info; + info.shortNames = ["h"]; + info.longNames = ["help"]; + info.description = "Show this help message and exit"; + info.required = false; + info.minValuesCount = 0; + info.maxValuesCount = 0; + info.allowBooleanNegation = false; + return info; + }(); + + auto parse(COMMAND_STACK)(const Config config, const COMMAND_STACK cmdStack, string argName, string[] rawValues) { - static auto parse(T, Command)(const Command[] cmdStack, Config* config, ref T receiver, string argName, string[] rawValues) - { - import std.stdio: stdout; - - import std.algorithm: map; - import std.array: join, array; + import std.stdio: stdout; + import std.algorithm: map; + import std.array: join, array; - string progName = cmdStack.map!((ref _) => _.displayName.length > 0 ? _.displayName : getProgramName()).join(" "); + string progName = cmdStack.map!((ref _) => _.displayName.length > 0 ? _.displayName : getProgramName()).join(" "); - auto args = cmdStack.map!((ref _) => &_.arguments).array; + auto args = cmdStack.map!((ref _) => &_.arguments).array; - auto output = stdout.lockingTextWriter(); - printHelp(_ => output.put(_), cmdStack[$-1], args, config, progName); + auto output = stdout.lockingTextWriter(); + printHelp(_ => output.put(_), config, cmdStack[$-1], args, progName); - return Result(0); - } + return Result(0); } - ArgumentUDA!(HelpArgumentParsingFunction) desc; - desc.info.names = ["h","help"]; - desc.info.description = "Show this help message and exit"; - desc.info.required = false; - desc.info.minValuesCount = 0; - desc.info.maxValuesCount = 0; - desc.info.allowBooleanNegation = false; - desc.info.ignoreInDefaultCommand = true; - return desc; + auto parse(Config config, COMMAND_STACK, RECEIVER)(const COMMAND_STACK cmdStack, ref RECEIVER, string argName, string[] rawValues) + { + return parse(config, cmdStack, argName, rawValues); + } } unittest { - auto h = HelpArgumentUDA(); - assert(h.info.names == ["h","help"]); - assert(!h.info.required); - assert(h.info.minValuesCount == 0); - assert(h.info.maxValuesCount == 0); - assert(!h.info.allowBooleanNegation); - assert(h.info.ignoreInDefaultCommand); + assert(HelpArgumentUDA.init.info.shortNames == ["h"]); + assert(HelpArgumentUDA.init.info.longNames == ["help"]); + assert(!HelpArgumentUDA.init.info.required); + assert(HelpArgumentUDA.init.info.minValuesCount == 0); + assert(HelpArgumentUDA.init.info.maxValuesCount == 0); + assert(!HelpArgumentUDA.init.info.allowBooleanNegation); } private bool isHelpArgument(string name) { - static foreach(n; HelpArgumentUDA().info.names) + import std.range: chain; + + static foreach(n; chain(HelpArgumentUDA.init.info.shortNames, HelpArgumentUDA.init.info.longNames)) if(n == name) return true; @@ -259,15 +263,12 @@ private void wrap(void delegate(string) sink, { import std.string: lineSplitter; import std.range: enumerate; - import std.algorithm: map; + import std.algorithm: map, splitter; import std.typecons: tuple; - import std.regex: ctRegex, splitter; if(s.length == 0) return; - enum whitespaces = ctRegex!(`\s+`); - foreach(lineIdx, line; s.lineSplitter.enumerate) { size_t col = 0; @@ -283,11 +284,11 @@ private void wrap(void delegate(string) sink, col = indent.length; } - foreach(wordIdx, word; line.splitter(whitespaces).map!(_ => _, getUnstyledTextLength).enumerate) + foreach(wordIdx, word; line.splitter.map!(_ => _, getUnstyledTextLength).enumerate) { if(wordIdx > 0) { - if (col + 1 + word[1] > columns) + if(col + 1 + word[1] > columns) { sink("\n"); sink(indent); @@ -374,7 +375,7 @@ unittest /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -private void printInvocation(NAMES)(void delegate(string) sink, const ref Style style, in ArgumentInfo info, NAMES names) +private void printInvocation(void delegate(string) sink, const ref Style style, in ArgumentInfo info, const(string)[] names) { if(info.positional) sink(style.positionalArgumentValue(printValue(info))); @@ -387,7 +388,7 @@ private void printInvocation(NAMES)(void delegate(string) sink, const ref Style if(i > 0) sink(", "); - sink(style.namedArgumentName(name)); + sink(style.argumentName(name)); if(info.maxValuesCount.get > 0) { @@ -400,17 +401,14 @@ private void printInvocation(NAMES)(void delegate(string) sink, const ref Style unittest { - auto test(bool positional)() + auto test(bool positional) { - enum info = { - ArgumentInfo info; - info.placeholder = "v"; - info.minValuesCount = 1; - info.maxValuesCount = 1; - static if (positional) - info.position = 0; - return info; - }(); + ArgumentInfo info; + info.placeholder = "v"; + info.minValuesCount = 1; + info.maxValuesCount = 1; + if(positional) + info.position = 0; import std.array: appender; auto a = appender!string; @@ -419,8 +417,8 @@ unittest return a[]; } - assert(test!false == "-f v, --foo v"); - assert(test!true == "v"); + assert(test(false) == "-f v, --foo v"); + assert(test(true) == "v"); } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -438,20 +436,17 @@ private void printUsage(void delegate(string) sink, const ref Style style, in Ar unittest { - auto test(bool required, bool positional)() + auto test(bool required, bool positional) { - enum info = { - ArgumentInfo info; - info.names ~= "foo"; - info.displayNames ~= "--foo"; - info.placeholder = "v"; - info.required = required; - info.minValuesCount = 1; - info.maxValuesCount = 1; - static if (positional) - info.position = 0; - return info; - }(); + ArgumentInfo info; + info.longNames ~= "foo"; + info.displayNames ~= "--foo"; + info.placeholder = "v"; + info.required = required; + info.minValuesCount = 1; + info.maxValuesCount = 1; + if(positional) + info.position = 0; import std.array: appender; auto a = appender!string; @@ -460,17 +455,17 @@ unittest return a[]; } - assert(test!(false, false) == "[--foo v]"); - assert(test!(false, true) == "[v]"); - assert(test!(true, false) == "--foo v"); - assert(test!(true, true) == "v"); + assert(test(false, false) == "[--foo v]"); + assert(test(false, true) == "[v]"); + assert(test(true, false) == "--foo v"); + assert(test(true, true) == "v"); } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// private void createUsage(void delegate(string) sink, const ref Style style, string progName, const(Arguments)* arguments, bool hasSubCommands) { - import std.algorithm: filter, each, map; + import std.algorithm: filter, each; alias print = (r) => r .filter!((ref _) => !_.hideFromHelp) @@ -483,9 +478,9 @@ private void createUsage(void delegate(string) sink, const ref Style style, stri sink(progName); // named args - print(arguments.arguments.filter!((ref _) => !_.positional)); + print(arguments.namedArguments); // positional args - print(arguments.positionalArguments.map!(ref (_) => arguments.arguments[_])); + print(arguments.positionalArguments); // sub commands if(hasSubCommands) sink(style.subcommandName(" []")); @@ -512,7 +507,7 @@ private string getUsage(Command)(const ref Style style, const ref Command cmd, s /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -private auto getSections(ARGUMENTS)(const ref Style style, ARGUMENTS arguments) +private auto getSections(const ref Style style, const(Arguments*)[] arguments) { import std.algorithm: filter, map; import std.array: appender, array; @@ -525,7 +520,8 @@ private auto getSections(ARGUMENTS)(const ref Style style, ARGUMENTS arguments) if(_.hideFromHelp) return false; - if(isHelpArgument(_.names[0])) + if(_.shortNames.length > 0 && isHelpArgument(_.shortNames[0]) || + _.longNames.length > 0 && isHelpArgument(_.longNames[0])) { if(hideHelpArg) return false; @@ -564,8 +560,8 @@ private auto getSections(ARGUMENTS)(const ref Style style, ARGUMENTS arguments) sections[index].entries.match!( (ref Item[] items) { - items ~= group.arguments - .map!(_ => &args.arguments[_]) + items ~= group.argIndex + .map!(_ => &args.info[_]) .filter!((const _) => showArg(*_)) .map!((const _) => getItem(*_)) .array; @@ -625,20 +621,17 @@ private auto getSection(Command)(const ref Style style, const ref Command cmd, S return section; } -package(argparse) void printHelp(ARGUMENTS, Command)(void delegate(string) sink, const ref Command cmd, ARGUMENTS arguments, Config* config, string progName) +package(argparse) void printHelp(Command)(void delegate(string) sink, const Config config, const ref Command cmd, const(Arguments*)[] arguments, string progName) { import std.algorithm: each; - bool enableStyling = config.stylingMode == Config.StylingMode.on || - config.stylingMode == Config.StylingMode.autodetect && detectSupport(); - - auto helpStyle = enableStyling ? config.helpStyle : Style.None; + auto style = ansiStylingArgument ? config.styling : Style.None; - auto argSections = getSections(helpStyle, arguments); - auto section = getSection(helpStyle, cmd, argSections, helpStyle.programName(progName)); + auto argSections = getSections(style, arguments); + auto section = getSection(style, cmd, argSections, style.programName(progName)); immutable helpPosition = section.maxItemNameLength(20) + 4; immutable indent = spaces(helpPosition + 2); - print(enableStyling ? sink : (string _) { _.getUnstyledText.each!sink; }, section, "", indent); + print(ansiStylingArgument ? sink : (string _) { _.getUnstyledText.each!sink; }, section, "", indent); } \ No newline at end of file diff --git a/source/argparse/internal/hooks.d b/source/argparse/internal/hooks.d deleted file mode 100644 index 6469b46..0000000 --- a/source/argparse/internal/hooks.d +++ /dev/null @@ -1,92 +0,0 @@ -module argparse.internal.hooks; - -import std.traits: getUDAs; - -import argparse.config; - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -private struct Hook -{ - struct ParsingDone(alias func) - { - static auto opCall(Args...)(auto ref Args args) - { - import core.lifetime: forward; - return func(forward!args); - } - } -} - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -private enum hookCall(TYPE, alias symbol, hook) = (ref TYPE receiver, const Config* config) -{ - auto target = &__traits(getMember, receiver, symbol); - hook(*target, config); -}; - -private auto parsingDoneHandlers(TYPE, symbols...)() -{ - void delegate(ref TYPE, const Config*)[] handlers; - - static foreach(symbol; symbols) - {{ - alias member = __traits(getMember, TYPE, symbol); - - static if(__traits(compiles, getUDAs!(typeof(member), Hook.ParsingDone)) && getUDAs!(typeof(member), Hook.ParsingDone).length > 0) - { - static foreach(hook; getUDAs!(typeof(member), Hook.ParsingDone)) - handlers ~= hookCall!(TYPE, symbol, hook); - } - }} - - return handlers; -} - -unittest -{ - static void onParsingDone(S)(ref S receiver, const Config* config) - { - } - @(Hooks.onParsingDone!(onParsingDone)) - static struct S - { - } - struct T - { - S a; - } - - assert(parsingDoneHandlers!(T, __traits(allMembers, T)).length == 1); -} - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -package(argparse) struct Hooks -{ - alias onParsingDone(alias func) = Hook.ParsingDone!func; - - package template Handlers(TYPE, symbols...) - { - enum parsingDone = parsingDoneHandlers!(TYPE, symbols); - } -} - -package struct HookHandlers -{ - private void delegate(const Config* config)[] parsingDone; - - - void bind(TYPE, symbols...)(ref TYPE receiver) - { - static foreach(handler; parsingDoneHandlers!(TYPE, symbols)) - parsingDone ~= (const Config* config) => handler(receiver, config); - } - - void onParsingDone(const Config* config) const - { - foreach(dg; parsingDone) - dg(config); - } -} diff --git a/source/argparse/internal/parsehelpers.d b/source/argparse/internal/parsehelpers.d index e9c951c..1c9d2e1 100644 --- a/source/argparse/internal/parsehelpers.d +++ b/source/argparse/internal/parsehelpers.d @@ -142,12 +142,17 @@ package(argparse) template ValueInList(alias values, TYPE) { static auto ValueInList(Param!TYPE param) { + import std.algorithm: map; import std.array : assocArray, join; import std.range : repeat, front; import std.conv: to; enum valuesAA = assocArray(values, false.repeat); - enum allowedValues = values.to!(string[]).join(','); + enum allowedValues = values.to!(string[]); + + auto valueStyle = (param.name.length == 0 || param.name[0] != param.config.namedArgPrefix) ? + param.config.styling.positionalArgumentValue : + param.config.styling.namedArgumentValue; static if(is(typeof(values.front) == TYPE)) auto paramValues = [param.value]; @@ -156,7 +161,8 @@ package(argparse) template ValueInList(alias values, TYPE) foreach(value; paramValues) if(!(value in valuesAA)) - return Result.Error("Invalid value '", value, "' for argument '", param.name, "'.\nValid argument values are: ", allowedValues); + return Result.Error("Invalid value '", valueStyle(value.to!string), "' for argument '", param.config.styling.argumentName(param.name), + "'.\nValid argument values are: ", allowedValues.map!(_ => valueStyle(_)).join(",")); return Result.Success; } @@ -176,11 +182,14 @@ unittest { enum values = ["a","b","c"]; - assert(ValueInList!(values, string)(Param!string(null, "", "b"))); - assert(!ValueInList!(values, string)(Param!string(null, "", "d"))); + import argparse.config; + Config config; + + assert(ValueInList!(values, string)(Param!string(&config, "", "b"))); + assert(!ValueInList!(values, string)(Param!string(&config, "", "d"))); - assert(ValueInList!(values, string)(RawParam(null, "", ["b"]))); - assert(ValueInList!(values, string)(RawParam(null, "", ["b","a"]))); - assert(!ValueInList!(values, string)(RawParam(null, "", ["d"]))); - assert(!ValueInList!(values, string)(RawParam(null, "", ["b","d"]))); + assert(ValueInList!(values, string)(RawParam(&config, "", ["b"]))); + assert(ValueInList!(values, string)(RawParam(&config, "", ["b","a"]))); + assert(!ValueInList!(values, string)(RawParam(&config, "", ["d"]))); + assert(!ValueInList!(values, string)(RawParam(&config, "", ["b","d"]))); } diff --git a/source/argparse/internal/parser.d b/source/argparse/internal/parser.d index 101dc64..a26c927 100644 --- a/source/argparse/internal/parser.d +++ b/source/argparse/internal/parser.d @@ -1,20 +1,28 @@ module argparse.internal.parser; import std.typecons: Nullable, nullable; +import std.sumtype: SumType; import argparse.config; import argparse.result; -import argparse.internal.arguments: Arguments; +import argparse.api.ansi: ansiStylingArgument; +import argparse.api.command: Default, SubCommands; +import argparse.internal.arguments: ArgumentInfo; import argparse.internal.command: Command, createCommand; +import argparse.internal.commandinfo: getCommandInfo; /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -private string[] consumeValuesFromCLI(ref string[] args, ulong minValuesCount, ulong maxValuesCount, char namedArgChar) +private string[] consumeValuesFromCLI(Config config, ref string[] args, + ulong minValuesCount, ulong maxValuesCount, + bool delegate(string) isCommand) { import std.range: empty, front, popFront; string[] values; + values.reserve(minValuesCount); + // consume minimum number of values if(minValuesCount > 0) { if(minValuesCount < args.length) @@ -29,9 +37,12 @@ private string[] consumeValuesFromCLI(ref string[] args, ulong minValuesCount, u } } + // consume up to maximum number of values while(!args.empty && values.length < maxValuesCount && - (args.front.length == 0 || args.front[0] != namedArgChar)) + args.front != config.endOfArgs && + !isCommand(args.front) && + !(args.front.length > 0 && args.front[0] == config.namedArgPrefix)) { values ~= args.front; args.popFront(); @@ -41,357 +52,905 @@ private string[] consumeValuesFromCLI(ref string[] args, ulong minValuesCount, u } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -package struct Parser +private string[] splitSingleLetterNames(string arg) { - import std.sumtype: SumType; + // Split "-ABC" into ["-A","-B","-C"] + import std.array: array; + import std.algorithm: map; + import std.conv: to; - struct Unknown {} - struct EndOfArgs {} - struct Positional {} - struct NamedShort { - string name; - string nameWithDash; - string value = null; // null when there is no "=value" - } - struct NamedLong { - string name; - string nameWithDash; - string value = null; // null when there is no "=value" - } + char prefix = arg[0]; - alias Argument = SumType!(Unknown, EndOfArgs, Positional, NamedShort, NamedLong); - - Config* config; + return arg[1..$].map!(_ => [prefix, _].to!string).array; +} +private string[] splitSingleLetterNames(string arg, char assignChar, string value) +{ + // Split "-ABC=" into ["-A","-B","-C="] - string[] args; - string[] unrecognizedArgs; + auto res = splitSingleLetterNames(arg); - bool[size_t] idxParsedArgs; - size_t idxNextPositional = 0; + // append value to the last argument + res[$-1] ~= assignChar ~ value; + return res; +} - Command[] cmdStack; +unittest +{ + assert(splitSingleLetterNames("-a") == ["-a"]); + assert(splitSingleLetterNames("-abc") == ["-a","-b","-c"]); + assert(splitSingleLetterNames("-a",'=',"") == ["-a="]); + assert(splitSingleLetterNames("-a",'=',"value") == ["-a=value"]); + assert(splitSingleLetterNames("-abc",'=',"") == ["-a","-b","-c="]); + assert(splitSingleLetterNames("-abc",'=',"value") == ["-a","-b","-c=value"]); +} +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +struct Unknown { + string value; +} +struct EndOfArgs { + string[] args; +} +struct Argument { + size_t index; + const(ArgumentInfo)* info; + Result delegate() parse; + Result delegate() complete; - Argument splitArgumentNameValue(string arg) + this(string name, FindResult r, string[] values) { - import std.string : indexOf; - - if(arg.length == 0) - return Argument.init; + index = r.arg.index; + info = r.arg.info; - if(arg == config.endOfArgs) - return Argument(EndOfArgs.init); + parse = () => r.arg.parse(r.cmdStack, name, values); + complete = () => r.arg.complete(r.cmdStack, name, values); + } +} +struct SubCommand { + Command delegate() cmdInit; +} - if(arg[0] != config.namedArgChar) - return Argument(Positional.init); +alias Entry = SumType!(Unknown, EndOfArgs, Argument, SubCommand); - if(arg.length == 1 || arg.length == 2 && arg[1] == config.namedArgChar) - return Argument.init; +private Entry getNextEntry(bool bundling)(Config config, ref string[] args, + FindResult delegate(bool) findPositionalArg, + FindResult delegate(string) findNamedArg, + Command delegate() delegate(string) findCommand) +{ + import std.range: popFront; - auto idxAssignChar = config.assignChar == char.init ? -1 : arg.indexOf(config.assignChar); + assert(args.length > 0); - immutable string nameWithDash = idxAssignChar < 0 ? arg : arg[0 .. idxAssignChar]; - immutable string value = idxAssignChar < 0 ? null : arg[idxAssignChar + 1 .. $]; + auto arg0 = args[0]; - return arg[1] == config.namedArgChar - ? Argument(NamedLong (config.convertCase(nameWithDash[2..$]), nameWithDash, value)) - : Argument(NamedShort(config.convertCase(nameWithDash[1..$]), nameWithDash, value)); + if(arg0.length == 0) + { + args.popFront; + return Entry(Unknown(arg0)); } - auto parseArgument(bool completionMode, FOUNDARG)(const Command[] cmdStack, const ref Command cmd, FOUNDARG foundArg, string value, string nameWithDash) + // Is it "--"? + if(arg0 == config.endOfArgs) { - scope(exit) idxParsedArgs[foundArg.index] = true; - - auto rawValues = value !is null ? [value] : consumeValuesFromCLI(args, foundArg.arg.minValuesCount.get, foundArg.arg.maxValuesCount.get, config.namedArgChar); - - static if(completionMode) - return cmd.completeArgument(cmdStack, config, foundArg.index, nameWithDash, rawValues); - else - return cmd.parseArgument(cmdStack, config, foundArg.index, nameWithDash, rawValues); + scope(success) args = []; // nothing else left to parse + return Entry(EndOfArgs(args[1..$])); // skip "--" } - auto parseSubCommand(const Command[] cmdStack1, const ref Command cmd) + // Is it named argument? + if(arg0[0] == config.namedArgPrefix) { - import std.range: front, popFront; - - auto subcmd = cmd.getSubCommand(cmdStack, config.convertCase(args.front)); - if(subcmd.isNull) - return Result.UnknownArgument; - - if(cmdStack1.length < cmdStack.length) - cmdStack.length = cmdStack1.length; - - addCommand(subcmd.get, false); - - args.popFront(); + import std.string : indexOf; + import std.algorithm : startsWith; - return Result.Success; + // Is it a long name ("--...")? + if(arg0.length > 1 && arg0[1] == config.namedArgPrefix) + { + // cases (from higher to lower priority): + // --foo=val => --foo val + // --abc ... => --abc ... + // --no-abc => --abc false < only for boolean flags + + // Look for assign character + immutable idxAssignChar = config.assignChar == char.init ? -1 : arg0.indexOf(config.assignChar); + if(idxAssignChar > 0) + { + // "--=" case + immutable usedName = arg0[0 .. idxAssignChar]; + immutable value = arg0[idxAssignChar + 1 .. $]; + immutable argName = config.convertCase(usedName[2..$]); // 2 to remove "--" prefix + + auto res = findNamedArg(argName); + if(res.arg) + { + args.popFront; + return Entry(Argument(usedName, res, [value])); + } + } + else + { + // Just "--" + immutable argName = config.convertCase(arg0[2..$]); // 2 to remove "--" prefix + + { + auto res = findNamedArg(argName); + if (res.arg) + { + args.popFront; + auto values = consumeValuesFromCLI(config, args, res.arg.info.minValuesCount.get, res.arg.info.maxValuesCount.get, n => (findCommand(n) !is null)); + return Entry(Argument(arg0, res, values)); + } + } + + if(argName.startsWith(config.convertCase("no-"))) + { + // It is a boolean flag specified as "--no-" + auto res = findNamedArg(argName[3..$]); // remove "no-" prefix + if(res.arg && res.arg.info.allowBooleanNegation) + { + args.popFront; + return Entry(Argument(arg0, res, ["false"])); + } + } + } + } + else + { + // It is a short name: "-..." + + // cases (from higher to lower priority): + // -foo=val => -foo val < similar to "--..." + // -abc=val => -a -b -c=val < only if config.bundling is true + // -abc => -abc < similar to "--..." + // => -a bc + // => -a -b -c < only if config.bundling is true + + // Look for assign character + immutable idxAssignChar = config.assignChar == char.init ? -1 : arg0.indexOf(config.assignChar); + if(idxAssignChar > 0) + { + // "-=" case + auto usedName = arg0[0 .. idxAssignChar]; + auto value = arg0[idxAssignChar + 1 .. $]; + auto argName = config.convertCase(usedName[1..$]); // 1 to remove "-" prefix + + { + auto res = findNamedArg(argName); + if (res.arg) + { + args.popFront; + return Entry(Argument(usedName, res, [value])); + } + } + + static if(bundling) + if(argName.length > 1) // Ensure that there is something to split + { + // Try to process "-ABC=" case where "A","B","C" are single-letter arguments + // The above example is equivalent to ["-A","-B","-C="] + + // Look for the first argument ("-A" from the example above) + auto res = findNamedArg([argName[0]]); + if(res.arg) + { + // We don't need first argument because we've already got it + auto restArgs = splitSingleLetterNames(usedName, config.assignChar, value)[1..$]; + + // Replace first element with set of single-letter arguments + args = restArgs ~ args[1..$]; + + // Due to bundling argument has no value + return Entry(Argument(usedName[0..2], res, [])); + } + } + } + else + { + // Just "-" + immutable argName = config.convertCase(arg0[1..$]); // 1 to remove "-" prefix + + { + auto res = findNamedArg(argName); + if (res.arg) + { + args.popFront; + auto values = consumeValuesFromCLI(config, args, res.arg.info.minValuesCount.get, res.arg.info.maxValuesCount.get, n => (findCommand(n) !is null)); + return Entry(Argument(arg0, res, values)); + } + } + + if(argName.length > 1) // Ensure that there is something to split + { + // Try to process "-ABC" case where "A" is a single-letter argument + + // Look for the first argument ("-A" from the example above) + auto res = findNamedArg([argName[0]]); + if(res.arg) + { + // If argument accepts at least one value then the rest is that value + if(res.arg.info.minValuesCount.get > 0) + { + auto value = arg0[2..$]; + args.popFront; + return Entry(Argument(arg0[0..2], res, [value])); + } + + static if(bundling) + { + // Process "ABC" as "-A","-B","-C" + + // We don't need first argument because we've already got it + auto restArgs = splitSingleLetterNames(arg0)[1..$]; + + // Replace first element with set of single-letter arguments + args = restArgs ~ args[1..$]; + + // Due to bundling argument has no value + return Entry(Argument(arg0[0..2], res, [])); + } + } + } + } + } } - - auto parse(bool completionMode)(const Command[] cmdStack, const ref Command cmd, Unknown) + else { - static if(completionMode) + // Check for required positional argument in the current command + auto res = findPositionalArg(true); + if(res.arg && res.arg.info.required) { - if(args.length == 1) - return Result(0, Result.Status.success, "", cmd.suggestions(args[0])); + auto values = consumeValuesFromCLI(config, args, res.arg.info.minValuesCount.get, res.arg.info.maxValuesCount.get, n => (findCommand(n) !is null)); + return Entry(Argument(res.arg.info.placeholder, res, values)); } - return Result.UnknownArgument; - } - - auto parse(bool completionMode)(const Command[] cmdStack, const ref Command cmd, EndOfArgs) - { - static if(!completionMode) + // Is it sub command? + auto cmdInit = findCommand(arg0); + if(cmdInit !is null) { - import std.range: popFront; + args.popFront; + return Entry(SubCommand(cmdInit)); + } - args.popFront(); // remove "--" + // Check for optional positional argument in the current command + if(res.arg) + { + auto values = consumeValuesFromCLI(config, args, res.arg.info.minValuesCount.get, res.arg.info.maxValuesCount.get, n => (findCommand(n) !is null)); + return Entry(Argument(res.arg.info.placeholder, res, values)); + } - cmd.setTrailingArgs(args); - unrecognizedArgs ~= args; + // Check for positional argument in sub commands + res = findPositionalArg(false); + if(res.arg) + { + auto values = consumeValuesFromCLI(config, args, res.arg.info.minValuesCount.get, res.arg.info.maxValuesCount.get, n => (findCommand(n) !is null)); + return Entry(Argument(res.arg.info.placeholder, res, values)); } + } - args = []; + args.popFront; + return Entry(Unknown(arg0)); +} - return Result.Success; - } +unittest +{ + auto test(string[] args) => getNextEntry!false(Config.init, args, null, null, null); - auto parse(bool completionMode)(const Command[] cmdStack, const ref Command cmd, Positional) - { - auto foundArg = cmd.findPositionalArgument(idxNextPositional); - if(foundArg.arg is null) - return parseSubCommand(cmdStack, cmd); + assert(test([""]) == Entry(Unknown(""))); + assert(test(["--","a","-b","c"]) == Entry(EndOfArgs(["a","-b","c"]))); +} - auto res = parseArgument!completionMode(cmdStack, cmd, foundArg, null, foundArg.arg.names[0]); - if(!res) - return res; +unittest +{ + auto test(string[] args) => getNextEntry!false(Config.init, args, null, null, null); - idxNextPositional++; + assert(test([""]) == Entry(Unknown(""))); + assert(test(["--","a","-b","c"]) == Entry(EndOfArgs(["a","-b","c"]))); +} - return Result.Success; - } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - auto parse(bool completionMode)(const Command[] cmdStack, const ref Command cmd, NamedLong arg) - { - import std.algorithm : startsWith; - import std.range: popFront; +private auto findCommand(ref Command[] cmdStack, string name) +{ + import std.range: back; - auto foundArg = cmd.findNamedArgument(arg.name); + // Look up in comand stack + foreach_reverse(ref cmd; cmdStack) + { + auto res = cmd.getSubCommand(name); + if(res) + return res; + } + // Look up through default subcommands + for(auto stack = cmdStack[]; stack.back.defaultSubCommand !is null;) + { + stack ~= stack.back.defaultSubCommand(); - if(foundArg.arg is null && arg.name.startsWith("no-")) + auto res = stack.back.getSubCommand(name); + if(res) { - foundArg = cmd.findNamedArgument(arg.name[3..$]); - if(foundArg.arg is null || !foundArg.arg.allowBooleanNegation) - return Result.UnknownArgument; + // update stack + cmdStack = stack; - arg.value = "false"; + return res; } + } + return null; +} + +private struct FindResult +{ + Command.Argument arg; - if(foundArg.arg is null) - return Result.UnknownArgument; + const(Command)[] cmdStack; +} - if(cmd.isDefault && foundArg.arg.ignoreInDefaultCommand) - return Result.UnknownArgument; +private FindResult findArgument(ref Command[] cmdStack, string name) +{ + import std.range: back, popBack; - args.popFront(); - return parseArgument!completionMode(cmdStack, cmd, foundArg, arg.value, arg.nameWithDash); + // Look up in command stack + for(auto stack = cmdStack[]; stack.length > 0; stack.popBack) + { + auto res = stack.back.findNamedArgument(name); + if(res) + return FindResult(res, stack); } - auto parse(bool completionMode)(const Command[] cmdStack, const ref Command cmd, NamedShort arg) + // Look up through default subcommands + for(auto stack = cmdStack[]; stack.back.defaultSubCommand !is null;) { - import std.range: popFront; + stack ~= stack.back.defaultSubCommand(); - auto foundArg = cmd.findNamedArgument(arg.name); - if(foundArg.arg !is null) + auto res = stack.back.findNamedArgument(name); + if(res) { - if(cmd.isDefault && foundArg.arg.ignoreInDefaultCommand) - return Result.UnknownArgument; + // update stack + cmdStack = stack; - args.popFront(); - return parseArgument!completionMode(cmdStack, cmd, foundArg, arg.value, arg.nameWithDash); + return FindResult(res, stack); } + } + + return FindResult.init; +} - // Try to parse "-ABC..." where "A","B","C" are different single-letter arguments - do +private FindResult findArgument(ref Command[] cmdStack, ref size_t[] idxPositionalStack, size_t position, bool currentStackOnly) +{ + import std.range: back; + + if(currentStackOnly) + { + // Look up in current command stack + // Actual stack can be longer than the one we looked up through last time + // because parsing of named argument can add default commands into it + for(auto stackSize = idxPositionalStack.length; stackSize <= cmdStack.length; ++stackSize) { - auto name = [arg.name[0]]; - foundArg = cmd.findNamedArgument(name); - if(foundArg.arg is null) - return Result.UnknownArgument; - - // In case of bundling there can be no or one argument value - if(config.bundling && foundArg.arg.minValuesCount.get > 1) - return Result.UnknownArgument; - - // In case of NO bundling there MUST be one argument value - if(!config.bundling && foundArg.arg.minValuesCount.get != 1) - return Result.UnknownArgument; - - string value; - if(foundArg.arg.minValuesCount == 0) - arg.name = arg.name[1..$]; - else - { - // Bundling case: try to parse "-ABvalue" where "A","B" are different single-letter arguments and "value" is a value for "B" - // No bundling case: try to parse "-Avalue" where "A" is a single-letter argument and "value" is its value - value = arg.name[1..$]; - arg.name = ""; - } + if(idxPositionalStack.length < stackSize) + idxPositionalStack ~= position; + + auto stack = cmdStack[0..stackSize]; - auto res = parseArgument!completionMode(cmdStack, cmd, foundArg, value, "-"~name); - if(!res) - return res; + auto res = stack.back.findPositionalArgument(position - idxPositionalStack[$-1]); + if(res) + return FindResult(res, stack); } - while(arg.name.length > 0); + } + else + { + for(auto stack = cmdStack[], posStack = idxPositionalStack; stack.back.defaultSubCommand !is null;) + { + stack ~= stack.back.defaultSubCommand(); + posStack ~= position; - args.popFront(); - return Result.Success; + auto res = stack.back.findPositionalArgument(0); // position is always 0 in new sub command + if(res) + { + // update stack + cmdStack = stack; + idxPositionalStack = posStack; + + return FindResult(res, stack); + } + } } + return FindResult.init; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +private struct Parser +{ + Config config; + string[] unrecognizedArgs; + + bool[size_t] idxParsedArgs; + + + size_t idxNextPositional = 0; + + size_t[] idxPositionalStack; + Command[] cmdStack; + + invariant(cmdStack.length >= idxPositionalStack.length); + + /////////////////////////////////////////////////////////////////////// - auto parse(bool completionMode)(const Command[] cmdStack, const ref Command cmd, Argument arg) + auto parse(Argument a, Result delegate() parseFunc) { - import std.sumtype: match; + auto res = parseFunc(); + if(!res) + return res; + + idxParsedArgs[a.index] = true; + + if(a.info.positional) + idxNextPositional++; - return arg.match!(_ => parse!completionMode(cmdStack, cmd, _)); + return Result.Success; } + auto parse(EndOfArgs) + { + return Result.Success; + } + auto parse(SubCommand subcmd) + { + addCommand(subcmd.cmdInit()); - auto parse(bool completionMode)(Argument arg) + return Result.Success; + } + auto parse(Unknown u) { - import std.range: front, popFront, popBack, back; + unrecognizedArgs ~= u.value; - auto result = Result.Success; + return Result.Success; + } - const argsCount = args.length; + /////////////////////////////////////////////////////////////////////// - foreach_reverse(index, cmdParser; cmdStack) - { - auto cmdStack1 = cmdStack[0..index+1]; + auto parseAll(bool completionMode, bool bundling)(string[] args) + { + import std.range: empty, join; + import std.sumtype : match; + import std.algorithm : map; + while(!args.empty) + { static if(completionMode) { - auto res = parse!true(cmdStack1, cmdParser, arg); - - if(res) - result.suggestions ~= res.suggestions; + if(args.length > 1) + { + auto res = getNextEntry!bundling( + config, args, + _ => findArgument(cmdStack, idxPositionalStack, idxNextPositional, _), + _ => findArgument(cmdStack, _), + _ => findCommand(cmdStack, _), + ) + .match!( + (Argument a) => parse(a, a.complete), + _ => parse(_)); + if (!res) + return res; + } + else + { + // Provide suggestions for the last argument only + + return Result(0, Result.Status.success, "", cmdStack.map!((ref _) => _.suggestions(args[0])).join); + } } else { - auto res = parse!false(cmdStack1, cmdParser, arg); - - if(res.status != Result.Status.unknownArgument) + auto res = getNextEntry!bundling( + config, args, + _ => findArgument(cmdStack, idxPositionalStack, idxNextPositional, _), + _ => findArgument(cmdStack, _), + _ => findCommand(cmdStack, _), + ) + .match!( + (Argument a) => parse(a, a.parse), + (EndOfArgs e) + { + import std.range: back; + + cmdStack.back.setTrailingArgs(e.args); + unrecognizedArgs ~= e.args; + + return parse(e); + }, + _ => parse(_) + ); + if (!res) return res; } } - if(args.length > 0 && argsCount == args.length) - { - unrecognizedArgs ~= args.front; - args.popFront(); - } - - return result; + return cmdStack[0].checkRestrictions(idxParsedArgs); } - auto parseAll(bool completionMode)() + void addCommand(Command cmd) { - import std.range: empty, front, back; + cmdStack ~= cmd; + idxPositionalStack ~= idxNextPositional; + } +} - while(!args.empty) - { - 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; // res contains suggestions - } +private Result callParser(bool completionMode, bool bundling)( + Config config, Command cmd, string[] args, out string[] unrecognizedArgs, +) +{ + ansiStylingArgument.isEnabled = config.stylingMode == Config.StylingMode.on; - return cmdStack[0].arguments.checkRestrictions(idxParsedArgs, config); - } + Parser parser = { config }; + parser.addCommand(cmd); - void addCommand(Command cmd, bool addDefaultCommand) + auto res = parser.parseAll!(completionMode, bundling)(args); + + static if(!completionMode) { - cmdStack ~= cmd; + if(res) + unrecognizedArgs = parser.unrecognizedArgs; + } + return res; +} - if(addDefaultCommand) - { - auto subcmd = cmd.getDefaultSubCommand(cmdStack); - if(!subcmd.isNull) - { - cmdStack ~= subcmd.get; +package(argparse) Result callParser(Config config, bool completionMode, COMMAND)(ref COMMAND receiver, string[] args, out string[] unrecognizedArgs) +if(config.stylingMode != Config.StylingMode.autodetect) +{ + return callParser!(completionMode, config.bundling)( + config, createCommand!config(receiver, getCommandInfo!COMMAND(config)), args, unrecognizedArgs, + ); +} - //import std.stdio : writeln, stderr;stderr.writeln("-- addCommand 1 ", cmd.getSubCommand); - // - //cmd = cmd.getSubCommand(DEFAULT_COMMAND); - //import std.stdio : writeln, stderr;stderr.writeln("-- addCommand 2 ", cmd); - } - } - //do - //{ - // cmd = cmd.getSubCommand(DEFAULT_COMMAND); - // if(cmd.parse !is null) - // cmdStack ~= cmd; - //} - //while(cmd.parse !is null); - } +private auto enableStyling(Config config, bool enable) +{ + config.stylingMode = enable ? Config.StylingMode.on : Config.StylingMode.off; + return config; } unittest { - Config config; - assert(Parser(&config).splitArgumentNameValue("") == Parser.Argument(Parser.Unknown.init)); - assert(Parser(&config).splitArgumentNameValue("-") == Parser.Argument(Parser.Unknown.init)); - assert(Parser(&config).splitArgumentNameValue("--") == Parser.Argument(Parser.EndOfArgs.init)); - assert(Parser(&config).splitArgumentNameValue("abc=4") == Parser.Argument(Parser.Positional.init)); - assert(Parser(&config).splitArgumentNameValue("-abc") == Parser.Argument(Parser.NamedShort("abc", "-abc", null))); - assert(Parser(&config).splitArgumentNameValue("--abc") == Parser.Argument(Parser.NamedLong("abc", "--abc", null))); - assert(Parser(&config).splitArgumentNameValue("-abc=fd") == Parser.Argument(Parser.NamedShort("abc", "-abc", "fd"))); - assert(Parser(&config).splitArgumentNameValue("--abc=fd") == Parser.Argument(Parser.NamedLong("abc", "--abc", "fd"))); - assert(Parser(&config).splitArgumentNameValue("-abc=") == Parser.Argument(Parser.NamedShort("abc", "-abc", ""))); - assert(Parser(&config).splitArgumentNameValue("--abc=") == Parser.Argument(Parser.NamedLong("abc", "--abc", ""))); - assert(Parser(&config).splitArgumentNameValue("-=abc") == Parser.Argument(Parser.NamedShort("", "-", "abc"))); - assert(Parser(&config).splitArgumentNameValue("--=abc") == Parser.Argument(Parser.NamedLong("", "--", "abc"))); + assert(enableStyling(Config.init, true).stylingMode == Config.StylingMode.on); + assert(enableStyling(Config.init, false).stylingMode == Config.StylingMode.off); +} + +package(argparse) Result callParser(Config config, bool completionMode, COMMAND)(ref COMMAND receiver, string[] args, out string[] unrecognizedArgs) +if(config.stylingMode == Config.StylingMode.autodetect) +{ + import argparse.ansi: detectSupport; + + + if(detectSupport()) + return callParser!(enableStyling(config, true), completionMode, COMMAND)(receiver, args, unrecognizedArgs); + else + return callParser!(enableStyling(config, false), completionMode, COMMAND)(receiver, args, unrecognizedArgs); } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -package(argparse) static Result callParser(Config origConfig, bool completionMode, COMMAND)(ref COMMAND receiver, string[] args, out string[] unrecognizedArgs) +unittest { - import argparse.ansi: detectSupport; + struct c1 { + string foo; + string boo; + } + struct cmd { + string foo; + SumType!(c1) c; + } + + { + cmd c; + string[] unrecognizedArgs; + assert(callParser!(enableStyling(Config.init, false), false)(c, ["--foo","FOO"], unrecognizedArgs)); + assert(unrecognizedArgs.length == 0); + assert(c == cmd("FOO")); + } + { + cmd c; + string[] unrecognizedArgs; + assert(callParser!(enableStyling(Config.init, false), false)(c, ["--boo","BOO"], unrecognizedArgs)); + assert(unrecognizedArgs == ["--boo","BOO"]); + assert(c == cmd.init); + } + { + cmd c; + string[] unrecognizedArgs; + assert(callParser!(enableStyling(Config.init, false), false)(c, ["--foo","FOO","--boo","BOO"], unrecognizedArgs)); + assert(unrecognizedArgs == ["--boo","BOO"]); + assert(c == cmd("FOO")); + } + { + cmd c; + string[] unrecognizedArgs; + assert(callParser!(enableStyling(Config.init, false), false)(c, ["--boo","BOO","--foo","FOO"], unrecognizedArgs)); + assert(unrecognizedArgs == ["--boo","BOO"]); + assert(c == cmd("FOO")); + } + { + cmd c; + string[] unrecognizedArgs; + assert(callParser!(enableStyling(Config.init, false), false)(c, ["c1","--boo","BOO","--foo","FOO"], unrecognizedArgs)); + assert(unrecognizedArgs.length == 0); + assert(c == cmd("", typeof(c.c)(c1("FOO","BOO")))); + } + { + cmd c; + string[] unrecognizedArgs; + assert(callParser!(enableStyling(Config.init, false), false)(c, ["--foo","FOO","c1","--boo","BOO","--foo","FAA"], unrecognizedArgs)); + assert(unrecognizedArgs.length == 0); + assert(c == cmd("FOO", typeof(c.c)(c1("FAA","BOO")))); + } +} + +unittest +{ + struct c1 { + string foo; + string boo; + } + struct cmd { + string foo; + SumType!(Default!c1) c; + } + + { + cmd c; + string[] unrecognizedArgs; + assert(callParser!(enableStyling(Config.init, false), false)(c, ["--foo","FOO"], unrecognizedArgs)); + assert(unrecognizedArgs.length == 0); + assert(c == cmd("FOO")); + } + { + cmd c; + string[] unrecognizedArgs; + assert(callParser!(enableStyling(Config.init, false), false)(c, ["--boo","BOO"], unrecognizedArgs)); + assert(unrecognizedArgs.length == 0); + assert(c == cmd("", typeof(c.c)(Default!c1(c1("","BOO"))))); + } + { + cmd c; + string[] unrecognizedArgs; + assert(callParser!(enableStyling(Config.init, false), false)(c, ["--foo","FOO","--boo","BOO"], unrecognizedArgs)); + assert(unrecognizedArgs.length == 0); + assert(c == cmd("FOO", typeof(c.c)(Default!c1(c1("","BOO"))))); + } + { + cmd c; + string[] unrecognizedArgs; + assert(callParser!(enableStyling(Config.init, false), false)(c, ["--boo","BOO","--foo","FOO"], unrecognizedArgs)); + assert(unrecognizedArgs.length == 0); + assert(c == cmd("", typeof(c.c)(Default!c1(c1("FOO","BOO"))))); + } + { + cmd c; + string[] unrecognizedArgs; + assert(callParser!(enableStyling(Config.init, false), false)(c, ["c1","--boo","BOO","--foo","FOO"], unrecognizedArgs)); + assert(unrecognizedArgs.length == 0); + assert(c == cmd("", typeof(c.c)(Default!c1(c1("FOO","BOO"))))); + } + { + cmd c; + string[] unrecognizedArgs; + assert(callParser!(enableStyling(Config.init, false), false)(c, ["--foo","FOO","c1","--boo","BOO","--foo","FAA"], unrecognizedArgs)); + assert(unrecognizedArgs.length == 0); + assert(c == cmd("FOO", typeof(c.c)(Default!c1(c1("FAA","BOO"))))); + } +} - auto config = origConfig; - config.setStylingModeHandlers ~= (Config.StylingMode mode) { config.stylingMode = mode; }; - auto parser = Parser(&config, args); +unittest +{ + import argparse.api.argument: PositionalArgument; + + struct c1 { + @PositionalArgument(0) + string foo; + @PositionalArgument(1) + string boo; + } + struct cmd { + @PositionalArgument(0) + string foo; + @SubCommands + SumType!(c1) c; + } + + { + cmd c; + string[] unrecognizedArgs; + assert(callParser!(enableStyling(Config.init, false), false)(c, ["FOO"], unrecognizedArgs)); + assert(unrecognizedArgs.length == 0); + assert(c == cmd("FOO")); + } + { + cmd c; + string[] unrecognizedArgs; + assert(callParser!(enableStyling(Config.init, false), false)(c, ["FOO","FAA"], unrecognizedArgs)); + assert(unrecognizedArgs == ["FAA"]); + assert(c == cmd("FOO")); + } + { + cmd c; + string[] unrecognizedArgs; + assert(callParser!(enableStyling(Config.init, false), false)(c, ["FOO","FAA","BOO"], unrecognizedArgs)); + assert(unrecognizedArgs == ["FAA","BOO"]); + assert(c == cmd("FOO")); + } + { + cmd c; + string[] unrecognizedArgs; + assert(callParser!(enableStyling(Config.init, false), false)(c, ["c1","FOO"], unrecognizedArgs)); + assert(unrecognizedArgs == ["FOO"]); + assert(c == cmd("c1")); + } + { + cmd c; + string[] unrecognizedArgs; + assert(callParser!(enableStyling(Config.init, false), false)(c, ["FOO","c1","FAA"], unrecognizedArgs)); + assert(unrecognizedArgs.length == 0); + assert(c == cmd("FOO", typeof(c.c)(c1("FAA")))); + } + { + cmd c; + string[] unrecognizedArgs; + assert(callParser!(enableStyling(Config.init, false), false)(c, ["FOO","c1","FAA","BOO"], unrecognizedArgs)); + assert(unrecognizedArgs.length == 0); + assert(c == cmd("FOO", typeof(c.c)(c1("FAA","BOO")))); + } +} + - auto cmd = createCommand!(origConfig, COMMAND)(receiver); - parser.addCommand(cmd, true); +unittest +{ + import argparse.api.argument: PositionalArgument; - auto res = parser.parseAll!completionMode; + struct c1 { + @PositionalArgument(0) + string foo; + @PositionalArgument(1) + string boo; + } + struct cmd { + @PositionalArgument(0) + string foo; + @SubCommands + SumType!(Default!c1) c; + } - static if(!completionMode) { - if(res) - { - unrecognizedArgs = parser.unrecognizedArgs; + cmd c; + string[] unrecognizedArgs; + assert(callParser!(enableStyling(Config.init, false), false)(c, ["FOO"], unrecognizedArgs)); + assert(unrecognizedArgs.length == 0); + assert(c == cmd("FOO")); + } + { + cmd c; + string[] unrecognizedArgs; + assert(callParser!(enableStyling(Config.init, false), false)(c, ["FOO","FAA"], unrecognizedArgs)); + assert(unrecognizedArgs.length == 0); + assert(c == cmd("FOO", typeof(c.c)(Default!c1(c1("FAA"))))); + } + { + cmd c; + string[] unrecognizedArgs; + assert(callParser!(enableStyling(Config.init, false), false)(c, ["FOO","FAA","BOO"], unrecognizedArgs)); + assert(unrecognizedArgs.length == 0); + assert(c == cmd("FOO", typeof(c.c)(Default!c1(c1("FAA","BOO"))))); + } + { + cmd c; + string[] unrecognizedArgs; + assert(callParser!(enableStyling(Config.init, false), false)(c, ["c1","FOO"], unrecognizedArgs)); + assert(unrecognizedArgs.length == 0); + assert(c == cmd("c1", typeof(c.c)(Default!c1(c1("FOO"))))); + } + { + cmd c; + string[] unrecognizedArgs; + assert(callParser!(enableStyling(Config.init, false), false)(c, ["FOO","c1","FAA"], unrecognizedArgs)); + assert(unrecognizedArgs.length == 0); + assert(c == cmd("FOO", typeof(c.c)(Default!c1(c1("FAA"))))); + } + { + cmd c; + string[] unrecognizedArgs; + assert(callParser!(enableStyling(Config.init, false), false)(c, ["FOO","c1","FAA","BOO"], unrecognizedArgs)); + assert(unrecognizedArgs.length == 0); + assert(c == cmd("FOO", typeof(c.c)(Default!c1(c1("FAA","BOO"))))); + } +} - if(config.stylingMode == Config.StylingMode.autodetect) - config.setStylingMode(detectSupport() ? Config.StylingMode.on : Config.StylingMode.off); - cmd.onParsingDone(&config); - } - else if(res.errorMsg.length > 0) - config.onError(res.errorMsg); +unittest +{ + import argparse.api.argument: PositionalArgument, NamedArgument; + + struct c2 { + @PositionalArgument(0) + string foo; + @PositionalArgument(1) + string boo; + @NamedArgument + string bar; + } + struct c1 { + @PositionalArgument(0) + string foo; + @PositionalArgument(1) + string boo; + @SubCommands + SumType!(Default!c2) c; + } + struct cmd { + @PositionalArgument(0) + string foo; + @SubCommands + SumType!(Default!c1) c; } - return res; + { + cmd c; + string[] unrecognizedArgs; + assert(callParser!(enableStyling(Config.init, false), false)(c, ["FOO","FAA","BOO","FEE","BEE"], unrecognizedArgs)); + assert(unrecognizedArgs.length == 0); + assert(c == cmd("FOO", typeof(c.c)(Default!c1(c1("FAA","BOO", typeof(c1.c)(Default!c2(c2("FEE","BEE")))))))); + } + { + cmd c; + string[] unrecognizedArgs; + assert(callParser!(enableStyling(Config.init, false), false)(c, ["--bar","BAR","FOO","FAA","BOO","FEE","BEE"], unrecognizedArgs)); + assert(unrecognizedArgs.length == 0); + assert(c == cmd("FOO", typeof(c.c)(Default!c1(c1("FAA","BOO", typeof(c1.c)(Default!c2(c2("FEE","BEE","BAR")))))))); + } } -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// \ No newline at end of file + +unittest +{ + import argparse.api.argument: PositionalArgument, NamedArgument; + + struct c2 { + @PositionalArgument(0) + string foo; + @PositionalArgument(1) + string boo; + @NamedArgument + string bar; + } + struct c1 { + @SubCommands + SumType!(Default!c2) c; + } + struct cmd { + @PositionalArgument(0) + string foo; + @SubCommands + SumType!(Default!c1) c; + } + + { + cmd c; + string[] unrecognizedArgs; + assert(callParser!(enableStyling(Config.init, false), false)(c, ["FOO","FAA","BOO"], unrecognizedArgs)); + assert(unrecognizedArgs.length == 0); + assert(c == cmd("FOO", typeof(c.c)(Default!c1(c1(typeof(c1.c)(Default!c2(c2("FAA","BOO")))))))); + } + { + cmd c; + string[] unrecognizedArgs; + assert(callParser!(enableStyling(Config.init, false), false)(c, ["--bar","BAR","FOO","FAA","BOO"], unrecognizedArgs)); + assert(unrecognizedArgs.length == 0); + assert(c == cmd("FOO", typeof(c.c)(Default!c1(c1(typeof(c1.c)(Default!c2(c2("FAA","BOO","BAR")))))))); + } + { + cmd c; + string[] unrecognizedArgs; + assert(callParser!(enableStyling(Config.init, false), false)(c, ["FOO","c2","FAA","BOO"], unrecognizedArgs)); + assert(unrecognizedArgs.length == 0); + assert(c == cmd("FOO", typeof(c.c)(Default!c1(c1(typeof(c1.c)(Default!c2(c2("FAA","BOO")))))))); + } +} \ No newline at end of file diff --git a/source/argparse/internal/restriction.d b/source/argparse/internal/restriction.d new file mode 100644 index 0000000..527301d --- /dev/null +++ b/source/argparse/internal/restriction.d @@ -0,0 +1,273 @@ +module argparse.internal.restriction; + +import argparse.config; +import argparse.result; +import argparse.internal.arguments: ArgumentInfo; + +import std.traits: getUDAs; + + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + +private auto RequiredArg(const Config config, const ArgumentInfo info, size_t index) +{ + return (in bool[size_t] cliArgs) + { + return (index in cliArgs) ? + Result.Success : + Result.Error("The following argument is required: '", config.styling.argumentName(info.displayName), "'"); + }; +} + +unittest +{ + auto f = RequiredArg(Config.init, ArgumentInfo([],[],[""]), 0); + + assert(f(bool[size_t].init).isError("argument is required")); + + assert(f([1:true]).isError("argument is required")); + + assert(f([0:true])); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +private auto RequiredTogether(const Config config, const(ArgumentInfo)[] allArgs) +{ + return (in bool[size_t] cliArgs, in size_t[] restrictionArgs) + { + size_t foundIndex = size_t.max; + size_t missedIndex = size_t.max; + + foreach(index; restrictionArgs) + { + if(index in cliArgs) + { + if(foundIndex == size_t.max) + foundIndex = index; + } + else if(missedIndex == size_t.max) + missedIndex = index; + + if(foundIndex != size_t.max && missedIndex != size_t.max) + return Result.Error("Missed argument '", config.styling.argumentName(allArgs[missedIndex].displayName), + "' - it is required by argument '", config.styling.argumentName(allArgs[foundIndex].displayName), "'"); + } + + return Result.Success; + }; +} + + +unittest +{ + auto f = RequiredTogether(Config.init, [ArgumentInfo([],[],["--a"]), ArgumentInfo([],[],["--b"]), ArgumentInfo([],[],["--c"])]); + + assert(f(bool[size_t].init, [0,1])); + + assert(f([0:true], [0,1]).isError("Missed argument","--a")); + assert(f([1:true], [0,1]).isError("Missed argument","--b")); + + assert(f([0:true, 1:true], [0,1])); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +private auto RequiredAnyOf(const Config config, const(ArgumentInfo)[] allArgs) +{ + return (in bool[size_t] cliArgs, in size_t[] restrictionArgs) + { + import std.algorithm: map; + import std.array: join; + + foreach(index; restrictionArgs) + if(index in cliArgs) + return Result.Success; + + return Result.Error("One of the following arguments is required: '", + restrictionArgs.map!(_ => config.styling.argumentName(allArgs[_].displayName)).join("', '"), "'"); + }; +} + + +unittest +{ + auto f = RequiredAnyOf(Config.init, [ArgumentInfo([],[],["--a"]), ArgumentInfo([],[],["--b"]), ArgumentInfo([],[],["--c"])]); + + assert(f(bool[size_t].init, [0,1]).isError("One of the following arguments is required","--a","--b")); + assert(f([2:true], [0,1]).isError("One of the following arguments is required","--a","--b")); + + assert(f([0:true], [0,1])); + assert(f([1:true], [0,1])); + + assert(f([0:true, 1:true], [0,1])); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +private auto MutuallyExclusive(const Config config, const(ArgumentInfo)[] allArgs) +{ + return (in bool[size_t] cliArgs, in size_t[] restrictionArgs) + { + size_t foundIndex = size_t.max; + + foreach(index; restrictionArgs) + if(index in cliArgs) + { + if(foundIndex == size_t.max) + foundIndex = index; + else + return Result.Error("Argument '", config.styling.argumentName(allArgs[foundIndex].displayName), + "' is not allowed with argument '", config.styling.argumentName(allArgs[index].displayName),"'"); + } + + return Result.Success; + }; +} + + +unittest +{ + auto f = MutuallyExclusive(Config.init, [ArgumentInfo([],[],["--a"]), ArgumentInfo([],[],["--b"]), ArgumentInfo([],[],["--c"])]); + + assert(f(bool[size_t].init, [0,1])); + + assert(f([0:true], [0,1])); + assert(f([1:true], [0,1])); + assert(f([2:true], [0,1])); + + assert(f([0:true, 2:true], [0,1])); + assert(f([1:true, 2:true], [0,1])); + + assert(f([0:true, 1:true], [0,1]).isError("is not allowed with argument","--a","--b")); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +package(argparse) struct RestrictionGroup +{ + string location; + + enum Type { together, exclusive } + Type type; + + bool required; + + private size_t[] argIndex; + + + private Result delegate(in bool[size_t] cliArgs, in size_t[] argIndex)[] checks; + + private void initialize(ref const Config config, const(ArgumentInfo)[] infos) + { + if(required) + checks ~= RequiredAnyOf(config, infos); + + final switch(type) + { + case Type.together: checks ~= RequiredTogether (config, infos); break; + case Type.exclusive: checks ~= MutuallyExclusive(config, infos); break; + } + } + + private Result check(in bool[size_t] cliArgs) const + { + foreach(check; checks) + { + auto res = check(cliArgs, argIndex); + if(!res) + return res; + } + + return Result.Success; + } +} + +unittest +{ + assert(!RestrictionGroup.init.required); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +private auto getRestrictionGroups(TYPE, string symbol)() +{ + RestrictionGroup[] restrictions; + + static if(symbol.length > 0) + static foreach(gr; getUDAs!(__traits(getMember, TYPE, symbol), RestrictionGroup)) + restrictions ~= gr; + + return restrictions; +} + +unittest +{ + struct T + { + @(RestrictionGroup("1")) + @(RestrictionGroup("2")) + @(RestrictionGroup("3")) + int a; + } + + assert(getRestrictionGroups!(T, "a") == [RestrictionGroup("1"), RestrictionGroup("2"), RestrictionGroup("3")]); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +package struct Restrictions +{ + private Result delegate(in bool[size_t] cliArgs)[] checks; + private RestrictionGroup[] groups; + private size_t[string] groupsByLocation; + + + package void add(TYPE, ArgumentInfo[] infos)(Config config) + { + static foreach(argIndex, info; infos) + static if(info.memberSymbol !is null) // to skip HelpArgumentUDA + { + static if(info.required) + checks ~= RequiredArg(config, info, argIndex); + + static if(info.memberSymbol) + static foreach(group; getRestrictionGroups!(TYPE, info.memberSymbol)) + {{ + auto groupIndex = (group.location in groupsByLocation); + if(groupIndex !is null) + groups[*groupIndex].argIndex ~= argIndex; + else + { + auto gIndex = groupsByLocation[group.location] = groups.length; + groups ~= group; + + groups[gIndex].initialize(config, infos); + groups[gIndex].argIndex ~= argIndex; + } + }} + } + } + + + package Result check(in bool[size_t] cliArgs) const + { + foreach(check; checks) + { + auto res = check(cliArgs); + if(!res) + return res; + } + + foreach(ref group; groups) + { + auto res = group.check(cliArgs); + if(!res) + return res; + } + + return Result.Success; + } +} + diff --git a/source/argparse/internal/style.d b/source/argparse/internal/style.d index 843a2c3..874bbf8 100644 --- a/source/argparse/internal/style.d +++ b/source/argparse/internal/style.d @@ -11,25 +11,28 @@ package(argparse) struct Style TextStyle programName; TextStyle subcommandName; TextStyle argumentGroupTitle; - TextStyle namedArgumentName; + TextStyle argumentName; TextStyle namedArgumentValue; TextStyle positionalArgumentValue; + TextStyle errorMessagePrefix; + enum None = Style.init; - enum Default = Style( - bold, // programName - bold, // subcommandName - bold.underline, // argumentGroupTitle - lightYellow, // namedArgumentName - italic, // namedArgumentValue - lightYellow, // positionalArgumentValue - ); + enum Style Default = { + programName: bold, + subcommandName: bold, + argumentGroupTitle: bold.underline, + argumentName: lightYellow, + namedArgumentValue: italic, + positionalArgumentValue: lightYellow, + errorMessagePrefix: red, + }; } unittest { assert(Style.Default.argumentGroupTitle("bbb") == bold.underline("bbb").toString); - assert(Style.Default.namedArgumentName("bbb") == lightYellow("bbb").toString); + assert(Style.Default.argumentName("bbb") == lightYellow("bbb").toString); assert(Style.Default.namedArgumentValue("bbb") == italic("bbb").toString); } \ No newline at end of file diff --git a/source/argparse/internal/subcommands.d b/source/argparse/internal/subcommands.d deleted file mode 100644 index 507f6a4..0000000 --- a/source/argparse/internal/subcommands.d +++ /dev/null @@ -1,29 +0,0 @@ -module argparse.internal.subcommands; - -import argparse.internal.commandinfo; - - - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - -package struct SubCommands -{ - size_t[string] byName; - - CommandInfo[] info; - - - void add(CommandInfo cmdInfo)() - { - immutable index = info.length; - - static foreach(name; cmdInfo.names) - {{ - assert(!(name in byName), "Duplicated name of subcommand: "~name); - byName[name] = index; - }} - - info ~= cmdInfo; - } -} \ No newline at end of file diff --git a/source/argparse/internal/utils.d b/source/argparse/internal/utils.d index 912cdfa..cce42ae 100644 --- a/source/argparse/internal/utils.d +++ b/source/argparse/internal/utils.d @@ -2,41 +2,13 @@ module argparse.internal.utils; /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// 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" -package auto partiallyApply(alias fun,C...)(C context) +package(argparse) string formatAllowedValues(T)(const(T)[] names) { - import std.traits: ParameterTypeTuple; - import core.lifetime: move, forward; - - return &new class(move(context)) - { - C context; - - this(C ctx) - { - foreach(i, ref c; context) - c = move(ctx[i]); - } - - auto opCall(ParameterTypeTuple!fun[context.length..$] args) const - { - return fun(context, forward!args); - } - }.opCall; -} - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -package(argparse) string formatAllowedValues(alias names)() -{ - import std.conv: to; - import std.array: join; import std.format: format; - return "{%s}".format(names.to!(string[]).join(',')); + return "{%-(%s,%)}".format(names); } unittest { - assert(formatAllowedValues!(["abc", "def", "ghi"]) == "{abc,def,ghi}"); + assert(formatAllowedValues(["abc", "def", "ghi"]) == "{abc,def,ghi}"); } \ No newline at end of file diff --git a/source/argparse/internal/valueparser.d b/source/argparse/internal/valueparser.d index 829a747..b7466f1 100644 --- a/source/argparse/internal/valueparser.d +++ b/source/argparse/internal/valueparser.d @@ -18,6 +18,13 @@ package(argparse) struct ValueParser(alias PreProcess, alias Action, alias NoValueAction) { + alias PreProcessArg = PreProcess; + alias PreValidationArg = PreValidation; + alias ParseArg = Parse; + alias ValidationArg = Validation; + alias ActionArg = Action; + alias NoValueActionArg = NoValueAction; + alias changePreProcess (alias func) = ValueParser!( func, PreValidation, Parse, Validation, Action, NoValueAction); alias changePreValidation(alias func) = ValueParser!(PreProcess, func, Parse, Validation, Action, NoValueAction); alias changeParse (alias func) = ValueParser!(PreProcess, PreValidation, func, Validation, Action, NoValueAction); @@ -27,35 +34,23 @@ package(argparse) struct ValueParser(alias PreProcess, template addDefaults(DefaultParseFunctions) { - static if(is(PreProcess == void)) - alias preProc = DefaultParseFunctions; - else - alias preProc = DefaultParseFunctions.changePreProcess!PreProcess; - - static if(is(PreValidation == void)) - alias preVal = preProc; - else - alias preVal = preProc.changePreValidation!PreValidation; - - static if(is(Parse == void)) - alias parse = preVal; - else - alias parse = preVal.changeParse!Parse; - - static if(is(Validation == void)) - alias val = parse; - else - alias val = parse.changeValidation!Validation; - - static if(is(Action == void)) - alias action = val; - else - alias action = val.changeAction!Action; + template Get(string symbol) + { + alias M = mixin(symbol); + static if(is(M == void)) + alias Get = __traits(getMember, DefaultParseFunctions, symbol); + else + alias Get = M; + } - static if(is(NoValueAction == void)) - alias addDefaults = action; - else - alias addDefaults = action.changeNoValueAction!NoValueAction; + alias addDefaults = ValueParser!( + Get!"PreProcessArg", + Get!"PreValidationArg", + Get!"ParseArg", + Get!"ValidationArg", + Get!"ActionArg", + Get!"NoValueActionArg", + ); } diff --git a/source/argparse/package.d b/source/argparse/package.d index ea160fd..73ab46e 100644 --- a/source/argparse/package.d +++ b/source/argparse/package.d @@ -19,6 +19,7 @@ public import argparse.result; version(unittest) { import argparse.internal.command : createCommand; + import argparse.internal.commandinfo : getCommandInfo; import argparse.internal.help : printHelp; import argparse.ansi : cleanStyleEnv, restoreStyleEnv; } @@ -50,8 +51,8 @@ unittest }(); T receiver; - auto a = createCommand!config(receiver); - assert(a.arguments.requiredGroup.arguments == [2,4]); + auto a = createCommand!config(receiver, getCommandInfo!T(config)); + assert(a.arguments.requiredGroup.argIndex == [2,4]); assert(a.arguments.argsNamed == ["a":0LU, "b":1LU, "c":2LU, "d":3LU, "e":4LU, "f":5LU]); assert(a.arguments.argsPositional == []); } @@ -70,8 +71,8 @@ unittest }(); T receiver; - auto a = createCommand!config(receiver); - assert(a.arguments.requiredGroup.arguments == []); + auto a = createCommand!config(receiver, getCommandInfo!T(config)); + assert(a.arguments.requiredGroup.argIndex == []); assert(a.arguments.argsNamed == ["a":0LU, "b":1LU, "c":2LU, "d":3LU, "e":4LU, "f":5LU]); assert(a.arguments.argsPositional == []); } @@ -84,7 +85,7 @@ unittest @(NamedArgument("2")) int a; } - static assert(!__traits(compiles, { T1 t; enum c = createCommand!(Config.init)(t); })); + static assert(!__traits(compiles, { T1 t; enum c = createCommand!(Config.init)(t, getCommandInfo!T1(Config.init)); })); struct T2 { @@ -93,21 +94,28 @@ unittest @(NamedArgument("1")) int b; } - static assert(!__traits(compiles, { T2 t; enum c = createCommand!(Config.init)(t); })); + static assert(!__traits(compiles, { T2 t; enum c = createCommand!(Config.init)(t, getCommandInfo!T2(Config.init)); })); struct T3 { @(PositionalArgument(0)) int a; @(PositionalArgument(0)) int b; } - static assert(!__traits(compiles, { T3 t; enum c = createCommand!(Config.init)(t); })); + static assert(!__traits(compiles, { T3 t; enum c = createCommand!(Config.init)(t, getCommandInfo!T3(Config.init)); })); struct T4 { @(PositionalArgument(0)) int a; @(PositionalArgument(2)) int b; } - static assert(!__traits(compiles, { T4 t; enum c = createCommand!(Config.init)(t); })); + static assert(!__traits(compiles, { T4 t; enum c = createCommand!(Config.init)(t, getCommandInfo!T4(Config.init)); })); + + struct T5 + { + @(PositionalArgument(0)) int[] a; + @(PositionalArgument(1)) int b; + } + static assert(!__traits(compiles, { T5 t; enum c = createCommand!(Config.init)(t, getCommandInfo!T5(Config.init)); })); } unittest @@ -149,7 +157,7 @@ unittest } params receiver; - auto a = createCommand!(Config.init)(receiver); + auto a = createCommand!(Config.init)(receiver, getCommandInfo!params(Config.init)); } unittest @@ -160,11 +168,6 @@ unittest string b; } - assert(CLI!T.parseArgs!((T t, string[] args) { - assert(t == T("A")); - assert(args == []); - return 12345; - })(["-a","A","--"]) == 12345); assert(CLI!T.parseArgs!((T t, string[] args) { assert(t == T("A")); assert(args == []); @@ -181,6 +184,39 @@ unittest } } +unittest +{ + struct T + { + string a; + } + + import std.exception; + + assert(collectExceptionMsg( + CLI!T.parseArgs!((T t, string[] args) { + assert(t == T.init); + assert(args.length == 0); + throw new Exception("My Message."); + })([])) + == "My Message."); + assert(collectExceptionMsg( + CLI!T.parseArgs!((T t, string[] args) { + assert(t == T("aa")); + assert(args == ["-g"]); + throw new Exception("My Message."); + })(["-a","aa","-g"])) + == "My Message."); + assert(CLI!T.parseArgs!((T t, string[] args) { + assert(t == T.init); + assert(args.length == 0); + })([]) == 0); + assert(CLI!T.parseArgs!((T t, string[] args) { + assert(t == T("aa")); + assert(args == ["-g"]); + })(["-a","aa","-g"]) == 0); +} + unittest { struct T @@ -202,11 +238,11 @@ unittest return 12345; })(["-a","aa","-g"]) == 12345); assert(CLI!T.parseArgs!((T t) { - assert(t.color == Config.StylingMode.on); + assert(t.color); return 12345; })(["--color"]) == 12345); assert(CLI!T.parseArgs!((T t) { - assert(t.color == Config.StylingMode.off); + assert(!t.color); return 12345; })(["--color","never"]) == 12345); } @@ -282,6 +318,7 @@ unittest { bool a; bool b; + string c; } enum config = { Config config; @@ -291,6 +328,7 @@ unittest assert(CLI!(config, T).parseArgs!((T t) { assert(t == T(true, true)); return 12345; })(["-a","-b"]) == 12345); assert(CLI!(config, T).parseArgs!((T t) { assert(t == T(true, true)); return 12345; })(["-ab"]) == 12345); + assert(CLI!(config, T).parseArgs!((T t) { assert(t == T(true, true, "foo")); return 12345; })(["-abc=foo"]) == 12345); } unittest @@ -478,7 +516,18 @@ unittest } assert(CLI!T.parseArgs!((T t) { assert(t == T(3)); return 12345; })(["-a", "3"]) == 12345); - assert(CLI!T.parseArgs!((T t) { assert(false); })(["-a", "2"]) != 0); // "kiwi" is not allowed + assert(CLI!T.parseArgs!((T t) { assert(false); })(["-a", "2"]) != 0); // "2" is not allowed +} + +unittest +{ + struct T + { + @(PositionalArgument(0).AllowedValues!([1,3,5])) int a; + } + + assert(CLI!T.parseArgs!((T t) { assert(t == T(3)); return 12345; })(["3"]) == 12345); + assert(CLI!T.parseArgs!((T t) { assert(false); })(["2"]) != 0); // "2" is not allowed } unittest @@ -831,27 +880,20 @@ unittest @TrailingArguments string[] args; } - auto test() - { - import std.array: appender; + import std.array: appender; - auto a = appender!string; - alias cfg = { - Config config; - config.stylingMode = Config.StylingMode.off; - return config; - }; - auto config = cfg(); - T receiver; - auto cmd = createCommand!(cfg())(receiver); - printHelp(_ => a.put(_), cmd, [&cmd.arguments], &config, "MYPROG"); - return a[]; - } + auto a = appender!string; + + T receiver; + auto cmd = createCommand!(Config.init)(receiver, getCommandInfo!T(Config.init)); + + auto isEnabled = ansiStylingArgument.isEnabled; + scope(exit) ansiStylingArgument.isEnabled = isEnabled; + ansiStylingArgument.isEnabled = false; - auto env = cleanStyleEnv(true); - scope(exit) restoreStyleEnv(env); + printHelp(_ => a.put(_), Config.init, cmd, [&cmd.arguments], "MYPROG"); - assert(test() == "Usage: MYPROG [-s S] [-p VALUE] -f {apple,pear} [-i {1,4,16,8}] [-h] param0 {q,a}\n\n"~ + assert(a[] == "Usage: MYPROG [-s S] [-p VALUE] -f {apple,pear} [-i {1,4,16,8}] [-h] param0 {q,a}\n\n"~ "custom description\n\n"~ "Required arguments:\n"~ " -f {apple,pear}, --fruit {apple,pear}\n"~ @@ -896,19 +938,17 @@ unittest import std.array: appender; - auto env = cleanStyleEnv(true); - scope(exit) restoreStyleEnv(env); - auto a = appender!string; - alias cfg = { - Config config; - config.stylingMode = Config.StylingMode.off; - return config; - }; - auto config = cfg(); + T receiver; - auto cmd = createCommand!(cfg())(receiver); - printHelp(_ => a.put(_), cmd, [&cmd.arguments], &config, "MYPROG"); + auto cmd = createCommand!(Config.init)(receiver, getCommandInfo!T(Config.init)); + + auto isEnabled = ansiStylingArgument.isEnabled; + scope(exit) ansiStylingArgument.isEnabled = isEnabled; + ansiStylingArgument.isEnabled = false; + + printHelp(_ => a.put(_), Config.init, cmd, [&cmd.arguments], "MYPROG"); + assert(a[] == "Usage: MYPROG [-a A] [-b B] [-c C] [-d D] [-h] p q\n\n"~ "group1:\n"~ @@ -952,19 +992,16 @@ unittest import std.array: appender; - auto env = cleanStyleEnv(true); - scope(exit) restoreStyleEnv(env); - auto a = appender!string; - alias cfg = { - Config config; - config.stylingMode = Config.StylingMode.off; - return config; - }; - auto config = cfg(); + T receiver; - auto cmd = createCommand!(cfg())(receiver); - printHelp(_ => a.put(_), cmd, [&cmd.arguments], &config, "MYPROG"); + auto cmd = createCommand!(Config.init)(receiver, getCommandInfo!T(Config.init)); + + auto isEnabled = ansiStylingArgument.isEnabled; + scope(exit) ansiStylingArgument.isEnabled = isEnabled; + ansiStylingArgument.isEnabled = false; + + printHelp(_ => a.put(_), Config.init, cmd, [&cmd.arguments], "MYPROG"); assert(a[] == "Usage: MYPROG [-c C] [-d D] [-h] []\n\n"~ "Available commands:\n"~ diff --git a/source/argparse/result.d b/source/argparse/result.d index 35c1aed..bae777d 100644 --- a/source/argparse/result.d +++ b/source/argparse/result.d @@ -30,10 +30,27 @@ struct Result version(unittest) { - package bool isError(string text) + package bool isError(string[] text...) { import std.algorithm: canFind; - return (!cast(bool) this) && errorMsg.canFind(text); + + if(cast(bool) this) + return false; // success is not an error + + foreach(s; text) + if(!errorMsg.canFind(s)) + return false; // can't find required text + + return true; // all required text is found } } +} + +unittest +{ + assert(!Result.Success.isError); + + auto r = Result.Error("some text",",","more text"); + assert(r.isError("some", "more")); + assert(!r.isError("other text")); } \ No newline at end of file