From ab8195b09d189d096863e1741cdc8edacdc53136 Mon Sep 17 00:00:00 2001 From: Andrey Zherikov Date: Sat, 23 Apr 2022 23:39:45 -0400 Subject: [PATCH] Refactor public API (#39) * Add parseKnownArgs and parseArgs to CLI * Forward CLI(Config config, COMMANDS...).main to CLI(Config config, COMMAND).main * Deprecate CLI(Config config) * Deprecate Main * Deprecate parseCLIKnownArgs and parseCLIArgs * Revert deletion of compile time example * Remove parenthesis * Add unit tests for CLI!T.parseArgs * Update readme --- README.md | 383 ++++++++++++------------ examples/getting_started/advanced/app.d | 2 +- examples/getting_started/basic/app.d | 27 +- source/argparse.d | 293 +++++++++++++++--- 4 files changed, 457 insertions(+), 248 deletions(-) diff --git a/README.md b/README.md index d5875da..c84f5a1 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ static struct Basic } // This mixin defines standard main function that parses command line and calls the provided function: -mixin Main.parseCLIArgs!(Basic, (args) +mixin CLI!Basic.main!((args) { // 'args' has 'Basic' type static assert(is(typeof(args) == Basic)); @@ -127,16 +127,21 @@ Optional arguments: Parser can even work at compile time, so you can do something like this: ```d -enum values = ([ - "--boolean", - "--number","100", - "--name","Jake", - "--array","1","2","3", - "--choice","foo", - "--callback", - "--callback1","cb-value", - "--callback2","cb-v1","cb-v2", -].parseCLIArgs!Basic).get; +enum values = { + Basic values; + assert(CLI!Basic.parseArgs(values, + [ + "--boolean", + "--number","100", + "--name","Jake", + "--array","1","2","3", + "--choice","foo", + "--callback", + "--callback1","cb-value", + "--callback2","cb-v1","cb-v2", + ])); + return values; +}(); static assert(values.name == "Jake"); static assert(values.unused == Basic.init.unused); @@ -149,13 +154,13 @@ static assert(values.array == [1,2,3]); For more sophisticated CLI usage, `argparse` provides few UDAs: ```d -static struct Extended +static struct Advanced { // Positional arguments are required by default @PositionalArgument(0) string name; - // Named arguments can be attributed in bulk + // Named arguments can be attributed in bulk (parentheses can be omitted) @NamedArgument { string unused = "some default value"; @@ -172,14 +177,14 @@ static struct Extended } // This mixin defines standard main function that parses command line and calls the provided function: -mixin Main.parseCLIKnownArgs!(Extended, (args, unparsed) +mixin CLI!Advanced.main!((args, unparsed) { - // 'args' has 'Extended' type - static assert(is(typeof(args) == Extended)); - + // 'args' has 'Advanced' type + static assert(is(typeof(args) == Advanced)); + // unparsed arguments has 'string[]' type static assert(is(typeof(unparsed) == string[])); - + // do whatever you need import std.stdio: writeln; args.writeln; @@ -275,13 +280,13 @@ type `string[]` with `TrailingArguments` UDA: ```d struct T { - @NamedArgument string a; - @NamedArgument string b; + string a; + string b; @TrailingArguments string[] args; } -static assert(["-a","A","--","-b","B"].parseCLIArgs!T.get == T("A","",["-b","B"])); +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. @@ -301,7 +306,7 @@ struct T int b; } -static assert(["-b", "4"].parseCLIArgs!T.get == T("not set", 4)); +assert(CLI!T.parseArgs!((T t) { assert(t == T("not set", 4)); })(["-b", "4"]) == 0); ``` ### Limit the allowed values @@ -315,8 +320,8 @@ struct T string fruit; } -static assert(["--fruit", "apple"].parseCLIArgs!T.get == T("apple")); -static assert(["--fruit", "kiwi"].parseCLIArgs!T.isNull); // "kiwi" is not allowed +assert(CLI!T.parseArgs!((T t) { assert(t == T("apple")); })(["--fruit", "apple"]) == 0); +assert(CLI!T.parseArgs!((T t) { assert(false); })(["--fruit", "kiwi"]) != 0); // "kiwi" is not allowed ``` For the value that is not in the allowed list, this error will be printed: @@ -331,17 +336,33 @@ listed in the `enum`. ## Calling the parser -### Wrappers for main function - -The recommended and most convenient way to use `argparse` is through the `Main` wrapper. It provides the standard -`main` function that parses command line arguments and calls provided function with an object that contains parsed -arguments. +`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 + supported use cases. +- `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`. -There are following mixins available: +### Wrappers for main function -- `Main.parseCLIArgs(TYPE, alias newMain, Config config = Config.init)` - parses arguments and ensures that there are no - unknown arguments are provided. -- `Main.parseCLIKnownArgs(TYPE, alias newMain, Config config = Config.init)` - parses known arguments only. +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 +an object that contains parsed arguments. + +`newMain` function must satisfy these requirements: +- It must accept `COMMAND` type as a first parameter if `CLI` template is used with one `COMMAND`. +- It must accept all `COMMANDS` types as a first parameter if `CLI` template is used with multiple `COMMANDS...`. + `argparse` uses `std.sumtype.match` for matching. Possible implementation of such `newMain` function would be a + function that is overridden for every command type from `COMMANDS`. Another example would be a lambda that does + compile-time checking of the type of the first parameter (see examples 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 the 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. **Usage examples:** @@ -352,7 +373,7 @@ struct T string b; } -mixin Main.parseCLIArgs!(T, (args) +mixin CLI!T.main!((args) { // 'args' has 'T' type static assert(is(typeof(args) == T)); @@ -365,179 +386,144 @@ mixin Main.parseCLIArgs!(T, (args) ``` ```d -struct T +struct cmd1 { string a; - string b; } -mixin Main.parseCLIKnownArgs!(T, (args, unparsed) +struct cmd2 { - // 'args' has 'T' type - static assert(is(typeof(args) == T)); + string b; +} +mixin CLI!(cmd1, cmd2).main!((args, unparsed) +{ + // 'args' has either 'cmd1' or 'cmd2' type + static if(is(typeof(args) == cmd1)) + writeln("cmd1: ", args); + else static if(is(typeof(args) == cmd2)) + writeln("cmd2: ", args); + else + static assert(false); // this would never happen + // unparsed arguments has 'string[]' type static assert(is(typeof(unparsed) == string[])); - // do whatever you need - import std.stdio: writeln; - args.writeln; - writeln("Unparsed args: ", unparsed); return 0; }); ``` ### Complete argument parsing -There is a top-level function `parseCLIArgs` that parses the command line. It has the following signatures: +`CLI` template provides `parseArgs` function that parses the command line and ensures that there are no unknown +arguments specified in command line. It has the following signature: -- `Result parseCLIArgs(T)(ref T receiver, string[] args, in Config config = Config.init)` +`Result parseArgs(ref COMMAND receiver, string[] args))` - **Parameters:** +**Parameters:** - - `receiver` - the object that's populated with parsed values. - - `args` - raw command line arguments. - - `config` - settings that are used for parsing. +- `receiver` - the object that's populated with parsed values. +- `args` - raw command line arguments. - **Return value:** +**Return value:** - An object that can be cast to `bool` to check whether the parsing was successful or not. - -- `Nullable!T parseCLIArgs(T)(string[] args, in Config config = Config.init)` - - **Parameters:** - - - `args` - raw command line arguments. - - `config` - settings that are used for parsing. - - **Return value:** - - If there is an error happened during the parsing then `null` is returned. Otherwise, an object of type `T` filled with - values from the command line. - -- `int parseCLIArgs(T, FUNC)(string[] args, FUNC func, in Config config = Config.init, T initialValue = T.init)` - - **Parameters:** - - - `args` - raw command line arguments. - - `func` - function that's called with object of type `T` filled with data parsed from command line. - - `config` - settings that are used for parsing. - - `initialValue` - initial value for the object passed to `func`. - - **Return value:** - - If there is an error happened during the parsing then `int.max` is returned. In other case if - `func` returns a value that can be cast to `int` then this value is returned. Otherwise, `0` is returned. +An object that can be cast to `bool` to check whether the parsing was successful or not. **Usage example:** ```d struct T { - @NamedArgument string a; - @NamedArgument string b; + string a; + string b; } -enum result1 = parseCLIArgs!T([ "-a", "A", "-b", "B"]); -assert(result1.get == T("A","B")); +assert(CLI!T.parseArgs!((T t) { assert(t == T("A","B")); })(["-a", "A", "-b", "B"]) == 0); ``` -If you want to parse multiple command lines into single object then you can do this easily: - -```d -T result2; -result2.parseCLIArgs([ "-a", "A" ]); -result2.parseCLIArgs([ "-b", "B" ]); -assert(result2 == T("A","B")); -``` - -You can even write your own `main` function that accepts - -```d -int my_main(T command) -{ - // do something - return 0; -} - -int main(string[] args) -{ - return args.parseCLIArgs!T(&my_main); -} -``` ### Partial argument parsing -Sometimes a program may only parse a few of the command-line arguments, passing the remaining arguments on to another -program. In these cases, `parseCLIKnownArgs` function can be used. It works much like `parseCLIArgs` except that it does -not produce an error when extra arguments are present. It has the following signatures: +Sometimes a program may only parse a few of the command-line arguments, processing the remaining arguments in different +way. In these cases, `CLI!(...).parseKnownArgs` function can be used. It works much like `CLI!(...).parseArgs` except +that it does not produce an error when extra arguments are present. It has the following signatures: -- `Result parseCLIKnownArgs(T)(ref T receiver, string[] args, out string[] unrecognizedArgs, in Config config = Config.init)` +- `Result parseKnownArgs(ref COMMAND receiver, string[] args, out string[] unrecognizedArgs)` **Parameters:** - `receiver` - the object that's populated with parsed values. - `args` - raw command line arguments. - `unrecognizedArgs` - raw command line arguments that were not parsed. - - `config` - settings that are used for parsing. **Return value:** An object that can be cast to `bool` to check whether the parsing was successful or not. -- `Result parseCLIKnownArgs(T)(ref T receiver, ref string[] args, in Config config = Config.init)` +- `Result parseKnownArgs(ref COMMAND receiver, ref string[] args)` **Parameters:** - `receiver` - the object that's populated with parsed values. - `args` - raw command line arguments that are modified to have parsed arguments removed. - - `config` - settings that are used for parsing. **Return value:** An object that can be cast to `bool` to check whether the parsing was successful or not. -- `Nullable!T parseCLIKnownArgs(T)(ref string[] args, in Config config = Config.init)` +**Usage example:** - **Parameters:** +```d +struct T +{ + string a; +} - - `args` - raw command line arguments that are modified to have parsed arguments removed. - - `config` - settings that are used for parsing. +auto args = [ "-a", "A", "-c", "C" ]; - **Return value:** +T result; +assert(CLI!T.parseKnownArgs!(result, args)); +assert(result == T("A")); +assert(args == ["-c", "C"]); +``` - If there is an error happened during the parsing then `null` is returned. Otherwise, an object of type `T` filled with - values from the command line. -- `int parseCLIKnownArgs(T, FUNC)(string[] args, FUNC func, in Config config = Config.init, T initialValue = T.init)` +### Calling a function after parsing - **Parameters:** +Sometimes it's useful to call some function with an object that has all command line arguments parsed. For this usage, +`argparse` provides `CLI!(...).parseArgs` template function that has the following signature: - - `args` - raw command line arguments. - - `func` - function that's called with object of type `T` filled with data parsed from command line and the - unrecognized arguments having the type of `string[]`. - - `config` - settings that are used for parsing. - - `initialValue` - initial value for the object passed to `func`. +`int parseArgs(alias newMain)(string[] args, COMMAND initialValue = COMMAND.init)` - **Return value:** +**Parameters:** - If there is an error happened during the parsing then `int.max` is returned. In other case if - `func` returns a value that can be cast to `int` then this value is returned. Otherwise, `0` is returned. +- `newMain` - function that's called with object of type `COMMAND` as a first parmeter filled with the data parsed from + command line; optionally it can take `string[]` as a second parameter which will contain unknown arguments + (`parseKnownArgs` function will be used underneath in this case). +- `args` - raw command line arguments. +- `initialValue` - initial value for the object passed to `func`. + +**Return value:** + +If there is an error happened during the parsing then non-zero value is returned. If `newMain` function returns a +value that can be cast to `int` then this value is returned. Otherwise, `0` is returned. **Usage example:** ```d -struct T +int my_main(T command) { - @NamedArgument string a; + // do something + return 0; } -auto args = [ "-a", "A", "-c", "C" ]; - -assert(parseCLIKnownArgs!T(args).get == T("A")); -assert(args == ["-c", "C"]); +int main(string[] args) +{ + return CLI!T.parseArgs!my_main(args); +} ``` + ## Argument dependencies ### Mutually exclusive arguments @@ -555,12 +541,12 @@ struct T } // Either or no argument is allowed -assert(parseCLIArgs!T(["-a","a"], (T t) {}) == 0); -assert(parseCLIArgs!T(["-b","b"], (T t) {}) == 0); -assert(parseCLIArgs!T([], (T t) {}) == 0); +assert(CLI!T.parseArgs!((T t) {})(["-a","a"]) == 0); +assert(CLI!T.parseArgs!((T t) {})(["-b","b"]) == 0); +assert(CLI!T.parseArgs!((T t) {})([]) == 0); // Both arguments are not allowed -assert(parseCLIArgs!T(["-a","a","-b","b"], (T t) { assert(false); }) != 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.** @@ -580,12 +566,12 @@ struct T } // Both or no argument is allowed -assert(parseCLIArgs!T(["-a","a","-b","b"], (T t) {}) == 0); -assert(parseCLIArgs!T([], (T t) {}) == 0); +assert(CLI!T.parseArgs!((T t) {})(["-a","a","-b","b"]) == 0); +assert(CLI!T.parseArgs!((T t) {})([]) == 0); // Only one argument is not allowed -assert(parseCLIArgs!T(["-a","a"], (T t) { assert(false); }) != 0); -assert(parseCLIArgs!T(["-b","b"], (T t) { assert(false); }) != 0); +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.** @@ -725,7 +711,7 @@ The default command is a command that is ran when user doesn't specify any comma To mark a command as default, one should use `Default` template: ```d - SumType!(sum, min, Default!max) cmd; +SumType!(sum, min, Default!max) cmd; ``` ## Help generation @@ -782,7 +768,7 @@ struct T @TrailingArguments string[] args; } -parseCLIArgs!T(["-h"]); +CLI!T.parseArgs!((T t) {})(["-h"]); ``` This example will print the following help message: @@ -879,11 +865,11 @@ struct T @NamedArgument bool b; } -static assert(["-b"] .parseCLIArgs!T.get == T(true)); -static assert(["-b","true"] .parseCLIArgs!T.get == T(true)); -static assert(["-b","false"].parseCLIArgs!T.get == T(false)); -static assert(["-b=true"] .parseCLIArgs!T.get == T(true)); -static assert(["-b=false"] .parseCLIArgs!T.get == T(false)); +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); ``` ### Numeric @@ -898,7 +884,7 @@ struct T @NamedArgument double d; } -static assert(["-i","-5","-u","8","-d","12.345"].parseCLIArgs!T.get == T(-5,8,12.345)); +assert(CLI!T.parseArgs!((T t) { assert(t == T(-5,8,12.345)); })(["-i","-5","-u","8","-d","12.345"]) == 0); ``` ### String @@ -911,7 +897,7 @@ struct T @NamedArgument string a; } -static assert(["-a","foo"].parseCLIArgs!T.get == T("foo")); +assert(CLI!T.parseArgs!((T t) { assert(t == T("foo")); })(["-a","foo"]) == 0); ``` ### Enum @@ -927,8 +913,8 @@ struct T @NamedArgument Fruit a; } -static assert(["-a","apple"].parseCLIArgs!T.get == T(T.Fruit.apple)); -static assert(["-a=pear"].parseCLIArgs!T.get == T(T.Fruit.pear)); +assert(CLI!T.parseArgs!((T t) { assert(t == T(T.Fruit.apple)); })(["-a","apple"]) == 0); +assert(CLI!T.parseArgs!((T t) { assert(t == T(T.Fruit.pear)); })(["-a=pear"]) == 0); ``` ### Counter @@ -941,7 +927,7 @@ struct T @(NamedArgument.Counter()) int a; } -static assert(["-a","-a","-a"].parseCLIArgs!T.get == T(3)); +assert(CLI!T.parseArgs!((T t) { assert(t == T(3)); })(["-a","-a","-a"]) == 0); ``` ### Array @@ -957,8 +943,8 @@ struct T @NamedArgument int[][] b; } -static assert(["-a","1","2","3","-a","4","5"].parseCLIArgs!T.get.a == [1,2,3,4,5]); -static assert(["-b","1","2","3","-b","4","5"].parseCLIArgs!T.get.b == [[1,2,3],[4,5]]); +assert(CLI!T.parseArgs!((T t) { assert(t.a == [1,2,3,4,5]); })(["-a","1","2","3","-a","4","5"]) == 0); +assert(CLI!T.parseArgs!((T t) { assert(t.b == [[1,2,3],[4,5]]); })(["-b","1","2","3","-b","4","5"]) == 0); ``` Alternatively you can set `Config.arraySep` to allow multiple elements in one parameter: @@ -969,10 +955,13 @@ struct T @NamedArgument int[] a; } -Config cfg; -cfg.arraySep = ','; +enum cfg = { + Config cfg; + cfg.arraySep = ','; + return cfg; +}(); -assert(["-a","1,2,3","-a","4","5"].parseCLIArgs!T(cfg).get == T([1,2,3,4,5])); +assert(CLI!(cfg, T).parseArgs!((T t) { assert(t == T([1,2,3,4,5])); })(["-a","1,2,3","-a","4","5"]) == 0); ``` #### Specifying number of values @@ -995,8 +984,8 @@ struct T int[] b; } -assert(["-a","1","2","3","-b","4","5"].parseCLIArgs!T.get == T([1,2,3],[4,5])); -assert(["-a","1","-b","4","5"].parseCLIArgs!T.get == T([1],[4,5])); +assert(CLI!T.parseArgs!((T t) { assert(t == T([1,2,3],[4,5])); })(["-a","1","2","3","-b","4","5"]) == 0); +assert(CLI!T.parseArgs!((T t) { assert(t == T([1],[4,5])); })(["-a","1","-b","4","5"]) == 0); ``` ### Associative array @@ -1010,7 +999,7 @@ struct T @NamedArgument int[string] a; } -static assert(["-a=foo=3","-a","boo=7"].parseCLIArgs!T.get.a == ["foo":3,"boo":7]); +assert(CLI!T.parseArgs!((T t) { assert(t == T(["foo":3,"boo":7])); })(["-a=foo=3","-a","boo=7"]) == 0); ``` Alternatively you can set `Config.arraySep` to allow multiple elements in one parameter: @@ -1021,11 +1010,14 @@ struct T @NamedArgument int[string] a; } -Config cfg; -cfg.arraySep = ','; +enum cfg = { + Config cfg; + cfg.arraySep = ','; + return cfg; +}(); -assert(["-a=foo=3,boo=7"].parseCLIArgs!T(cfg).get.a == ["foo":3,"boo":7]); -assert(["-a","foo=3,boo=7"].parseCLIArgs!T(cfg).get.a == ["foo":3,"boo":7]); +assert(CLI!(cfg, T).parseArgs!((T t) { assert(t == T(["foo":3,"boo":7])); })(["-a=foo=3,boo=7"]) == 0); +assert(CLI!(cfg, T).parseArgs!((T t) { assert(t == T(["foo":3,"boo":7])); })(["-a","foo=3,boo=7"]) == 0); ``` In general, the keys and values can be of any parsable types. @@ -1063,7 +1055,7 @@ static struct T @(NamedArgument("a")) void foo() { a++; } } -static assert(["-a","-a","-a","-a"].parseCLIArgs!T.get.a == 4); +assert(CLI!T.parseArgs!((T t) { assert(t == T(4)); })(["-a","-a","-a","-a"]) == 0); ``` ## Argument parsing customization @@ -1166,16 +1158,16 @@ Both `AllowNoValue` and `RequireNoValue` modifiers accept a value that should be command line. The difference between them can be seen in this example: ```d - struct T - { - @(NamedArgument.AllowNoValue !10) int a; - @(NamedArgument.RequireNoValue!20) int b; - } +struct T +{ + @(NamedArgument.AllowNoValue !10) int a; + @(NamedArgument.RequireNoValue!20) int b; +} - assert(["-a"].parseCLIArgs!T.get.a == 10); // use value from UDA - assert(["-b"].parseCLIArgs!T.get.b == 20); // use value from UDA - assert(["-a", "30"].parseCLIArgs!T.get.a == 30); // providing value is allowed - assert(["-b", "30"].parseCLIArgs!T.isNull); // providing value is not allowed +assert(CLI!T.parseArgs!((T t) { assert(t.a == 10); })(["-a"]) == 0); +assert(CLI!T.parseArgs!((T t) { assert(t.b == 20); })(["-b"]) == 0); +assert(CLI!T.parseArgs!((T t) { assert(t.a == 30); })(["-a","30"]) == 0); +assert(CLI!T.parseArgs!((T t) { assert(false); })(["-b","30"]) != 0); ``` ### Usage example @@ -1183,18 +1175,18 @@ command line. The difference between them can be seen in this example: All the above modifiers can be combined in any way: ```d - struct T - { - @(NamedArgument - .PreValidation!((string s) { return s.length > 1 && s[0] == '!'; }) - .Parse !((string s) { return s[1]; }) - .Validation !((char v) { return v >= '0' && v <= '9'; }) - .Action !((ref int a, char v) { a = v - '0'; }) - ) - int a; - } +struct T +{ + @(NamedArgument + .PreValidation!((string s) { return s.length > 1 && s[0] == '!'; }) + .Parse !((string s) { return s[1]; }) + .Validation !((char v) { return v >= '0' && v <= '9'; }) + .Action !((ref int a, char v) { a = v - '0'; }) + ) + int a; +} - static assert(["-a","!4"].parseCLIArgs!T.get.a == 4); +assert(CLI!T.parseArgs!((T t) { assert(t == T(4)); })(["-a","!4"]) == 0); ``` ## Parser customization @@ -1223,12 +1215,15 @@ struct T @NamedArgument string[] a; } -assert(["-a","1,2,3","-a","4","5"].parseCLIArgs!T.get == T(["1,2,3","4","5"])); +assert(CLI!T.parseArgs!((T t) { assert(t == T(["1,2,3","4","5"])); })(["-a","1,2,3","-a","4","5"]) == 0); -Config cfg; -cfg.arraySep = ','; +enum cfg = { + Config cfg; + cfg.arraySep = ','; + return cfg; +}(); -assert(["-a","1,2,3","-a","4","5"].parseCLIArgs!T(cfg).get == T(["1","2","3","4","5"])); +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 @@ -1259,12 +1254,10 @@ Default is `false`. ### Adding help generation - Add a -h/--help option to the parser. - Defaults to true. - `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 `parseCLIKnownArgs` or `parseCLIArgs` is called with function parameter then this callback will not be called. +If `CLI!(...).parseArgs(alias newMain)` or `CLI!(...).main(alias newMain)` is used then provided `newMain` function will +not be called. Default is `true`. diff --git a/examples/getting_started/advanced/app.d b/examples/getting_started/advanced/app.d index 115bfad..d22335b 100644 --- a/examples/getting_started/advanced/app.d +++ b/examples/getting_started/advanced/app.d @@ -23,7 +23,7 @@ static struct Advanced } // This mixin defines standard main function that parses command line and calls the provided function: -mixin Main.parseCLIKnownArgs!(Advanced, (args, unparsed) +mixin CLI!Advanced.main!((args, unparsed) { // 'args' has 'Advanced' type static assert(is(typeof(args) == Advanced)); diff --git a/examples/getting_started/basic/app.d b/examples/getting_started/basic/app.d index 434bb6a..ae32285 100644 --- a/examples/getting_started/basic/app.d +++ b/examples/getting_started/basic/app.d @@ -41,7 +41,7 @@ static struct Basic } // This mixin defines standard main function that parses command line and calls the provided function: -mixin Main.parseCLIArgs!(Basic, (args) +mixin CLI!Basic.main!((args) { // 'args' has 'Baisc' type static assert(is(typeof(args) == Basic)); @@ -53,16 +53,21 @@ mixin Main.parseCLIArgs!(Basic, (args) }); // Parser can even work at compile time -enum values = ([ - "--boolean", - "--number","100", - "--name","Jake", - "--array","1","2","3", - "--choice","foo", - "--callback", - "--callback1","cb-value", - "--callback2","cb-v1","cb-v2", -].parseCLIArgs!Basic).get; +enum values = { + Basic values; + assert(CLI!Basic.parseArgs(values, + [ + "--boolean", + "--number","100", + "--name","Jake", + "--array","1","2","3", + "--choice","foo", + "--callback", + "--callback1","cb-value", + "--callback2","cb-v1","cb-v2", + ])); + return values; +}(); static assert(values.name == "Jake"); static assert(values.unused == Basic.init.unused); diff --git a/source/argparse.d b/source/argparse.d index 86522f8..fafc142 100644 --- a/source/argparse.d +++ b/source/argparse.d @@ -756,17 +756,17 @@ unittest { struct T { - @(NamedArgument) + @NamedArgument int a; @(NamedArgument.Optional()) int b; @(NamedArgument.Required()) int c; - @(NamedArgument) + @NamedArgument int d; @(NamedArgument.Required()) int e; - @(NamedArgument) + @NamedArgument int f; } @@ -1206,16 +1206,26 @@ private Result parseCLIKnownArgs(T)(ref T receiver, return Result.Success; } +deprecated("Use CLI!(config, COMMAND).parseKnownArgs") Result parseCLIKnownArgs(T)(ref T receiver, string[] args, out string[] unrecognizedArgs, - in Config config = Config.init) + in Config config) { auto command = CommandArguments!T(config); return parseCLIKnownArgs(receiver, args, unrecognizedArgs, command, config); } -auto parseCLIKnownArgs(T)(ref T receiver, ref string[] args, in Config config = Config.init) +deprecated("Use CLI!COMMAND.parseKnownArgs") +Result parseCLIKnownArgs(T)(ref T receiver, + string[] args, + out string[] unrecognizedArgs) +{ + return CLI!T.parseKnownArgs(receiver, args, unrecognizedArgs); +} + +deprecated("Use CLI!(config, COMMAND).parseKnownArgs") +auto parseCLIKnownArgs(T)(ref T receiver, ref string[] args, in Config config) { string[] unrecognizedArgs; @@ -1226,7 +1236,14 @@ auto parseCLIKnownArgs(T)(ref T receiver, ref string[] args, in Config config = return res; } -Nullable!T parseCLIKnownArgs(T)(ref string[] args, in Config config = Config.init) +deprecated("Use CLI!COMMAND.parseKnownArgs") +auto parseCLIKnownArgs(T)(ref T receiver, ref string[] args) +{ + return CLI!T.parseKnownArgs(receiver, args); +} + +deprecated("Use CLI!(config, COMMAND).parseKnownArgs") +Nullable!T parseCLIKnownArgs(T)(ref string[] args, in Config config) { import std.typecons : nullable; @@ -1235,7 +1252,18 @@ Nullable!T parseCLIKnownArgs(T)(ref string[] args, in Config config = Config.ini return parseCLIKnownArgs(receiver, args, config) ? receiver.nullable : Nullable!T.init; } -int parseCLIKnownArgs(T, FUNC)(string[] args, FUNC func, in Config config = Config.init, T initialValue = T.init) +deprecated("Use CLI!COMMAND.parseKnownArgs") +Nullable!T parseCLIKnownArgs(T)(ref string[] args) +{ + import std.typecons : nullable; + + T receiver; + + return CLI!T.parseKnownArgs(receiver, args) ? receiver.nullable : Nullable!T.init; +} + +deprecated("Use CLI!(config, COMMAND).parseArgs") +int parseCLIKnownArgs(T, FUNC)(string[] args, FUNC func, in Config config, T initialValue = T.init) if(__traits(compiles, { func(T.init, args); })) { alias value = initialValue; @@ -1253,8 +1281,28 @@ if(__traits(compiles, { func(T.init, args); })) } } +deprecated("Use CLI!COMMAND.parseArgs") +int parseCLIKnownArgs(T, FUNC)(string[] args, FUNC func) +if(__traits(compiles, { func(T.init, args); })) +{ + T value; + + auto res = CLI!T.parseKnownArgs(value, args); + if(!res) + return res.resultCode; + + static if(__traits(compiles, { int a = cast(int) func(value, args); })) + return cast(int) func(value, args); + else + { + func(value, args); + return 0; + } +} + -auto parseCLIArgs(T)(ref T receiver, string[] args, in Config config = Config.init) +deprecated("Use CLI!(config, COMMAND).parseArgs") +auto parseCLIArgs(T)(ref T receiver, string[] args, in Config config) { string[] unrecognizedArgs; @@ -1269,7 +1317,14 @@ auto parseCLIArgs(T)(ref T receiver, string[] args, in Config config = Config.in return res; } -Nullable!T parseCLIArgs(T)(string[] args, in Config config = Config.init) +deprecated("Use CLI!COMMAND.parseArgs") +auto parseCLIArgs(T)(ref T receiver, string[] args) +{ + return CLI!T.parseArgs(receiver, args); +} + +deprecated("Use CLI!(config, COMMAND).parseArgs") +Nullable!T parseCLIArgs(T)(string[] args, in Config config) { import std.typecons : nullable; @@ -1278,7 +1333,18 @@ Nullable!T parseCLIArgs(T)(string[] args, in Config config = Config.init) return parseCLIArgs(receiver, args, config) ? receiver.nullable : Nullable!T.init; } -int parseCLIArgs(T, FUNC)(string[] args, FUNC func, in Config config = Config.init, T initialValue = T.init) +deprecated("Use CLI!COMMAND.parseArgs") +Nullable!T parseCLIArgs(T)(string[] args) +{ + import std.typecons : nullable; + + T receiver; + + return CLI!T.parseArgs(receiver, args) ? receiver.nullable : Nullable!T.init; +} + +deprecated("Use CLI!(config, COMMAND).parseArgs") +int parseCLIArgs(T, FUNC)(string[] args, FUNC func, in Config config, T initialValue = T.init) if(__traits(compiles, { func(T.init); })) { alias value = initialValue; @@ -1296,6 +1362,25 @@ if(__traits(compiles, { func(T.init); })) } } +deprecated("Use CLI!COMMAND.parseArgs") +int parseCLIArgs(T, FUNC)(string[] args, FUNC func) +if(__traits(compiles, { func(T.init); })) +{ + T value; + + auto res = CLI!T.parseArgs(value, args); + if(!res) + return res.resultCode; + + static if(__traits(compiles, { int a = cast(int) func(value); })) + return cast(int) func(value); + else + { + func(value); + return 0; + } +} + unittest { import std.exception; @@ -1490,6 +1575,7 @@ unittest int b; } + assert(CLI!T.parseArgs!((T t) { assert(t == T("not set", 4)); })(["-b", "4"]) == 0); static assert(["-b", "4"].parseCLIArgs!T.get == T("not set", 4)); assert(["-b", "4"].parseCLIArgs!T.get == T("not set", 4)); } @@ -1544,6 +1630,9 @@ unittest bool b; } + 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(false)); })(["-b=false"]) == 0); static assert(["-b"] .parseCLIArgs!T.get == T(true)); static assert(["-b=true"] .parseCLIArgs!T.get == T(true)); static assert(["-b=false"] .parseCLIArgs!T.get == T(false)); @@ -1595,22 +1684,17 @@ unittest assert(["-c","C","-b","B"].parseCLIArgs!T.get == T("C",null,typeof(T.cmd)(Default!(T.cmd2)(T.cmd2("B"))))); } +deprecated("Use CLI!(Config, COMMAND).main or CLI!(COMMAND).main") struct Main { mixin template parseCLIKnownArgs(TYPE, alias newMain, Config config = Config.init) { - int main(string[] argv) - { - return parseCLIKnownArgs!TYPE(argv[1..$], (TYPE values, string[] args) => newMain(values, args), config); - } + mixin CLI!(config, TYPE).main!newMain; } mixin template parseCLIArgs(TYPE, alias newMain, Config config = Config.init) { - int main(string[] argv) - { - return parseCLIArgs!TYPE(argv[1..$], (TYPE values) => newMain(values), config); - } + mixin CLI!(config, TYPE).main!newMain; } } @@ -1618,39 +1702,113 @@ template CLI(Config config, COMMANDS...) { mixin template main(alias newMain) { - int main(string[] argv) - { - import std.sumtype: SumType, match; + import std.sumtype: SumType, match; - struct Program - { - SumType!COMMANDS cmd; // Sub-commands - } + private struct Program + { + SumType!COMMANDS cmd; // Sub-commands + } - return parseCLIArgs!Program(argv[1..$], (Program prog) => prog.cmd.match!newMain, config); + private auto forwardMain(Args...)(Program prog, auto ref Args args) + { + import core.lifetime: forward; + return prog.cmd.match!(_ => newMain(_, forward!args)); } + + mixin CLI!(config, Program).main!forwardMain; } } template CLI(Config config, COMMAND) { + static Result parseKnownArgs(ref COMMAND receiver, string[] args, out string[] unrecognizedArgs) + { + auto parser = Parser(config, args); + + auto command = CommandArguments!COMMAND(config); + auto res = parser.parseAll(command, receiver); + if(!res) + return res; + + unrecognizedArgs = parser.unrecognizedArgs; + + return Result.Success; + } + + static Result parseKnownArgs(ref COMMAND receiver, ref string[] args) + { + string[] unrecognizedArgs; + + auto res = parseKnownArgs(receiver, args, unrecognizedArgs); + if(res) + args = unrecognizedArgs; + + return res; + } + + static Result parseArgs(ref COMMAND receiver, string[] args) + { + auto res = parseKnownArgs(receiver, args); + if(res && args.length > 0) + { + config.onError("Unrecognized arguments: ", args); + return Result.Failure; + } + + return res; + } + + static int parseArgs(alias newMain)(string[] args, COMMAND initialValue = COMMAND.init) + if(__traits(compiles, { newMain(COMMAND.init); })) + { + alias value = initialValue; + + auto res = parseArgs(value, args); + if(!res) + return res.resultCode; + + static if(__traits(compiles, { int a = cast(int) newMain(value); })) + return cast(int) newMain(value); + else + { + newMain(value); + return 0; + } + } + + static int parseArgs(alias newMain)(string[] args, COMMAND initialValue = COMMAND.init) + if(__traits(compiles, { newMain(COMMAND.init, string[].init); })) + { + alias value = initialValue; + + auto res = parseKnownArgs(value, args); + if(!res) + return res.resultCode; + + static if(__traits(compiles, { int a = cast(int) newMain(value, args); })) + return cast(int) newMain(value, args); + else + { + newMain(value, args); + return 0; + } + } + mixin template main(alias newMain) { int main(string[] argv) { - return parseCLIArgs!COMMAND(argv[1..$], (COMMAND cmd) => newMain(cmd), config); + return CLI!(config, COMMAND).parseArgs!(newMain)(argv[1..$]); } } } +deprecated("Use CLI!(Config, COMMAND) or CLI!(COMMAND)") template CLI(Config config) { mixin template main(COMMAND, alias newMain) { - int main(string[] argv) - { - return parseCLIArgs!COMMAND(argv[1..$], (COMMAND cmd) => newMain(cmd), config); - } + mixin CLI!(config, COMMAND).main!newMain; } } @@ -1664,8 +1822,8 @@ unittest int a; } - static assert(__traits(compiles, { mixin Main.parseCLIArgs!(T, (params) => 0); })); - static assert(__traits(compiles, { mixin Main.parseCLIKnownArgs!(T, (params, args) => 0); })); + static assert(__traits(compiles, { mixin CLI!T.main!((params) => 0); })); + static assert(__traits(compiles, { mixin CLI!T.main!((params, args) => 0); })); } @@ -2790,6 +2948,8 @@ unittest int[] b; } + assert(CLI!T.parseArgs!((T t) { assert(t == T([1,2,3],[4,5])); })(["-a","1","2","3","-b","4","5"]) == 0); + assert(CLI!T.parseArgs!((T t) { assert(t == T([1],[4,5])); })(["-a","1","-b","4","5"]) == 0); assert(["-a","1","2","3","-b","4","5"].parseCLIArgs!T.get == T([1,2,3],[4,5])); assert(["-a","1","-b","4","5"].parseCLIArgs!T.get == T([1],[4,5])); } @@ -2867,6 +3027,7 @@ unittest @(NamedArgument.Counter()) int a; } + assert(CLI!T.parseArgs!((T t) { assert(t == T(3)); })(["-a","-a","-a"]) == 0); static assert(["-a","-a","-a"].parseCLIArgs!T.get == T(3)); assert(["-a","-a","-a"].parseCLIArgs!T.get == T(3)); } @@ -2905,6 +3066,8 @@ unittest string fruit; } + assert(CLI!T.parseArgs!((T t) { assert(t == T("apple")); })(["--fruit", "apple"]) == 0); + assert(CLI!T.parseArgs!((T t) { assert(false); })(["--fruit", "kiwi"]) != 0); // "kiwi" is not allowed static assert(["--fruit", "apple"].parseCLIArgs!T.get == T("apple")); assert(["--fruit", "kiwi"].parseCLIArgs!T.isNull); } @@ -2960,6 +3123,7 @@ unittest @TrailingArguments string[] args; } + assert(CLI!T.parseArgs!((T t) { assert(t == T("A","",["-b","B"])); })(["-a","A","--","-b","B"]) == 0); static assert(["-a","A","--","-b","B"].parseCLIArgs!T.get == T("A","",["-b","B"])); assert(["-a","A","--","-b","B"].parseCLIArgs!T.get == T("A","",["-b","B"])); } @@ -2973,6 +3137,9 @@ unittest @NamedArgument("d","d1") double d; } + assert(CLI!T.parseArgs!((T t) { assert(t == T(-5,8,12.345)); })(["-i","-5","-u","8","-d","12.345"]) == 0); + assert(CLI!T.parseArgs!((T t) { assert(t == T(-5,8,12.345)); })(["-i","-5","-u1","8","-d1","12.345"]) == 0); + static assert(["-i","-5","-u","8","-d","12.345"].parseCLIArgs!T.get == T(-5,8,12.345)); static assert(["-i","-5","-u1","8","-d1","12.345"].parseCLIArgs!T.get == T(-5,8,12.345)); assert(["-i","-5","-u","8","-d","12.345"].parseCLIArgs!T.get == T(-5,8,12.345)); @@ -2983,10 +3150,13 @@ unittest { struct T { - @(NamedArgument) int[] a; - @(NamedArgument) int[][] b; + @NamedArgument int[] a; + @NamedArgument int[][] b; } + assert(CLI!T.parseArgs!((T t) { assert(t.a == [1,2,3,4,5]); })(["-a","1","2","3","-a","4","5"]) == 0); + assert(CLI!T.parseArgs!((T t) { assert(t.b == [[1,2,3],[4,5]]); })(["-b","1","2","3","-b","4","5"]) == 0); + static assert(["-a","1","2","3","-a","4","5"].parseCLIArgs!T.get.a == [1,2,3,4,5]); static assert(["-b","1","2","3","-b","4","5"].parseCLIArgs!T.get.b == [[1,2,3],[4,5]]); assert(["-a","1","2","3","-a","4","5"].parseCLIArgs!T.get.a == [1,2,3,4,5]); @@ -3000,9 +3170,13 @@ unittest @NamedArgument int[] a; } - Config cfg; - cfg.arraySep = ','; + enum cfg = { + Config cfg; + cfg.arraySep = ','; + return cfg; + }(); + assert(CLI!(cfg, T).parseArgs!((T t) { assert(t == T([1,2,3,4,5])); })(["-a","1,2,3","-a","4","5"]) == 0); assert(["-a","1,2,3","-a","4","5"].parseCLIArgs!T(cfg).get == T([1,2,3,4,5])); } @@ -3013,6 +3187,7 @@ unittest @NamedArgument int[string] a; } + assert(CLI!T.parseArgs!((T t) { assert(t == T(["foo":3,"boo":7])); })(["-a=foo=3","-a","boo=7"]) == 0); static assert(["-a=foo=3","-a","boo=7"].parseCLIArgs!T.get.a == ["foo":3,"boo":7]); assert(["-a=foo=3","-a","boo=7"].parseCLIArgs!T.get.a == ["foo":3,"boo":7]); } @@ -3024,9 +3199,14 @@ unittest @NamedArgument int[string] a; } - Config cfg; - cfg.arraySep = ','; + enum cfg = { + Config cfg; + cfg.arraySep = ','; + return cfg; + }(); + assert(CLI!(cfg, T).parseArgs!((T t) { assert(t == T(["foo":3,"boo":7])); })(["-a=foo=3,boo=7"]) == 0); + assert(CLI!(cfg, T).parseArgs!((T t) { assert(t == T(["foo":3,"boo":7])); })(["-a","foo=3,boo=7"]) == 0); assert(["-a=foo=3,boo=7"].parseCLIArgs!T(cfg).get.a == ["foo":3,"boo":7]); assert(["-a","foo=3,boo=7"].parseCLIArgs!T(cfg).get.a == ["foo":3,"boo":7]); } @@ -3040,6 +3220,8 @@ unittest @NamedArgument Fruit a; } + assert(CLI!T.parseArgs!((T t) { assert(t == T(T.Fruit.apple)); })(["-a","apple"]) == 0); + assert(CLI!T.parseArgs!((T t) { assert(t == T(T.Fruit.pear)); })(["-a=pear"]) == 0); static assert(["-a","apple"].parseCLIArgs!T.get == T(T.Fruit.apple)); static assert(["-a=pear"].parseCLIArgs!T.get == T(T.Fruit.pear)); assert(["-a","apple"].parseCLIArgs!T.get == T(T.Fruit.apple)); @@ -3053,11 +3235,16 @@ unittest @NamedArgument string[] a; } + assert(CLI!T.parseArgs!((T t) { assert(t == T(["1,2,3","4","5"])); })(["-a","1,2,3","-a","4","5"]) == 0); assert(["-a","1,2,3","-a","4","5"].parseCLIArgs!T.get == T(["1,2,3","4","5"])); - Config cfg; - cfg.arraySep = ','; + enum cfg = { + Config cfg; + cfg.arraySep = ','; + return cfg; + }(); + assert(CLI!(cfg, T).parseArgs!((T t) { assert(t == T(["1","2","3","4","5"])); })(["-a","1,2,3","-a","4","5"]) == 0); assert(["-a","1,2,3","-a","4","5"].parseCLIArgs!T(cfg).get == T(["1","2","3","4","5"])); } @@ -3069,6 +3256,10 @@ unittest @(NamedArgument.RequireNoValue!20) int b; } + assert(CLI!T.parseArgs!((T t) { assert(t.a == 10); })(["-a"]) == 0); + assert(CLI!T.parseArgs!((T t) { assert(t.b == 20); })(["-b"]) == 0); + assert(CLI!T.parseArgs!((T t) { assert(t.a == 30); })(["-a","30"]) == 0); + assert(CLI!T.parseArgs!((T t) { assert(false); })(["-b","30"]) != 0); static assert(["-a"].parseCLIArgs!T.get.a == 10); static assert(["-b"].parseCLIArgs!T.get.b == 20); static assert(["-a", "30"].parseCLIArgs!T.get.a == 30); @@ -3091,6 +3282,7 @@ unittest int a; } + assert(CLI!T.parseArgs!((T t) { assert(t == T(4)); })(["-a","!4"]) == 0); static assert(["-a","!4"].parseCLIArgs!T.get.a == 4); assert(["-a","!4"].parseCLIArgs!T.get.a == 4); } @@ -3104,6 +3296,7 @@ unittest @(NamedArgument("a")) void foo() { a++; } } + assert(CLI!T.parseArgs!((T t) { assert(t == T(4)); })(["-a","-a","-a","-a"]) == 0); static assert(["-a","-a","-a","-a"].parseCLIArgs!T.get.a == 4); assert(["-a","-a","-a","-a"].parseCLIArgs!T.get.a == 4); } @@ -4014,6 +4207,15 @@ unittest string b; } } + + // Either or no argument is allowed + assert(CLI!T.parseArgs!((T t) {})(["-a","a"]) == 0); + assert(CLI!T.parseArgs!((T t) {})(["-b","b"]) == 0); + assert(CLI!T.parseArgs!((T t) {})([]) == 0); + + // Both arguments are not allowed + assert(CLI!T.parseArgs!((T t) { assert(false); })(["-a","a","-b","b"]) != 0); + assert(parseCLIArgs!T(["-a","a","-b","b"], (T t) { assert(false); }) != 0); assert(parseCLIArgs!T(["-a","a"], (T t) {}) == 0); assert(parseCLIArgs!T(["-b","b"], (T t) {}) == 0); @@ -4031,6 +4233,15 @@ unittest string b; } } + + // Both or no argument is allowed + assert(CLI!T.parseArgs!((T t) {})(["-a","a","-b","b"]) == 0); + assert(CLI!T.parseArgs!((T t) {})([]) == 0); + + // Only one argument is not allowed + assert(CLI!T.parseArgs!((T t) { assert(false); })(["-a","a"]) != 0); + assert(CLI!T.parseArgs!((T t) { assert(false); })(["-b","b"]) != 0); + assert(parseCLIArgs!T(["-a","a","-b","b"], (T t) {}) == 0); assert(parseCLIArgs!T(["-a","a"], (T t) { assert(false); }) != 0); assert(parseCLIArgs!T(["-b","b"], (T t) { assert(false); }) != 0);