Comparing version 1.5.1 to 1.6.0
@@ -8,11 +8,14 @@ const mri = require('mri'); | ||
class Sade { | ||
constructor(name) { | ||
this.tree = {}; | ||
this.name = name; | ||
constructor(name, isOne) { | ||
let [bin, ...rest] = name.split(/\s+/); | ||
isOne = isOne || rest.length > 0; | ||
this.bin = bin; | ||
this.ver = '0.0.0'; | ||
this.default = ''; | ||
this.tree = {}; | ||
// set internal shapes; | ||
this.command(ALL); | ||
this.command(`${DEF} <command>`) | ||
.option('-v, --version', 'Displays current version'); | ||
this.command([DEF].concat(isOne ? rest : '<command>').join(' ')); | ||
this.single = isOne; | ||
this.curr = ''; // reset | ||
@@ -22,4 +25,8 @@ } | ||
command(str, desc, opts={}) { | ||
if (this.single) { | ||
throw new Error('Disable "single" mode to add commands'); | ||
} | ||
// All non-([|<) are commands | ||
let cmd=[], usage=[], rgx=/(\[|<)/; | ||
// All non-([|<) are commands | ||
str.split(/\s+/).forEach(x => { | ||
@@ -36,8 +43,9 @@ (rgx.test(x.charAt(0)) ? usage : cmd).push(x); | ||
// re-include `cmd` for commands | ||
cmd.includes('__') || usage.unshift(cmd); | ||
usage = usage.join(' '); // to string | ||
this.curr = cmd; | ||
if (opts.default) this.default=cmd; | ||
!~cmd.indexOf('__') && usage.unshift(cmd); // re-include `cmd` | ||
usage = usage.join(' '); // to string | ||
this.tree[cmd] = { usage, options:[], alias:{}, default:{}, examples:[] }; | ||
@@ -58,3 +66,3 @@ if (desc) this.describe(desc); | ||
let [flag, alias] = $.parse(str); | ||
(alias && alias.length > 1) && ([flag, alias]=[alias, flag]); | ||
if (alias && alias.length > 1) [flag, alias]=[alias, flag]; | ||
@@ -98,37 +106,39 @@ str = `--${flag}`; | ||
let argv = mri(arr.slice(offset), { alias }); | ||
let bin = this.name; | ||
let isSingle = this.single; | ||
let bin = this.bin; | ||
let tmp, name = ''; | ||
let isVoid, cmd; | ||
// Loop thru possible command(s) | ||
let tmp, name=''; | ||
let i=1, len=argv._.length + 1; | ||
for (; i < len; i++) { | ||
tmp = argv._.slice(0, i).join(' '); | ||
if (this.tree[tmp] !== void 0) { | ||
name=tmp; offset=(i + 2); // argv slicer | ||
if (isSingle) { | ||
cmd = this.tree[DEF]; | ||
} else { | ||
// Loop thru possible command(s) | ||
let i=1, len=argv._.length + 1; | ||
for (; i < len; i++) { | ||
tmp = argv._.slice(0, i).join(' '); | ||
if (this.tree[tmp] !== void 0) { | ||
name=tmp; offset=(i + 2); // argv slicer | ||
} | ||
} | ||
} | ||
let cmd = this.tree[name]; | ||
let isVoid = (cmd === void 0); | ||
cmd = this.tree[name]; | ||
isVoid = (cmd === void 0); | ||
if (isVoid) { | ||
if (this.default) { | ||
name = this.default; | ||
cmd = this.tree[name]; | ||
arr.unshift(name); | ||
offset++; | ||
} else if (tmp) { | ||
return $.error(bin, `Invalid command: ${tmp}`); | ||
} //=> else: cmd not specified, wait for now... | ||
if (isVoid) { | ||
if (this.default) { | ||
name = this.default; | ||
cmd = this.tree[name]; | ||
arr.unshift(name); | ||
offset++; | ||
} else if (tmp) { | ||
return $.error(bin, `Invalid command: ${tmp}`); | ||
} //=> else: cmd not specified, wait for now... | ||
} | ||
} | ||
if (argv.version) { | ||
return console.log(`${bin}, ${this.ver}`); | ||
} | ||
// show main help if relied on "default" for multi-cmd | ||
if (argv.help) return this.help(!isSingle && !isVoid && name); | ||
if (argv.version) return this._version(); | ||
if (argv.help) { | ||
return this.help(!isVoid && name); | ||
} | ||
if (cmd === void 0) { | ||
if (!isSingle && cmd === void 0) { | ||
return $.error(bin, 'No command specified.'); | ||
@@ -152,3 +162,3 @@ } | ||
if (args.length < reqs.length) { | ||
name && (bin += ` ${name}`); // for help text | ||
if (name) bin += ` ${name}`; // for help text | ||
return $.error(bin, 'Insufficient arguments!'); | ||
@@ -168,7 +178,11 @@ } | ||
console.log( | ||
$.help(this.name, this.tree, str || DEF) | ||
$.help(this.bin, this.tree, str || DEF, this.single) | ||
); | ||
} | ||
_version() { | ||
console.log(`${this.bin}, ${this.ver}`); | ||
} | ||
} | ||
module.exports = str => new Sade(str); | ||
module.exports = (str, isOne) => new Sade(str, isOne); |
@@ -39,12 +39,13 @@ const GAP = 4; | ||
exports.help = function (bin, tree, key) { | ||
exports.help = function (bin, tree, key, single) { | ||
let out='', cmd=tree[key], pfx=`$ ${bin}`, all=tree[ALL]; | ||
let prefix = s => `${pfx} ${s}`; | ||
let prefix = s => `${pfx} ${s}`.replace(/\s+/g, ' '); | ||
// update ALL & CMD options | ||
all.options.push(['-h, --help', 'Displays this message']); | ||
cmd.options = (cmd.options || []).concat(all.options); | ||
let tail = [['-h, --help', 'Displays this message']]; | ||
if (key === DEF) tail.unshift(['-v, --version', 'Displays current version']); | ||
cmd.options = (cmd.options || []).concat(all.options, tail); | ||
// write options placeholder | ||
(cmd.options.length > 0) && (cmd.usage += ' [options]'); | ||
if (cmd.options.length > 0) cmd.usage += ' [options]'; | ||
@@ -55,3 +56,3 @@ // description ~> text only; usage ~> prefixed | ||
if (key === DEF) { | ||
if (!single && key === DEF) { | ||
// General help :: print all non-internal commands & their 1st line of text | ||
@@ -58,0 +59,0 @@ let cmds = Object.keys(tree).filter(k => !/__/.test(k)); |
{ | ||
"name": "sade", | ||
"version": "1.5.1", | ||
"version": "1.6.0", | ||
"description": "Smooth (CLI) operator ๐ถ", | ||
@@ -5,0 +5,0 @@ "repository": "lukeed/sade", |
@@ -144,13 +144,91 @@ # sade [![Build Status](https://travis-ci.org/lukeed/sade.svg?branch=master)](https://travis-ci.org/lukeed/sade) | ||
## Single Command Mode | ||
In certain circumstances, you may only need `sade` for a single-command CLI application. | ||
> **Note:** Until `v1.6.0`, this made for an awkward pairing. | ||
To enable this, you may make use of the [`isSingle`](#issingle) argument. Doing so allows you to pass the program's entire [`usage` text](#usage-1) into the `name` argument. | ||
With "Single Command Mode" enabled, your entire binary operates as one command. This means that any [`prog.command`](#progcommandusage-desc-opts) calls are disallowed & will instead throw an Error. Of course, you may still define a program version, a description, an example or two, and declare options. You are customizing the program's attributes as a whole.<sup>*</sup> | ||
> <sup>*</sup> This is true for multi-command applications, too, up until your first `prog.command()` call! | ||
***Example*** | ||
Let's reconstruct [`sirv-cli`](https://github.com/lukeed/sirv), which is a single-command application that (optionally) accepts a directory from which to serve files. It also offers a slew of option flags: | ||
```js | ||
sade('sirv [dir]', true) | ||
.version('1.0.0') | ||
.describe('Run a static file server') | ||
.example('public -qeim 31536000') | ||
.example('--port 8080 --etag') | ||
.example('my-app --dev') | ||
.option('-D, --dev', 'Enable "dev" mode') | ||
.option('-e, --etag', 'Enable "Etag" header') | ||
// There are a lot... | ||
.option('-H, --host', 'Hostname to bind', 'localhost') | ||
.option('-p, --port', 'Port to bind', 5000) | ||
.action((dir, opts) => { | ||
// Program handler | ||
}) | ||
.parse(process.argv); | ||
``` | ||
When `sirv --help` is run, the generated help text is trimmed, fully aware that there's only one command in this program: | ||
``` | ||
Description | ||
Run a static file server | ||
Usage | ||
$ sirv [dir] [options] | ||
Options | ||
-D, --dev Enable "dev" mode | ||
-e, --etag Enable "Etag" header | ||
-H, --host Hostname to bind (default localhost) | ||
-p, --port Port to bind (default 5000) | ||
-v, --version Displays current version | ||
-h, --help Displays this message | ||
Examples | ||
$ sirv public -qeim 31536000 | ||
$ sirv --port 8080 --etag | ||
$ sirv my-app --dev | ||
``` | ||
## API | ||
### sade(name) | ||
### sade(name, isSingle) | ||
Returns: `Program` | ||
Returns your chainable Sade instance, aka your `Program`. | ||
#### name | ||
Type: `String`<br> | ||
Returns: `Program` | ||
Required: `true` | ||
The name of your bin/program. Returns the `Program` itself, wherein all other methods are available. | ||
The name of your `Program` / binary application. | ||
#### isSingle | ||
Type: `Boolean`<br> | ||
Default: `name.includes(' ');` | ||
If your `Program` is meant to have ***only one command***.<br> | ||
When `true`, this simplifies your generated `--help` output such that: | ||
* the "root-level help" is your _only_ help text | ||
* the "root-level help" does not display an `Available Commands` section | ||
* the "root-level help" does not inject `$ name <command>` into the `Usage` section | ||
* the "root-level help" does not display `For more info, run any command with the `--help` flag` text | ||
You may customize the `Usage` of your command by modifying the `name` argument directly.<br> | ||
Please read [Single Command Mode](#single-command-mode) for an example and more information. | ||
> **Important:** Whenever `name` includes a custom usage, then `isSingle` is automatically assumed and enforced! | ||
### prog.command(usage, desc, opts) | ||
@@ -157,0 +235,0 @@ |
24838
219
556