├── .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 |
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 |
--------------------------------------------------------------------------------