├── .editorconfig ├── .github └── workflows │ └── run-tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── dub.json ├── dub.selections.json ├── images ├── help.png └── logo.png └── source └── commandr ├── args.d ├── completion └── bash.d ├── help.d ├── option.d ├── package.d ├── parser.d ├── program.d ├── utils.d └── validators.d /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{d,di}] 4 | end_of_line = lf 5 | indent_style = space 6 | indent_size = 4 7 | tab_width = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | charset = utf-8 11 | 12 | [*.yml] 13 | end_of_line = lf 14 | indent_style = space 15 | indent_size = 2 16 | tab_width = 2 17 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | on: [push, pull_request, workflow_dispatch] 3 | jobs: 4 | test: 5 | name: DUB Tests 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | os: [ubuntu-latest, windows-latest, macOS-latest] 10 | dc: [dmd-latest, dmd-beta, ldc-latest, ldc-beta] 11 | exclude: 12 | - { os: macOS-latest, dc: dmd-latest } 13 | - { os: macOS-latest, dc: dmd-beta } 14 | 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: Install D Compiler 20 | uses: dlang-community/setup-dlang@v1 21 | with: 22 | compiler: ${{ matrix.dc }} 23 | 24 | - name: Run Unittests 25 | run: dub -q test 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dub 2 | docs.json 3 | __dummy.html 4 | commandr.so 5 | commandr.dylib 6 | commandr.dll 7 | commandr.a 8 | commandr.lib 9 | commandr-test-* 10 | *.exe 11 | *.o 12 | *.obj 13 | *.lst 14 | /commandr 15 | source/app.d 16 | libcommandr.a 17 | *.sw[npo] 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Robert Pasiński 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 | Logo 5 | 6 | 7 |

commandr

8 | 9 |

10 | A modern, powerful commmand line argument parser. 11 |
12 | Batteries included. 13 |
14 |
15 | 16 |
17 | ❗️ Report a bug 18 | · 19 | 💡 Request feature 20 |
21 |
22 | 23 | 24 | 25 | 26 |
27 |

28 |

29 | 30 | - - - 31 | 32 | **commandr** handles all kinds of command-line arguments with a nice and clean interface.
33 | Comes with help generation, shell auto-complete scripts and validation. 34 | 35 | 36 | ## Table of Contents 37 | 38 | - [Preview](#preview) 39 | - [Installation](#installation) 40 | - [FAQ](#faq) 41 | - [Features](#features) 42 | - [Getting Started](#getting-started) 43 | - [Basic usage](#basic-usage) 44 | - [Subcommands](#subcommands) 45 | - [Validation](#validation) 46 | - [Printing help](#printing-help) 47 | - [Bash autocompletion](#bash-autocompletion) 48 | - [Configuration](#configuration) 49 | - [Cheat-Sheet](#cheat-sheet) 50 | - [Defining Entries](#defining-entries) 51 | - [Reading Values](#reading-values) 52 | - [Property Matrix](#property-matrix) 53 | - [Roadmap](#roadmap) 54 | - [License](#license) 55 | 56 | 57 | ## Preview 58 | 59 |

60 | 61 |

62 | 63 | ## Installation 64 | 65 | Add this entry to your `dub.json` file: 66 | 67 | ```json 68 | "dependencies": { 69 | ... 70 | "commandr": "~>0.1" 71 | ... 72 | } 73 | ``` 74 | 75 | ## FAQ 76 | 77 | - **Does it use templates/compile-time magic?** 78 | 79 | No, at least currently not. Right now everything is done at runtime, so there's not much overhead on compilation time resources. 80 | In the future I'll probably look into generation of compile-time struct. 81 | 82 | The reason is that I want it to be rather simple and easy to learn, and having a lot of generated code hurts e.g. generated documentation 83 | and some minor things such as IDE auto-complete (even right now mixin-s cause some problems). 84 | 85 | - **Are the results typesafe? / Does it use UDA?** 86 | 87 | No, parsed arguments are returned in a `ProgramArgs` class instance that allow to fetch parsed data, 88 | 89 | However it should be possible to generate program definition from struct/class with UDA and then 90 | fill the parsed data into struct instance, but it is currently out of scope of this project (at least for now). 91 | 92 | 93 | ## Features 94 | 95 | - **Flags** (boolean values) 96 | - Short and long forms are supported (`-v`, `--verbose`) 97 | - Supports stacking of flags (`-vvv` is same as `-v -v -v`) 98 | 99 | - **Options** (taking a string value) 100 | - Short and long forms are supported (`-c test`, `--config test`) 101 | - Equals sign accepted (`-c=1`, `--config=test`) 102 | - Repeated options are supported (`-c 1 -c 2`) 103 | - Default values can be specified. 104 | - Options be marked as required. 105 | 106 | - **Arguments** (positional) 107 | - Required by default, can be marked as optional 108 | - Default values can be specified. 109 | - Repeated options are supported (only last argument) 110 | 111 | - **Commands** (git-style) 112 | - Infinitely recursive subcommands (you can go as deep as needed) 113 | - Contains own set of flags/options and arguments 114 | - Dedicated help output 115 | - Comfortable command handling with `ProgramArgs.on()` 116 | 117 | - **Provided help output** 118 | - Generated help output for your program and sub-commands 119 | - Can be configured to suit your needs, such as disabling colored output. 120 | - Provided usage, help and version information. 121 | - Completly detached from core `Program`, giving you complete freedom in writing your own help output. 122 | - You can categorize commands for better help output 123 | 124 | - **Consistency checking** 125 | - When you build your program model, `commandr` checks its consistency. 126 | - Detects name duplications as well as short/long options. 127 | - Detects required parameters with default value. 128 | 129 | - **BASH auto-complete script** 130 | - You can generate completion script with single function call 131 | - Completion script works on flags, options and sub-commands (at any depth) 132 | - Acknowledges difference between flags and options 133 | 134 | - **Validators** 135 | - Passed values can be checked for correctness 136 | - Simple process of creating custom validating logic 137 | - Provided validators for common cases: `EnumValidator`, `FileSystemValidator` and `DelegateValidator` 138 | 139 | - **Suggestions** 140 | - Suggestion with correct flag, option or sub-command name is provided when user passes invalid value 141 | - Also supported for `EnumValidator` (`acceptsValues`) 142 | 143 | 144 | ## Getting Started 145 | 146 | ### Basic Usage 147 | 148 | Simple example showing how to create a basic program and parse arguments: 149 | 150 | ```D 151 | import std.stdio; 152 | import commandr; 153 | 154 | void main(string[] args) { 155 | auto a = new Program("test", "1.0") 156 | .summary("Command line parser") 157 | .author("John Doe ") 158 | .add(new Flag("v", null, "turns on more verbose output") 159 | .name("verbose") 160 | .repeating) 161 | .add(new Option(null, "test", "some teeeest")) 162 | .add(new Argument("path", "Path to file to edit")) 163 | .parse(args); 164 | 165 | writeln("verbosity level", a.occurencesOf("verbose")); 166 | writeln("arg: ", a.arg("path")); 167 | } 168 | ``` 169 | 170 | ### Subcommands 171 | 172 | You can create subcommands in your program or command using `.add`. You can nest commands. 173 | 174 | Adding subcommands adds a virtual required argument at the end to your program. This makes you unable to declare repeating or optional arguments (because you cannot have required argument past these). 175 | 176 | Default command can be set with `.defaultCommand(name)` call after defining all commands. 177 | 178 | After parsing, every subcommand gets its own `ProgramArgs` instance, forming a hierarchy. Nested args inherit arguments from parent, so that options defined higher 179 | in hierarchy are copied. 180 | ProgramArgs defines a helper method `on`, that allows to dispatch method on specified command. 181 | 182 | ```D 183 | auto args = new Program("test", "1.0") 184 | .add(new Flag("v", null, "turns on more verbose output") 185 | .name("verbose") 186 | .repeating) 187 | .add(new Command("greet") 188 | .add(new Argument("name", "name of person to greet"))) 189 | .add(new Command("farewell") 190 | .add(new Argument("name", "name of person to say farewell"))) 191 | .parse(args); 192 | 193 | args 194 | .on("greet", (args) { 195 | // args.flag("verbose") works 196 | writefln("Hello %s!", args.arg("name")); 197 | }) 198 | .on("farewell", (args) { 199 | writefln("Bye %s!", args.arg("name")); 200 | }); 201 | ``` 202 | 203 | Delegate passed to `on` function receives `ProgramArgs` instance for that subcommand. Because it is also `ProgramArgs`, `on` chain can be nested, as in: 204 | 205 | ```D 206 | // assuming program has nested subcommands 207 | 208 | a.on("branch", (args) { 209 | args 210 | .on("add", (args) { 211 | writefln("adding branch %s", args.arg("name")); 212 | }) 213 | .on("rm", (args) { 214 | writefln("removing branch %s", args.arg("name")); 215 | }); 216 | }); 217 | ``` 218 | 219 | ### Validation 220 | 221 | You can attach one or more validators to options and arguments with `validate` method. Every validator has its own helper function that simplifies adding it do option (usually starting with `accepts`): 222 | 223 | ```D 224 | new Program("test") 225 | // adding validator manually 226 | .add(new Option("s", "scope", "") 227 | .validate(new EnumValidator(["local", "global", "system"])) 228 | ) 229 | // helper functionnew Program("test") 230 | .add(new Option("s", "scope", "") 231 | .acceptsValues(["local", "global", "system"])); 232 | ``` 233 | 234 | #### Built-in validators 235 | 236 | - **EnumValidator** - Allows to pass values from white-list. 237 | 238 | Helpers: `.acceptsValues(values)` 239 | 240 | - **FileSystemValidator** - Verifies whenever passed values are files/directories or just exist (depending on configuration). 241 | 242 | Helpers: `.acceptsFiles()`, `.acceptsDirectories()`, `.acceptsPaths(bool existing)` 243 | 244 | - **DelegateValidator** - Verifies whenever passed values with user-defined delegate. 245 | 246 | Helpers: `.validateWith(delegate)`, `validateEachWith(delegate)` 247 | 248 | 249 | You can create custom validators either by implementing `IValidator` interface, or by using `DelegateValidator`: 250 | 251 | ```D 252 | new Program("test") 253 | // adding validator manually 254 | .add(new Option("s", "scope", "") 255 | .validateEachWith(opt => opt.isDirectory, "must be a valid directory") 256 | ); 257 | ``` 258 | 259 | 260 | ### Printing help 261 | 262 | You can print help for program or any subcommand with `printHelp()` function: 263 | 264 | ```D 265 | program.printHelp(); // prints program help 266 | program.commands["test"].printHelp(); 267 | ``` 268 | 269 | To customise help output, pass `HelpOutput` struct instance: 270 | 271 | ```D 272 | HelpOutput helpOptions; 273 | helpOptions.colors = false; 274 | helpOptions.optionsLimit = 2; 275 | 276 | program.printHelp(helpOptions); 277 | ``` 278 | 279 | ### Bash autocompletion 280 | 281 | Commandr can generate BASH autocompletion script. During installation of your program you can save the generated script to `/etc/bash_completion.d/.bash` (or any other path depending on distro). 282 | 283 | ```D 284 | import commandr; 285 | import commandr.completion.bash; 286 | 287 | string script = program.createBashCompletionScript(); 288 | // save script to file 289 | ``` 290 | 291 | ### Configuration 292 | 293 | TODO 294 | 295 | 296 | ## Cheat-Sheet 297 | 298 | ### Defining entries 299 | 300 | Overview of available entries that can be added to program or command with `.add` method: 301 | 302 | What | Type | Example | Definition 303 | -------------|----------|-------------|--------------------------------------- 304 | **Flag** | bool | `--verbose` | `new Flag(abbrev?, full?, summary?)` 305 | **Option** | string[] | `--db=test` | `new Option(abbrev?, full?, summary?)` 306 | **Argument** | string[] | `123` | `new Argument(name, summary?)` 307 | 308 | 309 | ### Reading values 310 | 311 | Shows how to access values after parsing args. 312 | 313 | Examples assume `args` variable contains result of `parse()` or `parseArgs()` function calls (an instance of `ProgramArgs`) 314 | 315 | ```D 316 | ProgramArgs args = program.parse(args); 317 | ``` 318 | 319 | What | Type | Fetch 320 | -------------|----------|-------------------- 321 | **Flag** | bool | `args.flag(name)` 322 | **Flag** | int | `args.occurencesOf(name)` 323 | **Option** | string | `args.option(name)` 324 | **Option** | string[] | `args.options(name)` 325 | **Argument** | string | `args.arg(name)` 326 | **Argument** | string[] | `args.args(name)` 327 | 328 | 329 | ### Property Matrix 330 | 331 | 332 | 333 | Table below shows which fields exist and which don't (or should not be used). 334 | 335 | Column `name` contains name of the method to set the value. All methods return 336 | `this` to allow chaining. 337 | 338 | Name | Program | Command | Flag | Option | Argument 339 | ---------------------|---------|---------|------|--------|--------- 340 | `.name` | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: 341 | `.version_` | :heavy_check_mark: | :heavy_check_mark: | ❌ | ️❌ | ❌ 342 | `.summary` | :heavy_check_mark:️ | ️:heavy_check_mark: | ❌ | ️❌ | ❌ 343 | `.description` | ❌ | ️❌ | :heavy_check_mark: | ️:heavy_check_mark: | :heavy_check_mark: 344 | `.abbrev` | ❌ | ❌ | :heavy_check_mark: | :heavy_check_mark: | ❌ 345 | `.full` | ❌ | ❌ | :heavy_check_mark:️ | ️:heavy_check_mark: | ❌ 346 | `.tag` | ❌ | ❌ | ❌ | ️:heavy_check_mark: | :heavy_check_mark:️ 347 | `.defaultValue` | ❌ | ❌ | ❌ | ️:heavy_check_mark: | :heavy_check_mark:️ 348 | `.required` | ❌ | ❌ | ❌ | ️:heavy_check_mark: | :heavy_check_mark:️ 349 | `.optional` | ❌ | ❌ | ❌ | ️:heavy_check_mark: | :heavy_check_mark:️ 350 | `.repeating` | ❌ | ❌ | :heavy_check_mark: | ️:heavy_check_mark: | :heavy_check_mark:️ 351 | `.topic` | ❌ | :heavy_check_mark: | ❌ | ️❌ | ❌ 352 | `.topicGroup` | :heavy_check_mark: | :heavy_check_mark: | ❌ | ️❌ | ❌ 353 | `.authors` | :heavy_check_mark: | ❌ | ❌ | ️❌ | ❌ 354 | `.binaryName` | :heavy_check_mark: | ❌ | ❌ | ️❌ | ❌ 355 | 356 | 357 | ## Roadmap 358 | 359 | Current major missing features are: 360 | 361 | - Command/Option aliases 362 | - Combined short flags/options (e.g. `-qLob`) 363 | - EnumValidator/FileSystemValidator auto-completion hinting 364 | 365 | See the [open issues](https://github.com/robik/commandr/issues) for a list of proposed features (and known issues). 366 | -------------------------------------------------------------------------------- /dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "commandr", 3 | "authors": [ 4 | "Robert Pasiński" 5 | ], 6 | "description": "Modern command line argument parser", 7 | "copyright": "Copyright © 2019-2024, Robert Pasiński", 8 | "license": "MIT" 9 | } -------------------------------------------------------------------------------- /dub.selections.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileVersion": 1, 3 | "versions": { 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /images/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robik/commandr/5b535dff54cc6e389cba9b55aa84d249ab2e4bd1/images/help.png -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robik/commandr/5b535dff54cc6e389cba9b55aa84d249ab2e4bd1/images/logo.png -------------------------------------------------------------------------------- /source/commandr/args.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Parsed arguments module. 3 | * 4 | * This module contains functionality for handling the parsed command line arguments. 5 | * 6 | * `ProgramArgs` instance is created and returned by `parse` function. It contains values of flags, 7 | * options and arguments, which can be read with `.flag`, `.option` and `.arg` functions respectively. 8 | * Those functions work on _unique names_, not on option full/short names such as `-l` or `--help`. 9 | * 10 | * For repeating options and arguments, plural form functions can be used: `.options`, `.args`, 11 | * which return all values rather than last one. 12 | * 13 | * When a command or program has sub-commands returned `ProgramArgs` object forms a hierarchy, 14 | * where every subcommand is another instance of `ProgramArgs`, starting from root which is program args, going down 15 | * to selected sub-command. 16 | * 17 | * To simplify working with subcommands, you can use `on` command that allows to register command handlers 18 | * with a simple interface. E.g. consider git-like tool: 19 | * 20 | * --- 21 | * auto program = new Program("grit") 22 | * .add(new Flag("v", "verbose", "verbosity")) 23 | * .add(new Command("branch", "branch management") 24 | * .add(new Command("add", "adds branch") 25 | * .add(new Argument("name")) 26 | * ) 27 | * .add(new Command("rm", "removes branch") 28 | * .add(new Argument("name")) 29 | * ) 30 | * ) 31 | * ; 32 | * auto programArgs = program.parse(args); 33 | * programArgs 34 | * .on("branch", (args) { 35 | * writeln("verbosity: ", args.flag("verbose")); 36 | * args.on("rm", (args) { writeln("removing branch ", args.arg("name")); }) 37 | * .on("add", (args) { writeln("adding branch", args.arg("name")); }) 38 | * }) 39 | * --- 40 | * 41 | * See_Also: 42 | * ProgramArgs 43 | */ 44 | module commandr.args; 45 | 46 | import commandr.program; 47 | 48 | 49 | /** 50 | * Parsed program/command arguments. 51 | * 52 | * Note: All functions here work on flag/option/argument names, not short or long names. 53 | * option -> options multi 54 | * names 55 | * commands 56 | */ 57 | public class ProgramArgs { 58 | /// Program or command name 59 | public string name; 60 | 61 | package { 62 | int[string] _flags; 63 | string[][string] _options; 64 | string[][string] _args; 65 | string[] _args_rest; 66 | ProgramArgs _parent; 67 | ProgramArgs _command; 68 | } 69 | 70 | package ProgramArgs copy() { 71 | ProgramArgs a = new ProgramArgs(); 72 | a._flags = _flags.dup; 73 | a._options = _options.dup; 74 | a._args = _args.dup; 75 | return a; 76 | } 77 | 78 | /** 79 | * Checks for flag value. 80 | * 81 | * Params: 82 | * name - flag name to check 83 | * 84 | * Returns: 85 | * true if flag has been passed at least once, false otherwise. 86 | * 87 | * See_Also: 88 | * occurencesOf 89 | */ 90 | public bool hasFlag(string name) { 91 | return ((name in _flags) != null && _flags[name] > 0); 92 | } 93 | 94 | /// ditto 95 | public alias flag = hasFlag; 96 | 97 | /** 98 | * Gets number of flag occurences. 99 | * 100 | * For non-repeating flags, returns either 0 or 1. 101 | * 102 | * Params: 103 | * name - flag name to check 104 | * 105 | * Returns: 106 | * Number of flag occurences, 0 on none. 107 | * 108 | * See_Also: 109 | * hasFlag, flag 110 | */ 111 | public int occurencesOf(string name) { 112 | if (!hasFlag(name)) { 113 | return 0; 114 | } 115 | return _flags[name]; 116 | } 117 | 118 | /** 119 | * Gets option value. 120 | * 121 | * In case of repeating option, returns last value. 122 | * 123 | * Params: 124 | * name - name of option to get 125 | * defaultValue - default value if option is not set 126 | * 127 | * Returns: 128 | * Last option specified, or defaultValue if none 129 | * 130 | * See_Also: 131 | * options, optionAll 132 | */ 133 | public string option(string name, string defaultValue = null) { 134 | string[]* entryPtr = name in _options; 135 | if (!entryPtr) { 136 | return defaultValue; 137 | } 138 | 139 | if ((*entryPtr).length == 0) { 140 | return defaultValue; 141 | } 142 | 143 | return (*entryPtr)[$-1]; 144 | } 145 | 146 | /** 147 | * Gets all option values. 148 | * 149 | * In case of non-repeating option, returns array with one value. 150 | * 151 | * Params: 152 | * name - name of option to get 153 | * defaultValue - default value if option is not set 154 | * 155 | * Returns: 156 | * Option values, or defaultValue if none 157 | * 158 | * See_Also: 159 | * option 160 | */ 161 | public string[] optionAll(string name, string[] defaultValue = null) { 162 | string[]* entryPtr = name in _options; 163 | if (!entryPtr) { 164 | return defaultValue; 165 | } 166 | return *entryPtr; 167 | } 168 | 169 | /// ditto 170 | alias options = optionAll; 171 | 172 | /** 173 | * Gets argument value. 174 | * 175 | * In case of repeating arguments, returns last value. 176 | * 177 | * Params: 178 | * name - name of argument to get 179 | * defaultValue - default value if argument is missing 180 | * 181 | * Returns: 182 | * Argument values, or defaultValue if none 183 | * 184 | * See_Also: 185 | * args, argAll 186 | */ 187 | public string arg(string name, string defaultValue = null) { 188 | string[]* entryPtr = name in _args; 189 | if (!entryPtr) { 190 | return defaultValue; 191 | } 192 | 193 | if ((*entryPtr).length == 0) { 194 | return defaultValue; 195 | } 196 | 197 | return (*entryPtr)[$-1]; 198 | } 199 | 200 | /** 201 | * Gets all argument values. 202 | * 203 | * In case of non-repeating arguments, returns array with one value. 204 | * 205 | * Params: 206 | * name - name of argument to get 207 | * defaultValue - default value if argument is missing 208 | * 209 | * Returns: 210 | * Argument values, or defaultValue if none 211 | * 212 | * See_Also: 213 | * arg 214 | */ 215 | public string[] argAll(string name, string[] defaultValue = null) { 216 | string[]* entryPtr = name in _args; 217 | if (!entryPtr) { 218 | return defaultValue; 219 | } 220 | return *entryPtr; 221 | } 222 | 223 | /// ditto 224 | alias args = argAll; 225 | 226 | /** 227 | * Rest (unparsed) arguments. 228 | * 229 | * Useful, if you need to access unparsed arguments, 230 | * usually supplied after '--'. 231 | * 232 | * Returns: 233 | * array of arguments that were not handled by parser 234 | * 235 | * Examples: 236 | * --- 237 | * auto args = ["my-command", "--opt", "arg", "--", "other-arg", "o-arg"]; 238 | * auto res = parse(args); 239 | * 240 | * // Not we can access unparsed args as res.argsRest 241 | * assert(res.argsRest == ["other-arg", "o-arg"]) 242 | * --- 243 | */ 244 | public string[] argsRest() { 245 | return _args_rest; 246 | } 247 | 248 | /** 249 | * Gets subcommand arguments. 250 | * 251 | * See_Also: 252 | * on, parent 253 | */ 254 | public ProgramArgs command() { 255 | return _command; 256 | } 257 | 258 | /** 259 | * Gets parent `ProgramArgs`, if any. 260 | * 261 | * See_Also: 262 | * command 263 | */ 264 | public ProgramArgs parent() { 265 | return _parent; 266 | } 267 | 268 | /** 269 | * Calls `handler` if user specified `command` subcommand. 270 | * 271 | * Example: 272 | * --- 273 | * auto a = new Program() 274 | * .add(new Command("test")) 275 | * .parse(args); 276 | * 277 | * a.on("test", (a) { 278 | * writeln("Test!"); 279 | * }); 280 | * --- 281 | */ 282 | public typeof(this) on(string command, scope void delegate(ProgramArgs args) handler) { 283 | if (_command !is null && _command.name == command) { 284 | handler(_command); 285 | } 286 | 287 | return this; 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /source/commandr/completion/bash.d: -------------------------------------------------------------------------------- 1 | module commandr.completion.bash; 2 | 3 | import commandr.program; 4 | import commandr.option; 5 | import std.algorithm : map, filter; 6 | import std.array : Appender, join; 7 | import std.string : format; 8 | import std.range : empty, chain; 9 | 10 | 11 | /** 12 | * Creates bash completion script. 13 | * 14 | * Creates completion script for specified program, returning the script contents. 15 | * You need to create it once, and save the script in directory like 16 | * `/etc/bash_completion.d/` during installation. 17 | * 18 | * Params: 19 | * program - Program to create completion script. 20 | * 21 | * Returns: 22 | * Generated completion script contents. 23 | * 24 | * Examples: 25 | * --- 26 | * import std.file : write; 27 | * import std.string : format; 28 | * import commandr; 29 | * 30 | * auto prog = new Program("test"); 31 | * std.file.write("%s.bash".format(prog.binaryName), createBashCompletionScript(prog)); 32 | * --- 33 | */ 34 | string createBashCompletionScript(Program program) { 35 | Appender!string builder; 36 | 37 | builder ~= "#!/usr/bin/env bash\n"; 38 | builder ~= "# This file is autogenerated. DO NOT EDIT.\n"; 39 | 40 | builder ~= ` 41 | __get_args() { 42 | local max opts args name arg i count 43 | max=$1; shift 44 | opts=$1; shift 45 | args=($@) 46 | let i=0 47 | let count=0 48 | 49 | while [ $i -le ${#args[@]} ]; do 50 | arg=${args[i]} 51 | name="${arg%=*}" 52 | 53 | if [[ $name = -* ]]; then 54 | if [[ " $opts " = *"$name"* ]]; then 55 | if ! [[ $name = *"="* ]]; then 56 | let i+=1 57 | fi 58 | fi 59 | else 60 | let count+=1 61 | echo $arg 62 | fi 63 | let i+=1 64 | 65 | [ $count -ge $max ] && break 66 | done 67 | 68 | return $i 69 | } 70 | 71 | __function_exists() { 72 | declare -f -F $1 > /dev/null 73 | return $? 74 | } 75 | 76 | `; 77 | // works by creating completion functions for commands recursively 78 | completionFunc(program, builder); 79 | 80 | builder ~= "complete -F _%s_completion_main %s\n".format(program.binaryName, program.binaryName); 81 | 82 | return builder.data; 83 | } 84 | 85 | private void completionFunc(Command command, Appender!string builder) { 86 | foreach (cmd; command.commands) { 87 | completionFunc(cmd, builder); 88 | } 89 | 90 | auto argumentCount = command.arguments.length; 91 | auto commands = command.commands.keys.join(" "); 92 | 93 | auto shorts = command.abbrevations.map!(s => "-" ~ s); 94 | auto longs = command.fullNames.map!(l => "--" ~ l); 95 | 96 | auto options = command.options 97 | .map!(o => [o.abbrev ? "-" ~ o.abbrev : null, o.full ? "--" ~ o.full : null]) 98 | .join().filter!`a && a.length`.join(" "); 99 | 100 | builder ~= "_%s_completion() {\n".format(command.chain.join("_")); 101 | builder ~= " local args target\n\n"; 102 | 103 | builder ~= " __args=( $(__get_args %s \"%s\" \"${COMP_WORDS[@]:__args_start}\") )\n" 104 | .format(argumentCount + command.commands.length ? 1 : 0, options); 105 | builder ~= " args=$?\n\n"; 106 | 107 | builder ~= " if [ $COMP_CWORD -lt $(( $__args_start + $args )) ]; then\n"; 108 | builder ~= " if [[ \"$curr\" = -* ]]; then\n"; 109 | builder ~= " COMPREPLY=( $(compgen -W \"%s\" -- \"$curr\") )\n".format(chain(shorts, longs).join(" ")); 110 | 111 | if (command.commands.length > 0) { 112 | builder ~= " elif [ ${#__args[@]} -ge %s ]; then\n".format(command.arguments.length); 113 | builder ~= " COMPREPLY=( $(compgen -W \"%s\" -- \"$curr\") )\n".format(commands); 114 | builder ~= " fi\n"; 115 | 116 | builder ~= " elif [ ${#__args[@]} -gt 0 ] && [[ \" %s \" = *\" ${__args[@]: -1:1} \"* ]]; then\n".format(commands); 117 | builder ~= " target=\"_%s_${__args[@]: -1:1}_completion\"\n".format(command.chain.join("_")); 118 | builder ~= " let __args_start+=$args\n"; 119 | builder ~= " __function_exists $target && $target\n"; 120 | } 121 | else { 122 | builder ~= " fi\n"; 123 | } 124 | builder ~= " fi\n"; 125 | builder ~= "}\n\n"; 126 | } 127 | 128 | private void completionFunc(Program program, Appender!string builder) { 129 | foreach(command; program.commands) { 130 | completionFunc(command, builder); 131 | } 132 | 133 | completionFunc(cast(Command)program, builder); 134 | 135 | builder ~= "_%s_completion_main() {\n".format(program.binaryName); 136 | builder ~= " COMPREPLY=()\n"; 137 | builder ~= " __args_start=1\n"; 138 | builder ~= " curr=${COMP_WORDS[COMP_CWORD]}\n"; 139 | builder ~= " prev=${COMP_WORDS[COMP_CWORD-1]}\n\n"; 140 | builder ~= " _%s_completion\n".format(program.binaryName); 141 | 142 | builder ~= " unset __args __args_start curr prev\n"; 143 | builder ~= "}\n\n"; 144 | } 145 | -------------------------------------------------------------------------------- /source/commandr/help.d: -------------------------------------------------------------------------------- 1 | module commandr.help; 2 | 3 | import commandr.program; 4 | import commandr.option; 5 | import std.algorithm : filter, map, any, chunkBy, sort; 6 | import std.array : join, array; 7 | import std.conv : to; 8 | import std.stdio : writefln, writeln, write; 9 | import std.string : format; 10 | import std.range : chain, empty; 11 | 12 | 13 | /// 14 | struct HelpOutput { 15 | /// 16 | bool colors = true; 17 | // bool compact = false; 18 | 19 | /// 20 | // int maxWidth = 80; 21 | 22 | /// 23 | int indent = 24; 24 | /// 25 | int optionsLimit = 6; 26 | /// 27 | int commandLimit = 6; 28 | } 29 | 30 | /// 31 | void printHelp(Command program, HelpOutput output = HelpOutput.init) { 32 | HelpPrinter(output).printHelp(program); 33 | } 34 | 35 | /// 36 | void printUsage(Command program, HelpOutput output = HelpOutput.init) { 37 | HelpPrinter(output).printUsage(program); 38 | } 39 | 40 | 41 | struct HelpPrinter { 42 | HelpOutput config; 43 | 44 | public this(HelpOutput config) nothrow pure @safe { 45 | this.config = config; 46 | } 47 | 48 | void printHelp(Command program) { 49 | if (cast(Program)program) { 50 | writefln("%s: %s %s(%s)%s", program.name, program.summary, ansi("2"), program.version_, ansi("0")); 51 | writeln(); 52 | } 53 | else { 54 | writefln("%s: %s", program.chain.join(" "), program.summary); 55 | writeln(); 56 | } 57 | 58 | writefln("%sUSAGE%s", ansi("1"), ansi("0")); 59 | write(" $ "); // prefix for usage 60 | printUsage(program); 61 | writeln(); 62 | 63 | if (!program.flags.empty) { 64 | writefln("%sFLAGS%s", ansi("1"), ansi("0")); 65 | foreach(flag; program.flags) { 66 | printHelp(flag); 67 | } 68 | writeln(); 69 | } 70 | 71 | if (!program.options.empty) { 72 | writefln("%sOPTIONS%s", ansi("1"), ansi("0")); 73 | foreach(option; program.options) { 74 | printHelp(option); 75 | } 76 | writeln(); 77 | } 78 | 79 | if (!program.arguments.empty) { 80 | writefln("%sARGUMENTS%s", ansi("1"), ansi("0")); 81 | foreach(arg; program.arguments) { 82 | printHelp(arg); 83 | } 84 | writeln(); 85 | } 86 | 87 | if (program.commands.length > 0) { 88 | writefln("%sSUBCOMMANDS%s", ansi("1"), ansi("0")); 89 | printSubcommands(program.commands); 90 | writeln(); 91 | } 92 | } 93 | 94 | void printUsage(Program program) { 95 | string optionsUsage = "[options]"; 96 | 97 | // if there are not too many options 98 | if (program.options.length + program.flags.length <= config.optionsLimit) { 99 | optionsUsage = chain( 100 | program.flags.map!(f => optionUsage(f)), 101 | program.options.map!(o => optionUsage(o)) 102 | ).join(" "); 103 | } else { 104 | optionsUsage ~= " " ~ program.options.filter!(o => o.isRequired).map!(o => optionUsage(o)).join(" "); 105 | } 106 | 107 | string commands = program.commands.length == 0 ? "" : ( 108 | program.commands.length > config.commandLimit ? "COMMAND" : program.commands.keys.join("|") 109 | ); 110 | string args = program.arguments.map!(a => argUsage(a)).join(" "); 111 | 112 | writefln("%s %s %s%s", 113 | program.binaryName, 114 | optionsUsage, 115 | args.empty ? "" : args ~ " ", 116 | commands 117 | ); 118 | } 119 | 120 | void printUsage(Command command) { 121 | string optionsUsage = "[options]"; 122 | if (command.options.length + command.flags.length <= config.optionsLimit) { 123 | optionsUsage = chain( 124 | command.flags.map!(f => optionUsage(f)), 125 | command.options.map!(o => optionUsage(o)) 126 | ).join(" "); 127 | } else { 128 | optionsUsage ~= " " ~ command.options.filter!(o => o.isRequired).map!(o => optionUsage(o)).join(" "); 129 | } 130 | 131 | string commands = command.commands.length == 0 ? "" : ( 132 | command.commands.length > config.commandLimit ? "command" : command.commands.keys.join("|") 133 | ); 134 | string args = command.arguments.map!(a => argUsage(a)).join(" "); 135 | 136 | writefln("%s %s %s%s", 137 | usageChain(command), 138 | optionsUsage, 139 | args.empty ? "" : args ~ " ", 140 | commands 141 | ); 142 | } 143 | 144 | private void printHelp(Flag flag) { 145 | string left = optionNames(flag); 146 | writefln(" %-"~config.indent.to!string~"s %s%s%s", left, ansi("2"), flag.description, ansi("0")); 147 | } 148 | 149 | private void printHelp(Option option) { 150 | string left = optionNames(option); 151 | size_t length = left.length + option.tag.length + 1; 152 | string formatted = "%s %s%s%s".format(left, ansi("4"), option.tag, ansi("0")); 153 | size_t padLength = config.indent + (formatted.length - length); 154 | 155 | writefln(" %-"~padLength.to!string~"s %s%s%s", formatted, ansi("2"), option.description, ansi("0")); 156 | } 157 | 158 | private void printHelp(Argument arg) { 159 | writefln(" %-"~config.indent.to!string~"s %s%s%s", arg.tag, ansi("2"), arg.description, ansi("0")); 160 | } 161 | 162 | private void printSubcommands(Command[string] commands) { 163 | auto grouped = commands.values 164 | .sort!((a, b) { 165 | // Note, when we used chunkBy, it is expected that range 166 | // is already sorted by the key, thus before grouping, 167 | // we have to sort by topic first. 168 | // And then by name for better output 169 | // (because associative array do not preserver order). 170 | if (a.topic == b.topic) 171 | return a.name < b.name; 172 | return a.topic < b.topic; 173 | }) 174 | .chunkBy!(a => a.topic) 175 | .array; 176 | 177 | if (grouped.length == 1 && grouped[0][0] is null) { 178 | foreach(key, command; commands) { 179 | writefln(" %-"~config.indent.to!string~"s %s%s%s", key, ansi("2"), command.summary, ansi("0")); 180 | } 181 | } 182 | else { 183 | foreach (entry; grouped) { 184 | writefln(" %s%s%s:", ansi("4"), entry[0], ansi("0")); 185 | foreach(command; entry[1]) { 186 | writefln( 187 | " %-"~(config.indent - 2).to!string~"s %s%s%s", 188 | command.name, ansi("2"), command.summary, ansi("0") 189 | ); 190 | } 191 | writeln(); 192 | } 193 | } 194 | } 195 | 196 | private string usageChain(Command target) { 197 | Command[] commands = []; 198 | Command dest = target.parent; 199 | while (dest !is null) { 200 | commands ~= dest; 201 | dest = dest.parent; 202 | } 203 | 204 | string[] elements; 205 | 206 | foreach_reverse(command; commands) { 207 | elements ~= ansi("0") ~ command.name ~ ansi("2"); 208 | 209 | foreach (opt; command.options.filter!(o => o.isRequired)) { 210 | elements ~= optionUsage(opt) ~ ansi("2"); 211 | } 212 | 213 | foreach (arg; command.arguments.filter!(o => o.isRequired)) { 214 | elements ~= argUsage(arg); 215 | } 216 | } 217 | 218 | elements ~= ansi("0") ~ target.name; 219 | 220 | return elements.join(" "); 221 | } 222 | 223 | private string optionNames(T)(T o) { 224 | string names = ""; 225 | 226 | if (o.abbrev) { 227 | names ~= "-" ~ o.abbrev; 228 | } 229 | else { 230 | names ~= " "; 231 | } 232 | 233 | if (o.full) { 234 | if (o.abbrev) { 235 | names ~= ", "; 236 | } 237 | names ~= "--%s".format(o.full); 238 | } 239 | 240 | return names; 241 | } 242 | 243 | private string optionUsage(IOption o) { 244 | string result = o.displayName; 245 | 246 | if (cast(Option)o) { 247 | result = "%s %s%s%s".format(result, ansi("4"), (cast(Option)o).tag, ansi("0")); 248 | } 249 | 250 | if (!o.isRequired) { 251 | result = "[%s]".format(result); 252 | } 253 | 254 | return result; 255 | } 256 | 257 | private string argUsage(Argument arg) { 258 | return (arg.isRequired ? "%s" : "[%s]").format(arg.tag); 259 | } 260 | 261 | 262 | private string ansi(string code) { 263 | version(Windows) { 264 | return ""; 265 | } 266 | version(Posix) { 267 | import core.sys.posix.unistd : isatty, STDOUT_FILENO; 268 | 269 | if (config.colors && isatty(STDOUT_FILENO)) { 270 | return "\033[%sm".format(code); 271 | } 272 | 273 | return ""; 274 | } 275 | 276 | assert(0); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /source/commandr/option.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Program and command entries. 3 | * 4 | * This module contains interfaces and implementations of various entries. 5 | * Generally, most APIs expose builder-like pattern, where setters return 6 | * class instance to allow chaining. 7 | * 8 | * Entries are all things that can be added to program or command - that is 9 | * flags, options and arguments. All entries contain a name - which is an unique 10 | * identifier for every entry. Names must be a valid alpha numeric identifier. 11 | * 12 | * Result of parsing arguments (instance of `ProgramArgs`) allows reading 13 | * argument values by entry `name` (not by `-short` or `--long-forms`). 14 | * 15 | * See_Also: 16 | * Flag, Option, Argument 17 | */ 18 | module commandr.option; 19 | 20 | import commandr.validators; 21 | import commandr.program : InvalidProgramException; 22 | 23 | 24 | /** 25 | * Interface for all program or command entries - flags, options and arguments. 26 | */ 27 | interface IEntry { 28 | /** 29 | * Sets entry name. 30 | */ 31 | public typeof(this) name(string name) pure @safe; 32 | 33 | /** 34 | * Entry name. 35 | */ 36 | public string name() const pure nothrow @safe; 37 | 38 | /** 39 | * Display name for entry. 40 | * 41 | * For arguments, this is argument tag value. 42 | * For options and flags, this is either abbrevation with single dash prefix 43 | * or full name with double dash prefix. 44 | */ 45 | public string displayName() const pure nothrow @safe; 46 | 47 | /** 48 | * Sets entry help description (one-liner). 49 | */ 50 | public typeof(this) description(string description) pure @safe; 51 | 52 | /** 53 | * Entry help description (one-liner). 54 | */ 55 | public string description() const pure nothrow @safe @nogc; 56 | 57 | /** 58 | * Sets whenever entry can be repeated. 59 | */ 60 | public typeof(this) repeating(bool repeating = true) pure @safe; 61 | 62 | /** 63 | * Gets whenever entry can be repeated. 64 | */ 65 | public bool isRepeating() const pure nothrow @safe @nogc; 66 | 67 | /** 68 | * Sets entry required flag. 69 | */ 70 | public typeof(this) required(bool required = true) pure @safe; 71 | 72 | /** 73 | * Sets entry optional flag. 74 | */ 75 | public typeof(this) optional(bool optional = true) pure @safe; 76 | 77 | /** 78 | * Whenever entry is required. 79 | */ 80 | public bool isRequired() const pure nothrow @safe @nogc; 81 | 82 | /** 83 | * Sets entry default value. 84 | */ 85 | public typeof(this) defaultValue(string defaultValue) pure @safe; 86 | 87 | /** 88 | * Sets entry default value array. 89 | */ 90 | public typeof(this) defaultValue(string[] defaultValue) pure @safe; 91 | 92 | /** 93 | * Entry default value array. 94 | */ 95 | public string[] defaultValue() pure nothrow @safe @nogc; 96 | 97 | /** 98 | * Adds entry validator. 99 | */ 100 | public typeof(this) validate(IValidator validator) pure @safe; 101 | 102 | /** 103 | * Entry validators. 104 | */ 105 | public IValidator[] validators() pure nothrow @safe @nogc; 106 | } 107 | 108 | mixin template EntryImpl() { 109 | private string _name; 110 | private string _description; 111 | private bool _repeating = false; 112 | private bool _required = false; 113 | private string[] _default; 114 | private IValidator[] _validators; 115 | 116 | /// 117 | public typeof(this) name(string name) pure nothrow @safe @nogc { 118 | this._name = name; 119 | return this; 120 | } 121 | 122 | /// 123 | public string name() const pure nothrow @safe @nogc { 124 | return this._name; 125 | } 126 | 127 | /// 128 | public typeof(this) description(string description) pure nothrow @safe @nogc { 129 | this._description = description; 130 | return this; 131 | } 132 | 133 | /// 134 | public string description() const pure nothrow @safe @nogc { 135 | return this._description; 136 | } 137 | 138 | /// 139 | public typeof(this) repeating(bool repeating = true) pure nothrow @safe @nogc { 140 | this._repeating = repeating; 141 | return this; 142 | } 143 | 144 | /// 145 | public bool isRepeating() const pure nothrow @safe @nogc { 146 | return this._repeating; 147 | } 148 | 149 | /// 150 | public typeof(this) required(bool required = true) pure @safe { 151 | this._required = required; 152 | 153 | return this; 154 | } 155 | 156 | /// 157 | public typeof(this) optional(bool optional = true) pure @safe { 158 | this.required(!optional); 159 | return this; 160 | } 161 | 162 | /// 163 | public bool isRequired() const pure nothrow @safe @nogc { 164 | return this._required; 165 | } 166 | 167 | /// 168 | public typeof(this) defaultValue(string defaultValue) pure @safe { 169 | return this.defaultValue([defaultValue]); 170 | } 171 | 172 | /// 173 | public typeof(this) defaultValue(string[] defaultValue) pure @safe { 174 | this._default = defaultValue; 175 | this._required = false; 176 | return this; 177 | } 178 | 179 | /// 180 | public string[] defaultValue() pure nothrow @safe @nogc { 181 | return this._default; 182 | } 183 | 184 | /// 185 | public typeof(this) validate(IValidator validator) pure @safe { 186 | this._validators ~= validator; 187 | return this; 188 | } 189 | 190 | /// 191 | public IValidator[] validators() pure nothrow @safe @nogc { 192 | return this._validators; 193 | } 194 | } 195 | 196 | /** 197 | * Option interface. 198 | * 199 | * Used by flags and options, which both contain short and long names. 200 | * Either can be null but not both. 201 | */ 202 | interface IOption: IEntry { 203 | /** 204 | * Sets option full name (long-form). 205 | * 206 | * Set to null to disable long form. 207 | */ 208 | public typeof(this) full(string full) pure nothrow @safe @nogc; 209 | 210 | /** 211 | * Option full name (long-form). 212 | */ 213 | public string full() const pure nothrow @safe @nogc; 214 | 215 | /// ditto 216 | public alias long_ = full; 217 | 218 | /** 219 | * Sets option abbrevation (short-form). 220 | * 221 | * Set to null to disable short form. 222 | */ 223 | public typeof(this) abbrev(string abbrev) pure nothrow @safe @nogc; 224 | 225 | /** 226 | * Sets option abbrevation (short-form). 227 | */ 228 | public string abbrev() const pure nothrow @safe @nogc; 229 | 230 | /// ditto 231 | public alias short_ = abbrev; 232 | } 233 | 234 | mixin template OptionImpl() { 235 | private string _abbrev; 236 | private string _full; 237 | 238 | /// 239 | public string displayName() const nothrow pure @safe { 240 | if (_abbrev) { 241 | return "-" ~ _abbrev; 242 | } 243 | return "--" ~ _full; 244 | } 245 | 246 | /// 247 | public typeof(this) full(string full) pure nothrow @safe @nogc { 248 | this._full = full; 249 | return this; 250 | } 251 | 252 | /// 253 | public string full() const pure nothrow @safe @nogc { 254 | return this._full; 255 | } 256 | 257 | /// 258 | public alias long_ = full; 259 | 260 | /// 261 | public typeof(this) abbrev(string abbrev) pure nothrow @safe @nogc { 262 | this._abbrev = abbrev; 263 | return this; 264 | } 265 | 266 | /// 267 | public string abbrev() const pure nothrow @safe @nogc { 268 | return this._abbrev; 269 | } 270 | 271 | /// 272 | public alias short_ = abbrev; 273 | } 274 | 275 | 276 | /** 277 | * Represents a flag. 278 | * 279 | * Flag hold a single boolean value. 280 | * Flags are optional and cannot be set as required. 281 | */ 282 | public class Flag: IOption { 283 | mixin EntryImpl; 284 | mixin OptionImpl; 285 | 286 | /** 287 | * Creates new flag. 288 | * 289 | * Full flag name (long-form) is set to name parameter value. 290 | * 291 | * Params: 292 | * name - flag unique name. 293 | */ 294 | public this(string name) pure nothrow @safe @nogc { 295 | this._name = name; 296 | this._full = name; 297 | } 298 | 299 | 300 | /** 301 | * Creates new flag. 302 | * 303 | * Name defaults to long form value. 304 | * 305 | * Params: 306 | * abbrev - Flag short name (null for none) 307 | * full - Flag full name (null for none) 308 | * description - Flag help description 309 | */ 310 | public this(string abbrev, string full, string description) pure nothrow @safe @nogc { 311 | this._name = full; 312 | this._full = full; 313 | this._abbrev = abbrev; 314 | this._description = description; 315 | } 316 | } 317 | 318 | /** 319 | * Represents an option. 320 | * 321 | * Options hold any value as string (or array of strings). 322 | * Options by default are optional, but can be marked as required. 323 | * 324 | * Order in which options are passed does not matter. 325 | */ 326 | public class Option: IOption { 327 | mixin OptionImpl; 328 | mixin EntryImpl; 329 | 330 | private string _tag = "value"; 331 | 332 | 333 | /** 334 | * Creates new option. 335 | * 336 | * Full option name (long-form) is set to `name` parameter value. 337 | * 338 | * Params: 339 | * name - option unique name. 340 | */ 341 | public this(string name) pure nothrow @safe @nogc { 342 | this._name = name; 343 | this._full = name; 344 | } 345 | 346 | /** 347 | * Creates new option. 348 | * 349 | * Name defaults to long form value. 350 | * 351 | * Params: 352 | * abbrev - Option short name (null for none) 353 | * full - Option full name (null for none) 354 | * description - Option help description 355 | */ 356 | public this(string abbrev, string full, string description) pure nothrow @safe @nogc { 357 | this._name = full; 358 | this._full = full; 359 | this._abbrev = abbrev; 360 | this._description = description; 361 | } 362 | 363 | /** 364 | * Sets option value tag. 365 | * 366 | * A tag is a token displayed in place of option value. 367 | * Default tag is `value`. 368 | * 369 | * For example, for a option that takes path to configuration file, 370 | * one can create `--config` option and set `tag` to `config-path`, so that in 371 | * help it is displayed as `--config=config-path` instead of `--config=value` 372 | */ 373 | public typeof(this) tag(string tag) pure nothrow @safe @nogc { 374 | this._tag = tag; 375 | return this; 376 | } 377 | 378 | /** 379 | * Option value tag. 380 | */ 381 | public string tag() const pure nothrow @safe @nogc { 382 | return this._tag; 383 | } 384 | } 385 | 386 | /** 387 | * Represents an argument. 388 | * 389 | * Arguments are positional parameters passed to program and are required by default. 390 | * Only last argument can be repeating or optional. 391 | */ 392 | public class Argument: IEntry { 393 | mixin EntryImpl; 394 | 395 | private string _tag; 396 | 397 | /** 398 | * Creates new argument. 399 | * 400 | * Params: 401 | * name - Argument name 402 | * description - Help description 403 | */ 404 | public this(string name, string description = null) nothrow pure @safe @nogc { 405 | this._name = name; 406 | this._description = description; 407 | this._required = true; 408 | this._tag = name; 409 | } 410 | 411 | /** 412 | * Gets argument display name (tag or name). 413 | */ 414 | public string displayName() const nothrow pure @safe { 415 | return this._tag; 416 | } 417 | 418 | /** 419 | * Sets argument tag. 420 | * 421 | * A tag is a token displayed in place of argument. 422 | * By default it is name of the argument. 423 | */ 424 | public typeof(this) tag(string tag) pure nothrow @safe @nogc { 425 | this._tag = tag; 426 | return this; 427 | } 428 | 429 | /** 430 | * Argument tag 431 | */ 432 | public string tag() const pure nothrow @safe @nogc { 433 | return this._tag; 434 | } 435 | } 436 | 437 | /** 438 | * Thrown when user-passed data is invalid. 439 | * 440 | * This exception is thrown during parsing phase when user passed arguments (e.g. invalid option, invalid value). 441 | * 442 | * This exception is automatically caught if using `parse` function. 443 | */ 444 | public class InvalidArgumentsException: Exception { 445 | /** 446 | * Creates new InvalidArgumentException 447 | */ 448 | public this(string msg) nothrow pure @safe @nogc { 449 | super(msg); 450 | } 451 | } 452 | 453 | // options 454 | unittest { 455 | assert(!new Option("t", "test", "").isRequired); 456 | } 457 | 458 | // arguments 459 | unittest { 460 | assert(new Argument("test", "").isRequired); 461 | } 462 | -------------------------------------------------------------------------------- /source/commandr/package.d: -------------------------------------------------------------------------------- 1 | module commandr; 2 | 3 | public import commandr.option; 4 | public import commandr.program; 5 | public import commandr.parser; 6 | public import commandr.args; 7 | public import commandr.help; 8 | public import commandr.validators; 9 | public import commandr.utils; -------------------------------------------------------------------------------- /source/commandr/parser.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Argument parsing functionality. 3 | * 4 | * See_Also: 5 | * parse, parseArgs 6 | */ 7 | module commandr.parser; 8 | 9 | import commandr.program; 10 | import commandr.option; 11 | import commandr.args; 12 | import commandr.help; 13 | import commandr.utils; 14 | 15 | import std.algorithm : canFind, count, each; 16 | import std.stdio : writeln, writefln, stderr; 17 | import std.string : startsWith, indexOf, format; 18 | import std.range : empty; 19 | import std.typecons : Tuple; 20 | private import core.stdc.stdlib; 21 | 22 | 23 | /** 24 | * Parses program arguments. 25 | * 26 | * Returns instance of `ProgramArgs`, which allows working on parsed data. 27 | * 28 | * On top of parsing arguments, this function catches `InvalidArgumentsException`, 29 | * handles `--version` and `--help` flags as well as `help` subcommand. 30 | * Exception is handled by printing out the error message along with program usage information 31 | * and exiting. 32 | * 33 | * Version and help is handled by prining out information and exiting. 34 | * 35 | * If you want to only parse argument without additional handling, see `parseArgs`. 36 | * 37 | * Similarly to `parseArgs`, `args` array is taken by reference, after call it points to first 38 | * not-parsed argument (after `--`). 39 | * 40 | * See_Also: 41 | * `parseArgs` 42 | */ 43 | public ProgramArgs parse(Program program, ref string[] args, HelpOutput helpConfig = HelpOutput.init) { 44 | try { 45 | return parseArgs(program, args, helpConfig); 46 | } catch(InvalidArgumentsException e) { 47 | stderr.writeln("Error: ", e.msg); 48 | program.printUsage(helpConfig); 49 | exit(0); 50 | assert(0); 51 | } 52 | } 53 | 54 | 55 | /** 56 | * Parses args. 57 | * 58 | * Returns instance of `ProgramArgs`, which allows working on parsed data. 59 | * 60 | * Program model by default adds version flag and help flags and subcommand which need to be 61 | * handled by the caller. If you want to have the default behavior, use `parse` which handles 62 | * above flags. 63 | * 64 | * `args` array is taken by reference, after call it points to first not-parsed argument (after `--`). 65 | * 66 | * Throws: 67 | * InvalidArgumentException 68 | * 69 | * See_Also: 70 | * `parse` 71 | */ 72 | public ProgramArgs parseArgs(Program program, ref string[] args, HelpOutput helpConfig = HelpOutput.init) { 73 | args = args[1..$]; 74 | return program.parseArgs(args, new ProgramArgs(), helpConfig); 75 | } 76 | 77 | private ProgramArgs parseArgs( 78 | Command program, 79 | ref string[] args, 80 | ProgramArgs init, 81 | HelpOutput helpConfig = HelpOutput.init 82 | ) { 83 | // TODO: Split 84 | ProgramArgs result = init; 85 | result.name = program.name; 86 | size_t argIndex = 0; 87 | 88 | while (args.length) { 89 | string arg = args[0]; 90 | args = args[1..$]; 91 | immutable bool hasNext = args.length > 0; 92 | 93 | // end of args 94 | if (arg == "--") { 95 | break; 96 | } 97 | // option/flag 98 | else if (arg.startsWith("-")) { 99 | immutable bool isLong = arg.startsWith("--"); 100 | auto raw = parseRawOption(arg[1 + isLong..$]); 101 | 102 | // try matching flag, then fallback to option 103 | auto flag = isLong ? program.getFlagByFull(raw.name, true) : program.getFlagByShort(raw.name, true); 104 | int flagValue = 1; 105 | 106 | // repeating (-vvvv) 107 | if (!isLong && flag.isNull && raw.name.length > 1) { 108 | char letter = raw.name[0]; 109 | // all same character 110 | if (!raw.name.canFind!(l => l != letter)) { 111 | flagValue = cast(int)raw.name.length; 112 | raw.name = raw.name[0..1]; 113 | flag = program.getFlagByShort(raw.name, true); 114 | } 115 | } 116 | 117 | // flag exists, has value 118 | if (!flag.isNull && raw.value != null) { 119 | throw new InvalidArgumentsException("-%s is a flag, and cannot accept value".format(raw.name)); 120 | } 121 | // just exists 122 | else if (!flag.isNull) { 123 | auto flagName = flag.get().name; 124 | result._flags.setOrIncrease(flagName, flagValue); 125 | 126 | if (result._flags[flagName] > 1 && !flag.get().isRepeating) { 127 | throw new InvalidArgumentsException("flag -%s cannot be repeated".format(raw.name)); 128 | } 129 | 130 | continue; 131 | } 132 | 133 | // trying to match option 134 | auto option = isLong ? program.getOptionByFull(raw.name, true) : program.getOptionByShort(raw.name, true); 135 | if (option.isNull) { 136 | string suggestion = (isLong ? program.fullNames : program.abbrevations).matchingCandidate(raw.name); 137 | 138 | if (suggestion) { 139 | throw new InvalidArgumentsException( 140 | "unknown flag/option %s, did you mean %s%s?".format(arg, isLong ? "--" : "-", suggestion) 141 | ); 142 | } 143 | else { 144 | throw new InvalidArgumentsException("unknown flag/option %s".format(arg)); 145 | } 146 | } 147 | 148 | // no value 149 | if (raw.value is null) { 150 | if (!hasNext) { 151 | throw new InvalidArgumentsException( 152 | "option %s%s is missing value".format(isLong ? "--" : "-", raw.name) 153 | ); 154 | } 155 | auto next = args[0]; 156 | args = args[1..$]; 157 | if (next.startsWith("-")) { 158 | throw new InvalidArgumentsException( 159 | "option %s%s is missing value (if value starts with \'-\' character, prefix it with '\\')" 160 | .format(isLong ? "--" : "-", raw.name) 161 | ); 162 | } 163 | raw.value = next; 164 | } 165 | result._options.setOrAppend(option.get().name, raw.value); 166 | } 167 | // argument 168 | else if (argIndex < program.arguments.length) { 169 | Argument argument = program.arguments[argIndex]; 170 | if (!argument.isRepeating) { 171 | argIndex += 1; 172 | } 173 | result._args.setOrAppend(argument.name, arg); 174 | } 175 | // command 176 | else { 177 | if (program.commands.length == 0) { 178 | throw new InvalidArgumentsException("unknown (excessive) parameter %s".format(arg)); 179 | } 180 | else if ((arg in program.commands) is null) { 181 | string suggestion = program.commands.keys.matchingCandidate(arg); 182 | throw new InvalidArgumentsException("unknown command %s, did you mean %s?".format(arg, suggestion)); 183 | } 184 | else { 185 | result._command = program.commands[arg].parseArgs(args, result.copy()); 186 | result._command._parent = result; 187 | break; 188 | } 189 | } 190 | } 191 | 192 | if (args.length > 0) 193 | result._args_rest = args.dup; 194 | 195 | if (result.flag("help")) { 196 | program.printHelp(helpConfig); 197 | exit(0); 198 | } 199 | 200 | if (result.flag("version")) { 201 | writeln(program.version_); 202 | exit(0); 203 | } 204 | 205 | // fill defaults (before required) 206 | foreach(option; program.options) { 207 | if (result.option(option.name) is null && option.defaultValue) { 208 | result._options[option.name] = option.defaultValue; 209 | } 210 | } 211 | 212 | foreach(arg; program.arguments) { 213 | if (result.arg(arg.name) is null && arg.defaultValue) { 214 | result._args[arg.name] = arg.defaultValue; 215 | } 216 | } 217 | 218 | // post-process options: check required opts, illegal repetitions and validate 219 | foreach (option; program.options) { 220 | if (option.isRequired && result.option(option.name) is null) { 221 | throw new InvalidArgumentsException("missing required option %s".format(option.name)); 222 | } 223 | 224 | if (!option.isRepeating && result.options(option.name, []).length > 1) { 225 | throw new InvalidArgumentsException("expected only one value for option %s".format(option.name)); 226 | } 227 | 228 | if (option.validators.empty) { 229 | continue; 230 | } 231 | 232 | auto values = result.options(option.name); 233 | foreach (validator; option.validators) { 234 | validator.validate(option, values); 235 | } 236 | } 237 | 238 | // check required args & illegal repetitions 239 | foreach (arg; program.arguments) { 240 | if (arg.isRequired && result.arg(arg.name) is null) { 241 | throw new InvalidArgumentsException("missing required argument %s".format(arg.name)); 242 | } 243 | 244 | if (arg.validators.empty) { 245 | continue; 246 | } 247 | 248 | auto values = result.args(arg.name); 249 | foreach (validator; arg.validators) { 250 | validator.validate(arg, values); 251 | } 252 | } 253 | 254 | if (result.command is null && program.commands.length > 0) { 255 | if (program.defaultCommand !is null) { 256 | result._command = program.commands[program.defaultCommand].parseArgs(args, result.copy()); 257 | result._command._parent = result; 258 | } 259 | else { 260 | throw new InvalidArgumentsException("missing required subcommand"); 261 | } 262 | } 263 | 264 | return result; 265 | } 266 | 267 | package ProgramArgs parseArgsNoRef(Program p, string[] args) { 268 | return p.parseArgs(args); 269 | } 270 | 271 | // internal type for holding name-value pair 272 | private alias RawOption = Tuple!(string, "name", string, "value"); 273 | 274 | 275 | /* 276 | * Splits --option=value into a pair of strings on match, otherwise 277 | * returns a tuple with option name and null. 278 | */ 279 | private RawOption parseRawOption(string argument) { 280 | RawOption result; 281 | 282 | auto index = argument.indexOf("="); 283 | if (index > 0) { 284 | result.name = argument[0..index]; 285 | result.value = argument[index+1..$]; 286 | } 287 | else { 288 | result.name = argument; 289 | result.value = null; 290 | } 291 | 292 | return result; 293 | } 294 | 295 | private void setOrAppend(T)(ref T[][string] array, string name, T value) { 296 | if (name in array) { 297 | array[name] ~= value; 298 | } else { 299 | array[name] = [value]; 300 | } 301 | } 302 | 303 | private void setOrIncrease(ref int[string] array, string name, int value) { 304 | if (name in array) { 305 | array[name] += value; 306 | } else { 307 | array[name] = value; 308 | } 309 | } 310 | 311 | unittest { 312 | import std.exception : assertThrown, assertNotThrown; 313 | 314 | assertNotThrown!InvalidArgumentsException( 315 | new Program("test").parseArgsNoRef(["test"]) 316 | ); 317 | } 318 | 319 | // flags 320 | unittest { 321 | import std.exception : assertThrown, assertNotThrown; 322 | 323 | ProgramArgs a; 324 | 325 | a = new Program("test") 326 | .add(new Flag("t", "test", "")) 327 | .parseArgsNoRef(["test"]); 328 | assert(!a.flag("test")); 329 | assert(a.option("test") is null); 330 | assert(a.occurencesOf("test") == 0); 331 | 332 | a = new Program("test") 333 | .add(new Flag("t", "test", "")) 334 | .parseArgsNoRef(["test", "-t"]); 335 | assert(a.flag("test")); 336 | assert(a.option("test") is null); 337 | assert(a.occurencesOf("test") == 1); 338 | 339 | a = new Program("test") 340 | .add(new Flag("t", "test", "")) 341 | .parseArgsNoRef(["test", "--test"]); 342 | assert(a.flag("test")); 343 | assert(a.occurencesOf("test") == 1); 344 | 345 | assertThrown!InvalidArgumentsException( 346 | new Program("test") 347 | .add(new Flag("t", "test", "")) // no repeating 348 | .parseArgsNoRef(["test", "--test", "-t"]) 349 | ); 350 | 351 | assertThrown!InvalidArgumentsException( 352 | new Program("test") 353 | .add(new Flag("t", "test", "")) // no repeating 354 | .parseArgsNoRef(["test", "-tt"]) 355 | ); 356 | 357 | assertThrown!InvalidArgumentsException( 358 | new Program("test") 359 | .add(new Flag("t", "test", "")) // no repeating 360 | .parseArgsNoRef(["test", "--tt"]) 361 | ); 362 | } 363 | 364 | // options 365 | unittest { 366 | import std.exception : assertThrown, assertNotThrown; 367 | import std.range : empty; 368 | 369 | ProgramArgs a; 370 | 371 | a = new Program("test") 372 | .add(new Option("t", "test", "")) 373 | .parseArgsNoRef(["test"]); 374 | assert(a.option("test") is null); 375 | assert(a.occurencesOf("test") == 0); 376 | 377 | a = new Program("test") 378 | .add(new Option("t", "test", "")) 379 | .parseArgsNoRef(["test", "-t", "5"]); 380 | assert(a.option("test") == "5"); 381 | assert(a.occurencesOf("test") == 0); 382 | 383 | a = new Program("test") 384 | .add(new Option("t", "test", "")) 385 | .parseArgsNoRef(["test", "-t=5"]); 386 | assert(a.option("test") == "5"); 387 | assert(a.occurencesOf("test") == 0); 388 | 389 | a = new Program("test") 390 | .add(new Option("t", "test", "")) 391 | .parseArgsNoRef(["test", "--test", "bar"]); 392 | assert(a.option("test") == "bar"); 393 | assert(a.occurencesOf("test") == 0); 394 | 395 | a = new Program("test") 396 | .add(new Option("t", "test", "")) 397 | .parseArgsNoRef(["test", "--test=bar"]); 398 | assert(a.option("test") == "bar"); 399 | assert(a.occurencesOf("test") == 0); 400 | 401 | assertThrown!InvalidArgumentsException( 402 | new Program("test") 403 | .add(new Option("t", "test", "")) 404 | .parseArgsNoRef(["test", "--test=a", "-t", "k"]) 405 | ); 406 | 407 | assertThrown!InvalidArgumentsException( 408 | new Program("test") 409 | .add(new Option("t", "test", "")) // no repeating 410 | .parseArgsNoRef(["test", "--test", "-t"]) 411 | ); 412 | 413 | assertThrown!InvalidArgumentsException( 414 | new Program("test") 415 | .add(new Option("t", "test", "")) // no value 416 | .parseArgsNoRef(["test", "--test"]) 417 | ); 418 | } 419 | 420 | // arguments 421 | unittest { 422 | import std.exception : assertThrown, assertNotThrown; 423 | import std.range : empty; 424 | 425 | ProgramArgs a; 426 | 427 | a = new Program("test") 428 | .add(new Argument("test", "").optional) 429 | .parseArgsNoRef(["test"]); 430 | assert(a.occurencesOf("test") == 0); 431 | 432 | a = new Program("test") 433 | .add(new Argument("test", "")) 434 | .parseArgsNoRef(["test", "t"]); 435 | assert(a.occurencesOf("test") == 0); 436 | assert(a.arg("test") == "t"); 437 | 438 | assertThrown!InvalidArgumentsException( 439 | new Program("test") 440 | .parseArgsNoRef(["test", "test", "t"]) 441 | ); 442 | 443 | assertThrown!InvalidArgumentsException( 444 | new Program("test") 445 | .add(new Argument("test", "")) // no value 446 | .parseArgsNoRef(["test", "test", "test"]) 447 | ); 448 | } 449 | 450 | // required 451 | unittest { 452 | import std.exception : assertThrown, assertNotThrown; 453 | 454 | assertThrown!InvalidArgumentsException( 455 | new Program("test") 456 | .add(new Option("t", "test", "").required) 457 | .parseArgsNoRef(["test"]) 458 | ); 459 | 460 | assertThrown!InvalidArgumentsException( 461 | new Program("test") 462 | .add(new Option("t", "test", "")) 463 | .add(new Argument("path", "").required) 464 | .parseArgsNoRef(["test", "--test", "bar"]) 465 | ); 466 | 467 | assertThrown!InvalidArgumentsException( 468 | new Program("test") 469 | .add(new Argument("test", "").required) // no value 470 | .parseArgsNoRef(["test"]) 471 | ); 472 | } 473 | 474 | // repating 475 | unittest { 476 | ProgramArgs a; 477 | 478 | a = new Program("test") 479 | .add(new Flag("t", "test", "").repeating) 480 | .parseArgsNoRef(["test", "--test", "-t"]); 481 | assert(a.flag("test")); 482 | assert(a.occurencesOf("test") == 2); 483 | 484 | a = new Program("test") 485 | .add(new Option("t", "test", "").repeating) 486 | .parseArgsNoRef(["test", "--test=a", "-t", "k"]); 487 | assert(a.option("test") == "k"); 488 | assert(a.optionAll("test") == ["a", "k"]); 489 | assert(a.occurencesOf("test") == 0); 490 | } 491 | 492 | // default value 493 | unittest { 494 | ProgramArgs a; 495 | 496 | a = new Program("test") 497 | .add(new Option("t", "test", "") 498 | .defaultValue("reee")) 499 | .parseArgsNoRef(["test"]); 500 | assert(a.option("test") == "reee"); 501 | 502 | a = new Program("test") 503 | .add(new Option("t", "test", "") 504 | .defaultValue("reee")) 505 | .parseArgsNoRef(["test", "--test", "aaa"]); 506 | assert(a.option("test") == "aaa"); 507 | 508 | a = new Program("test") 509 | .add(new Argument("test", "") 510 | .optional 511 | .defaultValue("reee")) 512 | .parseArgsNoRef(["test"]); 513 | assert(a.arg("test") == "reee"); 514 | 515 | a = new Program("test") 516 | .add(new Argument("test", "") 517 | .optional 518 | .defaultValue("reee")) 519 | .parseArgsNoRef(["test", "bar"]); 520 | assert(a.args("test") == ["bar"]); 521 | } 522 | 523 | // rest 524 | unittest { 525 | ProgramArgs a; 526 | auto args = ["test", "--", "bar"]; 527 | a = new Program("test") 528 | .add(new Argument("test", "") 529 | .optional 530 | .defaultValue("reee")) 531 | .parseArgs(args); 532 | assert(a.args("test") == ["reee"]); 533 | assert(a.argsRest == ["bar"]); 534 | assert(args == ["bar"]); 535 | } 536 | 537 | // subcommands 538 | unittest { 539 | import std.exception : assertThrown; 540 | 541 | assertThrown!InvalidArgumentsException( 542 | new Program("test") 543 | .add(new Argument("test", "")) 544 | .add(new Command("a")) 545 | .add(new Command("b") 546 | .add(new Command("c"))) 547 | .parseArgsNoRef(["test", "cccc"]) 548 | ); 549 | 550 | assertThrown!InvalidArgumentsException( 551 | new Program("test") 552 | .add(new Argument("test", "")) 553 | .add(new Command("a")) 554 | .add(new Command("b") 555 | .add(new Command("c"))) 556 | .parseArgsNoRef(["test", "cccc", "a", "c"]) 557 | ); 558 | 559 | ProgramArgs a; 560 | a = new Program("test") 561 | .add(new Argument("test", "")) 562 | .add(new Command("a")) 563 | .add(new Command("b") 564 | .add(new Command("c"))) 565 | .parseArgsNoRef(["test", "cccc", "b", "c"]); 566 | assert(a.args("test") == ["cccc"]); 567 | assert(a.command !is null); 568 | assert(a.command.name == "b"); 569 | assert(a.command.command !is null); 570 | assert(a.command.command.name == "c"); 571 | 572 | 573 | auto args = ["test", "cccc", "a", "--", "c"]; 574 | a = new Program("test") 575 | .add(new Argument("test", "")) 576 | .add(new Command("a")) 577 | .add(new Command("b") 578 | .add(new Command("c"))) 579 | .parseArgs(args); 580 | assert(a.args("test") == ["cccc"]); 581 | assert(a.command !is null); 582 | assert(a.command.name == "a"); 583 | assert(a.command.argsRest == ["c"]); 584 | assert(args == ["c"]); 585 | 586 | assertThrown!InvalidArgumentsException( 587 | new Program("test") 588 | .add(new Argument("test", "")) 589 | .add(new Command("a")) 590 | .add(new Command("b") 591 | .add(new Command("c"))) 592 | .parseArgsNoRef(["test", "cccc", "b", "--", "c"]) 593 | ); 594 | } 595 | 596 | // default subcommand 597 | unittest { 598 | ProgramArgs a; 599 | 600 | a = new Program("test") 601 | .add(new Command("a")) 602 | .add(new Command("b") 603 | .add(new Command("c"))) 604 | .defaultCommand("a") 605 | .parseArgsNoRef(["test"]); 606 | 607 | assert(a.command !is null); 608 | assert(a.command.name == "a"); 609 | 610 | 611 | a = new Program("test") 612 | .add(new Command("a")) 613 | .add(new Command("b") 614 | .add(new Command("c")) 615 | .defaultCommand("c")) 616 | .defaultCommand("b") 617 | .parseArgsNoRef(["test"]); 618 | 619 | assert(a.command !is null); 620 | assert(a.command.name == "b"); 621 | assert(a.command.command !is null); 622 | assert(a.command.command.name == "c"); 623 | } 624 | -------------------------------------------------------------------------------- /source/commandr/program.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Program data model 3 | * 4 | * This module along with `commandr.option` contains all types needed to build 5 | * your program model - program options, flags, arguments and all subcommands. 6 | * 7 | * After creating your program model, you can use it to: 8 | * - parse the arguments with `parse` or `parseArgs` 9 | * - print help with `printHelp` or just the usage with `printUsage` 10 | * - create completion script with `createBashCompletionScript` 11 | * 12 | * Examples: 13 | * --- 14 | * auto program = new Program("grit") 15 | * .add(new Flag("v", "verbose", "verbosity")) 16 | * .add(new Command("branch", "branch management") 17 | * .add(new Command("add", "adds branch") 18 | * .add(new Argument("name")) 19 | * ) 20 | * .add(new Command("rm", "removes branch") 21 | * .add(new Argument("name")) 22 | * ) 23 | * ) 24 | * ; 25 | * --- 26 | * 27 | * See_Also: 28 | * `Command`, `Program`, `parse` 29 | */ 30 | module commandr.program; 31 | 32 | import commandr.option; 33 | import commandr.utils; 34 | import std.algorithm : all, reverse, map, filter; 35 | import std.ascii : isAlphaNum; 36 | import std.array : array; 37 | import std.range : empty, chainRanges = chain; 38 | import std.string : format; 39 | 40 | 41 | /** 42 | * Thrown when program definition contains error. 43 | * 44 | * Errors include (but not limited to): duplicate entry name, option with no short and no long value. 45 | */ 46 | public class InvalidProgramException : Exception { 47 | /// Creates new instance of InvalidProgramException 48 | public this(string msg) nothrow pure @safe { 49 | super(msg); 50 | } 51 | } 52 | 53 | /** 54 | * Represents a command. 55 | * 56 | * Commands contain basic information such as name, version summary as well as 57 | * flags, options, arguments and sub-commands. 58 | * 59 | * `Program` is a `Command` as well, thus all methods are available in `Program`. 60 | * 61 | * See_Also: 62 | * `Program` 63 | */ 64 | public class Command { 65 | private string _name; 66 | private string _version; 67 | private string _summary; 68 | private string _topic; 69 | private string _topicStart; 70 | private Object[string] _nameMap; 71 | private Flag[] _flags; 72 | private Option[] _options; 73 | private Argument[] _arguments; 74 | private Command[string] _commands; 75 | private Command _parent; 76 | private string _defaultCommand; 77 | 78 | /** 79 | * Creates new instance of Command. 80 | * 81 | * Params: 82 | * name - command name 83 | * summary - command summary (one-liner) 84 | * version_ - command version 85 | */ 86 | public this(string name, string summary = null, string version_ = "1.0") pure @safe { 87 | this._name = name; 88 | this._summary = summary; 89 | this._version = version_; 90 | this.add(new Flag("h", "help", "prints help")); 91 | } 92 | 93 | /** 94 | * Sets command name 95 | * 96 | * Params: 97 | * name - unique name 98 | */ 99 | public typeof(this) name(string name) nothrow pure @nogc @safe { 100 | this._name = name; 101 | return this; 102 | } 103 | 104 | /** 105 | * Program name 106 | */ 107 | public string name() nothrow pure @nogc @safe { 108 | return this._name; 109 | } 110 | 111 | /** 112 | * Sets command version 113 | */ 114 | public typeof(this) version_(string version_) nothrow pure @nogc @safe { 115 | this._version = version_; 116 | return this; 117 | } 118 | 119 | /** 120 | * Program version 121 | */ 122 | public string version_() nothrow pure @nogc @safe { 123 | return this._version; 124 | } 125 | 126 | /** 127 | * Sets program summary (one-liner) 128 | */ 129 | public typeof(this) summary(string summary) nothrow pure @nogc @safe { 130 | this._summary = summary; 131 | return this; 132 | } 133 | 134 | /** 135 | * Program summary 136 | */ 137 | public string summary() nothrow pure @nogc @safe { 138 | return this._summary; 139 | } 140 | 141 | 142 | /** 143 | * Adds option 144 | * 145 | * Throws: 146 | * `InvalidProgramException` 147 | */ 148 | public typeof(this) add(Option option) pure @safe { 149 | validateName(option.name); 150 | validateOption(option); 151 | 152 | if (option.isRequired && option.defaultValue) { 153 | throw new InvalidProgramException("cannot have required option with default value"); 154 | } 155 | 156 | _options ~= option; 157 | _nameMap[option.name] = option; 158 | return this; 159 | } 160 | 161 | /** 162 | * Command options 163 | */ 164 | public Option[] options() nothrow pure @nogc @safe { 165 | return this._options; 166 | } 167 | 168 | /** 169 | * Adds command flag 170 | * 171 | * Throws: 172 | * `InvalidProgramException` 173 | */ 174 | public typeof(this) add(Flag flag) pure @safe { 175 | validateName(flag.name); 176 | validateOption(flag); 177 | 178 | if (flag.defaultValue) { 179 | throw new InvalidProgramException("flag %s cannot have default value".format(flag.name)); 180 | } 181 | 182 | if (flag.isRequired) { 183 | throw new InvalidProgramException("flag %s cannot be required".format(flag.name)); 184 | } 185 | 186 | if (flag.validators) { 187 | throw new InvalidProgramException("flag %s cannot have validators".format(flag.name)); 188 | } 189 | 190 | _flags ~= flag; 191 | _nameMap[flag.name] = flag; 192 | return this; 193 | } 194 | 195 | /** 196 | * Command flags 197 | */ 198 | public Flag[] flags() nothrow pure @nogc @safe { 199 | return this._flags; 200 | } 201 | 202 | /** 203 | * Adds command argument 204 | * 205 | * Throws: 206 | * `InvalidProgramException` 207 | */ 208 | public typeof(this) add(Argument argument) pure @safe { 209 | validateName(argument.name); 210 | 211 | if (_arguments.length && _arguments[$-1].isRepeating) { 212 | throw new InvalidProgramException("cannot add arguments past repeating"); 213 | } 214 | 215 | if (argument.isRequired && _arguments.length > 0 && !_arguments[$-1].isRequired) { 216 | throw new InvalidProgramException("cannot add required argument past optional one"); 217 | } 218 | 219 | if (argument.isRequired && argument.defaultValue) { 220 | throw new InvalidProgramException("cannot have required argument with default value"); 221 | } 222 | 223 | this._arguments ~= argument; 224 | _nameMap[argument.name] = argument; 225 | return this; 226 | } 227 | 228 | /** 229 | * Command arguments 230 | */ 231 | public Argument[] arguments() nothrow pure @nogc @safe { 232 | return this._arguments; 233 | } 234 | 235 | /** 236 | * Registers subcommand 237 | * 238 | * Throws: 239 | * `InvalidProgramException` 240 | */ 241 | public typeof(this) add(Command command) pure @safe { 242 | if (command.name in this._commands) { 243 | throw new InvalidProgramException("duplicate command %s".format(command.name)); 244 | } 245 | 246 | // this is also checked by adding argument, but we want better error message 247 | if (!_arguments.empty && _arguments[$-1].isRepeating) { 248 | throw new InvalidProgramException("cannot have sub-commands and repeating argument"); 249 | } 250 | 251 | if (!_arguments.empty && !_arguments[$-1].isRequired) { 252 | throw new InvalidProgramException("cannot have sub-commands and non-required argument"); 253 | } 254 | 255 | // TODO: may be update only if command do not have topic yet, 256 | // or only when _topicStart is set. 257 | // Because otherwise, topic set on command is overwritten here. 258 | command._topic = this._topicStart; 259 | command._parent = this; 260 | _commands[command.name] = command; 261 | 262 | return this; 263 | } 264 | 265 | /** 266 | * Command sub-commands 267 | */ 268 | public Command[string] commands() nothrow pure @nogc @safe { 269 | return this._commands; 270 | } 271 | 272 | /** 273 | * Sets default command. 274 | */ 275 | public typeof(this) defaultCommand(string name) pure @safe { 276 | if (name !is null) { 277 | if ((name in _commands) is null) { 278 | throw new InvalidProgramException("setting default command to non-existing one"); 279 | } 280 | } 281 | this._defaultCommand = name; 282 | 283 | return this; 284 | } 285 | 286 | /** 287 | * Gets default command 288 | */ 289 | public string defaultCommand() nothrow pure @safe @nogc { 290 | return this._defaultCommand; 291 | } 292 | 293 | public typeof(this) topicGroup(string topic) pure @safe { 294 | this._topicStart = topic; 295 | return this; 296 | } 297 | 298 | public typeof(this) topic(string topic) nothrow pure @safe @nogc { 299 | this._topic = topic; 300 | return this; 301 | } 302 | 303 | public string topic() nothrow pure @safe @nogc { 304 | return _topic; 305 | } 306 | 307 | /** 308 | * Gets command chain. 309 | * 310 | * Chain is a array of strings which contains all parent command names. 311 | * For a deeply nested sub command like `git branch add`, `add` sub-command 312 | * chain would return `["git", "branch", "add"]`. 313 | */ 314 | public string[] chain() pure nothrow @safe { 315 | string[] chain = [this.name]; 316 | Command curr = this._parent; 317 | while (curr !is null) { 318 | chain ~= curr.name; 319 | curr = curr._parent; 320 | } 321 | 322 | chain.reverse(); 323 | return chain; 324 | } 325 | 326 | public Command parent() nothrow pure @safe @nogc { 327 | return _parent; 328 | } 329 | 330 | public string[] fullNames() nothrow pure @safe { 331 | return chainRanges( 332 | _flags.map!(f => f.full), 333 | _options.map!(o => o.full) 334 | ).filter!`a && a.length`.array; 335 | } 336 | 337 | public string[] abbrevations() nothrow pure @safe { 338 | return chainRanges( 339 | _flags.map!(f => f.abbrev), 340 | _options.map!(o => o.abbrev) 341 | ).filter!`a && a.length`.array; 342 | } 343 | 344 | private void addBasicOptions() { 345 | this.add(new Flag(null, "version", "prints version")); 346 | } 347 | 348 | private void validateName(string name) pure @safe { 349 | if (!name) { 350 | throw new InvalidProgramException("name cannot be empty"); 351 | } 352 | 353 | if (name[0] == '-') 354 | throw new InvalidProgramException("invalid name '%s' -- cannot begin with '-'".format(name)); 355 | 356 | if (!name.all!(c => isAlphaNum(c) || c == '_' || c == '-')()) { 357 | throw new InvalidProgramException("invalid name '%s' passed".format(name)); 358 | } 359 | 360 | auto entryPtr = name in _nameMap; 361 | if (entryPtr !is null) { 362 | throw new InvalidProgramException( 363 | "duplicate name %s which is already used".format(name) 364 | ); 365 | } 366 | } 367 | 368 | private void validateOption(IOption option) pure @safe { 369 | if (!option.abbrev && !option.full) { 370 | throw new InvalidProgramException( 371 | "option/flag %s must have either long or short form".format(option.name) 372 | ); 373 | } 374 | 375 | if (option.abbrev) { 376 | auto flag = this.getFlagByShort(option.abbrev); 377 | if (!flag.isNull) { 378 | throw new InvalidProgramException( 379 | "duplicate abbrevation -%s, flag %s already uses it".format(option.abbrev, flag.get().name) 380 | ); 381 | } 382 | 383 | auto other = this.getOptionByShort(option.abbrev); 384 | if (!other.isNull) { 385 | throw new InvalidProgramException( 386 | "duplicate abbrevation -%s, option %s already uses it".format(option.abbrev, other.get().name) 387 | ); 388 | } 389 | } 390 | 391 | if (option.full) { 392 | auto flag = this.getFlagByFull(option.full); 393 | if (!flag.isNull) { 394 | throw new InvalidProgramException( 395 | "duplicate -%s, flag %s with this already exists".format(option.full, flag.get().name) 396 | ); 397 | } 398 | 399 | auto other = this.getOptionByFull(option.full); 400 | if (!other.isNull) { 401 | throw new InvalidProgramException( 402 | "duplicate --%s, option %s with this already exists".format(option.full, other.get().name) 403 | ); 404 | } 405 | } 406 | 407 | if (option.isRequired && option.defaultValue) { 408 | throw new InvalidProgramException("cannot have required option with default value"); 409 | } 410 | } 411 | } 412 | 413 | /** 414 | * Represents program. 415 | * 416 | * This is the entry-point for building your program model. 417 | */ 418 | public class Program: Command { 419 | private string _binaryName; 420 | private string[] _authors; 421 | 422 | /** 423 | * Creates new instance of `Program`. 424 | * 425 | * Params: 426 | * name - Program name 427 | * version_ - Program version 428 | */ 429 | public this(string name, string version_ = "1.0") { 430 | super(name, null, version_); 431 | this.addBasicOptions(); 432 | } 433 | 434 | /** 435 | * Sets program name 436 | */ 437 | public override typeof(this) name(string name) nothrow pure @nogc @safe { 438 | return cast(Program)super.name(name); 439 | } 440 | 441 | /** 442 | * Program name 443 | */ 444 | public override string name() const nothrow pure @nogc @safe { 445 | return this._name; 446 | } 447 | 448 | /** 449 | * Sets program version 450 | */ 451 | public override typeof(this) version_(string version_) nothrow pure @nogc @safe { 452 | return cast(Program)super.version_(version_); 453 | } 454 | 455 | /** 456 | * Program version 457 | */ 458 | public override string version_() const nothrow pure @nogc @safe { 459 | return this._version; 460 | } 461 | 462 | /** 463 | * Sets program summary (one-liner) 464 | */ 465 | public override typeof(this) summary(string summary) nothrow pure @nogc @safe { 466 | return cast(Program)super.summary(summary); 467 | } 468 | 469 | /** 470 | * Program summary (one-liner) 471 | */ 472 | public override string summary() nothrow pure @nogc @safe { 473 | return this._summary; 474 | } 475 | 476 | /// Proxy call to `Command.add` returning `Program`. 477 | public typeof(this) add(T: IEntry)(T data) pure @safe { 478 | super.add(data); 479 | return this; 480 | } 481 | 482 | public override typeof(this) add(Command command) pure @safe { 483 | super.add(command); 484 | return this; 485 | } 486 | 487 | public override typeof(this) defaultCommand(string name) pure @safe { 488 | super.defaultCommand(name); 489 | return this; 490 | } 491 | 492 | public override string defaultCommand() nothrow pure @safe @nogc { 493 | return this._defaultCommand; 494 | } 495 | 496 | /** 497 | * Sets program binary name 498 | */ 499 | public typeof(this) binaryName(string binaryName) nothrow pure @nogc @safe { 500 | this._binaryName = binaryName; 501 | return this; 502 | } 503 | 504 | /** 505 | * Program binary name 506 | */ 507 | public string binaryName() const nothrow pure @nogc @safe { 508 | return (this._binaryName !is null) ? this._binaryName : this._name; 509 | } 510 | 511 | /** 512 | * Adds program author 513 | */ 514 | public typeof(this) author(string author) nothrow pure @safe { 515 | this._authors ~= author; 516 | return this; 517 | } 518 | 519 | /** 520 | * Sets program authors 521 | */ 522 | public typeof(this) authors(string[] authors) nothrow pure @nogc @safe { 523 | this._authors = authors; 524 | return this; 525 | } 526 | 527 | /** 528 | * Program authors 529 | */ 530 | public string[] authors() nothrow pure @nogc @safe { 531 | return this._authors; 532 | } 533 | 534 | /** 535 | * Sets topic group for the following commands. 536 | */ 537 | public override typeof(this) topicGroup(string topic) pure @safe { 538 | super.topicGroup(topic); 539 | return this; 540 | } 541 | 542 | /** 543 | * Sets topic group for this command. 544 | */ 545 | public override typeof(this) topic(string topic) nothrow pure @safe @nogc { 546 | super.topic(topic); 547 | return this; 548 | } 549 | 550 | /** 551 | * Topic group for this command. 552 | */ 553 | public override string topic() nothrow pure @safe @nogc { 554 | return _topic; 555 | } 556 | } 557 | 558 | unittest { 559 | import std.range : empty; 560 | 561 | auto program = new Program("test"); 562 | assert(program.name == "test"); 563 | assert(program.binaryName == "test"); 564 | assert(program.version_ == "1.0"); 565 | assert(program.summary is null); 566 | assert(program.authors.empty); 567 | assert(program.flags.length == 2); 568 | assert(program.flags[0].name == "help"); 569 | assert(program.flags[0].abbrev == "h"); 570 | assert(program.flags[0].full == "help"); 571 | assert(program.flags[1].name == "version"); 572 | assert(program.flags[1].abbrev is null); 573 | assert(program.flags[1].full == "version"); 574 | assert(program.options.empty); 575 | assert(program.arguments.empty); 576 | } 577 | 578 | unittest { 579 | auto program = new Program("test").name("bar"); 580 | assert(program.name == "bar"); 581 | assert(program.binaryName == "bar"); 582 | } 583 | 584 | unittest { 585 | auto program = new Program("test", "0.1"); 586 | assert(program.version_ == "0.1"); 587 | } 588 | 589 | unittest { 590 | auto program = new Program("test", "0.1").version_("2.0").version_("kappa"); 591 | assert(program.version_ == "kappa"); 592 | } 593 | 594 | unittest { 595 | auto program = new Program("test").binaryName("kappa"); 596 | assert(program.name == "test"); 597 | assert(program.binaryName == "kappa"); 598 | } 599 | 600 | // name conflicts 601 | unittest { 602 | import std.exception : assertThrown; 603 | 604 | // FLAGS 605 | // flag-flag 606 | assertThrown!InvalidProgramException( 607 | new Program("test") 608 | .add(new Flag("a", "aaa", "desc").name("nnn")) 609 | .add(new Flag("b", "bbb", "desc").name("nnn")) 610 | ); 611 | 612 | // flag-option 613 | assertThrown!InvalidProgramException( 614 | new Program("test") 615 | .add(new Flag("a", "aaa", "desc").name("nnn")) 616 | .add(new Option("b", "bbb", "desc").name("nnn")) 617 | ); 618 | 619 | // flag-argument 620 | assertThrown!InvalidProgramException( 621 | new Program("test") 622 | .add(new Flag("a", "aaa", "desc").name("nnn")) 623 | .add(new Argument("nnn")) 624 | ); 625 | 626 | 627 | // OPTIONS 628 | // option-flag 629 | assertThrown!InvalidProgramException( 630 | new Program("test") 631 | .add(new Option("a", "aaa", "desc").name("nnn")) 632 | .add(new Flag("b", "bbb", "desc").name("nnn")) 633 | ); 634 | 635 | // option-option 636 | assertThrown!InvalidProgramException( 637 | new Program("test") 638 | .add(new Option("a", "aaa", "desc").name("nnn")) 639 | .add(new Option("b", "bbb", "desc").name("nnn")) 640 | ); 641 | 642 | // option-argument 643 | assertThrown!InvalidProgramException( 644 | new Program("test") 645 | .add(new Option("a", "aaa", "desc").name("nnn")) 646 | .add(new Argument("nnn")) 647 | ); 648 | 649 | 650 | // ARGUMENTS 651 | // argument-flag 652 | assertThrown!InvalidProgramException( 653 | new Program("test") 654 | .add(new Argument("nnn")) 655 | .add(new Flag("b", "bbb", "desc").name("nnn")) 656 | ); 657 | 658 | // argument-option 659 | assertThrown!InvalidProgramException( 660 | new Program("test") 661 | .add(new Argument("nnn")) 662 | .add(new Option("b", "bbb", "desc").name("nnn")) 663 | ); 664 | 665 | // argument-argument 666 | assertThrown!InvalidProgramException( 667 | new Program("test") 668 | .add(new Argument("nnn")) 669 | .add(new Argument("nnn")) 670 | ); 671 | } 672 | 673 | // abbrev conflicts 674 | unittest { 675 | import std.exception : assertThrown; 676 | 677 | // FLAGS 678 | // flag-flag 679 | assertThrown!InvalidProgramException( 680 | new Program("test") 681 | .add(new Flag("a", "aaa", "desc")) 682 | .add(new Flag("a", "bbb", "desc")) 683 | ); 684 | 685 | // flag-option 686 | assertThrown!InvalidProgramException( 687 | new Program("test") 688 | .add(new Flag("a", "aaa", "desc")) 689 | .add(new Option("a", "bbb", "desc")) 690 | ); 691 | 692 | // FLAGS 693 | // option-flag 694 | assertThrown!InvalidProgramException( 695 | new Program("test") 696 | .add(new Option("a", "aaa", "desc")) 697 | .add(new Flag("a", "bbb", "desc")) 698 | ); 699 | 700 | // option-option 701 | assertThrown!InvalidProgramException( 702 | new Program("test") 703 | .add(new Option("a", "aaa", "desc")) 704 | .add(new Option("a", "bbb", "desc")) 705 | ); 706 | } 707 | 708 | // full name conflicts 709 | unittest { 710 | import std.exception : assertThrown; 711 | 712 | // FLAGS 713 | // flag-flag 714 | assertThrown!InvalidProgramException( 715 | new Program("test") 716 | .add(new Flag("a", "aaa", "desc")) 717 | .add(new Flag("b", "aaa", "desc")) 718 | ); 719 | 720 | // flag-option 721 | assertThrown!InvalidProgramException( 722 | new Program("test") 723 | .add(new Flag("a", "aaa", "desc")) 724 | .add(new Option("b", "aaa", "desc")) 725 | ); 726 | 727 | // FLAGS 728 | // option-flag 729 | assertThrown!InvalidProgramException( 730 | new Program("test") 731 | .add(new Option("a", "aaa", "desc")) 732 | .add(new Flag("b", "aaa", "desc")) 733 | ); 734 | 735 | // option-option 736 | assertThrown!InvalidProgramException( 737 | new Program("test") 738 | .add(new Option("a", "aaa", "desc")) 739 | .add(new Option("b", "aaa", "desc")) 740 | ); 741 | } 742 | 743 | // repeating 744 | unittest { 745 | import std.exception : assertThrown; 746 | 747 | assertThrown!InvalidProgramException( 748 | new Program("test") 749 | .add(new Argument("file", "path").repeating) 750 | .add(new Argument("dir", "desc")) 751 | ); 752 | } 753 | 754 | // invalid option 755 | unittest { 756 | import std.exception : assertThrown; 757 | 758 | assertThrown!InvalidProgramException( 759 | new Program("test") 760 | .add(new Flag(null, null, "")) 761 | ); 762 | 763 | assertThrown!InvalidProgramException( 764 | new Program("test") 765 | .add(new Option(null, null, "")) 766 | ); 767 | } 768 | 769 | // required args out of order 770 | unittest { 771 | import std.exception : assertThrown; 772 | 773 | assertThrown!InvalidProgramException( 774 | new Program("test") 775 | .add(new Argument("file", "path").optional) 776 | .add(new Argument("dir", "desc")) 777 | ); 778 | } 779 | 780 | // default required 781 | unittest { 782 | import std.exception : assertThrown; 783 | 784 | assertThrown!InvalidProgramException( 785 | new Program("test") 786 | .add(new Option("d", "dir", "desc").defaultValue("test").required) 787 | ); 788 | 789 | assertThrown!InvalidProgramException( 790 | new Program("test") 791 | .add(new Argument("dir", "desc").defaultValue("test").required) 792 | ); 793 | } 794 | 795 | // flags 796 | unittest { 797 | import std.exception : assertThrown; 798 | import commandr.validators; 799 | 800 | assertThrown!InvalidProgramException( 801 | new Program("test") 802 | .add(new Flag("a", "bb", "desc") 803 | .acceptsValues(["a"])) 804 | ); 805 | } 806 | 807 | // subcommands 808 | unittest { 809 | import std.exception : assertThrown; 810 | 811 | assertThrown!InvalidProgramException( 812 | new Program("test") 813 | .add(new Argument("test", "").defaultValue("test")) 814 | .add(new Command("a")) 815 | .add(new Command("b")) 816 | ); 817 | } 818 | 819 | // default command 820 | unittest { 821 | import std.exception : assertThrown, assertNotThrown; 822 | import commandr.validators; 823 | 824 | assertThrown!InvalidProgramException( 825 | new Program("test") 826 | .defaultCommand("a") 827 | .add(new Command("a", "desc")) 828 | ); 829 | 830 | assertThrown!InvalidProgramException( 831 | new Program("test") 832 | .add(new Command("a", "desc")) 833 | .defaultCommand("b") 834 | ); 835 | 836 | assertNotThrown!InvalidProgramException( 837 | new Program("test") 838 | .add(new Command("a", "desc")) 839 | .defaultCommand(null) 840 | ); 841 | } 842 | 843 | // topics 844 | unittest { 845 | import std.exception : assertThrown, assertNotThrown; 846 | import commandr.validators; 847 | 848 | auto p = new Program("test") 849 | .add(new Command("a", "desc")) 850 | .topic("z") 851 | .topicGroup("general purpose") 852 | .add(new Command("b", "desc")) 853 | .add(new Command("c", "desc")) 854 | .topicGroup("other") 855 | .add(new Command("d", "desc")) 856 | ; 857 | 858 | assert(p.topic == "z"); 859 | assert(p.commands["b"].topic == "general purpose"); 860 | assert(p.commands["c"].topic == "general purpose"); 861 | assert(p.commands["d"].topic == "other"); 862 | } 863 | -------------------------------------------------------------------------------- /source/commandr/utils.d: -------------------------------------------------------------------------------- 1 | module commandr.utils; 2 | 3 | import commandr; 4 | import std.array : array; 5 | import std.algorithm : find, map, levenshteinDistance; 6 | import std.typecons : Tuple, Nullable; 7 | import std.range : isInputRange, ElementType; 8 | 9 | 10 | // helpers 11 | 12 | private Nullable!T wrapIntoNullable(T)(T[] data) pure nothrow @safe @nogc { 13 | Nullable!T result; 14 | if (data.length > 0) { 15 | result = data[0]; 16 | } 17 | return result; 18 | } 19 | 20 | unittest { 21 | assert(wrapIntoNullable(cast(string[])[]).isNull); 22 | 23 | auto wrapped = wrapIntoNullable(["test"]); 24 | assert(!wrapped.isNull); 25 | assert(wrapped.get() == "test"); 26 | 27 | wrapped = wrapIntoNullable(["test", "bar"]); 28 | assert(!wrapped.isNull); 29 | assert(wrapped.get() == "test"); 30 | } 31 | 32 | Nullable!Option getOptionByFull(T)(T aggregate, string name, bool useDefault = false) nothrow pure @safe { 33 | auto ret = aggregate.options.find!(o => o.full == name).wrapIntoNullable; 34 | if (ret.isNull && aggregate.defaultCommand !is null && useDefault) 35 | ret = aggregate.commands[aggregate.defaultCommand].getOptionByFull(name, useDefault); 36 | return ret; 37 | } 38 | 39 | Nullable!Flag getFlagByFull(T)(T aggregate, string name, bool useDefault = false) nothrow pure @safe { 40 | auto ret = aggregate.flags.find!(o => o.full == name).wrapIntoNullable; 41 | if (ret.isNull && aggregate.defaultCommand !is null && useDefault) 42 | ret = aggregate.commands[aggregate.defaultCommand].getFlagByFull(name, useDefault); 43 | return ret; 44 | } 45 | 46 | Nullable!Option getOptionByShort(T)(T aggregate, string name, bool useDefault = false) nothrow pure @safe { 47 | auto ret = aggregate.options.find!(o => o.abbrev == name).wrapIntoNullable; 48 | if (ret.isNull && aggregate.defaultCommand !is null && useDefault) 49 | ret = aggregate.commands[aggregate.defaultCommand].getOptionByShort(name, useDefault); 50 | return ret; 51 | } 52 | 53 | Nullable!Flag getFlagByShort(T)(T aggregate, string name, bool useDefault = false) nothrow pure @safe { 54 | auto ret = aggregate.flags.find!(o => o.abbrev == name).wrapIntoNullable; 55 | if (ret.isNull && aggregate.defaultCommand !is null && useDefault) 56 | ret = aggregate.commands[aggregate.defaultCommand].getFlagByShort(name, useDefault); 57 | return ret; 58 | } 59 | 60 | string getEntryKindName(IEntry entry) nothrow pure @safe { 61 | if (cast(Option)entry) { 62 | return "option"; 63 | } 64 | 65 | else if (cast(Flag)entry) { 66 | return "flag"; 67 | } 68 | 69 | else if (cast(Argument)entry) { 70 | return "argument"; 71 | } 72 | else { 73 | return null; 74 | } 75 | } 76 | 77 | string matchingCandidate(string[] values, string current) @safe { 78 | auto distances = values.map!(v => levenshteinDistance(v, current)); 79 | 80 | immutable long index = distances.minIndex; 81 | if (index < 0) { 82 | return null; 83 | } 84 | 85 | return values[index]; 86 | } 87 | 88 | unittest { 89 | assert (matchingCandidate(["test", "bar"], "tst") == "test"); 90 | assert (matchingCandidate(["test", "bar"], "barr") == "bar"); 91 | assert (matchingCandidate([], "barr") == null); 92 | } 93 | 94 | 95 | // minIndex is not in GDC (ugh) 96 | ptrdiff_t minIndex(T)(T range) if(isInputRange!T) { 97 | ptrdiff_t index, minIndex; 98 | ElementType!T min = ElementType!T.max; 99 | 100 | foreach(el; range) { 101 | if (el < min) { 102 | min = el; 103 | minIndex = index; 104 | } 105 | index += 1; 106 | } 107 | 108 | if (min == ElementType!T.max) { 109 | return -1; 110 | } 111 | 112 | return minIndex; 113 | } 114 | 115 | unittest { 116 | assert([1, 0].minIndex == 1); 117 | assert([0, 1, 2].minIndex == 0); 118 | assert([2, 1, 2].minIndex == 1); 119 | assert([2, 1, 0].minIndex == 2); 120 | assert([1, 1, 0].minIndex == 2); 121 | assert([0, 1, 0].minIndex == 0); 122 | assert((cast(int[])[]).minIndex == -1); 123 | } 124 | -------------------------------------------------------------------------------- /source/commandr/validators.d: -------------------------------------------------------------------------------- 1 | /** 2 | * User input validation. 3 | * 4 | * This module contains core functionality for veryfing values as well as some 5 | * basic validators. 6 | * 7 | * Options and arguments (no flags, as they cannot take value) can have any number of validators attached. 8 | * Validators are objects that verify user input after parsing values - every validator is run once 9 | * per option/argument that it is attached to with complete vector of user input (e.g. for repeating values). 10 | * 11 | * Validators are ran in order they are defined aborting on first failure with `ValidationException`. 12 | * Exception contains the validator that caused the exception (optionally) along with error message. 13 | * 14 | * Validators can be attached using `validate` method, or using helper methods in form of `accepts*`: 15 | * 16 | * --- 17 | * new Program("test") 18 | * .add(new Option("t", "test", "description") 19 | * .acceptsValues(["test", "bar"]) 20 | * ) 21 | * // both are equivalent 22 | * .add(new Option("T", "test2", "description") 23 | * .validate(new EnumValidator(["test", "bar"])) 24 | * ) 25 | * --- 26 | * 27 | * ## Custom Validators 28 | * 29 | * To add custom validating logic, you can either create custom Validator class that implements `IValidator` interface 30 | * or use `DelegateValidator` passing delegate with your validating logic: 31 | * 32 | * --- 33 | * new Program("test") 34 | * .add(new Option("t", "test", "description") 35 | * .validateWith((entry, value) { 36 | * if (value == "test") throw new ValidationException("Value must be test"); 37 | * }) 38 | * .validateWith(arg => isNumericString(arg), "must be numeric") 39 | * ) 40 | * --- 41 | * 42 | * Validators already provided: 43 | * - `EnumValidator` (`acceptsValues`) 44 | * - `FileSystemValidator` (`acceptsFiles`, `acceptsDirectories`) 45 | * - `DelegateValidator` (`validateWith`, `validateEachWith`) 46 | * 47 | * See_Also: 48 | * IValidator, ValidationException 49 | */ 50 | module commandr.validators; 51 | 52 | import commandr.option : IEntry, InvalidArgumentsException; 53 | import commandr.utils : getEntryKindName, matchingCandidate; 54 | import std.algorithm : canFind, any, each; 55 | import std.array : join; 56 | import std.string : format; 57 | import std.file : exists, isDir, isFile; 58 | import std.typecons : Nullable; 59 | 60 | 61 | /** 62 | * Validation error. 63 | * 64 | * This exception is thrown when an invalid value has been passed to an option/value 65 | * that has validators assigned (manually or through accepts* functions). 66 | * 67 | * Exception is thrown on first validator failure. Validators are run in definition order. 68 | * 69 | * Because this exception extends `InvalidArgumentsException`, there's no need to 70 | * catch it explicitly unless needed. 71 | */ 72 | public class ValidationException: InvalidArgumentsException { 73 | /** 74 | * Validator that caused the error. 75 | */ 76 | IValidator validator; 77 | 78 | /// Creates new instance of ValidationException 79 | public this(IValidator validator, string msg) nothrow pure @safe @nogc { 80 | super(msg); 81 | this.validator = validator; 82 | } 83 | } 84 | 85 | /** 86 | * Interface for validators. 87 | */ 88 | public interface IValidator { 89 | /** 90 | * Checks whenever specified input is valid. 91 | * 92 | * Params: 93 | * entry - Information about checked entry. 94 | * values - Array of values to validate. 95 | * 96 | * Throws: 97 | * ValidationException 98 | */ 99 | void validate(IEntry entry, string[] values); 100 | } 101 | 102 | 103 | /** 104 | * Input whitelist check. 105 | * 106 | * Validates whenever input is contained in list of valid/accepted values. 107 | * 108 | * Examples: 109 | * --- 110 | * new Program("test") 111 | * .add(new Option("s", "scope", "working scope") 112 | * .acceptsValues(["user", "system"]) 113 | * ) 114 | * --- 115 | * 116 | * See_Also: 117 | * acceptsValues 118 | */ 119 | public class EnumValidator: IValidator { 120 | // TODO: Throw InvalidValidatorException: InvalidProgramException on empty matches? 121 | /** 122 | * List of allowed values. 123 | */ 124 | string[] allowedValues; 125 | 126 | /// Creates new instance of EnumValidator 127 | public this(string[] values) nothrow pure @safe @nogc { 128 | this.allowedValues = values; 129 | } 130 | 131 | /// Validates input 132 | public void validate(IEntry entry, string[] args) @safe { 133 | foreach(arg; args) { 134 | if (!allowedValues.canFind(arg)) { 135 | string suggestion = allowedValues.matchingCandidate(arg); 136 | if (suggestion) { 137 | suggestion = " (did you mean %s?)".format(suggestion); 138 | } else { 139 | suggestion = ""; 140 | } 141 | 142 | throw new ValidationException(this, 143 | "%s %s must be one of following values: %s%s".format( 144 | entry.getEntryKindName(), entry.name, allowedValues.join(", "), suggestion 145 | ) 146 | ); 147 | } 148 | } 149 | } 150 | } 151 | 152 | /** 153 | * Helper function to define allowed values for an option or argument. 154 | * 155 | * This function is meant to be used with UFCS, so that it can be placed 156 | * within option definition chain. 157 | * 158 | * Params: 159 | * entry - entry to define allowed values to 160 | * values - list of allowed values 161 | * 162 | * Examples: 163 | * --- 164 | * new Program("test") 165 | * .add(new Option("s", "scope", "working scope") 166 | * .acceptsValues(["user", "system"]) 167 | * ) 168 | * --- 169 | * 170 | * See_Also: 171 | * EnumValidator 172 | */ 173 | public T acceptsValues(T : IEntry)(T entry, string[] values) @safe { 174 | return entry.validate(new EnumValidator(values)); 175 | } 176 | 177 | 178 | /** 179 | * Specified expected entry type for `FileSystemValidator`. 180 | */ 181 | public enum FileType { 182 | /// 183 | Directory, 184 | 185 | /// 186 | File 187 | } 188 | 189 | 190 | /** 191 | * FileSystem validator. 192 | * 193 | * See_Also: 194 | * acceptsFiles, acceptsDirectories, acceptsPath 195 | */ 196 | public class FileSystemValidator: IValidator { 197 | /// Exists contraint 198 | Nullable!bool exists; 199 | 200 | /// Entry type contraint 201 | Nullable!FileType type; 202 | 203 | /** 204 | * Creates new FileSystem validator. 205 | * 206 | * This constructor creates a `FileSystemValidator` that checks only 207 | * whenever the path points to a existing (or not) item. 208 | * 209 | * Params: 210 | * exists - Whenever passed path should exist. 211 | */ 212 | public this(bool exists) nothrow pure @safe @nogc { 213 | this.exists = exists; 214 | } 215 | 216 | /** 217 | * Creates new FileSystem validator. 218 | * 219 | * This constructor creates a `FileSystemValidator` that checks 220 | * whenever the path points to a existing item of specified type. 221 | * 222 | * Params: 223 | * type - Expected item type 224 | */ 225 | public this(FileType type) { 226 | this.exists = true; 227 | this.type = type; 228 | } 229 | 230 | /// Validates input 231 | public void validate(IEntry entry, string[] args) { 232 | foreach (arg; args) { 233 | if (!this.exists.isNull) { 234 | validateExists(entry, arg, this.exists.get()); 235 | } 236 | 237 | if (!this.type.isNull) { 238 | validateType(entry, arg, this.type.get()); 239 | } 240 | } 241 | } 242 | 243 | private void validateExists(IEntry entry, string arg, bool exists) { 244 | if (arg.exists() == exists) { 245 | return; 246 | } 247 | 248 | throw new ValidationException(this, 249 | "%s %s value must point to a %s that %sexists".format( 250 | entry.getEntryKindName(), 251 | entry.name, 252 | this.type.isNull 253 | ? "file/directory" 254 | : this.type.get() == FileType.Directory ? "directory" : "file", 255 | exists ? "" : "not " 256 | ) 257 | ); 258 | } 259 | 260 | private void validateType(IEntry entry, string arg, FileType type) { 261 | switch (type) { 262 | case FileType.File: 263 | if (!arg.isFile) { 264 | throw new ValidationException(this, 265 | "value specified in %s %s must be a valid file".format( 266 | entry.getEntryKindName(), entry.name, 267 | ) 268 | ); 269 | } 270 | break; 271 | 272 | case FileType.Directory: 273 | if (!arg.isDir) { 274 | throw new ValidationException(this, 275 | "value specified in %s %s must be a valid file".format( 276 | entry.getEntryKindName(), entry.name, 277 | ) 278 | ); 279 | } 280 | break; 281 | 282 | default: 283 | assert(0); 284 | } 285 | } 286 | } 287 | 288 | /** 289 | * Helper function to require passing a path pointing to existing file. 290 | * 291 | * This function is meant to be used with UFCS, so that it can be placed 292 | * within option definition chain. 293 | * 294 | * Examples: 295 | * --- 296 | * new Program("test") 297 | * .add(new Option("c", "config", "path to config file") 298 | * .accpetsFiles() 299 | * ) 300 | * --- 301 | * 302 | * See_Also: 303 | * FileSystemValidator, acceptsDirectories, acceptsPaths 304 | */ 305 | public T acceptsFiles(T: IEntry)(T entry) { 306 | return entry.validate(new FileSystemValidator(FileType.File)); 307 | } 308 | 309 | 310 | /** 311 | * Helper function to require passing a path pointing to existing directory. 312 | * 313 | * This function is meant to be used with UFCS, so that it can be placed 314 | * within option definition chain. 315 | * 316 | * Examples: 317 | * --- 318 | * new Program("ls") 319 | * .add(new Argument("directory", "directory to list") 320 | * .acceptsDirectories() 321 | * ) 322 | * --- 323 | * 324 | * See_Also: 325 | * FileSystemValidator, acceptsFiles, acceptsPaths 326 | */ 327 | public T acceptsDirectories(T: IEntry)(T entry) { 328 | return entry.validate(new FileSystemValidator(FileType.Directory)); 329 | } 330 | 331 | 332 | /** 333 | * Helper function to require passing a path pointing to existing file or directory. 334 | * 335 | * This function is meant to be used with UFCS, so that it can be placed 336 | * within option definition chain. 337 | * 338 | * Params: 339 | * existing - whenever path target must exist 340 | * 341 | * Examples: 342 | * --- 343 | * new Program("rm") 344 | * .add(new Argument("target", "target to remove") 345 | * .acceptsPaths(true) 346 | * ) 347 | * --- 348 | * 349 | * See_Also: 350 | * FileSystemValidator, acceptsDirectories, acceptsFiles 351 | */ 352 | public T acceptsPaths(T: IEntry)(T entry, bool existing) { 353 | return entry.validate(new FileSystemValidator(existing)); 354 | } 355 | 356 | /** 357 | * Validates input based on delegate. 358 | * 359 | * Delegate receives all arguments that `IValidator.validate` receives, that is 360 | * information about entry being checked and an array of values to perform check on. 361 | * 362 | * For less verbose usage, check `validateWith` and `validateEachWith` helper functions. 363 | * 364 | * Examples: 365 | * --- 366 | * new Program("rm") 367 | * .add(new Argument("target", "target to remove") 368 | * .validate(new DelegateValidator((entry, args) { 369 | * foreach (arg; args) { 370 | * if (arg == "foo") throw new ValidationException("invalid number"); // would throw with "invalid number" 371 | * } 372 | * })) 373 | * // or 374 | * .validateEachWith((entry, arg) { 375 | * if (arg == "5") throw new ValidationException("invalid number"); // would throw with "invalid number" 376 | * }) 377 | * // or 378 | * .validateEachWith(arg => isGood(arg), "must be good") // would throw with "flag a must be good" 379 | * ) 380 | * --- 381 | * 382 | * See_Also: 383 | * validateWith, validateEachWith 384 | */ 385 | public class DelegateValidator : IValidator { 386 | /// Validator function type 387 | alias ValidatorFunc = void delegate(IEntry, string[]); 388 | 389 | /// Validator function 390 | ValidatorFunc validator; 391 | 392 | /// Creates instance of DelegateValidator 393 | public this(ValidatorFunc validator) nothrow pure @safe @nogc { 394 | this.validator = validator; 395 | } 396 | 397 | /// Validates input 398 | public void validate(IEntry entry, string[] args) { 399 | this.validator(entry, args); 400 | } 401 | } 402 | 403 | /** 404 | * Helper function to add custom validating delegate. 405 | * 406 | * This function is meant to be used with UFCS, so that it can be placed 407 | * within option definition chain. 408 | * 409 | * In contrast with `validateEachWith`, this functions makes a single call to delegate with all values. 410 | * 411 | * Params: 412 | * validator - delegate performing validation of all values 413 | * 414 | * Examples: 415 | * --- 416 | * new Program("rm") 417 | * .add(new Argument("target", "target to remove") 418 | * .validateWith((entry, args) { 419 | * foreach (arg; args) { 420 | * // do something 421 | * } 422 | * }) 423 | * ) 424 | * --- 425 | * 426 | * See_Also: 427 | * DelegateValidator, validateEachWith 428 | */ 429 | public T validateWith(T: IEntry)(T entry, DelegateValidator.ValidatorFunc validator) { 430 | return entry.validate(new DelegateValidator(validator)); 431 | } 432 | 433 | 434 | /** 435 | * Helper function to add custom validating delegate. 436 | * 437 | * This function is meant to be used with UFCS, so that it can be placed 438 | * within option definition chain. 439 | * 440 | * In contrast with `validateWith`, this functions makes call to delegate for every value. 441 | * 442 | * Params: 443 | * validator - delegate performing validation of single value 444 | * 445 | * Examples: 446 | * --- 447 | * new Program("rm") 448 | * .add(new Argument("target", "target to remove") 449 | * .validateEachWith((entry, arg) { 450 | * // do something 451 | * }) 452 | * ) 453 | * --- 454 | * 455 | * See_Also: 456 | * DelegateValidator, validateWith 457 | */ 458 | public T validateEachWith(T: IEntry)(T entry, void delegate(IEntry, string) validator) { 459 | return validateWith!T(entry, (e, args) { args.each!(a => validator(e, a)); }); 460 | } 461 | 462 | 463 | /** 464 | * Helper function to add custom validating delegate. 465 | * 466 | * This function is meant to be used with UFCS, so that it can be placed 467 | * within option definition chain. 468 | * 469 | * This function automatically prepends entry information to your error message, 470 | * so that call to `new Option("", "foo", "").validateWith(a => a.isDir, "must be a directory")` 471 | * on failure would throw `ValidationException` with message `option foo must be a directory`. 472 | * 473 | * Params: 474 | * validator - delegate performing validation, returning true on success 475 | * message - error message 476 | * 477 | * Examples: 478 | * --- 479 | * new Program("rm") 480 | * .add(new Argument("target", "target to remove") 481 | * .validateEachWith(arg => arg.isSymLink, "must be a symlink") 482 | * ) 483 | * --- 484 | * 485 | * See_Also: 486 | * DelegateValidator, validateWith 487 | */ 488 | public T validateEachWith(T: IEntry)(T entry, bool delegate(string) validator, string errorMessage) { 489 | return entry.validateEachWith((entry, arg) { 490 | if (!validator(arg)) { 491 | throw new ValidationException(null, "%s %s %s".format(entry.getEntryKindName(), entry.name, errorMessage)); 492 | } 493 | }); 494 | } 495 | 496 | // enum 497 | unittest { 498 | import commandr.program; 499 | import commandr.option; 500 | import commandr.parser; 501 | import std.exception : assertThrown, assertNotThrown; 502 | 503 | assertNotThrown!ValidationException( 504 | new Program("test") 505 | .add(new Option("t", "type", "foo") 506 | .acceptsValues(["a", "b"]) 507 | ) 508 | .parseArgsNoRef(["test"]) 509 | ); 510 | 511 | assertNotThrown!ValidationException( 512 | new Program("test") 513 | .add(new Option("t", "type", "foo") 514 | .acceptsValues(["a", "b"]) 515 | ) 516 | .parseArgsNoRef(["test", "--type", "a"]) 517 | ); 518 | 519 | assertNotThrown!ValidationException( 520 | new Program("test") 521 | .add(new Option("t", "type", "foo") 522 | .acceptsValues(["a", "b"]) 523 | .repeating 524 | ) 525 | .parseArgsNoRef(["test", "--type", "a", "--type", "b"]) 526 | ); 527 | 528 | assertThrown!ValidationException( 529 | new Program("test") 530 | .add(new Option("t", "type", "foo") 531 | .acceptsValues(["a", "b"]) 532 | .repeating 533 | ) 534 | .parseArgsNoRef(["test", "--type", "c", "--type", "b"]) 535 | ); 536 | 537 | assertThrown!ValidationException( 538 | new Program("test") 539 | .add(new Option("t", "type", "foo") 540 | .acceptsValues(["a", "b"]) 541 | .repeating 542 | ) 543 | .parseArgsNoRef(["test", "--type", "a", "--type", "z"]) 544 | ); 545 | } 546 | 547 | // delegate 548 | unittest { 549 | import commandr.program; 550 | import commandr.option; 551 | import commandr.parser; 552 | import std.exception : assertThrown, assertNotThrown; 553 | import std.string : isNumeric; 554 | 555 | assertNotThrown!ValidationException( 556 | new Program("test") 557 | .add(new Option("t", "type", "foo") 558 | .validateEachWith(a => isNumeric(a), "must be an integer") 559 | ) 560 | .parseArgsNoRef(["test", "--type", "50"]) 561 | ); 562 | 563 | assertThrown!ValidationException( 564 | new Program("test") 565 | .add(new Option("t", "type", "foo") 566 | .validateEachWith(a => isNumeric(a), "must be an integer") 567 | ) 568 | .parseArgsNoRef(["test", "--type", "a"]) 569 | ); 570 | } 571 | --------------------------------------------------------------------------------