Skip to content

Commit f608d51

Browse files
committed
Document the new APIs
1 parent 7c07144 commit f608d51

File tree

1 file changed

+189
-4
lines changed

1 file changed

+189
-4
lines changed
Lines changed: 189 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,190 @@
1-
## ts-command-line
1+
# ts-command-line
22

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

Comments
 (0)