pax_global_header00006660000000000000000000000064141656211140014513gustar00rootroot0000000000000052 comment=1cdf3e464ca899dcd6e758a50eb82cf7cf484771 sade-1.8.1/000077500000000000000000000000001416562111400124365ustar00rootroot00000000000000sade-1.8.1/.editorconfig000066400000000000000000000003231416562111400151110ustar00rootroot00000000000000# http://editorconfig.org root = true [*] indent_size = 2 indent_style = tab end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.{json,yml,md}] indent_style = space sade-1.8.1/.github/000077500000000000000000000000001416562111400137765ustar00rootroot00000000000000sade-1.8.1/.github/FUNDING.yml000066400000000000000000000000171416562111400156110ustar00rootroot00000000000000github: lukeed sade-1.8.1/.github/workflows/000077500000000000000000000000001416562111400160335ustar00rootroot00000000000000sade-1.8.1/.github/workflows/ci.yml000066400000000000000000000006241416562111400171530ustar00rootroot00000000000000name: CI on: [push, pull_request] jobs: test: name: Node.js v${{ matrix.nodejs }} runs-on: ubuntu-latest timeout-minutes: 3 strategy: matrix: nodejs: [6, 8, 10, 12, 14] steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: node-version: ${{ matrix.nodejs }} - run: npm install - run: npm run build - run: npm test sade-1.8.1/.gitignore000066400000000000000000000000631416562111400144250ustar00rootroot00000000000000node_modules .DS_Store *-lock.* *.lock *.log /lib sade-1.8.1/deno/000077500000000000000000000000001416562111400133635ustar00rootroot00000000000000sade-1.8.1/deno/mod.js000066400000000000000000000127271416562111400145110ustar00rootroot00000000000000import { parse } from "https://deno.land/std@0.106.0/flags/mod.ts"; import * as $ from "./utils.js"; const ALL = "__all__"; const DEF = "__default__"; class Sade { constructor(name, isOne) { const [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].concat(isOne ? rest : "").join(" ")); this.single = isOne; this.curr = ""; // reset } command(str, desc, opts = {}) { if (this.single) { throw new Error('Disable "single" mode to add commands'); } // All non-([|<) are commands let cmd = [], usage = []; const rgx = /(\[|<)/; str.split(/\s+/).forEach((x) => { (rgx.test(x.charAt(0)) ? usage : cmd).push(x); }); // Back to string~! cmd = cmd.join(" "); if (cmd in this.tree) { throw new Error(`Command already exists: ${cmd}`); } // re-include `cmd` for commands cmd.includes("__") || usage.unshift(cmd); usage = usage.join(" "); // to string this.curr = cmd; if (opts.default) this.default = cmd; this.tree[cmd] = { usage, alibi: [], options: [], alias: {}, default: {}, examples: [], }; if (opts.alias) this.alias(opts.alias); if (desc) this.describe(desc); return this; } describe(str) { this.tree[this.curr || DEF].describe = Array.isArray(str) ? str : $.sentences(str); return this; } alias(...names) { if (this.single) throw new Error('Cannot call `alias()` in "single" mode'); if (!this.curr) { throw new Error("Cannot call `alias()` before defining a command"); } const arr = this.tree[this.curr].alibi = this.tree[this.curr].alibi.concat( ...names, ); arr.forEach((key) => this.tree[key] = this.curr); return this; } option(str, desc, val) { const cmd = this.tree[this.curr || ALL]; let [flag, alias] = $.parse(str); if (alias && alias.length > 1) [flag, alias] = [alias, flag]; str = `--${flag}`; if (alias && alias.length > 0) { str = `-${alias}, ${str}`; const old = cmd.alias[alias]; cmd.alias[alias] = (old || []).concat(flag); } const arr = [str, desc || ""]; if (val !== void 0) { arr.push(val); cmd.default[flag] = val; } else if (!alias) { cmd.default[flag] = void 0; } cmd.options.push(arr); return this; } action(handler) { this.tree[this.curr || DEF].handler = handler; return this; } example(str) { this.tree[this.curr || DEF].examples.push(str); return this; } version(str) { this.ver = str; return this; } parse(arr, opts = {}) { arr = ["", ""].concat(arr); let offset = 2, tmp, idx, isVoid, cmd; const alias = { h: "help", v: "version" }; const argv = parse(arr.slice(offset), { alias }); const isSingle = this.single; let bin = this.bin; let name = ""; if (isSingle) { cmd = this.tree[DEF]; } else { // Loop thru possible command(s) let i = 1, xyz; const len = argv._.length + 1; for (; i < len; i++) { tmp = argv._.slice(0, i).join(" "); xyz = this.tree[tmp]; if (typeof xyz === "string") { idx = (name = xyz).split(" "); arr.splice(arr.indexOf(argv._[0]), i, ...idx); i += (idx.length - i); } else if (xyz) { name = tmp; } else if (name) { break; } } 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... } } // 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 (!isSingle && cmd === void 0) { return $.error(bin, "No command specified."); } const all = this.tree[ALL]; // merge all objects :: params > command > all opts.alias = Object.assign(all.alias, cmd.alias, opts.alias); opts.default = Object.assign(all.default, cmd.default, opts.default); tmp = name.split(" "); idx = arr.indexOf(tmp[0], 2); if (~idx) arr.splice(idx, tmp.length); const vals = parse(arr.slice(offset), opts); if (!vals || typeof vals === "string") { return $.error(bin, vals || "Parsed unknown option flag(s)!"); } const segs = cmd.usage.split(/\s+/); const reqs = segs.filter((x) => x.charAt(0) === "<"); const args = vals._.splice(0, reqs.length); if (args.length < reqs.length) { if (name) bin += ` ${name}`; // for help text return $.error(bin, "Insufficient arguments!"); } segs.filter((x) => x.charAt(0) === "[").forEach((_) => { args.push(vals._.shift()); // adds `undefined` per [slot] if no more }); args.push(vals); // flags & co are last const handler = cmd.handler; return opts.lazy ? { args, name, handler } : handler.apply(null, args); } help(str) { console.log( $.help(this.bin, this.tree, str || DEF, this.single), ); } _version() { console.log(`${this.bin}, ${this.ver}`); } } export default (str, isOne) => new Sade(str, isOne); sade-1.8.1/deno/utils.js000066400000000000000000000055041416562111400150650ustar00rootroot00000000000000const GAP = 4; const __ = " "; const ALL = "__all__"; const DEF = "__default__"; const NL = "\n"; function format(arr) { if (!arr.length) return ""; const len = maxLen(arr.map((x) => x[0])) + GAP; const join = (a) => a[0] + " ".repeat(len - a[0].length) + a[1] + (a[2] == null ? "" : ` (default ${a[2]})`); return arr.map(join); } function maxLen(arr) { let c = 0, d = 0, l = 0, i = arr.length; if (i) { while (i--) { d = arr[i].length; if (d > c) { l = i; c = d; } } } return arr[l].length; } function noop(s) { return s; } function section(str, arr, fn) { if (!arr || !arr.length) return ""; let i = 0, out = ""; out += (NL + __ + str); for (; i < arr.length; i++) { out += (NL + __ + __ + fn(arr[i])); } return out + NL; } export const help = function (bin, tree, key, single) { let out = ""; const cmd = tree[key], pfx = `$ ${bin}`, all = tree[ALL]; const prefix = (s) => `${pfx} ${s}`.replace(/\s+/g, " "); // update ALL & CMD options const 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 if (cmd.options.length > 0) cmd.usage += " [options]"; // description ~> text only; usage ~> prefixed out += section("Description", cmd.describe, noop); out += section("Usage", [cmd.usage], prefix); if (!single && key === DEF) { let key; const rgx = /^__/; let help = ""; const cmds = []; // General help :: print all non-(alias|internal) commands & their 1st line of helptext for (key in tree) { if (typeof tree[key] == "string" || rgx.test(key)) continue; if (cmds.push([key, (tree[key].describe || [""])[0]]) < 3) { help += (NL + __ + __ + `${pfx} ${key} --help`); } } out += section("Available Commands", format(cmds), noop); out += (NL + __ + "For more info, run any command with the `--help` flag") + help + NL; } else if (!single && key !== DEF) { // Command help :: print its aliases if any out += section("Aliases", cmd.alibi, prefix); } out += section("Options", format(cmd.options), noop); out += section("Examples", cmd.examples.map(prefix), noop); return out; }; export const error = function (bin, str, num = 1) { let out = section("ERROR", [str], noop); out += (NL + __ + `Run \`$ ${bin} --help\` for more info.` + NL); console.error(out); Deno.exit(num); }; // Strips leading `-|--` & extra space(s) export const parse = function (str) { return (str || "").split(/^-{1,2}|,|\s+-{1,2}|\s+/).filter(Boolean); }; // @see https://stackoverflow.com/a/18914855/3577474 export const sentences = function (str) { return (str || "").replace(/([.?!])\s*(?=[A-Z])/g, "$1|").split("|"); }; sade-1.8.1/index.d.ts000066400000000000000000000016521416562111400143430ustar00rootroot00000000000000import type * as mri from 'mri'; type Arrayable = T | T[]; declare function sade(usage: string, isSingle?: boolean): sade.Sade; declare namespace sade { export type Handler = (...args: any[]) => any; export type Value = number | string | boolean | null; export interface LazyOutput { name: string; handler: Handler; args: string[]; } export interface Sade { command(usage: string, description?: string, options?: { alias?: Arrayable; default?: boolean; }): Sade; option(flag: string, description?: string, value?: Value): Sade; action(handler: Handler): Sade; describe(text: Arrayable): Sade; alias(...names: string[]): Sade; example(usage: string): Sade; parse(arr: string[], opts: { lazy: true } & mri.Options): LazyOutput; parse(arr: string[], opts?: { lazy?: boolean } & mri.Options): void; version(value: string): Sade; help(cmd?: string): void; } } export = sade; sade-1.8.1/license000066400000000000000000000021421416562111400140020ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) Luke Edwards (https://lukeed.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. sade-1.8.1/package.json000066400000000000000000000014641416562111400147310ustar00rootroot00000000000000{ "name": "sade", "version": "1.8.1", "description": "Smooth (CLI) operator 🎶", "repository": "lukeed/sade", "module": "lib/index.mjs", "main": "lib/index.js", "types": "index.d.ts", "license": "MIT", "files": [ "*.d.ts", "lib" ], "author": { "name": "Luke Edwards", "email": "luke.edwards05@gmail.com", "url": "https://lukeed.com" }, "scripts": { "build": "rollup -c", "test": "tape -r esm test/*.js | tap-spec" }, "dependencies": { "mri": "^1.1.0" }, "engines": { "node": ">=6" }, "keywords": [ "cli", "cli-app", "commander", "arguments", "parser", "yargs", "argv" ], "devDependencies": { "esm": "3.2.25", "rollup": "1.32.1", "tap-spec": "4.1.2", "tape": "4.14.0", "terser": "4.8.0" } } sade-1.8.1/readme.md000066400000000000000000000472241416562111400142260ustar00rootroot00000000000000# sade [![Build Status](https://travis-ci.org/lukeed/sade.svg?branch=master)](https://travis-ci.org/lukeed/sade) > Smooth (CLI) Operator 🎶 Sade is a small but powerful tool for building command-line interface (CLI) applications for Node.js that are fast, responsive, and helpful! It enables default commands, git-like subcommands, option flags with aliases, default option values with type-casting, required-vs-optional argument handling, command validation, and automated help text generation! Your app's UX will be as smooth as butter... just like [Sade's voice](https://www.youtube.com/watch?v=4TYv2PhG89A). 😉 ## Install ``` $ npm install --save sade ``` ## Usage ***Input:*** ```js #!/usr/bin/env node const sade = require('sade'); const prog = sade('my-cli'); prog .version('1.0.5') .option('--global, -g', 'An example global flag') .option('-c, --config', 'Provide path to custom config', 'foo.config.js'); prog .command('build ') .describe('Build the source directory. Expects an `index.js` entry file.') .option('-o, --output', 'Change the name of the output file', 'bundle.js') .example('build src build --global --config my-conf.js') .example('build app public -o main.js') .action((src, dest, opts) => { console.log(`> building from ${src} to ${dest}`); console.log('> these are extra opts', opts); }); prog.parse(process.argv); ``` ***Output:*** ```a $ my-cli --help Usage $ my-cli [options] Available Commands build Build the source directory. For more info, run any command with the `--help` flag $ my-cli build --help Options -v, --version Displays current version -g, --global An example global flag -c, --config Provide path to custom config (default foo.config.js) -h, --help Displays this message $ my-cli build --help Description Build the source directory. Expects an `index.js` entry file. Usage $ my-cli build [options] Options -o, --output Change the name of the output file (default bundle.js) -g, --global An example global flag -c, --config Provide path to custom config (default foo.config.js) -h, --help Displays this message Examples $ my-cli build src build --global --config my-conf.js $ my-cli build app public -o main.js ``` ## Tips - **Define your global/program-wide version, options, description, and/or examples first.**
_Once you define a Command, you can't access the global-scope again._ - **Define all commands & options in the order that you want them to appear.**
_Sade will not mutate or sort your CLI for you. Global options print before local options._ - **Required arguments without values will error & exit**
_An `Insufficient arguments!` error will be displayed along with a help prompt._ - **Don't worry about manually displaying help~!**
_Your help text is displayed automatically... including command-specific help text!_ - **Automatic default/basic patterns**
_Usage text will always append `[options]` & `--help` and `--version` are done for you._ - **Only define what you want to display!**
_Help text sections (example, options, etc) will only display if you provide values._ ## Subcommands Subcommands are defined & parsed like any other command! When defining their [`usage`](#usage-1), everything up until the first argument (`[foo]` or ``) is interpreted as the command string. They should be defined in the order that you want them to appear in your general `--help` output. Lastly, it is _not_ necessary to define the subcommand's "base" as an additional command. However, if you choose to do so, it's recommended that you define it first for better visibility. ```js const prog = sade('git'); // Not necessary for subcommands to work, but it's here anyway! prog .command('remote') .describe('Manage set of tracked repositories') .action(opts => { console.log('~> Print current remotes...'); }); prog .command('remote add ', 'Demo...') .action((name, url, opts) => { console.log(`~> Adding a new remote (${name}) to ${url}`); }); prog .command('remote rename ', 'Demo...') .action((old, nxt, opts) => { console.log(`~> Renaming from ${old} to ${nxt}~!`); }); ``` ## 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.* > * 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 ``` ## Command Aliases Command aliases are alternative names (aliases) for a command. They are often used as shortcuts or as typo relief! The aliased names do not appear in the general help text.
Instead, they only appear within the Command-specific help text under an "Aliases" section. ***Limitations*** * You cannot assign aliases while in [Single Command Mode](#single-command-mode) * You cannot call [`prog.alias()`](#progaliasnames) before defining any Commands (via `prog.commmand()`) * You, the developer, must keep track of which aliases have already been used and/or exist as Command names ***Example*** Let's reconstruct the `npm install` command as a Sade program: ```js sade('npm') // ... .command('install [package]', 'Install a package', { alias: ['i', 'add', 'isntall'] }) .option('-P, --save-prod', 'Package will appear in your dependencies.') .option('-D, --save-dev', 'Package will appear in your devDependencies.') .option('-O, --save-optional', 'Package will appear in your optionalDependencies') .option('-E, --save-exact', 'Save exact versions instead of using a semver range operator') // ... ``` When we run `npm --help` we'll see this general help text: ``` Usage $ npm [options] Available Commands install Install a package For more info, run any command with the `--help` flag $ npm install --help Options -v, --version Displays current version -h, --help Displays this message ``` When we run `npm install --help` — ***or*** the help flag with any of `install`'s aliases — we'll see this command-specific help text: ``` Description Install a package Usage $ npm install [package] [options] Aliases $ npm i $ npm add $ npm isntall Options -P, --save-prod Package will appear in your dependencies. -D, --save-dev Package will appear in your devDependencies. -O, --save-optional Package will appear in your optionalDependencies -E, --save-exact Save exact versions instead of using a semver range operator -h, --help Displays this message ``` ## API ### sade(name, isSingle) Returns: `Program` Returns your chainable Sade instance, aka your `Program`. #### name Type: `String`
Required: `true` The name of your `Program` / binary application. #### isSingle Type: `Boolean`
Default: `name.includes(' ');` If your `Program` is meant to have ***only one command***.
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 ` 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.
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) Create a new Command for your Program. This changes the current state of your Program. All configuration methods (`prog.describe`, `prog.action`, etc) will apply to this Command until another Command has been created! #### usage Type: `String` The usage pattern for your current Command. This will be included in the general or command-specific `--help` output. _Required_ arguments are wrapped with `<` and `>` characters; for example, `` and ``. _Optional_ arguments are wrapped with `[` and `]` characters; for example, `[foo]` and `[bar]`. All arguments are ***positionally important***, which means they are passed to your current Command's [`handler`](#handler) function in the order that they were defined. When optional arguments are defined but don't receive a value, their positionally-equivalent function parameter will be `undefined`. > **Important:** You **must** define & expect required arguments _before_ optional arguments! ```js sade('foo') .command('greet ') .action((adjective, noun, opts) => { console.log(`Hello, ${adjective} ${noun}!`); }) .command('drive [color] [speed]') .action((vehicle, color, speed, opts) => { let arr = ['Driving my']; arr.push(color ? `${color} ${vehicle}` : vehicle); speed && arr.push(`at ${speed}`); opts.yolo && arr.push('...YOLO!!'); let str = arr.join(' '); console.log(str); }); ``` ```sh $ foo greet beautiful person # //=> Hello, beautiful person! $ foo drive car # //=> Driving my car $ foo drive car red # //=> Driving my red card $ foo drive car blue 100mph --yolo # //=> Driving my blue car at 100mph ...YOLO!! ``` #### desc Type: `String`
Default: `''` The Command's description. The value is passed directly to [`prog.describe`](#progdescribetext). #### opts Type: `Object`
Default: `{}` ##### opts.alias Type: `String|Array` Optionally define one or more aliases for the current Command.
When declared, the `opts.alias` value is passed _directly_ to the [`prog.alias`](#progaliasnames) method. ```js // Program A is equivalent to Program B // --- const A = sade('bin') .command('build', 'My build command', { alias: 'b' }) .command('watch', 'My watch command', { alias: ['w', 'dev'] }); const B = sade('bin') .command('build', 'My build command').alias('b') .command('watch', 'My watch command').alias('w', 'dev'); ``` ##### opts.default Type: `Boolean` Manually set/force the current Command to be the Program's default command. This ensures that the current Command will run if no command was specified. > **Important:** If you run your Program without a Command _and_ without specifying a default command, your Program will exit with a `No command specified` error. ```js const prog = sade('greet'); prog.command('hello'); //=> only runs if :: `$ greet hello` // $ greet //=> error: No command specified. prog.command('howdy', '', { default:true }); //=> runs as `$ greet` OR `$ greet howdy` // $ greet //=> runs 'howdy' handler // $ greet foobar //=> error: Invalid command ``` ### prog.describe(text) Add a description to the current Command. #### text Type: `String|Array` The description text for the current Command. This will be included in the general or command-specific `--help` output. Internally, your description will be separated into an `Array` of sentences. For general `--help` output, ***only*** the first sentence will be displayed. However, **all sentences** will be printed for command-specific `--help` text. > **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. ### prog.alias(...names) Define one or more aliases for the current Command. > **Important:** An error will be thrown if:
1) the program is in [Single Command Mode](#single-command-mode); or
2) `prog.alias` is called before any `prog.command`. #### names Type: `String` The list of alternative names (aliases) for the current Command.
For example, you may want to define shortcuts and/or common typos for the Command's full name. > **Important:** Sade _does not_ check if the incoming `names` are already in use by other Commands or their aliases.
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. 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). ```js sade('bin') .command('hello ', 'Greet someone by their name', { alias: ['hey', 'yo'] }) .alias('hi', 'howdy') .alias('hola', 'oi'); //=> hello aliases: hey, yo, hi, howdy, hola, oi ``` ### prog.action(handler) Attach a callback to the current Command. #### handler Type: `Function` The function to run when the current Command is executed. Its parameters are based (positionally) on your Command's [`usage`](#usage-1) definition. All options, flags, and extra/unknown values are included as the last parameter. > **Note:** Optional arguments are also passed as parameters & may be `undefined`! ```js sade('foo') .command('cp ') .option('-f, --force', 'Overwrite without confirmation') .option('-c, --clone-dir', 'Copy files to additional directory') .option('-v, --verbose', 'Enable verbose output') .action((src, dest, opts) => { console.log(`Copying files from ${src} --> ${dest}`); opts.c && console.log(`ALSO copying files from ${src} --> ${opts['clone-dir']}`); console.log('My options:', opts); }) // $ foo cp original my-copy -v //=> Copying files from original --> my-copy //=> My options: { _:[], v:true, verbose:true } // $ foo cp original my-copy --clone-dir my-backup //=> Copying files from original --> my-copy //=> ALSO copying files from original --> my-backup //=> My options: { _:[], c:'my-backup', 'clone-dir':'my-backup' } ``` ### prog.example(str) Add an example for the current Command. #### str Type: `String` The example string to add. This will be included in the general or command-specific `--help` output. > **Note:** Your example's `str` will be prefixed with your Program's [`name`](#sadename). ### prog.option(flags, desc, value) Add an Option to the current Command. #### flags Type: `String` The Option's flags, which may optionally include an alias. You may use a comma (`,`) or a space (` `) to separate the flags. > **Note:** The short & long flags can be declared in any order. However, the alias will always be displayed first. > **Important:** If using hyphenated flag names, they will be accessible **as declared** within your [`action()`](#progactionhandler) handler! ```js prog.option('--global'); // no alias prog.option('-g, --global'); // alias first, comma prog.option('--global -g'); // alias last, space // etc... ``` #### desc Type: `String` The description for the Option. #### value Type: `String` The **default** value for the Option. Flags and aliases, if parsed, are `true` by default. See [`mri`](https://github.com/lukeed/mri#minimist) for more info. > **Note:** You probably only want to define a default `value` if you're expecting a `String` or `Number` value type. If you _do_ pass a `String` or `Number` value type, your flag value will be casted to the same type. See [`mri#options.default`](https://github.com/lukeed/mri#optionsdefault) for info~! ### prog.version(str) The `--version` and `-v` flags will automatically output the Program version. #### str Type: `String`
Default: `0.0.0` The new version number for your Program. > **Note:** Your Program `version` is `0.0.0` until you change it. ### prog.parse(arr, opts) Parse a set of CLI arguments. #### arr Type: `Array` Your Program's `process.argv` input. > **Important:** Do not `.slice(2)`! Doing so will break parsing~! #### opts Type: `Object`
Default: `{}` Additional `process.argv` parsing config. See [`mri`'s options](https://github.com/lukeed/mri#mriargs-options) for details. > **Important:** These values _override_ any internal values! ```js prog .command('hello') .option('-f, --force', 'My flag'); //=> currently has alias pair: f <--> force prog.parse(process.argv, { alias: { f: ['foo', 'fizz'] }, default: { abc: 123 } }); //=> ADDS alias pair: f <--> foo //=> REMOVES alias pair: f <--> force //=> ADDS alias pair: f <--> fizz //=> ADDS default: abc -> 123 (number) ``` #### opts.unknown Type: `Function`
Default: `undefined` Callback to run when an unspecified option flag has been found. This is [passed directly to `mri`](https://github.com/lukeed/mri#optionsunknown). Your handler will receive the unknown flag (string) as its only argument.
You may return a string, which will be used as a custom error message. Otherwise, a default message is displayed. ```js sade('sirv') .command('start [dir]') .parse(process.argv, { unknown: arg => `Custom error message: ${arg}` }); /* $ sirv start --foobar ERROR Custom error message: --foobar Run `$ sirv --help` for more info. */ ``` #### opts.lazy Type: `Boolean`
Default: `false` If true, Sade will not immediately execute the `action` handler. Instead, `parse()` will return an object of `{ name, args, handler }` shape, wherein the `name` is the command name, `args` is all arguments that _would be_ passed to the action handler, and `handler` is the function itself. From this, you may choose when to run the `handler` function. You also have the option to further modify the `args` for any reason, if needed. ```js let { name, args, handler } = prog.parse(process.argv, { lazy:true }); console.log('> Received command: ', name); // later on... handler.apply(null, args); ``` ### prog.help(cmd) Manually display the help text for a given command. If no command name is provided, the general/global help is printed. Your general and command-specific help text is automatically attached to the `--help` and `-h` flags. > **Note:** You don't have to call this directly! It's automatically run when you `bin --help` #### cmd Type: `String`
Default: `null` The name of the command for which to display help. Otherwise displays the general help. ## License MIT © [Luke Edwards](https://lukeed.com) sade-1.8.1/rollup.config.js000066400000000000000000000012311416562111400155520ustar00rootroot00000000000000import { minify } from 'terser'; import * as pkg from './package.json'; /** * @type {import('rollup').RollupOptions} */ const config = { input: 'src/index.js', output: [{ format: 'esm', file: pkg.module, interop: false, freeze: false, strict: false }, { format: 'cjs', file: pkg.main, exports: 'default', preferConst: true, interop: false, freeze: false, strict: false }], external: [ ...Object.keys(pkg.dependencies), ...require('module').builtinModules, ], plugins: [ { name: 'terser', renderChunk(code) { return minify(code, { module: true, toplevel: false }) } } ] } export default config; sade-1.8.1/src/000077500000000000000000000000001416562111400132255ustar00rootroot00000000000000sade-1.8.1/src/index.js000066400000000000000000000115751416562111400147030ustar00rootroot00000000000000import mri from 'mri'; import * as $ from './utils'; import { ALL, DEF } from './utils'; class Sade { 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].concat(isOne ? rest : '').join(' ')); this.single = isOne; this.curr = ''; // reset } command(str, desc, opts={}) { if (this.single) { throw new Error('Disable "single" mode to add commands'); } // All non-([|<) are commands let cmd=[], usage=[], rgx=/(\[|<)/; str.split(/\s+/).forEach(x => { (rgx.test(x.charAt(0)) ? usage : cmd).push(x); }); // Back to string~! cmd = cmd.join(' '); if (cmd in this.tree) { throw new Error(`Command already exists: ${cmd}`); } // re-include `cmd` for commands cmd.includes('__') || usage.unshift(cmd); usage = usage.join(' '); // to string this.curr = cmd; if (opts.default) this.default=cmd; this.tree[cmd] = { usage, alibi:[], options:[], alias:{}, default:{}, examples:[] }; if (opts.alias) this.alias(opts.alias); if (desc) this.describe(desc); return this; } describe(str) { this.tree[this.curr || DEF].describe = Array.isArray(str) ? str : $.sentences(str); return this; } alias(...names) { if (this.single) throw new Error('Cannot call `alias()` in "single" mode'); if (!this.curr) throw new Error('Cannot call `alias()` before defining a command'); let arr = this.tree[this.curr].alibi = this.tree[this.curr].alibi.concat(...names); arr.forEach(key => this.tree[key] = this.curr); return this; } option(str, desc, val) { let cmd = this.tree[ this.curr || ALL ]; let [flag, alias] = $.parse(str); if (alias && alias.length > 1) [flag, alias]=[alias, flag]; str = `--${flag}`; if (alias && alias.length > 0) { str = `-${alias}, ${str}`; let old = cmd.alias[alias]; cmd.alias[alias] = (old || []).concat(flag); } let arr = [str, desc || '']; if (val !== void 0) { arr.push(val); cmd.default[flag] = val; } else if (!alias) { cmd.default[flag] = void 0; } cmd.options.push(arr); return this; } action(handler) { this.tree[ this.curr || DEF ].handler = handler; return this; } example(str) { this.tree[ this.curr || DEF ].examples.push(str); return this; } version(str) { this.ver = str; return this; } parse(arr, opts={}) { arr = arr.slice(); // copy let offset=2, tmp, idx, isVoid, cmd; let alias = { h:'help', v:'version' }; let argv = mri(arr.slice(offset), { alias }); let isSingle = this.single; let bin = this.bin; let name = ''; if (isSingle) { cmd = this.tree[DEF]; } else { // Loop thru possible command(s) let i=1, xyz, len=argv._.length + 1; for (; i < len; i++) { tmp = argv._.slice(0, i).join(' '); xyz = this.tree[tmp]; if (typeof xyz === 'string') { idx = (name=xyz).split(' '); arr.splice(arr.indexOf(argv._[0]), i, ...idx); i += (idx.length - i); } else if (xyz) { name = tmp; } else if (name) { break; } } 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... } } // 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 (!isSingle && cmd === void 0) { return $.error(bin, 'No command specified.'); } let all = this.tree[ALL]; // merge all objects :: params > command > all opts.alias = Object.assign(all.alias, cmd.alias, opts.alias); opts.default = Object.assign(all.default, cmd.default, opts.default); tmp = name.split(' '); idx = arr.indexOf(tmp[0], 2); if (!!~idx) arr.splice(idx, tmp.length); let vals = mri(arr.slice(offset), opts); if (!vals || typeof vals === 'string') { return $.error(bin, vals || 'Parsed unknown option flag(s)!'); } let segs = cmd.usage.split(/\s+/); let reqs = segs.filter(x => x.charAt(0)==='<'); let args = vals._.splice(0, reqs.length); if (args.length < reqs.length) { if (name) bin += ` ${name}`; // for help text return $.error(bin, 'Insufficient arguments!'); } segs.filter(x => x.charAt(0)==='[').forEach(_ => { args.push(vals._.shift()); // adds `undefined` per [slot] if no more }); args.push(vals); // flags & co are last let handler = cmd.handler; return opts.lazy ? { args, name, handler } : handler.apply(null, args); } help(str) { console.log( $.help(this.bin, this.tree, str || DEF, this.single) ); } _version() { console.log(`${this.bin}, ${this.ver}`); } } export default (str, isOne) => new Sade(str, isOne); sade-1.8.1/src/utils.js000066400000000000000000000052471416562111400147330ustar00rootroot00000000000000export const ALL = '__all__'; export const DEF = '__default__'; export const GAP = 4; export const __ = ' '; export const NL = '\n'; export function format(arr) { if (!arr.length) return ''; let len = maxLen( arr.map(x => x[0]) ) + GAP; let join = a => a[0] + ' '.repeat(len - a[0].length) + a[1] + (a[2] == null ? '' : ` (default ${a[2]})`); return arr.map(join); } export function maxLen(arr) { let c=0, d=0, l=0, i=arr.length; if (i) while (i--) { d = arr[i].length; if (d > c) { l = i; c = d; } } return arr[l].length; } export function noop(s) { return s; } export function section(str, arr, fn) { if (!arr || !arr.length) return ''; let i=0, out=''; out += (NL + __ + str); for (; i < arr.length; i++) { out += (NL + __ + __ + fn(arr[i])); } return out + NL; } export function help(bin, tree, key, single) { let out='', cmd=tree[key], pfx=`$ ${bin}`, all=tree[ALL]; let prefix = s => `${pfx} ${s}`.replace(/\s+/g, ' '); // update ALL & CMD 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 if (cmd.options.length > 0) cmd.usage += ' [options]'; // description ~> text only; usage ~> prefixed out += section('Description', cmd.describe, noop); out += section('Usage', [cmd.usage], prefix); if (!single && key === DEF) { let key, rgx=/^__/, help='', cmds=[]; // General help :: print all non-(alias|internal) commands & their 1st line of helptext for (key in tree) { if (typeof tree[key] == 'string' || rgx.test(key)) continue; if (cmds.push([key, (tree[key].describe || [''])[0]]) < 3) { help += (NL + __ + __ + `${pfx} ${key} --help`); } } out += section('Available Commands', format(cmds), noop); out += (NL + __ + 'For more info, run any command with the `--help` flag') + help + NL; } else if (!single && key !== DEF) { // Command help :: print its aliases if any out += section('Aliases', cmd.alibi, prefix); } out += section('Options', format(cmd.options), noop); out += section('Examples', cmd.examples.map(prefix), noop); return out; } export function error(bin, str, num=1) { let out = section('ERROR', [str], noop); out += (NL + __ + `Run \`$ ${bin} --help\` for more info.` + NL); console.error(out); process.exit(num); } // Strips leading `-|--` & extra space(s) export function parse(str) { return (str || '').split(/^-{1,2}|,|\s+-{1,2}|\s+/).filter(Boolean); } // @see https://stackoverflow.com/a/18914855/3577474 export function sentences(str) { return (str || '').replace(/([.?!])\s*(?=[A-Z])/g, '$1|').split('|'); } sade-1.8.1/test/000077500000000000000000000000001416562111400134155ustar00rootroot00000000000000sade-1.8.1/test/fixtures/000077500000000000000000000000001416562111400152665ustar00rootroot00000000000000sade-1.8.1/test/fixtures/alias1.js000066400000000000000000000001711416562111400167750ustar00rootroot00000000000000#!/usr/bin/env node const sade = require('../../lib'); sade('bin [dir]') .alias('error') .parse(process.argv); sade-1.8.1/test/fixtures/alias2.js000066400000000000000000000002011416562111400167700ustar00rootroot00000000000000#!/usr/bin/env node const sade = require('../../lib'); sade('bin') .alias('foo') .command('bar ') .parse(process.argv); sade-1.8.1/test/fixtures/args.js000066400000000000000000000005151416562111400165610ustar00rootroot00000000000000#!/usr/bin/env node const sade = require('../../lib'); sade('bin') .command('foo ') .alias('f') .action(dir => { console.log(`~> ran "foo" with "${dir}" arg`); }) .command('bar [dir]') .alias('b') .action(dir => { dir = dir || '~default~'; console.log(`~> ran "bar" with "${dir}" arg`); }) .parse(process.argv); sade-1.8.1/test/fixtures/basic.js000066400000000000000000000002721416562111400167060ustar00rootroot00000000000000#!/usr/bin/env node const sade = require('../../lib'); sade('bin') .command('foo') .alias('f', 'fo') .action(() => { console.log('~> ran "foo" action'); }) .parse(process.argv); sade-1.8.1/test/fixtures/default.js000066400000000000000000000004721416562111400172530ustar00rootroot00000000000000#!/usr/bin/env node const sade = require('../../lib'); sade('bin') .command('foo [dir]', null, { alias: 'f', default:true }) .action(dir => console.log(`~> ran "foo" action w/ "${dir || '~EMPTY~'}" arg`)) .command('bar') .alias('b') .action(() => console.log('~> ran "bar" action')) .parse(process.argv); sade-1.8.1/test/fixtures/options.js000066400000000000000000000013351416562111400173210ustar00rootroot00000000000000#!/usr/bin/env node const sade = require('../../lib'); sade('bin') .option('-g, --global', 'global') .command('foo') .alias('f') .option('-l, --long', 'long flag') .option('-s, --short', 'short flag') .option('-h, --hello', 'override') .action(opts => { if (opts.long) return console.log('~> ran "long" option'); if (opts.short) return console.log('~> ran "short" option'); if (opts.hello) return console.log('~> ran "hello" option'); console.log(`~> default with ${JSON.stringify(opts)}`); }) .command('bar ') .alias('b') .option('--only', 'no short alias') .action((dir, opts) => { let pre = opts.only ? '~> (only)' : '~>'; console.log(pre + ` "bar" with "${dir}" value`); }) .parse(process.argv); sade-1.8.1/test/fixtures/repeat.js000066400000000000000000000003211416562111400171000ustar00rootroot00000000000000#!/usr/bin/env node const sade = require('../../lib'); sade('bin') .command('foo', 'original') .command('foo', 'duplicate') .action(() => { console.log('~> ran "foo" action'); }) .parse(process.argv); sade-1.8.1/test/fixtures/single1.js000066400000000000000000000004561416562111400171730ustar00rootroot00000000000000#!/usr/bin/env node const sade = require('../../lib'); sade('bin [dir]') .describe('hello description') .option('-g, --global', 'flag 1') .action((type, dir, opts) => { dir = dir || '~default~'; console.log(`~> ran "single" w/ "${type}" and "${dir}" values`); }) .parse(process.argv); sade-1.8.1/test/fixtures/single2.js000066400000000000000000000003731416562111400171720ustar00rootroot00000000000000#!/usr/bin/env node const sade = require('../../lib'); sade('bin', true) .describe('hello description') .option('-g, --global', 'flag 1') .action(opts => { console.log(`~> ran "single" with: ${JSON.stringify(opts)}`); }) .parse(process.argv); sade-1.8.1/test/fixtures/single3.js000066400000000000000000000003231416562111400171660ustar00rootroot00000000000000#!/usr/bin/env node const sade = require('../../lib'); sade('bin', true) .command('foo ') .action((bar, opts) => { console.log(`~> ran "foo" with: ${JSON.stringify(opts)}`); }) .parse(process.argv); sade-1.8.1/test/fixtures/subs.js000066400000000000000000000010561416562111400166020ustar00rootroot00000000000000#!/usr/bin/env node const sade = require('../../lib'); sade('bin') .command('remote', '', { alias: 'r' }) .action(opts => { console.log('~> ran "remote" action'); }) .command('remote add ', '', { alias: ['ra', 'remote new'] }) .action((name, uri, opts) => { console.log(`~> ran "remote add" with "${name}" and "${uri}" args`); }) .command('remote rename ', '', { alias: 'rr' }) .action((old, nxt, opts) => { console.log(`~> ran "remote rename" with "${old}" and "${nxt}" args`); }) .parse(process.argv); sade-1.8.1/test/fixtures/unknown1.js000066400000000000000000000005451416562111400174100ustar00rootroot00000000000000#!/usr/bin/env node const sade = require('../../lib'); sade('bin') .option('--bool', 'flag defined') .option('-g, --global', 'global flag') .command('foo', '', { alias: 'f' }) .option('-l, --local', 'command flag') .action(opts => { console.log(`~> ran "foo" with ${JSON.stringify(opts)}`); }) .parse(process.argv, { unknown: () => false }); sade-1.8.1/test/fixtures/unknown2.js000066400000000000000000000006461416562111400174130ustar00rootroot00000000000000#!/usr/bin/env node const sade = require('../../lib'); sade('bin') .option('-g, --global', 'global flag') .option('--flag1', 'no alias or default') .command('foo', '', { alias: 'f' }) .option('-l, --local', 'command flag') .option('--flag2', 'no alias or default') .action(opts => { console.log(`~> ran "foo" with ${JSON.stringify(opts)}`); }) .parse(process.argv, { unknown: x => `Custom error: ${x}` }); sade-1.8.1/test/index.js000066400000000000000000000306451416562111400150720ustar00rootroot00000000000000import test from 'tape'; import sade from '../src'; function isShapely(t, tree, key) { t.is(typeof tree[key].usage, 'string', `~> tree[${key}].usage is a string`); t.ok(Array.isArray(tree[key].options), `~> tree[${key}].options is an array`); t.ok(Array.isArray(tree[key].examples), `~> tree[${key}].examples is an array`); t.is(typeof tree[key].default, 'object', `~> tree[${key}].default is an object`); t.is(typeof tree[key].alias, 'object', `~> tree[${key}].alias is an object`); } test('sade', t => { t.is(typeof sade, 'function', 'exports a function'); t.end(); }); test('sade()', t => { let ctx = sade('foo'); t.ok(ctx.constructor && ctx.constructor.name === 'Sade', 'returns instance of Sade'); t.is(ctx.bin, 'foo', 'sets Program name to `foo`'); t.is(ctx.ver, '0.0.0', 'defaults `ver` to `0.0.0`'); t.is(ctx.curr, '', 'is empty command-name scope'); t.is(ctx.default, '', 'has no default command (yet)'); t.is(typeof ctx.tree, 'object', 'creates a `tree` object'); let k, keys = Object.keys(ctx.tree); t.is(keys.length, 2, 'internal `tree` has two keys'); for (k in ctx.tree) { isShapely(t, ctx.tree, k); } t.end(); }); test('prog.version (global)', t => { let ctx = sade('foo').version('1.0.0'); t.is(ctx.ver, '1.0.0', 'sets a new version~!'); t.end(); }); test('prog.option (global)', t => { let ctx = sade('foo'); t.is(ctx.tree.__all__.options.length, 0, 'no global options (default)'); ctx.option('--foo, -f', 'bar', 'baz.js'); let arr = ctx.tree.__all__.options; t.is(arr.length, 1, 'adds an option successfully'); let item = arr[0]; t.ok(Array.isArray(item), 'options entry is also an array'); t.is(item.length, 3, 'entry has 3 segments (flags, desc, default)'); t.is(item[0], '-f, --foo', 'flips the flags order; alias is first'); t.end(); }); test('prog.option (hypenated)', t => { let ctx = sade('foo'); ctx.option('--foo-bar, -f'); ctx.option('--foo-bar-baz'); let arr = ctx.tree.__all__.options; t.is(arr[0][0], '-f, --foo-bar', 'keeps mid-hyphen; flips order so alias is first'); t.is(arr[1][0], '--foo-bar-baz', 'keeps all mid-hyphens'); t.end(); }); test('prog.describe (global)', t => { let ctx = sade('foo').describe('Who is on first. What is on second.'); let arr = ctx.tree.__default__.describe; t.ok(Array.isArray(arr), 'adds a `describe` array for Program info'); t.is(arr.length, 2, 'splits the description into 2 sentence items'); t.end(); }); test('prog.example (global)', t => { let ctx = sade('foo').example('hello --local'); let arr = ctx.tree.__default__.examples; t.ok(Array.isArray(arr), 'adds a `examples` array for Program info'); t.is(arr.length, 1, 'contains the single example'); t.is(arr[0], 'hello --local', 'does not manipulate contents (yet)') t.end(); }); test('prog.command', t => { let ctx = sade('foo').command('bar'); let bar = ctx.tree.bar; t.ok(bar, 'adds `bar` key to the command tree'); isShapely(t, ctx.tree, 'bar'); t.is(bar.usage, 'bar', 'stores usage as is'); // Options t.is(bar.options.length, 0, 'has no options initially'); ctx.option('-f, --force', 'force'); t.is(bar.options.length, 1, 'adds new Command option successfully'); t.deepEqual(bar.alias, { f:['force'] }, 'adds option flag & alias'); // Examples t.is(bar.examples.length, 0, 'has no examples initially'); ctx.example('bar --force'); t.is(bar.examples.length, 1, 'adds new Command exmaple successfully'); t.is(bar.examples[0], 'bar --force', 'adds example, as written'); // Description t.ok(bar.describe === void 0, 'has no description initially'); ctx.describe('hello world'); t.ok(Array.isArray(bar.describe), 'adds new Command description as Array'); t.is(bar.describe[0], 'hello world', 'stores description, as written'); // Add new Command ctx.command('quz'); let quz = ctx.tree.quz; t.ok(quz, 'adds `quz` key to the command tree'); isShapely(t, ctx.tree, 'quz'); t.is(quz.usage, 'quz', 'stores usage as is'); // Show that command state changed ctx.describe('this is quz'); t.is(quz.describe[0], 'this is quz', 'adds description for second command'); t.is(bar.describe[0], 'hello world', 'does not affect first Command'); // Add third, with description & change default ctx.command('fizz ', 'FizzBuzz', { default:true }); let fizz = ctx.tree.fizz; t.ok(fizz, 'adds `fizz` key to the command tree'); isShapely(t, ctx.tree, 'fizz'); t.is(fizz.usage, 'fizz ', 'stores usage as is'); // Add Example ctx.example('fizz 15'); t.is(fizz.examples.length, 1, 'adds new Command exmaple successfully'); t.is(fizz.examples[0], 'fizz 15', 'adds example, as written'); t.is(bar.examples.length, 1, '1st command example count unchanged'); t.is(bar.examples[0], 'bar --force', 'first command example unaffected'); t.is(quz.examples.length, 0, '2nd command example count unchanged'); t.is(ctx.default, 'fizz', 'default command was updated~!'); t.end(); }); test('prog.action', t => { t.plan(13); let a='Bob', b, c, d, e; let ctx = sade('foo') .command('greet ') .option('--loud', 'Be loud?') .option('--with-kiss, -k', 'Super friendly?') .action((name, opts) => { t.is(name, a, '~> receives the required value as first parameter'); b && t.ok(opts.loud, '~> receives the `loud` flag (true) when parsed'); c && t.ok(opts['with-kiss'], '~> receives the `with-kiss` flag (true) when parsed :: preserves mid-hyphen'); d && t.is(opts['with-kiss'], 'cheek', '~> receives the `with-kiss` flag (`cheek`) when parsed :: preserves mid-hyphen'); e && t.is(opts['with-kiss'], false, '~> receive the `--no-with-kiss` flag (false) :: preserves mid-hyphen'); b = c = d = e = false; // reset }); // Simulate `process.argv` entry let run = args => ctx.parse(['', '', 'greet', a].concat(args || [])); let cmd = ctx.tree.greet; t.ok(cmd.handler, 'added a `handler` key to the command leaf'); t.is(typeof cmd.handler, 'function', 'the `handler` is a function'); run(); // +1 test (b=true) && run('--loud'); // +2 tests (c=true) && run('--with-kiss'); // +2 tests (d=true) && run('--with-kiss=cheek'); // +2 tests (d=true) && run(['--with-kiss', 'cheek']); // +2 tests (e=true) && run('--no-with-kiss'); // +2 tests }); test('prog.action (multi requires)', t => { t.plan(7); let a='aaa', b='bbb', c=false; let ctx = sade('foo') .command('build ') .option('-f, --force', 'Force foo overwrite') .action((src, dest, opts) => { t.is(src, a, '~> receives `src` param first'); t.is(dest, b, '~> receives `dest` param second'); c && t.ok(opts.force, '~> receives the `force` flag (true) when parsed'); c && t.ok(opts.f, '~> receives the `f` alias (true) when parsed'); }); t.is(ctx.tree.build.usage, 'build ', 'writes all required params to usage'); let run = _ => ctx.parse(['', '', 'build', a, b, c && '-f']); run(); // +2 tests (c=true) && run(); // +4 tests }); test('prog.action (multi optional)', t => { t.plan(7); let a='aaa', b='bbb', c=false; let ctx = sade('foo') .command('build [src] [dest]') .option('-f, --force', 'Force foo overwrite') .action((src, dest, opts) => { t.is(src, a, '~> receives `src` param first'); t.is(dest, b, '~> receives `dest` param second'); c && t.ok(opts.force, '~> receives the `force` flag (true) when parsed'); c && t.ok(opts.f, '~> receives the `f` alias (true) when parsed'); }); t.is(ctx.tree.build.usage, 'build [src] [dest]', 'writes all positional params to usage'); let run = _ => ctx.parse(['', '', 'build', a, b, c && '-f']); run(); // +2 tests (c=true) && run(); // +4 tests }); test('prog.parse :: safe :: default', t => { let ctx = sade('foo').command('build', '', { default: true }); let argv1 = ['', '', 'build']; let foo = ctx.parse(argv1, { lazy: true }); t.deepEqual(argv1, ['', '', 'build'], '~> argv unchanged'); t.deepEqual(foo.args, [{ _: [] }], '~> args correct'); let argv2 = ['', '']; let bar = ctx.parse(argv2, { lazy: true }); t.deepEqual(argv2, ['', ''], '~> argv unchanged'); t.deepEqual(bar.args, [{ _: [] }], '~> args correct'); t.end(); }); test('prog.parse :: safe :: alias', t => { let ctx = sade('foo').command('build').alias('b'); let argv1 = ['', '', 'build']; let foo = ctx.parse(argv1, { lazy: true }); t.deepEqual(argv1, ['', '', 'build'], '~> argv unchanged'); t.deepEqual(foo.args, [{ _: [] }], '~> args correct'); let argv2 = ['', '', 'b']; let bar = ctx.parse(argv2, { lazy: true }); t.deepEqual(argv2, ['', '', 'b'], '~> argv unchanged'); t.deepEqual(bar.args, [{ _: [] }], '~> args correct'); t.end(); }); test('prog.parse :: safe :: default :: flags', t => { let ctx = sade('foo').command('build ', '', { default: true }); let argv1 = ['', '', '-r', 'dotenv', 'build', 'public', '--fresh']; let foo = ctx.parse(argv1, { lazy: true }); t.deepEqual(argv1, ['', '', '-r', 'dotenv', 'build', 'public', '--fresh'], '~> argv unchanged'); t.deepEqual(foo.args, ['public', { _: [], r: 'dotenv', fresh: true }], '~> args correct'); let argv2 = ['', '', '-r', 'dotenv', 'public', '--fresh']; let bar = ctx.parse(argv2, { lazy: true }); t.deepEqual(argv2, ['', '', '-r', 'dotenv', 'public', '--fresh'], '~> argv unchanged'); t.deepEqual(bar.args, ['public', { _: [], r: 'dotenv', fresh: true }], '~> args correct'); t.end(); }); test('prog.parse :: safe :: alias :: flags', t => { let ctx = sade('foo').command('build ').alias('b'); let argv1 = ['', '', '-r', 'dotenv', 'build', 'public', '--fresh']; let foo = ctx.parse(argv1, { lazy: true }); t.deepEqual(argv1, ['', '', '-r', 'dotenv', 'build', 'public', '--fresh'], '~> argv unchanged'); t.deepEqual(foo.args, ['public', { _: [], r: 'dotenv', fresh: true }], '~> args correct'); let argv2 = ['', '', '-r', 'dotenv', 'b', 'public', '--fresh']; let bar = ctx.parse(argv2, { lazy: true }); t.deepEqual(argv2, ['', '', '-r', 'dotenv', 'b', 'public', '--fresh'], '~> argv unchanged'); t.deepEqual(bar.args, ['public', { _: [], r: 'dotenv', fresh: true }], '~> args correct'); t.end(); }); test('prog.parse :: lazy', t => { t.plan(14); let val='aaa', f=false; let ctx = sade('foo') .command('build ') .option('--force').action((src, opts) => { t.is(src, val, '~> receives `src` param first'); f && t.ok(opts.force, '~> receives the `force` flag (true) when parsed'); }); let run = _ => ctx.parse(['', '', 'build', val, f && '--force'], { lazy:true }); let foo = run(); t.is(foo.constructor, Object, 'returns an object'); t.same(Object.keys(foo), ['args', 'name', 'handler'], 'contains `args`,`name`,`handler` keys'); t.ok(Array.isArray(foo.args), '~> returns the array of arguments'); t.is(foo.args[0], val, '~> preserves the `src` value first'); t.is(foo.args[1].constructor, Object, '~> preserves the `opts` value last'); t.ok(Array.isArray(foo.args[1]._), '~> ensures `opts._` is still `[]` at least'); t.is(typeof foo.handler, 'function', '~> returns the action handler'); t.is(foo.name, 'build', '~> returns the command name'); foo.handler.apply(null, foo.args); // must be manual bcuz lazy; +1 test let bar = run(f=true); t.is(bar.constructor, Object, 'returns an object'); t.is(bar.args[1].constructor, Object, '~> preserves the `opts` value last'); t.is(bar.args[1].force, true, '~> attaches the `force:true` option'); bar.handler.apply(null, bar.args); // manual bcuz lazy; +2 tests }); test('prog.parse :: lazy :: single', t => { t.plan(14); let val='aaa', f=false; let ctx = sade('foo ').option('--force').action((src, opts) => { t.is(src, val, '~> receives `src` param first'); f && t.ok(opts.force, '~> receives the `force` flag (true) when parsed'); }); let run = _ => ctx.parse(['', '', val, f && '--force'], { lazy:true }); let foo = run(); t.is(foo.constructor, Object, 'returns an object'); t.same(Object.keys(foo), ['args', 'name', 'handler'], 'contains `args`,`name`,`handler` keys'); t.ok(Array.isArray(foo.args), '~> returns the array of arguments'); t.is(foo.args[0], val, '~> preserves the `src` value first'); t.is(foo.args[1].constructor, Object, '~> preserves the `opts` value last'); t.ok(Array.isArray(foo.args[1]._), '~> ensures `opts._` is still `[]` at least'); t.is(typeof foo.handler, 'function', '~> returns the action handler'); t.is(foo.name, '', '~> returns empty command name'); foo.handler.apply(null, foo.args); // must be manual bcuz lazy; +1 test let bar = run(f=true); t.is(bar.constructor, Object, 'returns an object'); t.is(bar.args[1].constructor, Object, '~> preserves the `opts` value last'); t.is(bar.args[1].force, true, '~> attaches the `force:true` option'); bar.handler.apply(null, bar.args); // manual bcuz lazy; +2 tests }); sade-1.8.1/test/usage.js000066400000000000000000001257131416562111400150700ustar00rootroot00000000000000import test from 'tape'; import { join } from 'path'; import { spawnSync } from 'child_process'; const fixtures = join(__dirname, 'fixtures'); function exec(file, argv=[]) { return spawnSync('node', [file, ...argv], { cwd:fixtures }); } test('(usage) basic', t => { let pid = exec('basic.js', ['foo']); t.is(pid.status, 0, 'exits without error code'); t.is(pid.stderr.length, 0, '~> stderr is empty'); t.is(pid.stdout.toString(), '~> ran "foo" action\n', '~> command invoked'); t.end(); }); test('(usage) basic :: error :: missing command', t => { let pid = exec('basic.js'); t.is(pid.status, 1, 'exits with error code'); t.is( pid.stderr.toString(), '\n ERROR\n No command specified.\n\n Run `$ bin --help` for more info.\n\n', '~> stderr has "No command specified" error message' ); t.is(pid.stdout.length, 0, '~> stdout is empty'); t.end(); }); test('(usage) basic :: error :: invalid command', t => { let pid = exec('basic.js', ['foobar']); t.is(pid.status, 1, 'exits with error code'); t.is( pid.stderr.toString(), '\n ERROR\n Invalid command: foobar\n\n Run `$ bin --help` for more info.\n\n', '~> stderr has "Invalid command: foobar" error message' ); t.is(pid.stdout.length, 0, '~> stdout is empty'); t.end(); }); test('(usage) basic :: error :: duplicate command', t => { let pid = exec('repeat.js', ['foo']); t.is(pid.status, 1, 'exits with error code'); t.is(pid.stdout.length, 0, '~> stdout is empty'); // throws an error in the process t.true(pid.stderr.toString().includes('Error: Command already exists: foo'), '~> threw Error w/ message'); t.end(); }); test('(usage) basic :: help', t => { let pid1 = exec('basic.js', ['-h']); t.is(pid1.status, 0, 'exits with error code'); t.true(pid1.stdout.toString().includes('Available Commands\n foo'), '~> shows global help w/ "Available Commands" text'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); let pid2 = exec('basic.js', ['foo', '-h']); t.is(pid2.status, 0, 'exits with error code'); t.true(pid2.stdout.toString().includes('Usage\n $ bin foo [options]'), '~> shows command help w/ "Usage" text'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) args.required', t => { let pid = exec('args.js', ['foo', 'value']); t.is(pid.status, 0, 'exits without error code'); t.is(pid.stdout.toString(), '~> ran "foo" with "value" arg\n', '~> command invoked'); t.is(pid.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) args.required :: error :: missing argument', t => { let pid = exec('args.js', ['foo']); t.is(pid.status, 1, 'exits with error code'); t.is( pid.stderr.toString(), '\n ERROR\n Insufficient arguments!\n\n Run `$ bin foo --help` for more info.\n\n', '~> stderr has "Insufficient arguments!" error message' ); t.is(pid.stdout.length, 0, '~> stdout is empty'); t.end(); }); test('(usage) args.optional', t => { let pid = exec('args.js', ['bar']); t.is(pid.status, 0, 'exits without error code'); t.is(pid.stdout.toString(), '~> ran "bar" with "~default~" arg\n', '~> command invoked'); t.is(pid.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) args.optional w/ value', t => { let pid = exec('args.js', ['bar', 'value']); t.is(pid.status, 0, 'exits without error code'); t.is(pid.stdout.toString(), '~> ran "bar" with "value" arg\n', '~> command invoked'); t.is(pid.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) options.long', t => { let pid1 = exec('options.js', ['foo', '--long']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stdout.toString(), '~> ran "long" option\n', '~> command invoked'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); let pid2 = exec('options.js', ['foo', '-l']); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stdout.toString(), '~> ran "long" option\n', '~> command invoked'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) options.short', t => { let pid1 = exec('options.js', ['foo', '--short']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stdout.toString(), '~> ran "short" option\n', '~> command invoked'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); let pid2 = exec('options.js', ['foo', '-s']); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stdout.toString(), '~> ran "short" option\n', '~> command invoked'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) options.hello', t => { let pid1 = exec('options.js', ['foo', '--hello']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stdout.toString(), '~> ran "hello" option\n', '~> command invoked'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); // shows that '-h' is always reserved let pid2 = exec('options.js', ['foo', '-h']); let stdout = pid2.stdout.toString(); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); t.not(stdout, '~> ran "long" option\n', '~> did NOT run custom "-h" option'); t.true(stdout.includes('-h, --help Displays this message'), '~~> shows `--help` text'); t.end(); }); test('(usage) options.extra', t => { let pid = exec('options.js', ['foo', '--extra=opts', '--404']); t.is(pid.status, 0, 'exits without error code'); t.is(pid.stdout.toString(), '~> default with {"404":true,"_":[],"extra":"opts"}\n', '~> command invoked'); t.is(pid.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) options.global', t => { let pid1 = exec('options.js', ['foo', '--global']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stdout.toString(), '~> default with {"_":[],"global":true,"g":true}\n', '~> command invoked'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); let pid2 = exec('options.js', ['foo', '-g', 'hello']); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stdout.toString(), '~> default with {"_":[],"g":"hello","global":"hello"}\n', '~> command invoked'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) options w/o alias', t => { let pid1 = exec('options.js', ['bar', 'hello']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); t.is(pid1.stdout.toString(), '~> "bar" with "hello" value\n', '~> command invoked'); let pid2 = exec('options.js', ['bar', 'hello', '--only']); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); t.is(pid2.stdout.toString(), '~> (only) "bar" with "hello" value\n', '~> command invoked'); let pid3 = exec('options.js', ['bar', 'hello', '-o']); t.is(pid3.status, 0, 'exits without error code'); t.is(pid3.stderr.length, 0, '~> stderr is empty'); t.is(pid3.stdout.toString(), '~> "bar" with "hello" value\n', '~> command invoked'); t.end(); }); test('(usage) unknown', t => { let pid1 = exec('unknown1.js', ['foo', '--global']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); t.is(pid1.stdout.toString(), '~> ran "foo" with {"_":[],"global":true,"g":true}\n', '~> command invoked'); let pid2 = exec('unknown1.js', ['foo', '-l']); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); t.is(pid2.stdout.toString(), '~> ran "foo" with {"_":[],"l":true,"local":true}\n', '~> command invoked'); let pid3 = exec('unknown1.js', ['foo', '--bar']); t.is(pid3.status, 1, 'exits with error code'); t.is(pid3.stdout.length, 0, '~> stdout is empty'); t.is( pid3.stderr.toString(), '\n ERROR\n Parsed unknown option flag(s)!\n\n Run `$ bin --help` for more info.\n\n', '~> stderr has "Parsed unknown option flag" error message (default)' ); t.end(); }); test('(usage) unknown.custom', t => { let pid1 = exec('unknown2.js', ['foo', '--global', '--local']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); t.is(pid1.stdout.toString(), '~> ran "foo" with {"_":[],"global":true,"local":true,"g":true,"l":true}\n', '~> command invoked'); let pid2 = exec('unknown2.js', ['foo', '--bar']); t.is(pid2.status, 1, 'exits with error code'); t.is(pid2.stdout.length, 0, '~> stdout is empty'); t.is( pid2.stderr.toString(), '\n ERROR\n Custom error: --bar\n\n Run `$ bin --help` for more info.\n\n', '~> stderr has "Custom error: --bar" error message' ); t.end(); }); test('(usage) unknown.plain', t => { let pid1 = exec('unknown2.js', ['foo', '--flag1', '--flag2']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); t.is(pid1.stdout.toString(), '~> ran "foo" with {"_":[],"flag1":true,"flag2":true}\n', '~> command invoked'); let pid2 = exec('unknown2.js', ['foo', '--flag3']); t.is(pid2.status, 1, 'exits with error code'); t.is(pid2.stdout.length, 0, '~> stdout is empty'); t.is( pid2.stderr.toString(), '\n ERROR\n Custom error: --flag3\n\n Run `$ bin --help` for more info.\n\n', '~> stderr has "Custom error: --flag3" error message' ); t.end(); }); test('(usage) subcommands', t => { let pid1 = exec('subs.js', ['remote']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stdout.toString(), '~> ran "remote" action\n', '~> ran parent'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); let pid2 = exec('subs.js', ['remote', 'rename', 'origin', 'foobar']); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stdout.toString(), '~> ran "remote rename" with "origin" and "foobar" args\n', '~> ran "rename" child'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); let pid3 = exec('subs.js', ['remote', 'add', 'origin', 'foobar']); t.is(pid3.status, 0, 'exits without error code'); t.is(pid3.stdout.toString(), '~> ran "remote add" with "origin" and "foobar" args\n', '~> ran "add" child'); t.is(pid3.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) subcommands :: help', t => { let pid1 = exec('subs.js', ['--help']); t.is(pid1.status, 0, 'exits without error code'); t.true(pid1.stdout.toString().includes('Available Commands\n remote \n remote add \n remote rename'), '~> shows global help w/ "Available Commands" text'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); let pid2 = exec('subs.js', ['remote', '--help']); t.is(pid2.status, 0, 'exits without error code'); t.true(pid2.stdout.toString().includes('Usage\n $ bin remote [options]'), '~> shows "remote" help text'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); let pid3 = exec('subs.js', ['remote', 'rename', '--help']); t.is(pid3.status, 0, 'exits without error code'); t.true(pid3.stdout.toString().includes('Usage\n $ bin remote rename [options]'), '~> shows "remote rename" help text'); t.is(pid3.stderr.length, 0, '~> stderr is empty'); t.end(); }); /* // TODO: Should this happen instead? test('(usage) subcommands :: error :: invalid command', t => { let pid = exec('subs.js', ['remote', 'foobar']); t.is(pid.status, 1, 'exits with error code'); t.is(pid.stdout.length, 0, '~> stdout is empty'); t.is( pid.stderr.toString(), '\n ERROR\n Invalid command: remote foobar.\n\n Run `$ bin --help` for more info.\n\n', '~> stderr has "Invalid command: remote foobar" error message' ); t.end(); }); */ test('(usage) default', t => { let pid1 = exec('default.js', []); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stdout.toString(), '~> ran "foo" action w/ "~EMPTY~" arg\n', '~> ran default command'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); let pid2 = exec('default.js', ['foo']); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stdout.toString(), '~> ran "foo" action w/ "~EMPTY~" arg\n', '~> ran default command (direct)'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); let pid3 = exec('default.js', ['bar']); t.is(pid3.status, 0, 'exits without error code'); t.is(pid3.stdout.toString(), '~> ran "bar" action\n', '~> ran "bar" command'); t.is(pid3.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) default :: args', t => { let pid1 = exec('default.js', ['hello']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stdout.toString(), '~> ran "foo" action w/ "hello" arg\n'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); let pid2 = exec('default.js', ['foo', 'hello']); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stdout.toString(), '~> ran "foo" action w/ "hello" arg\n', '~> ran default command (direct)'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) default :: help', t => { let pid1 = exec('default.js', ['--help']); t.is(pid1.status, 0, 'exits without error code'); t.true(pid1.stdout.toString().includes('Available Commands\n foo \n bar'), '~> shows global help w/ "Available Commands" text'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); let pid2 = exec('default.js', ['foo', '-h']); t.is(pid2.status, 0, 'exits without error code'); t.true(pid2.stdout.toString().includes('Usage\n $ bin foo [dir] [options]'), '~> shows command help w/ "Usage" text'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); let pid3 = exec('default.js', ['bar', '-h']); t.is(pid3.status, 0, 'exits without error code'); t.true(pid3.stdout.toString().includes('Usage\n $ bin bar [options]'), '~> shows command help w/ "Usage" text'); t.is(pid3.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) single :: error :: missing argument', t => { let pid = exec('single1.js', []); t.is(pid.status, 1, 'exits with error code'); t.is(pid.stdout.length, 0, '~> stdout is empty'); t.is( pid.stderr.toString(), '\n ERROR\n Insufficient arguments!\n\n Run `$ bin --help` for more info.\n\n', '~> stderr has "Insufficient arguments!" error message' ); t.end(); }); test('(usage) single', t => { let pid1 = exec('single1.js', ['type']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stdout.toString(), '~> ran "single" w/ "type" and "~default~" values\n', '~> ran single command'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); let pid2 = exec('single1.js', ['type', 'dir']); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stdout.toString(), '~> ran "single" w/ "type" and "dir" values\n', '~> ran single command'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) single is catch all', t => { let pid1 = exec('single2.js', ['type']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stdout.toString(), `~> ran "single" with: {"_":["type"]}\n`, '~> ran single command'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); let pid2 = exec('single2.js', ['type', 'dir', '--global']); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stdout.toString(), `~> ran "single" with: {"_":["type","dir"],"global":true,"g":true}\n`, '~> ran single command'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) single :: command() throws', t => { let pid = exec('single3.js', ['foo']); t.is(pid.status, 1, 'exits with error code'); t.is(pid.stdout.length, 0, '~> stdout is empty'); // throws an error in the process t.true(pid.stderr.toString().includes('Error: Disable "single" mode to add commands'), '~> threw Error w/ message'); t.end(); }); test('(usage) single :: help', t => { let pid1 = exec('single1.js', ['--help']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); t.false(pid1.stdout.toString().includes('Available Commands'), '~> global help does NOT show "Available Commands" text'); t.false(pid1.stdout.toString().includes('run any command with the `--help` flag'), '~> global help does NOT show "run any command with the `--help` flag" text'); let pid2 = exec('single1.js', ['--help']); t.is(pid2.status, 0, 'exits without error code'); t.true(pid2.stdout.toString().includes('Usage\n $ bin [dir] [options]'), '~> shows single-command help w/ "Usage" text'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); let pid3 = exec('single1.js', ['bar', '--help']); t.is(pid3.status, 0, 'exits without error code'); t.true(pid3.stdout.toString().includes('Usage\n $ bin [dir] [options]'), '~> shows single-command help w/ "Usage" text'); t.is(pid3.stderr.length, 0, '~> stderr is empty'); t.end(); }); // --- // Command Aliases // --- test('(usage) alias :: basic', t => { let pid1 = exec('basic.js', ['f']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); t.is(pid1.stdout.toString(), '~> ran "foo" action\n', '~> command invoked'); let pid2 = exec('basic.js', ['fo']); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); t.is(pid2.stdout.toString(), '~> ran "foo" action\n', '~> command invoked'); t.end(); }); test('(usage) alias :: basic :: error :: invalid command', t => { let pid = exec('basic.js', ['fff']); t.is(pid.status, 1, 'exits with error code'); t.is( pid.stderr.toString(), '\n ERROR\n Invalid command: fff\n\n Run `$ bin --help` for more info.\n\n', '~> stderr has "Invalid command: fff" error message' ); t.is(pid.stdout.length, 0, '~> stdout is empty'); t.end(); }); test('(usage) alias :: basic :: help', t => { let pid1 = exec('basic.js', ['-h']); t.is(pid1.status, 0, 'exits with error code'); t.true(pid1.stdout.toString().includes('Available Commands\n foo'), '~> shows global help w/ "Available Commands" text'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); let pid2 = exec('basic.js', ['f', '-h']); t.is(pid2.status, 0, 'exits with error code'); t.true(pid2.stdout.toString().includes('Usage\n $ bin foo [options]'), '~> shows command help w/ "Usage" text'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) alias :: args.required', t => { let pid = exec('args.js', ['f', 'value']); t.is(pid.status, 0, 'exits without error code'); t.is(pid.stdout.toString(), '~> ran "foo" with "value" arg\n', '~> command invoked'); t.is(pid.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) alias :: args.required :: error :: missing argument', t => { let pid = exec('args.js', ['f']); t.is(pid.status, 1, 'exits with error code'); t.is( pid.stderr.toString(), '\n ERROR\n Insufficient arguments!\n\n Run `$ bin foo --help` for more info.\n\n', '~> stderr has "Insufficient arguments!" error message' ); t.is(pid.stdout.length, 0, '~> stdout is empty'); t.end(); }); test('(usage) alias :: args.optional', t => { let pid = exec('args.js', ['b']); t.is(pid.status, 0, 'exits without error code'); t.is(pid.stdout.toString(), '~> ran "bar" with "~default~" arg\n', '~> command invoked'); t.is(pid.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) alias :: args.optional w/ value', t => { let pid = exec('args.js', ['b', 'value']); t.is(pid.status, 0, 'exits without error code'); t.is(pid.stdout.toString(), '~> ran "bar" with "value" arg\n', '~> command invoked'); t.is(pid.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) alias :: options.long', t => { let pid1 = exec('options.js', ['f', '--long']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stdout.toString(), '~> ran "long" option\n', '~> command invoked'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); let pid2 = exec('options.js', ['f', '-l']); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stdout.toString(), '~> ran "long" option\n', '~> command invoked'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) alias :: options.short', t => { let pid1 = exec('options.js', ['f', '--short']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stdout.toString(), '~> ran "short" option\n', '~> command invoked'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); let pid2 = exec('options.js', ['f', '-s']); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stdout.toString(), '~> ran "short" option\n', '~> command invoked'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) alias :: options.hello', t => { let pid1 = exec('options.js', ['f', '--hello']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stdout.toString(), '~> ran "hello" option\n', '~> command invoked'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); // shows that '-h' is always reserved let pid2 = exec('options.js', ['f', '-h']); let stdout = pid2.stdout.toString(); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); t.not(stdout, '~> ran "long" option\n', '~> did NOT run custom "-h" option'); t.true(stdout.includes('-h, --help Displays this message'), '~~> shows `--help` text'); t.end(); }); test('(usage) alias :: options.extra', t => { let pid = exec('options.js', ['f', '--extra=opts', '--404']); t.is(pid.status, 0, 'exits without error code'); t.is(pid.stdout.toString(), '~> default with {"404":true,"_":[],"extra":"opts"}\n', '~> command invoked'); t.is(pid.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) alias :: options.global', t => { let pid1 = exec('options.js', ['f', '--global']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stdout.toString(), '~> default with {"_":[],"global":true,"g":true}\n', '~> command invoked'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); let pid2 = exec('options.js', ['f', '-g', 'hello']); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stdout.toString(), '~> default with {"_":[],"g":"hello","global":"hello"}\n', '~> command invoked'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) alias :: options w/o alias', t => { let pid1 = exec('options.js', ['b', 'hello']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); t.is(pid1.stdout.toString(), '~> "bar" with "hello" value\n', '~> command invoked'); let pid2 = exec('options.js', ['b', 'hello', '--only']); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); t.is(pid2.stdout.toString(), '~> (only) "bar" with "hello" value\n', '~> command invoked'); let pid3 = exec('options.js', ['b', 'hello', '-o']); t.is(pid3.status, 0, 'exits without error code'); t.is(pid3.stderr.length, 0, '~> stderr is empty'); t.is(pid3.stdout.toString(), '~> "bar" with "hello" value\n', '~> command invoked'); t.end(); }); test('(usage) alias :: unknown', t => { let pid1 = exec('unknown1.js', ['f', '--global']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); t.is(pid1.stdout.toString(), '~> ran "foo" with {"_":[],"global":true,"g":true}\n', '~> command invoked'); let pid2 = exec('unknown1.js', ['f', '-l']); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); t.is(pid2.stdout.toString(), '~> ran "foo" with {"_":[],"l":true,"local":true}\n', '~> command invoked'); let pid3 = exec('unknown1.js', ['f', '--bar']); t.is(pid3.status, 1, 'exits with error code'); t.is(pid3.stdout.length, 0, '~> stdout is empty'); t.is( pid3.stderr.toString(), '\n ERROR\n Parsed unknown option flag(s)!\n\n Run `$ bin --help` for more info.\n\n', '~> stderr has "Parsed unknown option flag" error message (default)' ); t.end(); }); test('(usage) alias :: unknown.custom', t => { let pid1 = exec('unknown2.js', ['f', '--global', '--local']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); t.is(pid1.stdout.toString(), '~> ran "foo" with {"_":[],"global":true,"local":true,"g":true,"l":true}\n', '~> command invoked'); let pid2 = exec('unknown2.js', ['f', '--bar']); t.is(pid2.status, 1, 'exits with error code'); t.is(pid2.stdout.length, 0, '~> stdout is empty'); t.is( pid2.stderr.toString(), '\n ERROR\n Custom error: --bar\n\n Run `$ bin --help` for more info.\n\n', '~> stderr has "Custom error: --bar" error message' ); t.end(); }); test('(usage) alias :: unknown.plain', t => { let pid1 = exec('unknown2.js', ['f', '--flag1', '--flag2']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); t.is(pid1.stdout.toString(), '~> ran "foo" with {"_":[],"flag1":true,"flag2":true}\n', '~> command invoked'); let pid2 = exec('unknown2.js', ['f', '--flag3']); t.is(pid2.status, 1, 'exits with error code'); t.is(pid2.stdout.length, 0, '~> stdout is empty'); t.is( pid2.stderr.toString(), '\n ERROR\n Custom error: --flag3\n\n Run `$ bin --help` for more info.\n\n', '~> stderr has "Custom error: --flag3" error message' ); t.end(); }); test('(usage) alias :: subcommands', t => { let pid1 = exec('subs.js', ['r']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stdout.toString(), '~> ran "remote" action\n', '~> ran parent'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); let pid2 = exec('subs.js', ['rr', 'origin', 'foobar']); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stdout.toString(), '~> ran "remote rename" with "origin" and "foobar" args\n', '~> ran "rename" child'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); let pid3 = exec('subs.js', ['ra', 'origin', 'foobar']); t.is(pid3.status, 0, 'exits without error code'); t.is(pid3.stdout.toString(), '~> ran "remote add" with "origin" and "foobar" args\n', '~> ran "add" child'); t.is(pid3.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) alias :: subcommands :: help', t => { let pid1 = exec('subs.js', ['--help']); t.is(pid1.status, 0, 'exits without error code'); t.true(pid1.stdout.toString().includes('Available Commands\n remote \n remote add \n remote rename'), '~> shows global help w/ "Available Commands" text'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); let pid2 = exec('subs.js', ['r', '--help']); t.is(pid2.status, 0, 'exits without error code'); t.true(pid2.stdout.toString().includes('Usage\n $ bin remote [options]'), '~> shows "remote" help text'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); let pid3 = exec('subs.js', ['rr', '--help']); t.is(pid3.status, 0, 'exits without error code'); t.true(pid3.stdout.toString().includes('Usage\n $ bin remote rename [options]'), '~> shows "remote rename" help text'); t.is(pid3.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) alias :: default', t => { let pid1 = exec('default.js', []); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stdout.toString(), '~> ran "foo" action w/ "~EMPTY~" arg\n', '~> ran default command'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); let pid2 = exec('default.js', ['f']); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stdout.toString(), '~> ran "foo" action w/ "~EMPTY~" arg\n', '~> ran default command (direct)'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); let pid3 = exec('default.js', ['f', 'hello']); t.is(pid3.status, 0, 'exits without error code'); t.is(pid3.stdout.toString(), '~> ran "foo" action w/ "hello" arg\n', '~> ran default command (direct)'); t.is(pid3.stderr.length, 0, '~> stderr is empty'); let pid4 = exec('default.js', ['b']); t.is(pid4.status, 0, 'exits without error code'); t.is(pid4.stdout.toString(), '~> ran "bar" action\n', '~> ran "bar" command'); t.is(pid4.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) default :: args', t => { let pid1 = exec('default.js', ['hello']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stdout.toString(), '~> ran "foo" action w/ "hello" arg\n'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); let pid2 = exec('default.js', ['f', 'hello']); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stdout.toString(), '~> ran "foo" action w/ "hello" arg\n', '~> ran default command (direct)'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) alias :: default :: help', t => { let pid1 = exec('default.js', ['--help']); t.is(pid1.status, 0, 'exits without error code'); t.true(pid1.stdout.toString().includes('Available Commands\n foo \n bar'), '~> shows global help w/ "Available Commands" text'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); let pid2 = exec('default.js', ['f', '-h']); t.is(pid2.status, 0, 'exits without error code'); t.true(pid2.stdout.toString().includes('Usage\n $ bin foo [dir] [options]'), '~> shows command help w/ "Usage" text'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); let pid3 = exec('default.js', ['b', '-h']); t.is(pid3.status, 0, 'exits without error code'); t.true(pid3.stdout.toString().includes('Usage\n $ bin bar [options]'), '~> shows command help w/ "Usage" text'); t.is(pid3.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) alias :: single :: throws', t => { let pid = exec('alias1.js'); t.is(pid.status, 1, 'exits with error code'); t.is(pid.stdout.length, 0, '~> stdout is empty'); // throws an error in the process t.true(pid.stderr.toString().includes('Error: Cannot call `alias()` in "single" mode'), '~> threw Error w/ message'); t.end(); }); test('(usage) alias :: pre-command :: throws', t => { let pid = exec('alias2.js'); t.is(pid.status, 1, 'exits with error code'); t.is(pid.stdout.length, 0, '~> stdout is empty'); // throws an error in the process t.true(pid.stderr.toString().includes('Error: Cannot call `alias()` before defining a command'), '~> threw Error w/ message'); t.end(); }); // --- // Input Order // --- test('(usage) order :: basic', t => { let pid1 = exec('basic.js', ['--foo', 'bar', 'f']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); t.is(pid1.stdout.toString(), '~> ran "foo" action\n', '~> command invoked'); let pid2 = exec('basic.js', ['--foo', 'bar', 'fo']); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); t.is(pid2.stdout.toString(), '~> ran "foo" action\n', '~> command invoked'); t.end(); }); test('(usage) order :: basic :: error :: invalid command', t => { let pid = exec('basic.js', ['--foo', 'bar', 'fff']); t.is(pid.status, 1, 'exits with error code'); t.is( pid.stderr.toString(), '\n ERROR\n Invalid command: fff\n\n Run `$ bin --help` for more info.\n\n', '~> stderr has "Invalid command: fff" error message' ); t.is(pid.stdout.length, 0, '~> stdout is empty'); t.end(); }); test('(usage) order :: basic :: help', t => { let pid1 = exec('basic.js', ['--foo', 'bar', '-h']); t.is(pid1.status, 0, 'exits with error code'); t.true(pid1.stdout.toString().includes('Available Commands\n foo'), '~> shows global help w/ "Available Commands" text'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); let pid2 = exec('basic.js', ['--foo', 'bar', 'f', '-h']); t.is(pid2.status, 0, 'exits with error code'); t.true(pid2.stdout.toString().includes('Usage\n $ bin foo [options]'), '~> shows command help w/ "Usage" text'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) order :: args.required', t => { let pid = exec('args.js', ['--foo', 'bar', 'f', 'value']); t.is(pid.status, 0, 'exits without error code'); t.is(pid.stdout.toString(), '~> ran "foo" with "value" arg\n', '~> command invoked'); t.is(pid.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) order :: args.required :: error :: missing argument', t => { let pid = exec('args.js', ['--foo', 'bar', 'f']); t.is(pid.status, 1, 'exits with error code'); t.is( pid.stderr.toString(), '\n ERROR\n Insufficient arguments!\n\n Run `$ bin foo --help` for more info.\n\n', '~> stderr has "Insufficient arguments!" error message' ); t.is(pid.stdout.length, 0, '~> stdout is empty'); t.end(); }); test('(usage) order :: args.optional', t => { let pid = exec('args.js', ['--foo', 'bar', 'b']); t.is(pid.status, 0, 'exits without error code'); t.is(pid.stdout.toString(), '~> ran "bar" with "~default~" arg\n', '~> command invoked'); t.is(pid.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) order :: args.optional w/ value', t => { let pid = exec('args.js', ['--foo', 'bar', 'b', 'value']); t.is(pid.status, 0, 'exits without error code'); t.is(pid.stdout.toString(), '~> ran "bar" with "value" arg\n', '~> command invoked'); t.is(pid.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) order :: options.long', t => { let pid1 = exec('options.js', ['--foo', 'bar', 'f', '--long']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stdout.toString(), '~> ran "long" option\n', '~> command invoked'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); let pid2 = exec('options.js', ['--foo', 'bar', 'f', '-l']); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stdout.toString(), '~> ran "long" option\n', '~> command invoked'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) order :: options.short', t => { let pid1 = exec('options.js', ['--foo', 'bar', 'f', '--short']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stdout.toString(), '~> ran "short" option\n', '~> command invoked'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); let pid2 = exec('options.js', ['--foo', 'bar', 'f', '-s']); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stdout.toString(), '~> ran "short" option\n', '~> command invoked'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) order :: options.hello', t => { let pid1 = exec('options.js', ['--foo', 'bar', 'f', '--hello']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stdout.toString(), '~> ran "hello" option\n', '~> command invoked'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); // shows that '-h' is always reserved let pid2 = exec('options.js', ['--foo', 'bar', 'f', '-h']); let stdout = pid2.stdout.toString(); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); t.not(stdout, '~> ran "long" option\n', '~> did NOT run custom "-h" option'); t.true(stdout.includes('-h, --help Displays this message'), '~~> shows `--help` text'); t.end(); }); test('(usage) order :: options.extra', t => { let pid = exec('options.js', ['--foo', 'bar', 'f', '--extra=opts', '--404']); t.is(pid.status, 0, 'exits without error code'); t.is(pid.stdout.toString(), '~> default with {"404":true,"_":[],"foo":"bar","extra":"opts"}\n', '~> command invoked'); t.is(pid.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) order :: options.global', t => { let pid1 = exec('options.js', ['--foo', 'bar', 'f', '--global']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stdout.toString(), '~> default with {"_":[],"foo":"bar","global":true,"g":true}\n', '~> command invoked'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); let pid2 = exec('options.js', ['--foo', 'bar', 'f', '-g', 'hello']); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stdout.toString(), '~> default with {"_":[],"foo":"bar","g":"hello","global":"hello"}\n', '~> command invoked'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) order :: options w/o alias', t => { let pid1 = exec('options.js', ['--foo', 'bar', 'b', 'hello']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); t.is(pid1.stdout.toString(), '~> "bar" with "hello" value\n', '~> command invoked'); let pid2 = exec('options.js', ['--foo', 'bar', 'b', 'hello', '--only']); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); t.is(pid2.stdout.toString(), '~> (only) "bar" with "hello" value\n', '~> command invoked'); let pid3 = exec('options.js', ['--foo', 'bar', 'b', 'hello', '-o']); t.is(pid3.status, 0, 'exits without error code'); t.is(pid3.stderr.length, 0, '~> stderr is empty'); t.is(pid3.stdout.toString(), '~> "bar" with "hello" value\n', '~> command invoked'); t.end(); }); test('(usage) order :: unknown.custom', t => { let pid1 = exec('unknown2.js', ['f', '--global', '--local']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); t.is(pid1.stdout.toString(), '~> ran "foo" with {"_":[],"global":true,"local":true,"g":true,"l":true}\n', '~> command invoked'); let pid2 = exec('unknown2.js', ['--foo', 'bar', 'f', '--bar']); t.is(pid2.status, 1, 'exits with error code'); t.is(pid2.stdout.length, 0, '~> stdout is empty'); t.is( pid2.stderr.toString(), '\n ERROR\n Custom error: --foo\n\n Run `$ bin --help` for more info.\n\n', // came first '~> stderr has "Custom error: --foo" error message' // came first ); t.end(); }); test('(usage) order :: unknown.plain', t => { let pid1 = exec('unknown2.js', ['f', '--flag1', '--flag2']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); t.is(pid1.stdout.toString(), '~> ran "foo" with {"_":[],"flag1":true,"flag2":true}\n', '~> command invoked'); let pid2 = exec('unknown2.js', ['--foo', 'bar', 'f', '--flag3']); t.is(pid2.status, 1, 'exits with error code'); t.is(pid2.stdout.length, 0, '~> stdout is empty'); t.is( pid2.stderr.toString(), '\n ERROR\n Custom error: --foo\n\n Run `$ bin --help` for more info.\n\n', // came first '~> stderr has "Custom error: --foo" error message' // came first ); t.end(); }); test('(usage) order :: subcommands', t => { let pid1 = exec('subs.js', ['--foo', 'bar', 'r']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stdout.toString(), '~> ran "remote" action\n', '~> ran parent'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); let pid2 = exec('subs.js', ['--foo', 'bar', 'rr', 'origin', 'foobar']); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stdout.toString(), '~> ran "remote rename" with "origin" and "foobar" args\n', '~> ran "rename" child'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); let pid3 = exec('subs.js', ['--foo', 'bar', 'ra', 'origin', 'foobar']); t.is(pid3.status, 0, 'exits without error code'); t.is(pid3.stdout.toString(), '~> ran "remote add" with "origin" and "foobar" args\n', '~> ran "add" child'); t.is(pid3.stderr.length, 0, '~> stderr is empty'); let pid4 = exec('subs.js', ['--foo', 'bar', 'remote', 'new', 'origin', 'foobar']); t.is(pid4.status, 0, 'exits without error code'); t.is(pid4.stdout.toString(), '~> ran "remote add" with "origin" and "foobar" args\n', '~> ran "add" child'); t.is(pid4.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) order :: subcommands :: help', t => { let pid1 = exec('subs.js', ['--foo', 'bar', '--help']); t.is(pid1.status, 0, 'exits without error code'); t.true(pid1.stdout.toString().includes('Available Commands\n remote \n remote add \n remote rename'), '~> shows global help w/ "Available Commands" text'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); let pid2 = exec('subs.js', ['--foo', 'bar', 'r', '--help']); t.is(pid2.status, 0, 'exits without error code'); t.true(pid2.stdout.toString().includes('Usage\n $ bin remote [options]'), '~> shows "remote" help text'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); let pid3 = exec('subs.js', ['--foo', 'bar', 'rr', '--help']); t.is(pid3.status, 0, 'exits without error code'); t.true(pid3.stdout.toString().includes('Usage\n $ bin remote rename [options]'), '~> shows "remote rename" help text'); t.is(pid3.stderr.length, 0, '~> stderr is empty'); let pid4 = exec('subs.js', ['--foo', 'bar', 'remote', 'new', '--help']); t.is(pid4.status, 0, 'exits without error code'); t.is(pid4.stdout.toString(), '\n Usage\n $ bin remote add [options]\n\n Aliases\n $ bin ra\n $ bin remote new\n\n Options\n -h, --help Displays this message\n\n'); t.is(pid4.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) order :: default', t => { let pid1 = exec('default.js', ['--foo', 'bar']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stdout.toString(), '~> ran "foo" action w/ "~EMPTY~" arg\n', '~> ran default command'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); let pid2 = exec('default.js', ['--foo', 'bar', 'f']); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stdout.toString(), '~> ran "foo" action w/ "~EMPTY~" arg\n', '~> ran default command (direct)'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); let pid3 = exec('default.js', ['--foo', 'bar', 'b']); t.is(pid3.status, 0, 'exits without error code'); t.is(pid3.stdout.toString(), '~> ran "bar" action\n', '~> ran "bar" command'); t.is(pid3.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) default :: args', t => { let pid1 = exec('default.js', ['--foo', 'bar', 'hello']); t.is(pid1.status, 0, 'exits without error code'); t.is(pid1.stdout.toString(), '~> ran "foo" action w/ "hello" arg\n'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); let pid2 = exec('default.js', ['--foo', 'bar', 'foo', 'hello']); t.is(pid2.status, 0, 'exits without error code'); t.is(pid2.stdout.toString(), '~> ran "foo" action w/ "hello" arg\n', '~> ran default command (direct)'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); let pid3 = exec('default.js', ['--foo', 'bar', 'foo', 'hello']); t.is(pid3.status, 0, 'exits without error code'); t.is(pid3.stdout.toString(), '~> ran "foo" action w/ "hello" arg\n', '~> ran default command (direct)'); t.is(pid3.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) order :: default :: help', t => { let pid1 = exec('default.js', ['--foo', 'bar', '--help']); t.is(pid1.status, 0, 'exits without error code'); t.true(pid1.stdout.toString().includes('Available Commands\n foo \n bar'), '~> shows global help w/ "Available Commands" text'); t.is(pid1.stderr.length, 0, '~> stderr is empty'); let pid2 = exec('default.js', ['--foo', 'bar', 'f', '-h']); t.is(pid2.status, 0, 'exits without error code'); t.true(pid2.stdout.toString().includes('Usage\n $ bin foo [dir] [options]'), '~> shows command help w/ "Usage" text'); t.is(pid2.stderr.length, 0, '~> stderr is empty'); let pid3 = exec('default.js', ['--foo', 'bar', 'b', '-h']); t.is(pid3.status, 0, 'exits without error code'); t.true(pid3.stdout.toString().includes('Usage\n $ bin bar [options]'), '~> shows command help w/ "Usage" text'); t.is(pid3.stderr.length, 0, '~> stderr is empty'); t.end(); }); test('(usage) order :: single :: throws', t => { let pid = exec('alias1.js', ['--foo', 'bar']); t.is(pid.status, 1, 'exits with error code'); t.is(pid.stdout.length, 0, '~> stdout is empty'); // throws an error in the process t.true(pid.stderr.toString().includes('Error: Cannot call `alias()` in "single" mode'), '~> threw Error w/ message'); t.end(); }); test('(usage) order :: pre-command :: throws', t => { let pid = exec('alias2.js', ['--foo', 'bar']); t.is(pid.status, 1, 'exits with error code'); t.is(pid.stdout.length, 0, '~> stdout is empty'); // throws an error in the process t.true(pid.stderr.toString().includes('Error: Cannot call `alias()` before defining a command'), '~> threw Error w/ message'); t.end(); }); sade-1.8.1/test/utils.js000066400000000000000000000076061416562111400151240ustar00rootroot00000000000000import test from 'tape'; import sade from '../src/index'; import * as $ from '../src/utils'; test('utils.parse', t => { [ ['--foo', ['foo']], ['-foo', ['foo']], ['-f', ['f']], ['--foo-bar-baz', ['foo-bar-baz']], ['--foo--bar', ['foo--bar']], ['--foo-bar', ['foo-bar']], ['-f, --foo', ['f', 'foo']], ['--foo, -f', ['foo', 'f']], ['--foo-bar, -f', ['foo-bar', 'f']], [' -f , --foo ', ['f', 'foo']], [' --foo-bar , -f ', ['foo-bar', 'f']], ['-f --foo', ['f', 'foo']], ['--foo -f', ['foo', 'f']], ['--foo-bar -f', ['foo-bar', 'f']], ].forEach(arr => { t.same($.parse(arr[0]), arr[1], `(${arr[0]}) ~~> [${arr[1]}]`); }); t.end(); }); test('utils.sentences', t => { [ ['foo bar', ['foo bar']], // no . or cap ['foo. bar', ['foo. bar']], // no capital ['foo. Bar', ['foo.', 'Bar']], // has capital ['I haz $125.00 money. Hello', ['I haz $125.00 money.', 'Hello']], ['Hello. World!', ['Hello.', 'World!']] // trims ].forEach(arr => { t.same($.sentences(arr[0]), arr[1], `(${arr[0]}) ~~> [${arr[1]}]`); }); t.end(); }); test('utils.help', t => { let { bin, tree } = sade('foo').describe('global foo').command('bar', 'Hello. World.').command('fizz '); let foo = $.help(bin, tree, '__default__'); // global, 1 or 0 lines of desc per command t.is(foo, '\n Description\n global foo\n\n Usage\n $ foo [options]\n\n Available Commands\n bar Hello.\n fizz \n\n For more info, run any command with the `--help` flag\n $ foo bar --help\n $ foo fizz --help\n\n Options\n -v, --version Displays current version\n -h, --help Displays this message\n'); let bar = $.help(bin, tree, 'bar'); // two-line description t.is(bar, '\n Description\n Hello.\n World.\n\n Usage\n $ foo bar [options]\n\n Options\n -h, --help Displays this message\n'); let fizz = $.help(bin, tree, 'fizz'); // no description t.is(fizz, '\n Usage\n $ foo fizz [options]\n\n Options\n -h, --help Displays this message\n'); t.end(); }); test('utils.help :: single', t => { let { bin, tree } = sade('foo [baz]', true).describe('global foo').option('-p, --port', 'Custom port value', 8000); let text = $.help(bin, tree, '__default__', true); t.is(text, '\n Description\n global foo\n\n Usage\n $ foo [baz] [options]\n\n Options\n -p, --port Custom port value (default 8000)\n -v, --version Displays current version\n -h, --help Displays this message\n'); t.end(); }); test('utils.help :: alias', t => { let { bin, tree } = ( sade('bin') .describe('program description') .command('foo', 'Hello, foo!', { alias: 'f' }) .command('bar ', 'Heya, bar!', { alias: ['b', 'ba'] }) .command('baz ', 'Howdy, baz~!') .alias('bz', 'bb', 'bza') ); let txt = $.help(bin, tree, '__default__'); t.is(txt, '\n Description\n program description\n\n Usage\n $ bin [options]\n\n Available Commands\n foo Hello, foo!\n bar Heya, bar!\n baz Howdy, baz~!\n\n For more info, run any command with the `--help` flag\n $ bin foo --help\n $ bin bar --help\n\n Options\n -v, --version Displays current version\n -h, --help Displays this message\n'); let foo = $.help(bin, tree, 'foo'); t.is(foo, '\n Description\n Hello, foo!\n\n Usage\n $ bin foo [options]\n\n Aliases\n $ bin f\n\n Options\n -h, --help Displays this message\n'); let bar = $.help(bin, tree, 'bar'); t.is(bar, '\n Description\n Heya, bar!\n\n Usage\n $ bin bar [options]\n\n Aliases\n $ bin b\n $ bin ba\n\n Options\n -h, --help Displays this message\n'); let baz = $.help(bin, tree, 'baz'); t.is(baz, '\n Description\n Howdy, baz~!\n\n Usage\n $ bin baz [options]\n\n Aliases\n $ bin bz\n $ bin bb\n $ bin bza\n\n Options\n -h, --help Displays this message\n'); t.end(); });