|
1 | | -## ts-command-line |
| 1 | +# ts-command-line |
2 | 2 |
|
3 | | -An object-oriented command-line parser for TypeScript projects, |
4 | | -based on the [argparse](https://www.npmjs.com/package/argparse) |
5 | | -engine. |
| 3 | +This library makes it easy to create professional command-line tools for NodeJS. By "**professional**", we mean: |
| 4 | + |
| 5 | +- **no gotchas for users**: This requirement seems obvious, but try typing "`npm install --save-dex`" instead of "`npm install --save-dev`" sometime. The mistyped letter gets silently ignored, and the command appears to execute successfully! This can be extremely confusing and frustrating. It plagues many familiar NodeJS tools. For a great user experience, the command line should always use a strict parser that catches these mistakes. |
| 6 | + |
| 7 | +- **no gotchas for developers**: Most JavaScript command-line parsers store their output in a simple hash object. This is very convenient for small projects, but suppose many different source files participate in defining and reading command-line parameters: A misspelled variable name is indistinguishable from a real flag that was omitted. Even if you get the names right, the data type might be unpredictable (is that count `1` or `"1"`?). **ts-command-line** models each parameter type as a real TypeScript class. |
| 8 | + |
| 9 | +- **automatic documentation**: Some command-line libraries treat the `--help` docs as a separate exercise for the reader. **ts-command-line** requires documentation for every parameter, and automatically generates the `--help` for you. If you write long paragraphs, they will be word-wrapped correctly. (Yay!) |
| 10 | + |
| 11 | +- **structure and extensibility**: Instead of a simple function chain, **ts-command-line** provides a "scaffold" pattern that makes it easy to find and understand the command-line parser for tool project. The scaffold model is generally recommended, but there's also a "dynamic" model if you need it. (See below.) |
| 12 | + |
| 13 | +Internally, **ts-command-line** is based on [argparse](https://www.npmjs.com/package/argparse) and the Python approach to command-lines. Compared to other libraries, it doesn't provide zillions of alternative syntaxes and bells and whistles. But if you're looking for a simple, professional, railed experience for your command-line tool, give it a try! |
| 14 | + |
| 15 | + |
| 16 | +### Some Terminology |
| 17 | + |
| 18 | +Suppose that we want to parse a command-line like this: |
| 19 | + |
| 20 | +``` |
| 21 | +widget --verbose push --force --max-count 123 |
| 22 | +``` |
| 23 | + |
| 24 | +In this example, we can identify the following components: |
| 25 | + |
| 26 | +- **"parameter"**: The `--verbose`, `--force`, and `--max-count` are called *parameters*. The currently supported parameter types include: **flag** (i.e. boolean), **integer**, **string**, **choice** (i.e. enums), and **string list**. |
| 27 | + |
| 28 | +- **"argument"**: The value "123" is the *argument* for the `--max-count` integer parameter. (Flags don't have arguments, because their value is determined by whether the flag was provided or not.) |
| 29 | + |
| 30 | +- **"action"**: Similar to Git's command-line, the `push` token acts as sub-command with its own unique set of parameters. This means that **global parameters** come before the action and affect all actions, whereas **action parameters** come after the action and only affect that action. |
| 31 | + |
| 32 | + |
| 33 | +## Scaffold Model |
| 34 | + |
| 35 | +The scaffold model works by extending the abstract base classes `CommandLineParser` (for the overall command-line) and `CommandLineAction` for a specific subcommand. |
| 36 | + |
| 37 | +Continuing our example from above, suppose we want to start with a couple simple flags like this: |
| 38 | + |
| 39 | +``` |
| 40 | +widget --verbose push --force |
| 41 | +``` |
| 42 | + |
| 43 | +We could define a subclass for the "`push`" action like this: |
| 44 | + |
| 45 | +```typescript |
| 46 | +class PushAction extends CommandLineAction { |
| 47 | + private _force: CommandLineFlagParameter; |
| 48 | + |
| 49 | + public constructor() { |
| 50 | + super({ |
| 51 | + actionName: 'push', |
| 52 | + summary: 'Pushes a widget to the service', |
| 53 | + documentation: 'More detail about the "push" action' |
| 54 | + }); |
| 55 | + } |
| 56 | + |
| 57 | + protected onExecute(): Promise<void> { // abstract |
| 58 | + return BusinessLogic.doTheWork(this._force.value); |
| 59 | + } |
| 60 | + |
| 61 | + protected onDefineParameters(): void { // abstract |
| 62 | + this._force = this.defineFlagParameter({ |
| 63 | + parameterLongName: '--force', |
| 64 | + description: 'Push and overwrite any existing state' |
| 65 | + }); |
| 66 | + } |
| 67 | +} |
| 68 | +``` |
| 69 | + |
| 70 | +Then we might define the parser subclass like this: |
| 71 | + |
| 72 | +```typescript |
| 73 | +class WidgetCommandLine extends CommandLineParser { |
| 74 | + private _verbose: CommandLineFlagParameter; |
| 75 | + |
| 76 | + public constructor() { |
| 77 | + super({ |
| 78 | + toolFilename: 'widget', |
| 79 | + toolDescription: 'Documentation for the "widget" tool' |
| 80 | + }); |
| 81 | + |
| 82 | + this.addAction(new PushAction()); |
| 83 | + } |
| 84 | + |
| 85 | + protected onDefineParameters(): void { // abstract |
| 86 | + this._verbose = this.defineFlagParameter({ |
| 87 | + parameterLongName: '--verbose', |
| 88 | + description: 'Show extra logging detail' |
| 89 | + }); |
| 90 | + } |
| 91 | + |
| 92 | + protected onExecute(): Promise<void> { // override |
| 93 | + BusinessLogic.configureLogger(this._verbose.value); |
| 94 | + return super.onExecute(); |
| 95 | + } |
| 96 | +} |
| 97 | +``` |
| 98 | + |
| 99 | +To invoke the parser, the application entry point will do something like this: |
| 100 | + |
| 101 | +```typescript |
| 102 | +const commandLine: WidgetCommandLine = new WidgetCommandLine(); |
| 103 | +commandLine.execute(); |
| 104 | +``` |
| 105 | + |
| 106 | +When we run `widget --verbose push --force`, the `PushAction.onExecute()` method will get invoked and your business logic takes over. |
| 107 | + |
| 108 | + |
| 109 | +#### Testing out the docs |
| 110 | + |
| 111 | +If you invoke the tool as "`widget --help`", the docs are automatically generated: |
| 112 | + |
| 113 | +``` |
| 114 | +usage: widget [-h] [--verbose] <command> ... |
| 115 | +
|
| 116 | +Documentation for the "widget" tool |
| 117 | +
|
| 118 | +Positional arguments: |
| 119 | + <command> |
| 120 | + push Pushes a widget to the service |
| 121 | +
|
| 122 | +Optional arguments: |
| 123 | + -h, --help Show this help message and exit. |
| 124 | + --verbose Show extra logging detail |
| 125 | +
|
| 126 | +For detailed help about a specific command, use: widget <command> -h |
| 127 | +``` |
| 128 | + |
| 129 | +For help about the `push` action, the user can type "`widget push --help`", which shows this output: |
| 130 | + |
| 131 | +``` |
| 132 | +usage: widget push [-h] [--force] |
| 133 | +
|
| 134 | +More detail about the "push" action |
| 135 | +
|
| 136 | +Optional arguments: |
| 137 | + -h, --help Show this help message and exit. |
| 138 | + --force Push and overwrite any existing state |
| 139 | +``` |
| 140 | + |
| 141 | +## Dynamic Model |
| 142 | + |
| 143 | +Creating subclasses provides a simple, recognizable pattern that you can use across all your tooling projects. It's the generally recommended approach. However, there are some cases where we need to break out of the scaffold. For example: |
| 144 | + |
| 145 | +- Actions or parameters are discovered at runtime, e.g. from a config file |
| 146 | +- The actions and their implementations aren't closely coupled |
| 147 | + |
| 148 | +In this case, you can use the `DynamicCommandLineAction` and `DynamicCommandLineParser` classes which are not abstract (and not intended to be subclassed). Here's our above example rewritten for this model: |
| 149 | + |
| 150 | +```typescript |
| 151 | +// Define the parser |
| 152 | +const commandLineParser: DynamicCommandLineParser = new DynamicCommandLineParser({ |
| 153 | + toolFilename: 'widget', |
| 154 | + toolDescription: 'Documentation for the "widget" tool' |
| 155 | +}); |
| 156 | +commandLineParser.defineFlagParameter({ |
| 157 | + parameterLongName: '--verbose', |
| 158 | + description: 'Show extra logging detail' |
| 159 | +}); |
| 160 | + |
| 161 | +// Define the action |
| 162 | +const action: DynamicCommandLineAction = new DynamicCommandLineAction({ |
| 163 | + actionName: 'push', |
| 164 | + summary: 'Pushes a widget to the service', |
| 165 | + documentation: 'More detail about the "push" action' |
| 166 | +}); |
| 167 | +commandLineParser.addAction(action); |
| 168 | + |
| 169 | +action.defineFlagParameter({ |
| 170 | + parameterLongName: '--force', |
| 171 | + description: 'Push and overwrite any existing state' |
| 172 | +}); |
| 173 | + |
| 174 | +// Parse the command line |
| 175 | +commandLineParser.execute().then(() => { |
| 176 | + console.log('The action is: ' + commandLineParser.selectedAction!.actionName); |
| 177 | + console.log('The force flag is: ' + action.getFlagParameter('--force').value); |
| 178 | +}); |
| 179 | +``` |
| 180 | + |
| 181 | +You can also mix the two models. For example, we could augment the `WidgetCommandLine` from the original model by adding `DynamicAction` objects to it. |
| 182 | + |
| 183 | + |
| 184 | +### Real world examples |
| 185 | + |
| 186 | +Here are some GitHub projects that illustrate different use cases for **ts-command-line**: |
| 187 | + |
| 188 | +- [@microsoft/rush](https://www.npmjs.com/package/@microsoft/rush) |
| 189 | +- [@microsoft/api-extractor](https://www.npmjs.com/package/@microsoft/api-extractor) |
| 190 | +- [@microsoft/api-documenter](https://www.npmjs.com/package/@microsoft/api-documenter) |
0 commit comments