Skip to content

Commit fb1edcc

Browse files
authored
Feat: Command Aliases (#34)
* feat: add `alias` method & command option; - Closes #26 * feat: add "Aliases" section to command help text * chore: add `alias` test cases * chore: add tests for `utils.help` text output * chore: readme docs * fix: typo
1 parent 52909ec commit fb1edcc

File tree

14 files changed

+521
-14
lines changed

14 files changed

+521
-14
lines changed

lib/index.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ class Sade {
4545
this.curr = cmd;
4646
if (opts.default) this.default=cmd;
4747

48-
this.tree[cmd] = { usage, options:[], alias:{}, default:{}, examples:[] };
48+
this.tree[cmd] = { usage, alibi:[], options:[], alias:{}, default:{}, examples:[] };
49+
if (opts.alias) this.alias(opts.alias);
4950
if (desc) this.describe(desc);
5051

5152
return this;
@@ -56,6 +57,13 @@ class Sade {
5657
return this;
5758
}
5859

60+
alias(...names) {
61+
if (this.single) throw new Error('Cannot call `alias()` in "single" mode');
62+
if (!this.curr) throw new Error('Cannot call `alias()` before defining a command');
63+
this.tree[this.curr].alibi = this.tree[this.curr].alibi.concat(...names);
64+
return this;
65+
}
66+
5967
option(str, desc, val) {
6068
let cmd = this.tree[ this.curr || ALL ];
6169

@@ -110,11 +118,18 @@ class Sade {
110118
cmd = this.tree[DEF];
111119
} else {
112120
// Loop thru possible command(s)
113-
let i=1, len=argv._.length + 1;
121+
let k, i=1, len=argv._.length + 1;
114122
for (; i < len; i++) {
115123
tmp = argv._.slice(0, i).join(' ');
116124
if (this.tree[tmp] !== void 0) {
117125
name=tmp; offset=(i + 2); // argv slicer
126+
} else {
127+
for (k in this.tree) {
128+
if (this.tree[k].alibi.includes(tmp)) {
129+
name=k; offset=(i + 2);
130+
break;
131+
}
132+
}
118133
}
119134
}
120135

lib/utils.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ exports.help = function (bin, tree, key, single) {
6363
out += (NL + __ + __ + `${pfx} ${k} --help`);
6464
});
6565
out += NL;
66+
} else if (!single && key !== DEF) {
67+
// Command help :: print its aliases if any
68+
out += section('Aliases', cmd.alibi, prefix);
6669
}
6770

6871
out += section('Options', format(cmd.options), noop);

readme.md

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,75 @@ When `sirv --help` is run, the generated help text is trimmed, fully aware that
198198
$ sirv my-app --dev
199199
```
200200

201+
## Command Aliases
202+
203+
Command aliases are alternative names (aliases) for a command. They are often used as shortcuts or as typo relief!
204+
205+
The aliased names do not appear in the general help text.<br>
206+
Instead, they only appear within the Command-specific help text under an "Aliases" section.
207+
208+
***Limitations***
209+
210+
* You cannot assign aliases while in [Single Command Mode](#single-command-mode)
211+
* You cannot call [`prog.alias()`](#progaliasnames) before defining any Commands (via `prog.commmand()`)
212+
* You, the developer, must keep track of which aliases have already been used and/or exist as Command names
213+
214+
***Example***
215+
216+
Let's reconstruct the `npm install` command as a Sade program:
217+
218+
```js
219+
sade('npm')
220+
// ...
221+
.command('install [package]', 'Install a package', {
222+
alias: ['i', 'add', 'isntall']
223+
})
224+
.option('-P, --save-prod', 'Package will appear in your dependencies.')
225+
.option('-D, --save-dev', 'Package will appear in your devDependencies.')
226+
.option('-O, --save-optional', 'Package will appear in your optionalDependencies')
227+
.option('-E, --save-exact', 'Save exact versions instead of using a semver range operator')
228+
// ...
229+
```
230+
231+
When we run `npm --help` we'll see this general help text:
232+
233+
```
234+
Usage
235+
$ npm <command> [options]
236+
237+
Available Commands
238+
install Install a package
239+
240+
For more info, run any command with the `--help` flag
241+
$ npm install --help
242+
243+
Options
244+
-v, --version Displays current version
245+
-h, --help Displays this message
246+
```
247+
248+
When we run `npm install --help` &mdash; ***or*** the help flag with any of `install`'s aliases &mdash; we'll see this command-specific help text:
249+
250+
```
251+
Description
252+
Install a package
253+
254+
Usage
255+
$ npm install [package] [options]
256+
257+
Aliases
258+
$ npm i
259+
$ npm add
260+
$ npm isntall
261+
262+
Options
263+
-P, --save-prod Package will appear in your dependencies.
264+
-D, --save-dev Package will appear in your devDependencies.
265+
-O, --save-optional Package will appear in your optionalDependencies
266+
-E, --save-exact Save exact versions instead of using a semver range operator
267+
-h, --help Displays this message
268+
```
269+
201270

202271

203272
## API
@@ -298,6 +367,26 @@ The Command's description. The value is passed directly to [`prog.describe`](#pr
298367
Type: `Object`<br>
299368
Default: `{}`
300369

370+
##### opts.alias
371+
Type: `String|Array`
372+
373+
Optionally define one or more aliases for the current Command.<br>
374+
When declared, the `opts.alias` value is passed _directly_ to the [`prog.alias`](#progaliasnames) method.
375+
376+
```js
377+
// Program A is equivalent to Program B
378+
// ---
379+
380+
const A = sade('bin')
381+
.command('build', 'My build command', { alias: 'b' })
382+
.command('watch', 'My watch command', { alias: ['w', 'dev'] });
383+
384+
const B = sade('bin')
385+
.command('build', 'My build command').alias('b')
386+
.command('watch', 'My watch command').alias('w', 'dev');
387+
```
388+
389+
301390
##### opts.default
302391

303392
Type: `Boolean`
@@ -343,6 +432,34 @@ For general `--help` output, ***only*** the first sentence will be displayed. Ho
343432
> **Note:** Pass an `Array` if you don't want internal assumptions. However, the first item is _always_ displayed in general help, so it's recommended to keep it short.
344433
345434

435+
### prog.alias(...names)
436+
437+
Define one or more aliases for the current Command.
438+
439+
> **Important:** An error will be thrown if:<br>1) the program is in [Single Command Mode](#single-command-mode); or<br>2) `prog.alias` is called before any `prog.command`.
440+
441+
#### names
442+
443+
Type: `String`
444+
445+
The list of alternative names (aliases) for the current Command.<br>
446+
For example, you may want to define shortcuts and/or common typos for the Command's full name.
447+
448+
> **Important:** Sade _does not_ check if the incoming `names` are already in use by other Commands or their aliases.<br>During conflicts, the Command with the same `name` is given priority, otherwise the first Command (according to Program order) with `name` as an alias is chosen.
449+
450+
The `prog.alias()` is append-only, so calling it multiple times within a Command context will _keep_ all aliases, including those initially passed via [`opts.alias`](#optsdefault).
451+
452+
```js
453+
sade('bin')
454+
.command('hello <name>', 'Greet someone by their name', {
455+
alias: ['hey', 'yo']
456+
})
457+
.alias('hi', 'howdy')
458+
.alias('hola', 'oi');
459+
//=> hello aliases: hey, yo, hi, howdy, hola, oi
460+
```
461+
462+
346463
### prog.action(handler)
347464

348465
Attach a callback to the current Command.
@@ -392,7 +509,7 @@ Type: `String`
392509

393510
The example string to add. This will be included in the general or command-specific `--help` output.
394511

395-
> **Note:** Your example's `str` will be prefixed with your Programs's [`name`](#sadename).
512+
> **Note:** Your example's `str` will be prefixed with your Program's [`name`](#sadename).
396513
397514

398515
### prog.option(flags, desc, value)

test/fixtures/alias1.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/env node
2+
const sade = require('../../lib');
3+
4+
sade('bin <type> [dir]')
5+
.alias('error')
6+
.parse(process.argv);

test/fixtures/alias2.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env node
2+
const sade = require('../../lib');
3+
4+
sade('bin')
5+
.alias('foo')
6+
.command('bar <src>')
7+
.parse(process.argv);

test/fixtures/args.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ const sade = require('../../lib');
33

44
sade('bin')
55
.command('foo <dir>')
6+
.alias('f')
67
.action(dir => {
78
console.log(`~> ran "foo" with "${dir}" arg`);
89
})
910
.command('bar [dir]')
11+
.alias('b')
1012
.action(dir => {
1113
dir = dir || '~default~';
1214
console.log(`~> ran "bar" with "${dir}" arg`);

test/fixtures/basic.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const sade = require('../../lib');
33

44
sade('bin')
55
.command('foo')
6+
.alias('f', 'fo')
67
.action(() => {
78
console.log('~> ran "foo" action');
89
})

test/fixtures/default.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
const sade = require('../../lib');
33

44
sade('bin')
5-
.command('foo', null, { default:true })
5+
.command('foo', null, { alias: 'f', default:true })
66
.action(() => console.log('~> ran "foo" action'))
77

88
.command('bar')
9+
.alias('b')
910
.action(() => console.log('~> ran "bar" action'))
1011

1112
.parse(process.argv);

test/fixtures/options.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const sade = require('../../lib');
44
sade('bin')
55
.option('-g, --global', 'global')
66
.command('foo')
7+
.alias('f')
78
.option('-l, --long', 'long flag')
89
.option('-s, --short', 'short flag')
910
.option('-h, --hello', 'override')
@@ -15,6 +16,7 @@ sade('bin')
1516
})
1617

1718
.command('bar <dir>')
19+
.alias('b')
1820
.option('--only', 'no short alias')
1921
.action((dir, opts) => {
2022
let pre = opts.only ? '~> (only)' : '~>';

test/fixtures/subs.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@
22
const sade = require('../../lib');
33

44
sade('bin')
5-
.command('remote')
5+
.command('remote', '', { alias: 'r' })
66
.action(opts => {
77
console.log('~> ran "remote" action');
88
})
99

10-
.command('remote add <name> <url>')
10+
.command('remote add <name> <url>', '', { alias: 'ra' })
1111
.action((name, uri, opts) => {
12-
console.log(`~> ran "remote add" with "${name}" and "${uri}" args`);
12+
console.log(`~> ran "remote add" with "${name}" and "${uri}" args`);
1313
})
1414

15-
.command('remote rename <old> <new>')
15+
.command('remote rename <old> <new>', '', { alias: 'rr' })
1616
.action((old, nxt, opts) => {
1717
console.log(`~> ran "remote rename" with "${old}" and "${nxt}" args`);
1818
})

0 commit comments

Comments
 (0)