├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── test ├── fixtures │ ├── alias1.js │ ├── alias2.js │ ├── basic.js │ ├── repeat.js │ ├── single3.js │ ├── single2.js │ ├── single1.js │ ├── default.js │ ├── args.js │ ├── unknown1.js │ ├── unknown2.js │ ├── subs.js │ └── options.js ├── utils.js ├── index.js └── usage.js ├── .editorconfig ├── rollup.config.js ├── package.json ├── index.d.ts ├── license ├── src ├── utils.js └── index.js ├── deno ├── utils.js └── mod.js └── readme.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: lukeed 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | *-lock.* 4 | *.lock 5 | *.log 6 | 7 | /lib 8 | -------------------------------------------------------------------------------- /test/fixtures/alias1.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const sade = require('../../lib'); 3 | 4 | sade('bin [dir]') 5 | .alias('error') 6 | .parse(process.argv); 7 | -------------------------------------------------------------------------------- /test/fixtures/alias2.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const sade = require('../../lib'); 3 | 4 | sade('bin') 5 | .alias('foo') 6 | .command('bar ') 7 | .parse(process.argv); 8 | -------------------------------------------------------------------------------- /test/fixtures/basic.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const sade = require('../../lib'); 3 | 4 | sade('bin') 5 | .command('foo') 6 | .alias('f', 'fo') 7 | .action(() => { 8 | console.log('~> ran "foo" action'); 9 | }) 10 | .parse(process.argv); 11 | -------------------------------------------------------------------------------- /test/fixtures/repeat.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const sade = require('../../lib'); 3 | 4 | sade('bin') 5 | .command('foo', 'original') 6 | .command('foo', 'duplicate') 7 | .action(() => { 8 | console.log('~> ran "foo" action'); 9 | }) 10 | .parse(process.argv); 11 | -------------------------------------------------------------------------------- /test/fixtures/single3.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const sade = require('../../lib'); 3 | 4 | sade('bin', true) 5 | .command('foo ') 6 | .action((bar, opts) => { 7 | console.log(`~> ran "foo" with: ${JSON.stringify(opts)}`); 8 | }) 9 | .parse(process.argv); 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = tab 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.{json,yml,md}] 13 | indent_style = space 14 | -------------------------------------------------------------------------------- /test/fixtures/single2.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const sade = require('../../lib'); 3 | 4 | sade('bin', true) 5 | .describe('hello description') 6 | .option('-g, --global', 'flag 1') 7 | .action(opts => { 8 | console.log(`~> ran "single" with: ${JSON.stringify(opts)}`); 9 | }) 10 | .parse(process.argv); 11 | -------------------------------------------------------------------------------- /test/fixtures/single1.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const sade = require('../../lib'); 3 | 4 | sade('bin [dir]') 5 | .describe('hello description') 6 | .option('-g, --global', 'flag 1') 7 | .action((type, dir, opts) => { 8 | dir = dir || '~default~'; 9 | console.log(`~> ran "single" w/ "${type}" and "${dir}" values`); 10 | }) 11 | .parse(process.argv); 12 | -------------------------------------------------------------------------------- /test/fixtures/default.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const sade = require('../../lib'); 3 | 4 | sade('bin') 5 | .command('foo [dir]', null, { alias: 'f', default:true }) 6 | .action(dir => console.log(`~> ran "foo" action w/ "${dir || '~EMPTY~'}" arg`)) 7 | 8 | .command('bar') 9 | .alias('b') 10 | .action(() => console.log('~> ran "bar" action')) 11 | 12 | .parse(process.argv); 13 | -------------------------------------------------------------------------------- /test/fixtures/args.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const sade = require('../../lib'); 3 | 4 | sade('bin') 5 | .command('foo ') 6 | .alias('f') 7 | .action(dir => { 8 | console.log(`~> ran "foo" with "${dir}" arg`); 9 | }) 10 | .command('bar [dir]') 11 | .alias('b') 12 | .action(dir => { 13 | dir = dir || '~default~'; 14 | console.log(`~> ran "bar" with "${dir}" arg`); 15 | }) 16 | .parse(process.argv); 17 | -------------------------------------------------------------------------------- /test/fixtures/unknown1.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const sade = require('../../lib'); 3 | 4 | sade('bin') 5 | .option('--bool', 'flag defined') 6 | .option('-g, --global', 'global flag') 7 | 8 | .command('foo', '', { alias: 'f' }) 9 | .option('-l, --local', 'command flag') 10 | .action(opts => { 11 | console.log(`~> ran "foo" with ${JSON.stringify(opts)}`); 12 | }) 13 | 14 | .parse(process.argv, { 15 | unknown: () => false 16 | }); 17 | -------------------------------------------------------------------------------- /test/fixtures/unknown2.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const sade = require('../../lib'); 3 | 4 | sade('bin') 5 | .option('-g, --global', 'global flag') 6 | .option('--flag1', 'no alias or default') 7 | 8 | .command('foo', '', { alias: 'f' }) 9 | .option('-l, --local', 'command flag') 10 | .option('--flag2', 'no alias or default') 11 | .action(opts => { 12 | console.log(`~> ran "foo" with ${JSON.stringify(opts)}`); 13 | }) 14 | 15 | .parse(process.argv, { 16 | unknown: x => `Custom error: ${x}` 17 | }); 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Node.js v${{ matrix.nodejs }} 8 | runs-on: ubuntu-latest 9 | timeout-minutes: 3 10 | strategy: 11 | matrix: 12 | nodejs: [6, 8, 10, 12, 14] 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: ${{ matrix.nodejs }} 18 | 19 | - run: npm install 20 | - run: npm run build 21 | - run: npm test 22 | -------------------------------------------------------------------------------- /test/fixtures/subs.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const sade = require('../../lib'); 3 | 4 | sade('bin') 5 | .command('remote', '', { alias: 'r' }) 6 | .action(opts => { 7 | console.log('~> ran "remote" action'); 8 | }) 9 | 10 | .command('remote add ', '', { alias: ['ra', 'remote new'] }) 11 | .action((name, uri, opts) => { 12 | console.log(`~> ran "remote add" with "${name}" and "${uri}" args`); 13 | }) 14 | 15 | .command('remote rename ', '', { alias: 'rr' }) 16 | .action((old, nxt, opts) => { 17 | console.log(`~> ran "remote rename" with "${old}" and "${nxt}" args`); 18 | }) 19 | 20 | .parse(process.argv); 21 | -------------------------------------------------------------------------------- /test/fixtures/options.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const sade = require('../../lib'); 3 | 4 | sade('bin') 5 | .option('-g, --global', 'global') 6 | .command('foo') 7 | .alias('f') 8 | .option('-l, --long', 'long flag') 9 | .option('-s, --short', 'short flag') 10 | .option('-h, --hello', 'override') 11 | .action(opts => { 12 | if (opts.long) return console.log('~> ran "long" option'); 13 | if (opts.short) return console.log('~> ran "short" option'); 14 | if (opts.hello) return console.log('~> ran "hello" option'); 15 | console.log(`~> default with ${JSON.stringify(opts)}`); 16 | }) 17 | 18 | .command('bar ') 19 | .alias('b') 20 | .option('--only', 'no short alias') 21 | .action((dir, opts) => { 22 | let pre = opts.only ? '~> (only)' : '~>'; 23 | console.log(pre + ` "bar" with "${dir}" value`); 24 | }) 25 | .parse(process.argv); 26 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { minify } from 'terser'; 2 | import * as pkg from './package.json'; 3 | 4 | /** 5 | * @type {import('rollup').RollupOptions} 6 | */ 7 | const config = { 8 | input: 'src/index.js', 9 | output: [{ 10 | format: 'esm', 11 | file: pkg.module, 12 | interop: false, 13 | freeze: false, 14 | strict: false 15 | }, { 16 | format: 'cjs', 17 | file: pkg.main, 18 | exports: 'default', 19 | preferConst: true, 20 | interop: false, 21 | freeze: false, 22 | strict: false 23 | }], 24 | external: [ 25 | ...Object.keys(pkg.dependencies), 26 | ...require('module').builtinModules, 27 | ], 28 | plugins: [ 29 | { 30 | name: 'terser', 31 | renderChunk(code) { 32 | return minify(code, { 33 | module: true, 34 | toplevel: false 35 | }) 36 | } 37 | } 38 | ] 39 | } 40 | 41 | export default config; 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sade", 3 | "version": "1.8.1", 4 | "description": "Smooth (CLI) operator 🎶", 5 | "repository": "lukeed/sade", 6 | "module": "lib/index.mjs", 7 | "main": "lib/index.js", 8 | "types": "index.d.ts", 9 | "license": "MIT", 10 | "files": [ 11 | "*.d.ts", 12 | "lib" 13 | ], 14 | "author": { 15 | "name": "Luke Edwards", 16 | "email": "luke.edwards05@gmail.com", 17 | "url": "https://lukeed.com" 18 | }, 19 | "scripts": { 20 | "build": "rollup -c", 21 | "test": "tape -r esm test/*.js | tap-spec" 22 | }, 23 | "dependencies": { 24 | "mri": "^1.1.0" 25 | }, 26 | "engines": { 27 | "node": ">=6" 28 | }, 29 | "keywords": [ 30 | "cli", 31 | "cli-app", 32 | "commander", 33 | "arguments", 34 | "parser", 35 | "yargs", 36 | "argv" 37 | ], 38 | "devDependencies": { 39 | "esm": "3.2.25", 40 | "rollup": "1.32.1", 41 | "tap-spec": "4.1.2", 42 | "tape": "4.14.0", 43 | "terser": "4.8.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import type * as mri from 'mri'; 2 | 3 | type Arrayable = T | T[]; 4 | 5 | declare function sade(usage: string, isSingle?: boolean): sade.Sade; 6 | 7 | declare namespace sade { 8 | export type Handler = (...args: any[]) => any; 9 | export type Value = number | string | boolean | null; 10 | 11 | export interface LazyOutput { 12 | name: string; 13 | handler: Handler; 14 | args: string[]; 15 | } 16 | 17 | export interface Sade { 18 | command(usage: string, description?: string, options?: { 19 | alias?: Arrayable; 20 | default?: boolean; 21 | }): Sade; 22 | 23 | option(flag: string, description?: string, value?: Value): Sade; 24 | action(handler: Handler): Sade; 25 | describe(text: Arrayable): Sade; 26 | alias(...names: string[]): Sade; 27 | example(usage: string): Sade; 28 | 29 | parse(arr: string[], opts: { lazy: true } & mri.Options): LazyOutput; 30 | parse(arr: string[], opts?: { lazy?: boolean } & mri.Options): void; 31 | 32 | version(value: string): Sade; 33 | help(cmd?: string): void; 34 | } 35 | } 36 | 37 | export = sade; 38 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Luke Edwards (https://lukeed.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export const ALL = '__all__'; 2 | export const DEF = '__default__'; 3 | 4 | export const GAP = 4; 5 | export const __ = ' '; 6 | export const NL = '\n'; 7 | 8 | export function format(arr) { 9 | if (!arr.length) return ''; 10 | let len = maxLen( arr.map(x => x[0]) ) + GAP; 11 | let join = a => a[0] + ' '.repeat(len - a[0].length) + a[1] + (a[2] == null ? '' : ` (default ${a[2]})`); 12 | return arr.map(join); 13 | } 14 | 15 | export function maxLen(arr) { 16 | let c=0, d=0, l=0, i=arr.length; 17 | if (i) while (i--) { 18 | d = arr[i].length; 19 | if (d > c) { 20 | l = i; c = d; 21 | } 22 | } 23 | return arr[l].length; 24 | } 25 | 26 | export function noop(s) { 27 | return s; 28 | } 29 | 30 | export function section(str, arr, fn) { 31 | if (!arr || !arr.length) return ''; 32 | let i=0, out=''; 33 | out += (NL + __ + str); 34 | for (; i < arr.length; i++) { 35 | out += (NL + __ + __ + fn(arr[i])); 36 | } 37 | return out + NL; 38 | } 39 | 40 | export function help(bin, tree, key, single) { 41 | let out='', cmd=tree[key], pfx=`$ ${bin}`, all=tree[ALL]; 42 | let prefix = s => `${pfx} ${s}`.replace(/\s+/g, ' '); 43 | 44 | // update ALL & CMD options 45 | let tail = [['-h, --help', 'Displays this message']]; 46 | if (key === DEF) tail.unshift(['-v, --version', 'Displays current version']); 47 | cmd.options = (cmd.options || []).concat(all.options, tail); 48 | 49 | // write options placeholder 50 | if (cmd.options.length > 0) cmd.usage += ' [options]'; 51 | 52 | // description ~> text only; usage ~> prefixed 53 | out += section('Description', cmd.describe, noop); 54 | out += section('Usage', [cmd.usage], prefix); 55 | 56 | if (!single && key === DEF) { 57 | let key, rgx=/^__/, help='', cmds=[]; 58 | // General help :: print all non-(alias|internal) commands & their 1st line of helptext 59 | for (key in tree) { 60 | if (typeof tree[key] == 'string' || rgx.test(key)) continue; 61 | if (cmds.push([key, (tree[key].describe || [''])[0]]) < 3) { 62 | help += (NL + __ + __ + `${pfx} ${key} --help`); 63 | } 64 | } 65 | 66 | out += section('Available Commands', format(cmds), noop); 67 | out += (NL + __ + 'For more info, run any command with the `--help` flag') + help + NL; 68 | } else if (!single && key !== DEF) { 69 | // Command help :: print its aliases if any 70 | out += section('Aliases', cmd.alibi, prefix); 71 | } 72 | 73 | out += section('Options', format(cmd.options), noop); 74 | out += section('Examples', cmd.examples.map(prefix), noop); 75 | 76 | return out; 77 | } 78 | 79 | export function error(bin, str, num=1) { 80 | let out = section('ERROR', [str], noop); 81 | out += (NL + __ + `Run \`$ ${bin} --help\` for more info.` + NL); 82 | console.error(out); 83 | process.exit(num); 84 | } 85 | 86 | // Strips leading `-|--` & extra space(s) 87 | export function parse(str) { 88 | return (str || '').split(/^-{1,2}|,|\s+-{1,2}|\s+/).filter(Boolean); 89 | } 90 | 91 | // @see https://stackoverflow.com/a/18914855/3577474 92 | export function sentences(str) { 93 | return (str || '').replace(/([.?!])\s*(?=[A-Z])/g, '$1|').split('|'); 94 | } 95 | -------------------------------------------------------------------------------- /deno/utils.js: -------------------------------------------------------------------------------- 1 | const GAP = 4; 2 | const __ = " "; 3 | const ALL = "__all__"; 4 | const DEF = "__default__"; 5 | const NL = "\n"; 6 | 7 | function format(arr) { 8 | if (!arr.length) return ""; 9 | const len = maxLen(arr.map((x) => x[0])) + GAP; 10 | const join = (a) => 11 | a[0] + " ".repeat(len - a[0].length) + a[1] + 12 | (a[2] == null ? "" : ` (default ${a[2]})`); 13 | return arr.map(join); 14 | } 15 | 16 | function maxLen(arr) { 17 | let c = 0, d = 0, l = 0, i = arr.length; 18 | if (i) { 19 | while (i--) { 20 | d = arr[i].length; 21 | if (d > c) { 22 | l = i; 23 | c = d; 24 | } 25 | } 26 | } 27 | return arr[l].length; 28 | } 29 | 30 | function noop(s) { 31 | return s; 32 | } 33 | 34 | function section(str, arr, fn) { 35 | if (!arr || !arr.length) return ""; 36 | let i = 0, out = ""; 37 | out += (NL + __ + str); 38 | for (; i < arr.length; i++) { 39 | out += (NL + __ + __ + fn(arr[i])); 40 | } 41 | return out + NL; 42 | } 43 | 44 | export const help = function (bin, tree, key, single) { 45 | let out = ""; 46 | const cmd = tree[key], pfx = `$ ${bin}`, all = tree[ALL]; 47 | const prefix = (s) => `${pfx} ${s}`.replace(/\s+/g, " "); 48 | 49 | // update ALL & CMD options 50 | const tail = [["-h, --help", "Displays this message"]]; 51 | if (key === DEF) tail.unshift(["-v, --version", "Displays current version"]); 52 | cmd.options = (cmd.options || []).concat(all.options, tail); 53 | 54 | // write options placeholder 55 | if (cmd.options.length > 0) cmd.usage += " [options]"; 56 | 57 | // description ~> text only; usage ~> prefixed 58 | out += section("Description", cmd.describe, noop); 59 | out += section("Usage", [cmd.usage], prefix); 60 | 61 | if (!single && key === DEF) { 62 | let key; 63 | const rgx = /^__/; 64 | let help = ""; 65 | const cmds = []; 66 | 67 | // General help :: print all non-(alias|internal) commands & their 1st line of helptext 68 | for (key in tree) { 69 | if (typeof tree[key] == "string" || rgx.test(key)) continue; 70 | if (cmds.push([key, (tree[key].describe || [""])[0]]) < 3) { 71 | help += (NL + __ + __ + `${pfx} ${key} --help`); 72 | } 73 | } 74 | 75 | out += section("Available Commands", format(cmds), noop); 76 | out += (NL + __ + "For more info, run any command with the `--help` flag") + 77 | help + NL; 78 | } else if (!single && key !== DEF) { 79 | // Command help :: print its aliases if any 80 | out += section("Aliases", cmd.alibi, prefix); 81 | } 82 | 83 | out += section("Options", format(cmd.options), noop); 84 | out += section("Examples", cmd.examples.map(prefix), noop); 85 | 86 | return out; 87 | }; 88 | 89 | export const error = function (bin, str, num = 1) { 90 | let out = section("ERROR", [str], noop); 91 | out += (NL + __ + `Run \`$ ${bin} --help\` for more info.` + NL); 92 | console.error(out); 93 | Deno.exit(num); 94 | }; 95 | 96 | // Strips leading `-|--` & extra space(s) 97 | export const parse = function (str) { 98 | return (str || "").split(/^-{1,2}|,|\s+-{1,2}|\s+/).filter(Boolean); 99 | }; 100 | 101 | // @see https://stackoverflow.com/a/18914855/3577474 102 | export const sentences = function (str) { 103 | return (str || "").replace(/([.?!])\s*(?=[A-Z])/g, "$1|").split("|"); 104 | }; 105 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | import test from 'tape'; 2 | import sade from '../src/index'; 3 | import * as $ from '../src/utils'; 4 | 5 | test('utils.parse', t => { 6 | [ 7 | ['--foo', ['foo']], 8 | ['-foo', ['foo']], 9 | ['-f', ['f']], 10 | ['--foo-bar-baz', ['foo-bar-baz']], 11 | ['--foo--bar', ['foo--bar']], 12 | ['--foo-bar', ['foo-bar']], 13 | ['-f, --foo', ['f', 'foo']], 14 | ['--foo, -f', ['foo', 'f']], 15 | ['--foo-bar, -f', ['foo-bar', 'f']], 16 | [' -f , --foo ', ['f', 'foo']], 17 | [' --foo-bar , -f ', ['foo-bar', 'f']], 18 | ['-f --foo', ['f', 'foo']], 19 | ['--foo -f', ['foo', 'f']], 20 | ['--foo-bar -f', ['foo-bar', 'f']], 21 | ].forEach(arr => { 22 | t.same($.parse(arr[0]), arr[1], `(${arr[0]}) ~~> [${arr[1]}]`); 23 | }); 24 | t.end(); 25 | }); 26 | 27 | test('utils.sentences', t => { 28 | [ 29 | ['foo bar', ['foo bar']], // no . or cap 30 | ['foo. bar', ['foo. bar']], // no capital 31 | ['foo. Bar', ['foo.', 'Bar']], // has capital 32 | ['I haz $125.00 money. Hello', ['I haz $125.00 money.', 'Hello']], 33 | ['Hello. World!', ['Hello.', 'World!']] // trims 34 | ].forEach(arr => { 35 | t.same($.sentences(arr[0]), arr[1], `(${arr[0]}) ~~> [${arr[1]}]`); 36 | }); 37 | t.end(); 38 | }); 39 | 40 | test('utils.help', t => { 41 | let { bin, tree } = sade('foo').describe('global foo').command('bar', 'Hello. World.').command('fizz '); 42 | 43 | let foo = $.help(bin, tree, '__default__'); // global, 1 or 0 lines of desc per command 44 | 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'); 45 | 46 | let bar = $.help(bin, tree, 'bar'); // two-line description 47 | 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'); 48 | 49 | let fizz = $.help(bin, tree, 'fizz'); // no description 50 | t.is(fizz, '\n Usage\n $ foo fizz [options]\n\n Options\n -h, --help Displays this message\n'); 51 | 52 | t.end(); 53 | }); 54 | 55 | test('utils.help :: single', t => { 56 | let { bin, tree } = sade('foo [baz]', true).describe('global foo').option('-p, --port', 'Custom port value', 8000); 57 | 58 | let text = $.help(bin, tree, '__default__', true); 59 | 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'); 60 | 61 | t.end(); 62 | }); 63 | 64 | test('utils.help :: alias', t => { 65 | let { bin, tree } = ( 66 | sade('bin') 67 | .describe('program description') 68 | .command('foo', 'Hello, foo!', { alias: 'f' }) 69 | .command('bar ', 'Heya, bar!', { alias: ['b', 'ba'] }) 70 | .command('baz ', 'Howdy, baz~!') 71 | .alias('bz', 'bb', 'bza') 72 | ); 73 | 74 | let txt = $.help(bin, tree, '__default__'); 75 | 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'); 76 | 77 | let foo = $.help(bin, tree, 'foo'); 78 | 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'); 79 | 80 | let bar = $.help(bin, tree, 'bar'); 81 | 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'); 82 | 83 | let baz = $.help(bin, tree, 'baz'); 84 | 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'); 85 | 86 | t.end(); 87 | }); 88 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import mri from 'mri'; 2 | import * as $ from './utils'; 3 | import { ALL, DEF } from './utils'; 4 | 5 | class Sade { 6 | constructor(name, isOne) { 7 | let [bin, ...rest] = name.split(/\s+/); 8 | isOne = isOne || rest.length > 0; 9 | 10 | this.bin = bin; 11 | this.ver = '0.0.0'; 12 | this.default = ''; 13 | this.tree = {}; 14 | // set internal shapes; 15 | this.command(ALL); 16 | this.command([DEF].concat(isOne ? rest : '').join(' ')); 17 | this.single = isOne; 18 | this.curr = ''; // reset 19 | } 20 | 21 | command(str, desc, opts={}) { 22 | if (this.single) { 23 | throw new Error('Disable "single" mode to add commands'); 24 | } 25 | 26 | // All non-([|<) are commands 27 | let cmd=[], usage=[], rgx=/(\[|<)/; 28 | str.split(/\s+/).forEach(x => { 29 | (rgx.test(x.charAt(0)) ? usage : cmd).push(x); 30 | }); 31 | 32 | // Back to string~! 33 | cmd = cmd.join(' '); 34 | 35 | if (cmd in this.tree) { 36 | throw new Error(`Command already exists: ${cmd}`); 37 | } 38 | 39 | // re-include `cmd` for commands 40 | cmd.includes('__') || usage.unshift(cmd); 41 | usage = usage.join(' '); // to string 42 | 43 | this.curr = cmd; 44 | if (opts.default) this.default=cmd; 45 | 46 | this.tree[cmd] = { usage, alibi:[], options:[], alias:{}, default:{}, examples:[] }; 47 | if (opts.alias) this.alias(opts.alias); 48 | if (desc) this.describe(desc); 49 | 50 | return this; 51 | } 52 | 53 | describe(str) { 54 | this.tree[this.curr || DEF].describe = Array.isArray(str) ? str : $.sentences(str); 55 | return this; 56 | } 57 | 58 | alias(...names) { 59 | if (this.single) throw new Error('Cannot call `alias()` in "single" mode'); 60 | if (!this.curr) throw new Error('Cannot call `alias()` before defining a command'); 61 | let arr = this.tree[this.curr].alibi = this.tree[this.curr].alibi.concat(...names); 62 | arr.forEach(key => this.tree[key] = this.curr); 63 | return this; 64 | } 65 | 66 | option(str, desc, val) { 67 | let cmd = this.tree[ this.curr || ALL ]; 68 | 69 | let [flag, alias] = $.parse(str); 70 | if (alias && alias.length > 1) [flag, alias]=[alias, flag]; 71 | 72 | str = `--${flag}`; 73 | if (alias && alias.length > 0) { 74 | str = `-${alias}, ${str}`; 75 | let old = cmd.alias[alias]; 76 | cmd.alias[alias] = (old || []).concat(flag); 77 | } 78 | 79 | let arr = [str, desc || '']; 80 | 81 | if (val !== void 0) { 82 | arr.push(val); 83 | cmd.default[flag] = val; 84 | } else if (!alias) { 85 | cmd.default[flag] = void 0; 86 | } 87 | 88 | cmd.options.push(arr); 89 | return this; 90 | } 91 | 92 | action(handler) { 93 | this.tree[ this.curr || DEF ].handler = handler; 94 | return this; 95 | } 96 | 97 | example(str) { 98 | this.tree[ this.curr || DEF ].examples.push(str); 99 | return this; 100 | } 101 | 102 | version(str) { 103 | this.ver = str; 104 | return this; 105 | } 106 | 107 | parse(arr, opts={}) { 108 | arr = arr.slice(); // copy 109 | let offset=2, tmp, idx, isVoid, cmd; 110 | let alias = { h:'help', v:'version' }; 111 | let argv = mri(arr.slice(offset), { alias }); 112 | let isSingle = this.single; 113 | let bin = this.bin; 114 | let name = ''; 115 | 116 | if (isSingle) { 117 | cmd = this.tree[DEF]; 118 | } else { 119 | // Loop thru possible command(s) 120 | let i=1, xyz, len=argv._.length + 1; 121 | for (; i < len; i++) { 122 | tmp = argv._.slice(0, i).join(' '); 123 | xyz = this.tree[tmp]; 124 | if (typeof xyz === 'string') { 125 | idx = (name=xyz).split(' '); 126 | arr.splice(arr.indexOf(argv._[0]), i, ...idx); 127 | i += (idx.length - i); 128 | } else if (xyz) { 129 | name = tmp; 130 | } else if (name) { 131 | break; 132 | } 133 | } 134 | 135 | cmd = this.tree[name]; 136 | isVoid = (cmd === void 0); 137 | 138 | if (isVoid) { 139 | if (this.default) { 140 | name = this.default; 141 | cmd = this.tree[name]; 142 | arr.unshift(name); 143 | offset++; 144 | } else if (tmp) { 145 | return $.error(bin, `Invalid command: ${tmp}`); 146 | } //=> else: cmd not specified, wait for now... 147 | } 148 | } 149 | 150 | // show main help if relied on "default" for multi-cmd 151 | if (argv.help) return this.help(!isSingle && !isVoid && name); 152 | if (argv.version) return this._version(); 153 | 154 | if (!isSingle && cmd === void 0) { 155 | return $.error(bin, 'No command specified.'); 156 | } 157 | 158 | let all = this.tree[ALL]; 159 | // merge all objects :: params > command > all 160 | opts.alias = Object.assign(all.alias, cmd.alias, opts.alias); 161 | opts.default = Object.assign(all.default, cmd.default, opts.default); 162 | 163 | tmp = name.split(' '); 164 | idx = arr.indexOf(tmp[0], 2); 165 | if (!!~idx) arr.splice(idx, tmp.length); 166 | 167 | let vals = mri(arr.slice(offset), opts); 168 | if (!vals || typeof vals === 'string') { 169 | return $.error(bin, vals || 'Parsed unknown option flag(s)!'); 170 | } 171 | 172 | let segs = cmd.usage.split(/\s+/); 173 | let reqs = segs.filter(x => x.charAt(0)==='<'); 174 | let args = vals._.splice(0, reqs.length); 175 | 176 | if (args.length < reqs.length) { 177 | if (name) bin += ` ${name}`; // for help text 178 | return $.error(bin, 'Insufficient arguments!'); 179 | } 180 | 181 | segs.filter(x => x.charAt(0)==='[').forEach(_ => { 182 | args.push(vals._.shift()); // adds `undefined` per [slot] if no more 183 | }); 184 | 185 | args.push(vals); // flags & co are last 186 | let handler = cmd.handler; 187 | return opts.lazy ? { args, name, handler } : handler.apply(null, args); 188 | } 189 | 190 | help(str) { 191 | console.log( 192 | $.help(this.bin, this.tree, str || DEF, this.single) 193 | ); 194 | } 195 | 196 | _version() { 197 | console.log(`${this.bin}, ${this.ver}`); 198 | } 199 | } 200 | 201 | export default (str, isOne) => new Sade(str, isOne); 202 | -------------------------------------------------------------------------------- /deno/mod.js: -------------------------------------------------------------------------------- 1 | import { parse } from "https://deno.land/std@0.106.0/flags/mod.ts"; 2 | import * as $ from "./utils.js"; 3 | 4 | const ALL = "__all__"; 5 | const DEF = "__default__"; 6 | 7 | class Sade { 8 | constructor(name, isOne) { 9 | const [bin, ...rest] = name.split(/\s+/); 10 | isOne = isOne || rest.length > 0; 11 | 12 | this.bin = bin; 13 | this.ver = "0.0.0"; 14 | this.default = ""; 15 | this.tree = {}; 16 | // set internal shapes; 17 | this.command(ALL); 18 | this.command([DEF].concat(isOne ? rest : "").join(" ")); 19 | this.single = isOne; 20 | this.curr = ""; // reset 21 | } 22 | 23 | command(str, desc, opts = {}) { 24 | if (this.single) { 25 | throw new Error('Disable "single" mode to add commands'); 26 | } 27 | 28 | // All non-([|<) are commands 29 | let cmd = [], usage = []; 30 | const rgx = /(\[|<)/; 31 | str.split(/\s+/).forEach((x) => { 32 | (rgx.test(x.charAt(0)) ? usage : cmd).push(x); 33 | }); 34 | 35 | // Back to string~! 36 | cmd = cmd.join(" "); 37 | 38 | if (cmd in this.tree) { 39 | throw new Error(`Command already exists: ${cmd}`); 40 | } 41 | 42 | // re-include `cmd` for commands 43 | cmd.includes("__") || usage.unshift(cmd); 44 | usage = usage.join(" "); // to string 45 | 46 | this.curr = cmd; 47 | if (opts.default) this.default = cmd; 48 | 49 | this.tree[cmd] = { 50 | usage, 51 | alibi: [], 52 | options: [], 53 | alias: {}, 54 | default: {}, 55 | examples: [], 56 | }; 57 | if (opts.alias) this.alias(opts.alias); 58 | if (desc) this.describe(desc); 59 | 60 | return this; 61 | } 62 | 63 | describe(str) { 64 | this.tree[this.curr || DEF].describe = Array.isArray(str) 65 | ? str 66 | : $.sentences(str); 67 | return this; 68 | } 69 | 70 | alias(...names) { 71 | if (this.single) throw new Error('Cannot call `alias()` in "single" mode'); 72 | if (!this.curr) { 73 | throw new Error("Cannot call `alias()` before defining a command"); 74 | } 75 | const arr = this.tree[this.curr].alibi = this.tree[this.curr].alibi.concat( 76 | ...names, 77 | ); 78 | arr.forEach((key) => this.tree[key] = this.curr); 79 | return this; 80 | } 81 | 82 | option(str, desc, val) { 83 | const cmd = this.tree[this.curr || ALL]; 84 | 85 | let [flag, alias] = $.parse(str); 86 | if (alias && alias.length > 1) [flag, alias] = [alias, flag]; 87 | 88 | str = `--${flag}`; 89 | if (alias && alias.length > 0) { 90 | str = `-${alias}, ${str}`; 91 | const old = cmd.alias[alias]; 92 | cmd.alias[alias] = (old || []).concat(flag); 93 | } 94 | 95 | const arr = [str, desc || ""]; 96 | 97 | if (val !== void 0) { 98 | arr.push(val); 99 | cmd.default[flag] = val; 100 | } else if (!alias) { 101 | cmd.default[flag] = void 0; 102 | } 103 | 104 | cmd.options.push(arr); 105 | return this; 106 | } 107 | 108 | action(handler) { 109 | this.tree[this.curr || DEF].handler = handler; 110 | return this; 111 | } 112 | 113 | example(str) { 114 | this.tree[this.curr || DEF].examples.push(str); 115 | return this; 116 | } 117 | 118 | version(str) { 119 | this.ver = str; 120 | return this; 121 | } 122 | 123 | parse(arr, opts = {}) { 124 | arr = ["", ""].concat(arr); 125 | let offset = 2, tmp, idx, isVoid, cmd; 126 | 127 | const alias = { h: "help", v: "version" }; 128 | const argv = parse(arr.slice(offset), { alias }); 129 | const isSingle = this.single; 130 | 131 | let bin = this.bin; 132 | let name = ""; 133 | 134 | if (isSingle) { 135 | cmd = this.tree[DEF]; 136 | } else { 137 | // Loop thru possible command(s) 138 | let i = 1, xyz; 139 | const len = argv._.length + 1; 140 | 141 | for (; i < len; i++) { 142 | tmp = argv._.slice(0, i).join(" "); 143 | xyz = this.tree[tmp]; 144 | if (typeof xyz === "string") { 145 | idx = (name = xyz).split(" "); 146 | arr.splice(arr.indexOf(argv._[0]), i, ...idx); 147 | i += (idx.length - i); 148 | } else if (xyz) { 149 | name = tmp; 150 | } else if (name) { 151 | break; 152 | } 153 | } 154 | 155 | cmd = this.tree[name]; 156 | isVoid = (cmd === void 0); 157 | 158 | if (isVoid) { 159 | if (this.default) { 160 | name = this.default; 161 | cmd = this.tree[name]; 162 | arr.unshift(name); 163 | offset++; 164 | } else if (tmp) { 165 | return $.error(bin, `Invalid command: ${tmp}`); 166 | } //=> else: cmd not specified, wait for now... 167 | } 168 | } 169 | 170 | // show main help if relied on "default" for multi-cmd 171 | if (argv.help) return this.help(!isSingle && !isVoid && name); 172 | if (argv.version) return this._version(); 173 | 174 | if (!isSingle && cmd === void 0) { 175 | return $.error(bin, "No command specified."); 176 | } 177 | 178 | const all = this.tree[ALL]; 179 | // merge all objects :: params > command > all 180 | opts.alias = Object.assign(all.alias, cmd.alias, opts.alias); 181 | opts.default = Object.assign(all.default, cmd.default, opts.default); 182 | 183 | tmp = name.split(" "); 184 | idx = arr.indexOf(tmp[0], 2); 185 | if (~idx) arr.splice(idx, tmp.length); 186 | 187 | const vals = parse(arr.slice(offset), opts); 188 | if (!vals || typeof vals === "string") { 189 | return $.error(bin, vals || "Parsed unknown option flag(s)!"); 190 | } 191 | 192 | const segs = cmd.usage.split(/\s+/); 193 | const reqs = segs.filter((x) => x.charAt(0) === "<"); 194 | const args = vals._.splice(0, reqs.length); 195 | 196 | if (args.length < reqs.length) { 197 | if (name) bin += ` ${name}`; // for help text 198 | return $.error(bin, "Insufficient arguments!"); 199 | } 200 | 201 | segs.filter((x) => x.charAt(0) === "[").forEach((_) => { 202 | args.push(vals._.shift()); // adds `undefined` per [slot] if no more 203 | }); 204 | 205 | args.push(vals); // flags & co are last 206 | const handler = cmd.handler; 207 | return opts.lazy ? { args, name, handler } : handler.apply(null, args); 208 | } 209 | 210 | help(str) { 211 | console.log( 212 | $.help(this.bin, this.tree, str || DEF, this.single), 213 | ); 214 | } 215 | 216 | _version() { 217 | console.log(`${this.bin}, ${this.ver}`); 218 | } 219 | } 220 | 221 | export default (str, isOne) => new Sade(str, isOne); 222 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import test from 'tape'; 2 | import sade from '../src'; 3 | 4 | function isShapely(t, tree, key) { 5 | t.is(typeof tree[key].usage, 'string', `~> tree[${key}].usage is a string`); 6 | t.ok(Array.isArray(tree[key].options), `~> tree[${key}].options is an array`); 7 | t.ok(Array.isArray(tree[key].examples), `~> tree[${key}].examples is an array`); 8 | t.is(typeof tree[key].default, 'object', `~> tree[${key}].default is an object`); 9 | t.is(typeof tree[key].alias, 'object', `~> tree[${key}].alias is an object`); 10 | } 11 | 12 | test('sade', t => { 13 | t.is(typeof sade, 'function', 'exports a function'); 14 | t.end(); 15 | }); 16 | 17 | test('sade()', t => { 18 | let ctx = sade('foo'); 19 | t.ok(ctx.constructor && ctx.constructor.name === 'Sade', 'returns instance of Sade'); 20 | t.is(ctx.bin, 'foo', 'sets Program name to `foo`'); 21 | t.is(ctx.ver, '0.0.0', 'defaults `ver` to `0.0.0`'); 22 | t.is(ctx.curr, '', 'is empty command-name scope'); 23 | t.is(ctx.default, '', 'has no default command (yet)'); 24 | t.is(typeof ctx.tree, 'object', 'creates a `tree` object'); 25 | let k, keys = Object.keys(ctx.tree); 26 | t.is(keys.length, 2, 'internal `tree` has two keys'); 27 | for (k in ctx.tree) { 28 | isShapely(t, ctx.tree, k); 29 | } 30 | t.end(); 31 | }); 32 | 33 | test('prog.version (global)', t => { 34 | let ctx = sade('foo').version('1.0.0'); 35 | t.is(ctx.ver, '1.0.0', 'sets a new version~!'); 36 | t.end(); 37 | }); 38 | 39 | test('prog.option (global)', t => { 40 | let ctx = sade('foo'); 41 | t.is(ctx.tree.__all__.options.length, 0, 'no global options (default)'); 42 | ctx.option('--foo, -f', 'bar', 'baz.js'); 43 | let arr = ctx.tree.__all__.options; 44 | t.is(arr.length, 1, 'adds an option successfully'); 45 | let item = arr[0]; 46 | t.ok(Array.isArray(item), 'options entry is also an array'); 47 | t.is(item.length, 3, 'entry has 3 segments (flags, desc, default)'); 48 | t.is(item[0], '-f, --foo', 'flips the flags order; alias is first'); 49 | t.end(); 50 | }); 51 | 52 | test('prog.option (hypenated)', t => { 53 | let ctx = sade('foo'); 54 | ctx.option('--foo-bar, -f'); 55 | ctx.option('--foo-bar-baz'); 56 | let arr = ctx.tree.__all__.options; 57 | t.is(arr[0][0], '-f, --foo-bar', 'keeps mid-hyphen; flips order so alias is first'); 58 | t.is(arr[1][0], '--foo-bar-baz', 'keeps all mid-hyphens'); 59 | t.end(); 60 | }); 61 | 62 | test('prog.describe (global)', t => { 63 | let ctx = sade('foo').describe('Who is on first. What is on second.'); 64 | let arr = ctx.tree.__default__.describe; 65 | t.ok(Array.isArray(arr), 'adds a `describe` array for Program info'); 66 | t.is(arr.length, 2, 'splits the description into 2 sentence items'); 67 | t.end(); 68 | }); 69 | 70 | test('prog.example (global)', t => { 71 | let ctx = sade('foo').example('hello --local'); 72 | let arr = ctx.tree.__default__.examples; 73 | t.ok(Array.isArray(arr), 'adds a `examples` array for Program info'); 74 | t.is(arr.length, 1, 'contains the single example'); 75 | t.is(arr[0], 'hello --local', 'does not manipulate contents (yet)') 76 | t.end(); 77 | }); 78 | 79 | test('prog.command', t => { 80 | let ctx = sade('foo').command('bar'); 81 | let bar = ctx.tree.bar; 82 | t.ok(bar, 'adds `bar` key to the command tree'); 83 | isShapely(t, ctx.tree, 'bar'); 84 | t.is(bar.usage, 'bar', 'stores usage as is'); 85 | 86 | // Options 87 | t.is(bar.options.length, 0, 'has no options initially'); 88 | ctx.option('-f, --force', 'force'); 89 | t.is(bar.options.length, 1, 'adds new Command option successfully'); 90 | t.deepEqual(bar.alias, { f:['force'] }, 'adds option flag & alias'); 91 | 92 | // Examples 93 | t.is(bar.examples.length, 0, 'has no examples initially'); 94 | ctx.example('bar --force'); 95 | t.is(bar.examples.length, 1, 'adds new Command exmaple successfully'); 96 | t.is(bar.examples[0], 'bar --force', 'adds example, as written'); 97 | 98 | // Description 99 | t.ok(bar.describe === void 0, 'has no description initially'); 100 | ctx.describe('hello world'); 101 | t.ok(Array.isArray(bar.describe), 'adds new Command description as Array'); 102 | t.is(bar.describe[0], 'hello world', 'stores description, as written'); 103 | 104 | // Add new Command 105 | ctx.command('quz'); 106 | let quz = ctx.tree.quz; 107 | t.ok(quz, 'adds `quz` key to the command tree'); 108 | isShapely(t, ctx.tree, 'quz'); 109 | t.is(quz.usage, 'quz', 'stores usage as is'); 110 | 111 | // Show that command state changed 112 | ctx.describe('this is quz'); 113 | t.is(quz.describe[0], 'this is quz', 'adds description for second command'); 114 | t.is(bar.describe[0], 'hello world', 'does not affect first Command'); 115 | 116 | // Add third, with description & change default 117 | ctx.command('fizz ', 'FizzBuzz', { default:true }); 118 | let fizz = ctx.tree.fizz; 119 | t.ok(fizz, 'adds `fizz` key to the command tree'); 120 | isShapely(t, ctx.tree, 'fizz'); 121 | t.is(fizz.usage, 'fizz ', 'stores usage as is'); 122 | 123 | // Add Example 124 | ctx.example('fizz 15'); 125 | t.is(fizz.examples.length, 1, 'adds new Command exmaple successfully'); 126 | t.is(fizz.examples[0], 'fizz 15', 'adds example, as written'); 127 | 128 | t.is(bar.examples.length, 1, '1st command example count unchanged'); 129 | t.is(bar.examples[0], 'bar --force', 'first command example unaffected'); 130 | t.is(quz.examples.length, 0, '2nd command example count unchanged'); 131 | 132 | t.is(ctx.default, 'fizz', 'default command was updated~!'); 133 | 134 | t.end(); 135 | }); 136 | 137 | test('prog.action', t => { 138 | t.plan(13); 139 | let a='Bob', b, c, d, e; 140 | 141 | let ctx = sade('foo') 142 | .command('greet ') 143 | .option('--loud', 'Be loud?') 144 | .option('--with-kiss, -k', 'Super friendly?') 145 | .action((name, opts) => { 146 | t.is(name, a, '~> receives the required value as first parameter'); 147 | b && t.ok(opts.loud, '~> receives the `loud` flag (true) when parsed'); 148 | c && t.ok(opts['with-kiss'], '~> receives the `with-kiss` flag (true) when parsed :: preserves mid-hyphen'); 149 | d && t.is(opts['with-kiss'], 'cheek', '~> receives the `with-kiss` flag (`cheek`) when parsed :: preserves mid-hyphen'); 150 | e && t.is(opts['with-kiss'], false, '~> receive the `--no-with-kiss` flag (false) :: preserves mid-hyphen'); 151 | b = c = d = e = false; // reset 152 | }); 153 | 154 | // Simulate `process.argv` entry 155 | let run = args => ctx.parse(['', '', 'greet', a].concat(args || [])); 156 | 157 | let cmd = ctx.tree.greet; 158 | t.ok(cmd.handler, 'added a `handler` key to the command leaf'); 159 | t.is(typeof cmd.handler, 'function', 'the `handler` is a function'); 160 | 161 | run(); // +1 test 162 | (b=true) && run('--loud'); // +2 tests 163 | (c=true) && run('--with-kiss'); // +2 tests 164 | (d=true) && run('--with-kiss=cheek'); // +2 tests 165 | (d=true) && run(['--with-kiss', 'cheek']); // +2 tests 166 | (e=true) && run('--no-with-kiss'); // +2 tests 167 | }); 168 | 169 | test('prog.action (multi requires)', t => { 170 | t.plan(7); 171 | 172 | let a='aaa', b='bbb', c=false; 173 | 174 | let ctx = sade('foo') 175 | .command('build ') 176 | .option('-f, --force', 'Force foo overwrite') 177 | .action((src, dest, opts) => { 178 | t.is(src, a, '~> receives `src` param first'); 179 | t.is(dest, b, '~> receives `dest` param second'); 180 | c && t.ok(opts.force, '~> receives the `force` flag (true) when parsed'); 181 | c && t.ok(opts.f, '~> receives the `f` alias (true) when parsed'); 182 | }); 183 | 184 | t.is(ctx.tree.build.usage, 'build ', 'writes all required params to usage'); 185 | 186 | let run = _ => ctx.parse(['', '', 'build', a, b, c && '-f']); 187 | 188 | run(); // +2 tests 189 | (c=true) && run(); // +4 tests 190 | }); 191 | 192 | test('prog.action (multi optional)', t => { 193 | t.plan(7); 194 | 195 | let a='aaa', b='bbb', c=false; 196 | 197 | let ctx = sade('foo') 198 | .command('build [src] [dest]') 199 | .option('-f, --force', 'Force foo overwrite') 200 | .action((src, dest, opts) => { 201 | t.is(src, a, '~> receives `src` param first'); 202 | t.is(dest, b, '~> receives `dest` param second'); 203 | c && t.ok(opts.force, '~> receives the `force` flag (true) when parsed'); 204 | c && t.ok(opts.f, '~> receives the `f` alias (true) when parsed'); 205 | }); 206 | 207 | t.is(ctx.tree.build.usage, 'build [src] [dest]', 'writes all positional params to usage'); 208 | 209 | let run = _ => ctx.parse(['', '', 'build', a, b, c && '-f']); 210 | 211 | run(); // +2 tests 212 | (c=true) && run(); // +4 tests 213 | }); 214 | 215 | test('prog.parse :: safe :: default', t => { 216 | let ctx = sade('foo').command('build', '', { default: true }); 217 | 218 | let argv1 = ['', '', 'build']; 219 | let foo = ctx.parse(argv1, { lazy: true }); 220 | t.deepEqual(argv1, ['', '', 'build'], '~> argv unchanged'); 221 | t.deepEqual(foo.args, [{ _: [] }], '~> args correct'); 222 | 223 | let argv2 = ['', '']; 224 | let bar = ctx.parse(argv2, { lazy: true }); 225 | t.deepEqual(argv2, ['', ''], '~> argv unchanged'); 226 | t.deepEqual(bar.args, [{ _: [] }], '~> args correct'); 227 | 228 | t.end(); 229 | }); 230 | 231 | test('prog.parse :: safe :: alias', t => { 232 | let ctx = sade('foo').command('build').alias('b'); 233 | 234 | let argv1 = ['', '', 'build']; 235 | let foo = ctx.parse(argv1, { lazy: true }); 236 | t.deepEqual(argv1, ['', '', 'build'], '~> argv unchanged'); 237 | t.deepEqual(foo.args, [{ _: [] }], '~> args correct'); 238 | 239 | let argv2 = ['', '', 'b']; 240 | let bar = ctx.parse(argv2, { lazy: true }); 241 | t.deepEqual(argv2, ['', '', 'b'], '~> argv unchanged'); 242 | t.deepEqual(bar.args, [{ _: [] }], '~> args correct'); 243 | 244 | t.end(); 245 | }); 246 | 247 | test('prog.parse :: safe :: default :: flags', t => { 248 | let ctx = sade('foo').command('build ', '', { default: true }); 249 | 250 | let argv1 = ['', '', '-r', 'dotenv', 'build', 'public', '--fresh']; 251 | let foo = ctx.parse(argv1, { lazy: true }); 252 | t.deepEqual(argv1, ['', '', '-r', 'dotenv', 'build', 'public', '--fresh'], '~> argv unchanged'); 253 | t.deepEqual(foo.args, ['public', { _: [], r: 'dotenv', fresh: true }], '~> args correct'); 254 | 255 | let argv2 = ['', '', '-r', 'dotenv', 'public', '--fresh']; 256 | let bar = ctx.parse(argv2, { lazy: true }); 257 | t.deepEqual(argv2, ['', '', '-r', 'dotenv', 'public', '--fresh'], '~> argv unchanged'); 258 | t.deepEqual(bar.args, ['public', { _: [], r: 'dotenv', fresh: true }], '~> args correct'); 259 | 260 | t.end(); 261 | }); 262 | 263 | test('prog.parse :: safe :: alias :: flags', t => { 264 | let ctx = sade('foo').command('build ').alias('b'); 265 | 266 | let argv1 = ['', '', '-r', 'dotenv', 'build', 'public', '--fresh']; 267 | let foo = ctx.parse(argv1, { lazy: true }); 268 | t.deepEqual(argv1, ['', '', '-r', 'dotenv', 'build', 'public', '--fresh'], '~> argv unchanged'); 269 | t.deepEqual(foo.args, ['public', { _: [], r: 'dotenv', fresh: true }], '~> args correct'); 270 | 271 | let argv2 = ['', '', '-r', 'dotenv', 'b', 'public', '--fresh']; 272 | let bar = ctx.parse(argv2, { lazy: true }); 273 | t.deepEqual(argv2, ['', '', '-r', 'dotenv', 'b', 'public', '--fresh'], '~> argv unchanged'); 274 | t.deepEqual(bar.args, ['public', { _: [], r: 'dotenv', fresh: true }], '~> args correct'); 275 | 276 | t.end(); 277 | }); 278 | 279 | test('prog.parse :: lazy', t => { 280 | t.plan(14); 281 | 282 | let val='aaa', f=false; 283 | 284 | let ctx = sade('foo') 285 | .command('build ') 286 | .option('--force').action((src, opts) => { 287 | t.is(src, val, '~> receives `src` param first'); 288 | f && t.ok(opts.force, '~> receives the `force` flag (true) when parsed'); 289 | }); 290 | 291 | let run = _ => ctx.parse(['', '', 'build', val, f && '--force'], { lazy:true }); 292 | 293 | let foo = run(); 294 | t.is(foo.constructor, Object, 'returns an object'); 295 | t.same(Object.keys(foo), ['args', 'name', 'handler'], 'contains `args`,`name`,`handler` keys'); 296 | t.ok(Array.isArray(foo.args), '~> returns the array of arguments'); 297 | t.is(foo.args[0], val, '~> preserves the `src` value first'); 298 | t.is(foo.args[1].constructor, Object, '~> preserves the `opts` value last'); 299 | t.ok(Array.isArray(foo.args[1]._), '~> ensures `opts._` is still `[]` at least'); 300 | t.is(typeof foo.handler, 'function', '~> returns the action handler'); 301 | t.is(foo.name, 'build', '~> returns the command name'); 302 | 303 | foo.handler.apply(null, foo.args); // must be manual bcuz lazy; +1 test 304 | 305 | let bar = run(f=true); 306 | t.is(bar.constructor, Object, 'returns an object'); 307 | t.is(bar.args[1].constructor, Object, '~> preserves the `opts` value last'); 308 | t.is(bar.args[1].force, true, '~> attaches the `force:true` option'); 309 | 310 | bar.handler.apply(null, bar.args); // manual bcuz lazy; +2 tests 311 | }); 312 | 313 | test('prog.parse :: lazy :: single', t => { 314 | t.plan(14); 315 | 316 | let val='aaa', f=false; 317 | 318 | let ctx = sade('foo ').option('--force').action((src, opts) => { 319 | t.is(src, val, '~> receives `src` param first'); 320 | f && t.ok(opts.force, '~> receives the `force` flag (true) when parsed'); 321 | }); 322 | 323 | let run = _ => ctx.parse(['', '', val, f && '--force'], { lazy:true }); 324 | 325 | let foo = run(); 326 | t.is(foo.constructor, Object, 'returns an object'); 327 | t.same(Object.keys(foo), ['args', 'name', 'handler'], 'contains `args`,`name`,`handler` keys'); 328 | t.ok(Array.isArray(foo.args), '~> returns the array of arguments'); 329 | t.is(foo.args[0], val, '~> preserves the `src` value first'); 330 | t.is(foo.args[1].constructor, Object, '~> preserves the `opts` value last'); 331 | t.ok(Array.isArray(foo.args[1]._), '~> ensures `opts._` is still `[]` at least'); 332 | t.is(typeof foo.handler, 'function', '~> returns the action handler'); 333 | t.is(foo.name, '', '~> returns empty command name'); 334 | 335 | foo.handler.apply(null, foo.args); // must be manual bcuz lazy; +1 test 336 | 337 | let bar = run(f=true); 338 | t.is(bar.constructor, Object, 'returns an object'); 339 | t.is(bar.args[1].constructor, Object, '~> preserves the `opts` value last'); 340 | t.is(bar.args[1].force, true, '~> attaches the `force:true` option'); 341 | 342 | bar.handler.apply(null, bar.args); // manual bcuz lazy; +2 tests 343 | }); 344 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # sade [![CI](https://github.com/lukeed/sade/workflows/CI/badge.svg)](https://github.com/lukeed/sade/actions?query=workflow%3ACI) [![licenses](https://licenses.dev/b/npm/sade)](https://licenses.dev/npm/sade) 2 | 3 | > Smooth (CLI) Operator 🎶 4 | 5 | Sade is a small but powerful tool for building command-line interface (CLI) applications for Node.js that are fast, responsive, and helpful! 6 | 7 | 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! 8 | 9 | Your app's UX will be as smooth as butter... just like [Sade's voice](https://www.youtube.com/watch?v=4TYv2PhG89A). 😉 10 | 11 | 12 | ## Install 13 | 14 | ``` 15 | $ npm install --save sade 16 | ``` 17 | 18 | 19 | ## Usage 20 | 21 | ***Input:*** 22 | 23 | ```js 24 | #!/usr/bin/env node 25 | 26 | const sade = require('sade'); 27 | 28 | const prog = sade('my-cli'); 29 | 30 | prog 31 | .version('1.0.5') 32 | .option('--global, -g', 'An example global flag') 33 | .option('-c, --config', 'Provide path to custom config', 'foo.config.js'); 34 | 35 | prog 36 | .command('build ') 37 | .describe('Build the source directory. Expects an `index.js` entry file.') 38 | .option('-o, --output', 'Change the name of the output file', 'bundle.js') 39 | .example('build src build --global --config my-conf.js') 40 | .example('build app public -o main.js') 41 | .action((src, dest, opts) => { 42 | console.log(`> building from ${src} to ${dest}`); 43 | console.log('> these are extra opts', opts); 44 | }); 45 | 46 | prog.parse(process.argv); 47 | ``` 48 | 49 | ***Output:*** 50 | 51 | ```a 52 | $ my-cli --help 53 | 54 | Usage 55 | $ my-cli [options] 56 | 57 | Available Commands 58 | build Build the source directory. 59 | 60 | For more info, run any command with the `--help` flag 61 | $ my-cli build --help 62 | 63 | Options 64 | -v, --version Displays current version 65 | -g, --global An example global flag 66 | -c, --config Provide path to custom config (default foo.config.js) 67 | -h, --help Displays this message 68 | 69 | 70 | $ my-cli build --help 71 | 72 | Description 73 | Build the source directory. 74 | Expects an `index.js` entry file. 75 | 76 | Usage 77 | $ my-cli build [options] 78 | 79 | Options 80 | -o, --output Change the name of the output file (default bundle.js) 81 | -g, --global An example global flag 82 | -c, --config Provide path to custom config (default foo.config.js) 83 | -h, --help Displays this message 84 | 85 | Examples 86 | $ my-cli build src build --global --config my-conf.js 87 | $ my-cli build app public -o main.js 88 | ``` 89 | 90 | ## Tips 91 | 92 | - **Define your global/program-wide version, options, description, and/or examples first.**
93 | _Once you define a Command, you can't access the global-scope again._ 94 | 95 | - **Define all commands & options in the order that you want them to appear.**
96 | _Sade will not mutate or sort your CLI for you. Global options print before local options._ 97 | 98 | - **Required arguments without values will error & exit**
99 | _An `Insufficient arguments!` error will be displayed along with a help prompt._ 100 | 101 | - **Don't worry about manually displaying help~!**
102 | _Your help text is displayed automatically... including command-specific help text!_ 103 | 104 | - **Automatic default/basic patterns**
105 | _Usage text will always append `[options]` & `--help` and `--version` are done for you._ 106 | 107 | - **Only define what you want to display!**
108 | _Help text sections (example, options, etc) will only display if you provide values._ 109 | 110 | 111 | ## Subcommands 112 | 113 | 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. 114 | 115 | They should be defined in the order that you want them to appear in your general `--help` output. 116 | 117 | 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. 118 | 119 | ```js 120 | const prog = sade('git'); 121 | 122 | // Not necessary for subcommands to work, but it's here anyway! 123 | prog 124 | .command('remote') 125 | .describe('Manage set of tracked repositories') 126 | .action(opts => { 127 | console.log('~> Print current remotes...'); 128 | }); 129 | 130 | prog 131 | .command('remote add ', 'Demo...') 132 | .action((name, url, opts) => { 133 | console.log(`~> Adding a new remote (${name}) to ${url}`); 134 | }); 135 | 136 | prog 137 | .command('remote rename ', 'Demo...') 138 | .action((old, nxt, opts) => { 139 | console.log(`~> Renaming from ${old} to ${nxt}~!`); 140 | }); 141 | ``` 142 | 143 | 144 | ## Single Command Mode 145 | 146 | In certain circumstances, you may only need `sade` for a single-command CLI application. 147 | 148 | > **Note:** Until `v1.6.0`, this made for an awkward pairing. 149 | 150 | 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. 151 | 152 | 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.* 153 | 154 | > * This is true for multi-command applications, too, up until your first `prog.command()` call! 155 | 156 | ***Example*** 157 | 158 | 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: 159 | 160 | ```js 161 | sade('sirv [dir]', true) 162 | .version('1.0.0') 163 | .describe('Run a static file server') 164 | .example('public -qeim 31536000') 165 | .example('--port 8080 --etag') 166 | .example('my-app --dev') 167 | .option('-D, --dev', 'Enable "dev" mode') 168 | .option('-e, --etag', 'Enable "Etag" header') 169 | // There are a lot... 170 | .option('-H, --host', 'Hostname to bind', 'localhost') 171 | .option('-p, --port', 'Port to bind', 5000) 172 | .action((dir, opts) => { 173 | // Program handler 174 | }) 175 | .parse(process.argv); 176 | ``` 177 | 178 | When `sirv --help` is run, the generated help text is trimmed, fully aware that there's only one command in this program: 179 | 180 | ``` 181 | Description 182 | Run a static file server 183 | 184 | Usage 185 | $ sirv [dir] [options] 186 | 187 | Options 188 | -D, --dev Enable "dev" mode 189 | -e, --etag Enable "Etag" header 190 | -H, --host Hostname to bind (default localhost) 191 | -p, --port Port to bind (default 5000) 192 | -v, --version Displays current version 193 | -h, --help Displays this message 194 | 195 | Examples 196 | $ sirv public -qeim 31536000 197 | $ sirv --port 8080 --etag 198 | $ sirv my-app --dev 199 | ``` 200 | 201 | ## Command Aliases 202 | 203 | Command aliases are alternative names (aliases) for a command. They are often used as shortcuts or as typo relief! 204 | 205 | The aliased names do not appear in the general help text.
206 | Instead, they only appear within the Command-specific help text under an "Aliases" section. 207 | 208 | ***Limitations*** 209 | 210 | * You cannot assign aliases while in [Single Command Mode](#single-command-mode) 211 | * You cannot call [`prog.alias()`](#progaliasnames) before defining any Commands (via `prog.commmand()`) 212 | * You, the developer, must keep track of which aliases have already been used and/or exist as Command names 213 | 214 | ***Example*** 215 | 216 | Let's reconstruct the `npm install` command as a Sade program: 217 | 218 | ```js 219 | sade('npm') 220 | // ... 221 | .command('install [package]', 'Install a package', { 222 | alias: ['i', 'add', 'isntall'] 223 | }) 224 | .option('-P, --save-prod', 'Package will appear in your dependencies.') 225 | .option('-D, --save-dev', 'Package will appear in your devDependencies.') 226 | .option('-O, --save-optional', 'Package will appear in your optionalDependencies') 227 | .option('-E, --save-exact', 'Save exact versions instead of using a semver range operator') 228 | // ... 229 | ``` 230 | 231 | When we run `npm --help` we'll see this general help text: 232 | 233 | ``` 234 | Usage 235 | $ npm [options] 236 | 237 | Available Commands 238 | install Install a package 239 | 240 | For more info, run any command with the `--help` flag 241 | $ npm install --help 242 | 243 | Options 244 | -v, --version Displays current version 245 | -h, --help Displays this message 246 | ``` 247 | 248 | When we run `npm install --help` — ***or*** the help flag with any of `install`'s aliases — we'll see this command-specific help text: 249 | 250 | ``` 251 | Description 252 | Install a package 253 | 254 | Usage 255 | $ npm install [package] [options] 256 | 257 | Aliases 258 | $ npm i 259 | $ npm add 260 | $ npm isntall 261 | 262 | Options 263 | -P, --save-prod Package will appear in your dependencies. 264 | -D, --save-dev Package will appear in your devDependencies. 265 | -O, --save-optional Package will appear in your optionalDependencies 266 | -E, --save-exact Save exact versions instead of using a semver range operator 267 | -h, --help Displays this message 268 | ``` 269 | 270 | 271 | 272 | ## API 273 | 274 | ### sade(name, isSingle) 275 | Returns: `Program` 276 | 277 | Returns your chainable Sade instance, aka your `Program`. 278 | 279 | #### name 280 | Type: `String`
281 | Required: `true` 282 | 283 | The name of your `Program` / binary application. 284 | 285 | #### isSingle 286 | Type: `Boolean`
287 | Default: `name.includes(' ');` 288 | 289 | If your `Program` is meant to have ***only one command***.
290 | When `true`, this simplifies your generated `--help` output such that: 291 | 292 | * the "root-level help" is your _only_ help text 293 | * the "root-level help" does not display an `Available Commands` section 294 | * the "root-level help" does not inject `$ name ` into the `Usage` section 295 | * the "root-level help" does not display `For more info, run any command with the `--help` flag` text 296 | 297 | You may customize the `Usage` of your command by modifying the `name` argument directly.
298 | Please read [Single Command Mode](#single-command-mode) for an example and more information. 299 | 300 | > **Important:** Whenever `name` includes a custom usage, then `isSingle` is automatically assumed and enforced! 301 | 302 | ### prog.command(usage, desc, opts) 303 | 304 | Create a new Command for your Program. This changes the current state of your Program. 305 | 306 | All configuration methods (`prog.describe`, `prog.action`, etc) will apply to this Command until another Command has been created! 307 | 308 | #### usage 309 | 310 | Type: `String` 311 | 312 | The usage pattern for your current Command. This will be included in the general or command-specific `--help` output. 313 | 314 | _Required_ arguments are wrapped with `<` and `>` characters; for example, `` and ``. 315 | 316 | _Optional_ arguments are wrapped with `[` and `]` characters; for example, `[foo]` and `[bar]`. 317 | 318 | 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. 319 | 320 | When optional arguments are defined but don't receive a value, their positionally-equivalent function parameter will be `undefined`. 321 | 322 | > **Important:** You **must** define & expect required arguments _before_ optional arguments! 323 | 324 | ```js 325 | sade('foo') 326 | 327 | .command('greet ') 328 | .action((adjective, noun, opts) => { 329 | console.log(`Hello, ${adjective} ${noun}!`); 330 | }) 331 | 332 | .command('drive [color] [speed]') 333 | .action((vehicle, color, speed, opts) => { 334 | let arr = ['Driving my']; 335 | arr.push(color ? `${color} ${vehicle}` : vehicle); 336 | speed && arr.push(`at ${speed}`); 337 | opts.yolo && arr.push('...YOLO!!'); 338 | let str = arr.join(' '); 339 | console.log(str); 340 | }); 341 | ``` 342 | 343 | ```sh 344 | $ foo greet beautiful person 345 | # //=> Hello, beautiful person! 346 | 347 | $ foo drive car 348 | # //=> Driving my car 349 | 350 | $ foo drive car red 351 | # //=> Driving my red card 352 | 353 | $ foo drive car blue 100mph --yolo 354 | # //=> Driving my blue car at 100mph ...YOLO!! 355 | ``` 356 | 357 | 358 | #### desc 359 | 360 | Type: `String`
361 | Default: `''` 362 | 363 | The Command's description. The value is passed directly to [`prog.describe`](#progdescribetext). 364 | 365 | #### opts 366 | 367 | Type: `Object`
368 | Default: `{}` 369 | 370 | ##### opts.alias 371 | Type: `String|Array` 372 | 373 | Optionally define one or more aliases for the current Command.
374 | When declared, the `opts.alias` value is passed _directly_ to the [`prog.alias`](#progaliasnames) method. 375 | 376 | ```js 377 | // Program A is equivalent to Program B 378 | // --- 379 | 380 | const A = sade('bin') 381 | .command('build', 'My build command', { alias: 'b' }) 382 | .command('watch', 'My watch command', { alias: ['w', 'dev'] }); 383 | 384 | const B = sade('bin') 385 | .command('build', 'My build command').alias('b') 386 | .command('watch', 'My watch command').alias('w', 'dev'); 387 | ``` 388 | 389 | 390 | ##### opts.default 391 | 392 | Type: `Boolean` 393 | 394 | 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. 395 | 396 | > **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. 397 | 398 | ```js 399 | const prog = sade('greet'); 400 | 401 | prog.command('hello'); 402 | //=> only runs if :: `$ greet hello` 403 | 404 | // $ greet 405 | //=> error: No command specified. 406 | 407 | prog.command('howdy', '', { default:true }); 408 | //=> runs as `$ greet` OR `$ greet howdy` 409 | 410 | // $ greet 411 | //=> runs 'howdy' handler 412 | 413 | // $ greet foobar 414 | //=> error: Invalid command 415 | ``` 416 | 417 | 418 | ### prog.describe(text) 419 | 420 | Add a description to the current Command. 421 | 422 | #### text 423 | 424 | Type: `String|Array` 425 | 426 | The description text for the current Command. This will be included in the general or command-specific `--help` output. 427 | 428 | Internally, your description will be separated into an `Array` of sentences. 429 | 430 | For general `--help` output, ***only*** the first sentence will be displayed. However, **all sentences** will be printed for command-specific `--help` text. 431 | 432 | > **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. 433 | 434 | 435 | ### prog.alias(...names) 436 | 437 | Define one or more aliases for the current Command. 438 | 439 | > **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`. 440 | 441 | #### names 442 | 443 | Type: `String` 444 | 445 | The list of alternative names (aliases) for the current Command.
446 | For example, you may want to define shortcuts and/or common typos for the Command's full name. 447 | 448 | > **Important:** Sade _does not_ check if the incoming `names` are already in use by other Commands or their aliases.
During conflicts, the Command with the same `name` is given priority, otherwise the first Command (according to Program order) with `name` as an alias is chosen. 449 | 450 | The `prog.alias()` is append-only, so calling it multiple times within a Command context will _keep_ all aliases, including those initially passed via [`opts.alias`](#optsdefault). 451 | 452 | ```js 453 | sade('bin') 454 | .command('hello ', 'Greet someone by their name', { 455 | alias: ['hey', 'yo'] 456 | }) 457 | .alias('hi', 'howdy') 458 | .alias('hola', 'oi'); 459 | //=> hello aliases: hey, yo, hi, howdy, hola, oi 460 | ``` 461 | 462 | 463 | ### prog.action(handler) 464 | 465 | Attach a callback to the current Command. 466 | 467 | #### handler 468 | 469 | Type: `Function` 470 | 471 | The function to run when the current Command is executed. 472 | 473 | Its parameters are based (positionally) on your Command's [`usage`](#usage-1) definition. 474 | 475 | All options, flags, and extra/unknown values are included as the last parameter. 476 | 477 | > **Note:** Optional arguments are also passed as parameters & may be `undefined`! 478 | 479 | ```js 480 | sade('foo') 481 | .command('cp ') 482 | .option('-f, --force', 'Overwrite without confirmation') 483 | .option('-c, --clone-dir', 'Copy files to additional directory') 484 | .option('-v, --verbose', 'Enable verbose output') 485 | .action((src, dest, opts) => { 486 | console.log(`Copying files from ${src} --> ${dest}`); 487 | opts.c && console.log(`ALSO copying files from ${src} --> ${opts['clone-dir']}`); 488 | console.log('My options:', opts); 489 | }) 490 | 491 | // $ foo cp original my-copy -v 492 | //=> Copying files from original --> my-copy 493 | //=> My options: { _:[], v:true, verbose:true } 494 | 495 | // $ foo cp original my-copy --clone-dir my-backup 496 | //=> Copying files from original --> my-copy 497 | //=> ALSO copying files from original --> my-backup 498 | //=> My options: { _:[], c:'my-backup', 'clone-dir':'my-backup' } 499 | ``` 500 | 501 | 502 | ### prog.example(str) 503 | 504 | Add an example for the current Command. 505 | 506 | #### str 507 | 508 | Type: `String` 509 | 510 | The example string to add. This will be included in the general or command-specific `--help` output. 511 | 512 | > **Note:** Your example's `str` will be prefixed with your Program's [`name`](#sadename). 513 | 514 | 515 | ### prog.option(flags, desc, value) 516 | 517 | Add an Option to the current Command. 518 | 519 | #### flags 520 | 521 | Type: `String` 522 | 523 | The Option's flags, which may optionally include an alias. 524 | 525 | You may use a comma (`,`) or a space (` `) to separate the flags. 526 | 527 | > **Note:** The short & long flags can be declared in any order. However, the alias will always be displayed first. 528 | 529 | > **Important:** If using hyphenated flag names, they will be accessible **as declared** within your [`action()`](#progactionhandler) handler! 530 | 531 | ```js 532 | prog.option('--global'); // no alias 533 | prog.option('-g, --global'); // alias first, comma 534 | prog.option('--global -g'); // alias last, space 535 | // etc... 536 | ``` 537 | 538 | #### desc 539 | 540 | Type: `String` 541 | 542 | The description for the Option. 543 | 544 | #### value 545 | 546 | Type: `String` 547 | 548 | The **default** value for the Option. 549 | 550 | Flags and aliases, if parsed, are `true` by default. See [`mri`](https://github.com/lukeed/mri#minimist) for more info. 551 | 552 | > **Note:** You probably only want to define a default `value` if you're expecting a `String` or `Number` value type. 553 | 554 | 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~! 555 | 556 | 557 | ### prog.version(str) 558 | 559 | The `--version` and `-v` flags will automatically output the Program version. 560 | 561 | #### str 562 | 563 | Type: `String`
564 | Default: `0.0.0` 565 | 566 | The new version number for your Program. 567 | 568 | > **Note:** Your Program `version` is `0.0.0` until you change it. 569 | 570 | ### prog.parse(arr, opts) 571 | 572 | Parse a set of CLI arguments. 573 | 574 | #### arr 575 | 576 | Type: `Array` 577 | 578 | Your Program's `process.argv` input. 579 | 580 | > **Important:** Do not `.slice(2)`! Doing so will break parsing~! 581 | 582 | #### opts 583 | 584 | Type: `Object`
585 | Default: `{}` 586 | 587 | Additional `process.argv` parsing config. See [`mri`'s options](https://github.com/lukeed/mri#mriargs-options) for details. 588 | 589 | > **Important:** These values _override_ any internal values! 590 | 591 | ```js 592 | prog 593 | .command('hello') 594 | .option('-f, --force', 'My flag'); 595 | //=> currently has alias pair: f <--> force 596 | 597 | prog.parse(process.argv, { 598 | alias: { 599 | f: ['foo', 'fizz'] 600 | }, 601 | default: { 602 | abc: 123 603 | } 604 | }); 605 | //=> ADDS alias pair: f <--> foo 606 | //=> REMOVES alias pair: f <--> force 607 | //=> ADDS alias pair: f <--> fizz 608 | //=> ADDS default: abc -> 123 (number) 609 | ``` 610 | 611 | #### opts.unknown 612 | 613 | Type: `Function`
614 | Default: `undefined` 615 | 616 | Callback to run when an unspecified option flag has been found. This is [passed directly to `mri`](https://github.com/lukeed/mri#optionsunknown). 617 | 618 | Your handler will receive the unknown flag (string) as its only argument.
619 | You may return a string, which will be used as a custom error message. Otherwise, a default message is displayed. 620 | 621 | ```js 622 | sade('sirv') 623 | .command('start [dir]') 624 | .parse(process.argv, { 625 | unknown: arg => `Custom error message: ${arg}` 626 | }); 627 | 628 | /* 629 | $ sirv start --foobar 630 | 631 | ERROR 632 | Custom error message: --foobar 633 | 634 | Run `$ sirv --help` for more info. 635 | */ 636 | ``` 637 | 638 | #### opts.lazy 639 | 640 | Type: `Boolean`
641 | Default: `false` 642 | 643 | 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. 644 | 645 | 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. 646 | 647 | ```js 648 | let { name, args, handler } = prog.parse(process.argv, { lazy:true }); 649 | console.log('> Received command: ', name); 650 | 651 | // later on... 652 | handler.apply(null, args); 653 | ``` 654 | 655 | ### prog.help(cmd) 656 | 657 | Manually display the help text for a given command. If no command name is provided, the general/global help is printed. 658 | 659 | Your general and command-specific help text is automatically attached to the `--help` and `-h` flags. 660 | 661 | > **Note:** You don't have to call this directly! It's automatically run when you `bin --help` 662 | 663 | #### cmd 664 | Type: `String`
665 | Default: `null` 666 | 667 | The name of the command for which to display help. Otherwise displays the general help. 668 | 669 | 670 | ## License 671 | 672 | MIT © [Luke Edwards](https://lukeed.com) 673 | -------------------------------------------------------------------------------- /test/usage.js: -------------------------------------------------------------------------------- 1 | import test from 'tape'; 2 | import { join } from 'path'; 3 | import { spawnSync } from 'child_process'; 4 | 5 | const fixtures = join(__dirname, 'fixtures'); 6 | 7 | function exec(file, argv=[]) { 8 | return spawnSync('node', [file, ...argv], { cwd:fixtures }); 9 | } 10 | 11 | test('(usage) basic', t => { 12 | let pid = exec('basic.js', ['foo']); 13 | t.is(pid.status, 0, 'exits without error code'); 14 | t.is(pid.stderr.length, 0, '~> stderr is empty'); 15 | t.is(pid.stdout.toString(), '~> ran "foo" action\n', '~> command invoked'); 16 | t.end(); 17 | }); 18 | 19 | test('(usage) basic :: error :: missing command', t => { 20 | let pid = exec('basic.js'); 21 | t.is(pid.status, 1, 'exits with error code'); 22 | t.is( 23 | pid.stderr.toString(), 24 | '\n ERROR\n No command specified.\n\n Run `$ bin --help` for more info.\n\n', 25 | '~> stderr has "No command specified" error message' 26 | ); 27 | t.is(pid.stdout.length, 0, '~> stdout is empty'); 28 | t.end(); 29 | }); 30 | 31 | test('(usage) basic :: error :: invalid command', t => { 32 | let pid = exec('basic.js', ['foobar']); 33 | t.is(pid.status, 1, 'exits with error code'); 34 | t.is( 35 | pid.stderr.toString(), 36 | '\n ERROR\n Invalid command: foobar\n\n Run `$ bin --help` for more info.\n\n', 37 | '~> stderr has "Invalid command: foobar" error message' 38 | ); 39 | t.is(pid.stdout.length, 0, '~> stdout is empty'); 40 | t.end(); 41 | }); 42 | 43 | test('(usage) basic :: error :: duplicate command', t => { 44 | let pid = exec('repeat.js', ['foo']); 45 | t.is(pid.status, 1, 'exits with error code'); 46 | t.is(pid.stdout.length, 0, '~> stdout is empty'); 47 | // throws an error in the process 48 | t.true(pid.stderr.toString().includes('Error: Command already exists: foo'), '~> threw Error w/ message'); 49 | t.end(); 50 | }); 51 | 52 | test('(usage) basic :: help', t => { 53 | let pid1 = exec('basic.js', ['-h']); 54 | t.is(pid1.status, 0, 'exits with error code'); 55 | t.true(pid1.stdout.toString().includes('Available Commands\n foo'), '~> shows global help w/ "Available Commands" text'); 56 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 57 | 58 | let pid2 = exec('basic.js', ['foo', '-h']); 59 | t.is(pid2.status, 0, 'exits with error code'); 60 | t.true(pid2.stdout.toString().includes('Usage\n $ bin foo [options]'), '~> shows command help w/ "Usage" text'); 61 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 62 | 63 | t.end(); 64 | }); 65 | 66 | 67 | 68 | test('(usage) args.required', t => { 69 | let pid = exec('args.js', ['foo', 'value']); 70 | t.is(pid.status, 0, 'exits without error code'); 71 | t.is(pid.stdout.toString(), '~> ran "foo" with "value" arg\n', '~> command invoked'); 72 | t.is(pid.stderr.length, 0, '~> stderr is empty'); 73 | t.end(); 74 | }); 75 | 76 | test('(usage) args.required :: error :: missing argument', t => { 77 | let pid = exec('args.js', ['foo']); 78 | t.is(pid.status, 1, 'exits with error code'); 79 | t.is( 80 | pid.stderr.toString(), 81 | '\n ERROR\n Insufficient arguments!\n\n Run `$ bin foo --help` for more info.\n\n', 82 | '~> stderr has "Insufficient arguments!" error message' 83 | ); 84 | t.is(pid.stdout.length, 0, '~> stdout is empty'); 85 | t.end(); 86 | }); 87 | 88 | test('(usage) args.optional', t => { 89 | let pid = exec('args.js', ['bar']); 90 | t.is(pid.status, 0, 'exits without error code'); 91 | t.is(pid.stdout.toString(), '~> ran "bar" with "~default~" arg\n', '~> command invoked'); 92 | t.is(pid.stderr.length, 0, '~> stderr is empty'); 93 | t.end(); 94 | }); 95 | 96 | test('(usage) args.optional w/ value', t => { 97 | let pid = exec('args.js', ['bar', 'value']); 98 | t.is(pid.status, 0, 'exits without error code'); 99 | t.is(pid.stdout.toString(), '~> ran "bar" with "value" arg\n', '~> command invoked'); 100 | t.is(pid.stderr.length, 0, '~> stderr is empty'); 101 | t.end(); 102 | }); 103 | 104 | 105 | 106 | test('(usage) options.long', t => { 107 | let pid1 = exec('options.js', ['foo', '--long']); 108 | t.is(pid1.status, 0, 'exits without error code'); 109 | t.is(pid1.stdout.toString(), '~> ran "long" option\n', '~> command invoked'); 110 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 111 | 112 | let pid2 = exec('options.js', ['foo', '-l']); 113 | t.is(pid2.status, 0, 'exits without error code'); 114 | t.is(pid2.stdout.toString(), '~> ran "long" option\n', '~> command invoked'); 115 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 116 | 117 | t.end(); 118 | }); 119 | 120 | test('(usage) options.short', t => { 121 | let pid1 = exec('options.js', ['foo', '--short']); 122 | t.is(pid1.status, 0, 'exits without error code'); 123 | t.is(pid1.stdout.toString(), '~> ran "short" option\n', '~> command invoked'); 124 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 125 | 126 | let pid2 = exec('options.js', ['foo', '-s']); 127 | t.is(pid2.status, 0, 'exits without error code'); 128 | t.is(pid2.stdout.toString(), '~> ran "short" option\n', '~> command invoked'); 129 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 130 | 131 | t.end(); 132 | }); 133 | 134 | test('(usage) options.hello', t => { 135 | let pid1 = exec('options.js', ['foo', '--hello']); 136 | t.is(pid1.status, 0, 'exits without error code'); 137 | t.is(pid1.stdout.toString(), '~> ran "hello" option\n', '~> command invoked'); 138 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 139 | 140 | // shows that '-h' is always reserved 141 | let pid2 = exec('options.js', ['foo', '-h']); 142 | let stdout = pid2.stdout.toString(); 143 | t.is(pid2.status, 0, 'exits without error code'); 144 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 145 | 146 | t.not(stdout, '~> ran "long" option\n', '~> did NOT run custom "-h" option'); 147 | t.true(stdout.includes('-h, --help Displays this message'), '~~> shows `--help` text'); 148 | 149 | t.end(); 150 | }); 151 | 152 | test('(usage) options.extra', t => { 153 | let pid = exec('options.js', ['foo', '--extra=opts', '--404']); 154 | t.is(pid.status, 0, 'exits without error code'); 155 | t.is(pid.stdout.toString(), '~> default with {"404":true,"_":[],"extra":"opts"}\n', '~> command invoked'); 156 | t.is(pid.stderr.length, 0, '~> stderr is empty'); 157 | t.end(); 158 | }); 159 | 160 | test('(usage) options.global', t => { 161 | let pid1 = exec('options.js', ['foo', '--global']); 162 | t.is(pid1.status, 0, 'exits without error code'); 163 | t.is(pid1.stdout.toString(), '~> default with {"_":[],"global":true,"g":true}\n', '~> command invoked'); 164 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 165 | 166 | let pid2 = exec('options.js', ['foo', '-g', 'hello']); 167 | t.is(pid2.status, 0, 'exits without error code'); 168 | t.is(pid2.stdout.toString(), '~> default with {"_":[],"g":"hello","global":"hello"}\n', '~> command invoked'); 169 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 170 | 171 | t.end(); 172 | }); 173 | 174 | test('(usage) options w/o alias', t => { 175 | let pid1 = exec('options.js', ['bar', 'hello']); 176 | t.is(pid1.status, 0, 'exits without error code'); 177 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 178 | t.is(pid1.stdout.toString(), '~> "bar" with "hello" value\n', '~> command invoked'); 179 | 180 | let pid2 = exec('options.js', ['bar', 'hello', '--only']); 181 | t.is(pid2.status, 0, 'exits without error code'); 182 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 183 | t.is(pid2.stdout.toString(), '~> (only) "bar" with "hello" value\n', '~> command invoked'); 184 | 185 | let pid3 = exec('options.js', ['bar', 'hello', '-o']); 186 | t.is(pid3.status, 0, 'exits without error code'); 187 | t.is(pid3.stderr.length, 0, '~> stderr is empty'); 188 | t.is(pid3.stdout.toString(), '~> "bar" with "hello" value\n', '~> command invoked'); 189 | 190 | t.end(); 191 | }); 192 | 193 | 194 | 195 | test('(usage) unknown', t => { 196 | let pid1 = exec('unknown1.js', ['foo', '--global']); 197 | t.is(pid1.status, 0, 'exits without error code'); 198 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 199 | t.is(pid1.stdout.toString(), '~> ran "foo" with {"_":[],"global":true,"g":true}\n', '~> command invoked'); 200 | 201 | let pid2 = exec('unknown1.js', ['foo', '-l']); 202 | t.is(pid2.status, 0, 'exits without error code'); 203 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 204 | t.is(pid2.stdout.toString(), '~> ran "foo" with {"_":[],"l":true,"local":true}\n', '~> command invoked'); 205 | 206 | let pid3 = exec('unknown1.js', ['foo', '--bar']); 207 | t.is(pid3.status, 1, 'exits with error code'); 208 | t.is(pid3.stdout.length, 0, '~> stdout is empty'); 209 | t.is( 210 | pid3.stderr.toString(), 211 | '\n ERROR\n Parsed unknown option flag(s)!\n\n Run `$ bin --help` for more info.\n\n', 212 | '~> stderr has "Parsed unknown option flag" error message (default)' 213 | ); 214 | 215 | t.end(); 216 | }); 217 | 218 | test('(usage) unknown.custom', t => { 219 | let pid1 = exec('unknown2.js', ['foo', '--global', '--local']); 220 | t.is(pid1.status, 0, 'exits without error code'); 221 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 222 | t.is(pid1.stdout.toString(), '~> ran "foo" with {"_":[],"global":true,"local":true,"g":true,"l":true}\n', '~> command invoked'); 223 | 224 | let pid2 = exec('unknown2.js', ['foo', '--bar']); 225 | t.is(pid2.status, 1, 'exits with error code'); 226 | t.is(pid2.stdout.length, 0, '~> stdout is empty'); 227 | t.is( 228 | pid2.stderr.toString(), 229 | '\n ERROR\n Custom error: --bar\n\n Run `$ bin --help` for more info.\n\n', 230 | '~> stderr has "Custom error: --bar" error message' 231 | ); 232 | 233 | t.end(); 234 | }); 235 | 236 | test('(usage) unknown.plain', t => { 237 | let pid1 = exec('unknown2.js', ['foo', '--flag1', '--flag2']); 238 | t.is(pid1.status, 0, 'exits without error code'); 239 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 240 | t.is(pid1.stdout.toString(), '~> ran "foo" with {"_":[],"flag1":true,"flag2":true}\n', '~> command invoked'); 241 | 242 | let pid2 = exec('unknown2.js', ['foo', '--flag3']); 243 | t.is(pid2.status, 1, 'exits with error code'); 244 | t.is(pid2.stdout.length, 0, '~> stdout is empty'); 245 | t.is( 246 | pid2.stderr.toString(), 247 | '\n ERROR\n Custom error: --flag3\n\n Run `$ bin --help` for more info.\n\n', 248 | '~> stderr has "Custom error: --flag3" error message' 249 | ); 250 | 251 | t.end(); 252 | }); 253 | 254 | 255 | 256 | test('(usage) subcommands', t => { 257 | let pid1 = exec('subs.js', ['remote']); 258 | t.is(pid1.status, 0, 'exits without error code'); 259 | t.is(pid1.stdout.toString(), '~> ran "remote" action\n', '~> ran parent'); 260 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 261 | 262 | let pid2 = exec('subs.js', ['remote', 'rename', 'origin', 'foobar']); 263 | t.is(pid2.status, 0, 'exits without error code'); 264 | t.is(pid2.stdout.toString(), '~> ran "remote rename" with "origin" and "foobar" args\n', '~> ran "rename" child'); 265 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 266 | 267 | let pid3 = exec('subs.js', ['remote', 'add', 'origin', 'foobar']); 268 | t.is(pid3.status, 0, 'exits without error code'); 269 | t.is(pid3.stdout.toString(), '~> ran "remote add" with "origin" and "foobar" args\n', '~> ran "add" child'); 270 | t.is(pid3.stderr.length, 0, '~> stderr is empty'); 271 | 272 | t.end(); 273 | }); 274 | 275 | test('(usage) subcommands :: help', t => { 276 | let pid1 = exec('subs.js', ['--help']); 277 | t.is(pid1.status, 0, 'exits without error code'); 278 | t.true(pid1.stdout.toString().includes('Available Commands\n remote \n remote add \n remote rename'), '~> shows global help w/ "Available Commands" text'); 279 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 280 | 281 | let pid2 = exec('subs.js', ['remote', '--help']); 282 | t.is(pid2.status, 0, 'exits without error code'); 283 | t.true(pid2.stdout.toString().includes('Usage\n $ bin remote [options]'), '~> shows "remote" help text'); 284 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 285 | 286 | let pid3 = exec('subs.js', ['remote', 'rename', '--help']); 287 | t.is(pid3.status, 0, 'exits without error code'); 288 | t.true(pid3.stdout.toString().includes('Usage\n $ bin remote rename [options]'), '~> shows "remote rename" help text'); 289 | t.is(pid3.stderr.length, 0, '~> stderr is empty'); 290 | 291 | t.end(); 292 | }); 293 | 294 | 295 | 296 | /* 297 | // TODO: Should this happen instead? 298 | test('(usage) subcommands :: error :: invalid command', t => { 299 | let pid = exec('subs.js', ['remote', 'foobar']); 300 | t.is(pid.status, 1, 'exits with error code'); 301 | t.is(pid.stdout.length, 0, '~> stdout is empty'); 302 | t.is( 303 | pid.stderr.toString(), 304 | '\n ERROR\n Invalid command: remote foobar.\n\n Run `$ bin --help` for more info.\n\n', 305 | '~> stderr has "Invalid command: remote foobar" error message' 306 | ); 307 | 308 | t.end(); 309 | }); 310 | */ 311 | 312 | 313 | 314 | test('(usage) default', t => { 315 | let pid1 = exec('default.js', []); 316 | t.is(pid1.status, 0, 'exits without error code'); 317 | t.is(pid1.stdout.toString(), '~> ran "foo" action w/ "~EMPTY~" arg\n', '~> ran default command'); 318 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 319 | 320 | let pid2 = exec('default.js', ['foo']); 321 | t.is(pid2.status, 0, 'exits without error code'); 322 | t.is(pid2.stdout.toString(), '~> ran "foo" action w/ "~EMPTY~" arg\n', '~> ran default command (direct)'); 323 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 324 | 325 | let pid3 = exec('default.js', ['bar']); 326 | t.is(pid3.status, 0, 'exits without error code'); 327 | t.is(pid3.stdout.toString(), '~> ran "bar" action\n', '~> ran "bar" command'); 328 | t.is(pid3.stderr.length, 0, '~> stderr is empty'); 329 | 330 | t.end(); 331 | }); 332 | 333 | test('(usage) default :: args', t => { 334 | let pid1 = exec('default.js', ['hello']); 335 | t.is(pid1.status, 0, 'exits without error code'); 336 | t.is(pid1.stdout.toString(), '~> ran "foo" action w/ "hello" arg\n'); 337 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 338 | 339 | let pid2 = exec('default.js', ['foo', 'hello']); 340 | t.is(pid2.status, 0, 'exits without error code'); 341 | t.is(pid2.stdout.toString(), '~> ran "foo" action w/ "hello" arg\n', '~> ran default command (direct)'); 342 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 343 | 344 | t.end(); 345 | }); 346 | 347 | test('(usage) default :: help', t => { 348 | let pid1 = exec('default.js', ['--help']); 349 | t.is(pid1.status, 0, 'exits without error code'); 350 | t.true(pid1.stdout.toString().includes('Available Commands\n foo \n bar'), '~> shows global help w/ "Available Commands" text'); 351 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 352 | 353 | let pid2 = exec('default.js', ['foo', '-h']); 354 | t.is(pid2.status, 0, 'exits without error code'); 355 | t.true(pid2.stdout.toString().includes('Usage\n $ bin foo [dir] [options]'), '~> shows command help w/ "Usage" text'); 356 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 357 | 358 | let pid3 = exec('default.js', ['bar', '-h']); 359 | t.is(pid3.status, 0, 'exits without error code'); 360 | t.true(pid3.stdout.toString().includes('Usage\n $ bin bar [options]'), '~> shows command help w/ "Usage" text'); 361 | t.is(pid3.stderr.length, 0, '~> stderr is empty'); 362 | 363 | t.end(); 364 | }); 365 | 366 | 367 | 368 | test('(usage) single :: error :: missing argument', t => { 369 | let pid = exec('single1.js', []); 370 | t.is(pid.status, 1, 'exits with error code'); 371 | t.is(pid.stdout.length, 0, '~> stdout is empty'); 372 | t.is( 373 | pid.stderr.toString(), 374 | '\n ERROR\n Insufficient arguments!\n\n Run `$ bin --help` for more info.\n\n', 375 | '~> stderr has "Insufficient arguments!" error message' 376 | ); 377 | 378 | t.end(); 379 | }); 380 | 381 | test('(usage) single', t => { 382 | let pid1 = exec('single1.js', ['type']); 383 | t.is(pid1.status, 0, 'exits without error code'); 384 | t.is(pid1.stdout.toString(), '~> ran "single" w/ "type" and "~default~" values\n', '~> ran single command'); 385 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 386 | 387 | let pid2 = exec('single1.js', ['type', 'dir']); 388 | t.is(pid2.status, 0, 'exits without error code'); 389 | t.is(pid2.stdout.toString(), '~> ran "single" w/ "type" and "dir" values\n', '~> ran single command'); 390 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 391 | 392 | t.end(); 393 | }); 394 | 395 | test('(usage) single is catch all', t => { 396 | let pid1 = exec('single2.js', ['type']); 397 | t.is(pid1.status, 0, 'exits without error code'); 398 | t.is(pid1.stdout.toString(), `~> ran "single" with: {"_":["type"]}\n`, '~> ran single command'); 399 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 400 | 401 | let pid2 = exec('single2.js', ['type', 'dir', '--global']); 402 | t.is(pid2.status, 0, 'exits without error code'); 403 | t.is(pid2.stdout.toString(), `~> ran "single" with: {"_":["type","dir"],"global":true,"g":true}\n`, '~> ran single command'); 404 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 405 | 406 | t.end(); 407 | }); 408 | 409 | test('(usage) single :: command() throws', t => { 410 | let pid = exec('single3.js', ['foo']); 411 | t.is(pid.status, 1, 'exits with error code'); 412 | t.is(pid.stdout.length, 0, '~> stdout is empty'); 413 | // throws an error in the process 414 | t.true(pid.stderr.toString().includes('Error: Disable "single" mode to add commands'), '~> threw Error w/ message'); 415 | t.end(); 416 | }); 417 | 418 | test('(usage) single :: help', t => { 419 | let pid1 = exec('single1.js', ['--help']); 420 | t.is(pid1.status, 0, 'exits without error code'); 421 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 422 | t.false(pid1.stdout.toString().includes('Available Commands'), '~> global help does NOT show "Available Commands" text'); 423 | 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'); 424 | 425 | let pid2 = exec('single1.js', ['--help']); 426 | t.is(pid2.status, 0, 'exits without error code'); 427 | t.true(pid2.stdout.toString().includes('Usage\n $ bin [dir] [options]'), '~> shows single-command help w/ "Usage" text'); 428 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 429 | 430 | let pid3 = exec('single1.js', ['bar', '--help']); 431 | t.is(pid3.status, 0, 'exits without error code'); 432 | t.true(pid3.stdout.toString().includes('Usage\n $ bin [dir] [options]'), '~> shows single-command help w/ "Usage" text'); 433 | t.is(pid3.stderr.length, 0, '~> stderr is empty'); 434 | 435 | t.end(); 436 | }); 437 | 438 | 439 | // --- 440 | // Command Aliases 441 | // --- 442 | 443 | 444 | test('(usage) alias :: basic', t => { 445 | let pid1 = exec('basic.js', ['f']); 446 | t.is(pid1.status, 0, 'exits without error code'); 447 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 448 | t.is(pid1.stdout.toString(), '~> ran "foo" action\n', '~> command invoked'); 449 | 450 | let pid2 = exec('basic.js', ['fo']); 451 | t.is(pid2.status, 0, 'exits without error code'); 452 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 453 | t.is(pid2.stdout.toString(), '~> ran "foo" action\n', '~> command invoked'); 454 | 455 | t.end(); 456 | }); 457 | 458 | test('(usage) alias :: basic :: error :: invalid command', t => { 459 | let pid = exec('basic.js', ['fff']); 460 | t.is(pid.status, 1, 'exits with error code'); 461 | t.is( 462 | pid.stderr.toString(), 463 | '\n ERROR\n Invalid command: fff\n\n Run `$ bin --help` for more info.\n\n', 464 | '~> stderr has "Invalid command: fff" error message' 465 | ); 466 | t.is(pid.stdout.length, 0, '~> stdout is empty'); 467 | t.end(); 468 | }); 469 | 470 | test('(usage) alias :: basic :: help', t => { 471 | let pid1 = exec('basic.js', ['-h']); 472 | t.is(pid1.status, 0, 'exits with error code'); 473 | t.true(pid1.stdout.toString().includes('Available Commands\n foo'), '~> shows global help w/ "Available Commands" text'); 474 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 475 | 476 | let pid2 = exec('basic.js', ['f', '-h']); 477 | t.is(pid2.status, 0, 'exits with error code'); 478 | t.true(pid2.stdout.toString().includes('Usage\n $ bin foo [options]'), '~> shows command help w/ "Usage" text'); 479 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 480 | 481 | t.end(); 482 | }); 483 | 484 | 485 | test('(usage) alias :: args.required', t => { 486 | let pid = exec('args.js', ['f', 'value']); 487 | t.is(pid.status, 0, 'exits without error code'); 488 | t.is(pid.stdout.toString(), '~> ran "foo" with "value" arg\n', '~> command invoked'); 489 | t.is(pid.stderr.length, 0, '~> stderr is empty'); 490 | t.end(); 491 | }); 492 | 493 | test('(usage) alias :: args.required :: error :: missing argument', t => { 494 | let pid = exec('args.js', ['f']); 495 | t.is(pid.status, 1, 'exits with error code'); 496 | t.is( 497 | pid.stderr.toString(), 498 | '\n ERROR\n Insufficient arguments!\n\n Run `$ bin foo --help` for more info.\n\n', 499 | '~> stderr has "Insufficient arguments!" error message' 500 | ); 501 | t.is(pid.stdout.length, 0, '~> stdout is empty'); 502 | t.end(); 503 | }); 504 | 505 | test('(usage) alias :: args.optional', t => { 506 | let pid = exec('args.js', ['b']); 507 | t.is(pid.status, 0, 'exits without error code'); 508 | t.is(pid.stdout.toString(), '~> ran "bar" with "~default~" arg\n', '~> command invoked'); 509 | t.is(pid.stderr.length, 0, '~> stderr is empty'); 510 | t.end(); 511 | }); 512 | 513 | test('(usage) alias :: args.optional w/ value', t => { 514 | let pid = exec('args.js', ['b', 'value']); 515 | t.is(pid.status, 0, 'exits without error code'); 516 | t.is(pid.stdout.toString(), '~> ran "bar" with "value" arg\n', '~> command invoked'); 517 | t.is(pid.stderr.length, 0, '~> stderr is empty'); 518 | t.end(); 519 | }); 520 | 521 | 522 | 523 | test('(usage) alias :: options.long', t => { 524 | let pid1 = exec('options.js', ['f', '--long']); 525 | t.is(pid1.status, 0, 'exits without error code'); 526 | t.is(pid1.stdout.toString(), '~> ran "long" option\n', '~> command invoked'); 527 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 528 | 529 | let pid2 = exec('options.js', ['f', '-l']); 530 | t.is(pid2.status, 0, 'exits without error code'); 531 | t.is(pid2.stdout.toString(), '~> ran "long" option\n', '~> command invoked'); 532 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 533 | 534 | t.end(); 535 | }); 536 | 537 | test('(usage) alias :: options.short', t => { 538 | let pid1 = exec('options.js', ['f', '--short']); 539 | t.is(pid1.status, 0, 'exits without error code'); 540 | t.is(pid1.stdout.toString(), '~> ran "short" option\n', '~> command invoked'); 541 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 542 | 543 | let pid2 = exec('options.js', ['f', '-s']); 544 | t.is(pid2.status, 0, 'exits without error code'); 545 | t.is(pid2.stdout.toString(), '~> ran "short" option\n', '~> command invoked'); 546 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 547 | 548 | t.end(); 549 | }); 550 | 551 | test('(usage) alias :: options.hello', t => { 552 | let pid1 = exec('options.js', ['f', '--hello']); 553 | t.is(pid1.status, 0, 'exits without error code'); 554 | t.is(pid1.stdout.toString(), '~> ran "hello" option\n', '~> command invoked'); 555 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 556 | 557 | // shows that '-h' is always reserved 558 | let pid2 = exec('options.js', ['f', '-h']); 559 | let stdout = pid2.stdout.toString(); 560 | t.is(pid2.status, 0, 'exits without error code'); 561 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 562 | 563 | t.not(stdout, '~> ran "long" option\n', '~> did NOT run custom "-h" option'); 564 | t.true(stdout.includes('-h, --help Displays this message'), '~~> shows `--help` text'); 565 | 566 | t.end(); 567 | }); 568 | 569 | test('(usage) alias :: options.extra', t => { 570 | let pid = exec('options.js', ['f', '--extra=opts', '--404']); 571 | t.is(pid.status, 0, 'exits without error code'); 572 | t.is(pid.stdout.toString(), '~> default with {"404":true,"_":[],"extra":"opts"}\n', '~> command invoked'); 573 | t.is(pid.stderr.length, 0, '~> stderr is empty'); 574 | t.end(); 575 | }); 576 | 577 | test('(usage) alias :: options.global', t => { 578 | let pid1 = exec('options.js', ['f', '--global']); 579 | t.is(pid1.status, 0, 'exits without error code'); 580 | t.is(pid1.stdout.toString(), '~> default with {"_":[],"global":true,"g":true}\n', '~> command invoked'); 581 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 582 | 583 | let pid2 = exec('options.js', ['f', '-g', 'hello']); 584 | t.is(pid2.status, 0, 'exits without error code'); 585 | t.is(pid2.stdout.toString(), '~> default with {"_":[],"g":"hello","global":"hello"}\n', '~> command invoked'); 586 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 587 | 588 | t.end(); 589 | }); 590 | 591 | test('(usage) alias :: options w/o alias', t => { 592 | let pid1 = exec('options.js', ['b', 'hello']); 593 | t.is(pid1.status, 0, 'exits without error code'); 594 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 595 | t.is(pid1.stdout.toString(), '~> "bar" with "hello" value\n', '~> command invoked'); 596 | 597 | let pid2 = exec('options.js', ['b', 'hello', '--only']); 598 | t.is(pid2.status, 0, 'exits without error code'); 599 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 600 | t.is(pid2.stdout.toString(), '~> (only) "bar" with "hello" value\n', '~> command invoked'); 601 | 602 | let pid3 = exec('options.js', ['b', 'hello', '-o']); 603 | t.is(pid3.status, 0, 'exits without error code'); 604 | t.is(pid3.stderr.length, 0, '~> stderr is empty'); 605 | t.is(pid3.stdout.toString(), '~> "bar" with "hello" value\n', '~> command invoked'); 606 | 607 | t.end(); 608 | }); 609 | 610 | 611 | 612 | test('(usage) alias :: unknown', t => { 613 | let pid1 = exec('unknown1.js', ['f', '--global']); 614 | t.is(pid1.status, 0, 'exits without error code'); 615 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 616 | t.is(pid1.stdout.toString(), '~> ran "foo" with {"_":[],"global":true,"g":true}\n', '~> command invoked'); 617 | 618 | let pid2 = exec('unknown1.js', ['f', '-l']); 619 | t.is(pid2.status, 0, 'exits without error code'); 620 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 621 | t.is(pid2.stdout.toString(), '~> ran "foo" with {"_":[],"l":true,"local":true}\n', '~> command invoked'); 622 | 623 | let pid3 = exec('unknown1.js', ['f', '--bar']); 624 | t.is(pid3.status, 1, 'exits with error code'); 625 | t.is(pid3.stdout.length, 0, '~> stdout is empty'); 626 | t.is( 627 | pid3.stderr.toString(), 628 | '\n ERROR\n Parsed unknown option flag(s)!\n\n Run `$ bin --help` for more info.\n\n', 629 | '~> stderr has "Parsed unknown option flag" error message (default)' 630 | ); 631 | 632 | t.end(); 633 | }); 634 | 635 | test('(usage) alias :: unknown.custom', t => { 636 | let pid1 = exec('unknown2.js', ['f', '--global', '--local']); 637 | t.is(pid1.status, 0, 'exits without error code'); 638 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 639 | t.is(pid1.stdout.toString(), '~> ran "foo" with {"_":[],"global":true,"local":true,"g":true,"l":true}\n', '~> command invoked'); 640 | 641 | let pid2 = exec('unknown2.js', ['f', '--bar']); 642 | t.is(pid2.status, 1, 'exits with error code'); 643 | t.is(pid2.stdout.length, 0, '~> stdout is empty'); 644 | t.is( 645 | pid2.stderr.toString(), 646 | '\n ERROR\n Custom error: --bar\n\n Run `$ bin --help` for more info.\n\n', 647 | '~> stderr has "Custom error: --bar" error message' 648 | ); 649 | 650 | t.end(); 651 | }); 652 | 653 | test('(usage) alias :: unknown.plain', t => { 654 | let pid1 = exec('unknown2.js', ['f', '--flag1', '--flag2']); 655 | t.is(pid1.status, 0, 'exits without error code'); 656 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 657 | t.is(pid1.stdout.toString(), '~> ran "foo" with {"_":[],"flag1":true,"flag2":true}\n', '~> command invoked'); 658 | 659 | let pid2 = exec('unknown2.js', ['f', '--flag3']); 660 | t.is(pid2.status, 1, 'exits with error code'); 661 | t.is(pid2.stdout.length, 0, '~> stdout is empty'); 662 | t.is( 663 | pid2.stderr.toString(), 664 | '\n ERROR\n Custom error: --flag3\n\n Run `$ bin --help` for more info.\n\n', 665 | '~> stderr has "Custom error: --flag3" error message' 666 | ); 667 | 668 | t.end(); 669 | }); 670 | 671 | 672 | 673 | test('(usage) alias :: subcommands', t => { 674 | let pid1 = exec('subs.js', ['r']); 675 | t.is(pid1.status, 0, 'exits without error code'); 676 | t.is(pid1.stdout.toString(), '~> ran "remote" action\n', '~> ran parent'); 677 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 678 | 679 | let pid2 = exec('subs.js', ['rr', 'origin', 'foobar']); 680 | t.is(pid2.status, 0, 'exits without error code'); 681 | t.is(pid2.stdout.toString(), '~> ran "remote rename" with "origin" and "foobar" args\n', '~> ran "rename" child'); 682 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 683 | 684 | let pid3 = exec('subs.js', ['ra', 'origin', 'foobar']); 685 | t.is(pid3.status, 0, 'exits without error code'); 686 | t.is(pid3.stdout.toString(), '~> ran "remote add" with "origin" and "foobar" args\n', '~> ran "add" child'); 687 | t.is(pid3.stderr.length, 0, '~> stderr is empty'); 688 | 689 | t.end(); 690 | }); 691 | 692 | test('(usage) alias :: subcommands :: help', t => { 693 | let pid1 = exec('subs.js', ['--help']); 694 | t.is(pid1.status, 0, 'exits without error code'); 695 | t.true(pid1.stdout.toString().includes('Available Commands\n remote \n remote add \n remote rename'), '~> shows global help w/ "Available Commands" text'); 696 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 697 | 698 | let pid2 = exec('subs.js', ['r', '--help']); 699 | t.is(pid2.status, 0, 'exits without error code'); 700 | t.true(pid2.stdout.toString().includes('Usage\n $ bin remote [options]'), '~> shows "remote" help text'); 701 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 702 | 703 | let pid3 = exec('subs.js', ['rr', '--help']); 704 | t.is(pid3.status, 0, 'exits without error code'); 705 | t.true(pid3.stdout.toString().includes('Usage\n $ bin remote rename [options]'), '~> shows "remote rename" help text'); 706 | t.is(pid3.stderr.length, 0, '~> stderr is empty'); 707 | 708 | t.end(); 709 | }); 710 | 711 | 712 | test('(usage) alias :: default', t => { 713 | let pid1 = exec('default.js', []); 714 | t.is(pid1.status, 0, 'exits without error code'); 715 | t.is(pid1.stdout.toString(), '~> ran "foo" action w/ "~EMPTY~" arg\n', '~> ran default command'); 716 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 717 | 718 | let pid2 = exec('default.js', ['f']); 719 | t.is(pid2.status, 0, 'exits without error code'); 720 | t.is(pid2.stdout.toString(), '~> ran "foo" action w/ "~EMPTY~" arg\n', '~> ran default command (direct)'); 721 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 722 | 723 | let pid3 = exec('default.js', ['f', 'hello']); 724 | t.is(pid3.status, 0, 'exits without error code'); 725 | t.is(pid3.stdout.toString(), '~> ran "foo" action w/ "hello" arg\n', '~> ran default command (direct)'); 726 | t.is(pid3.stderr.length, 0, '~> stderr is empty'); 727 | 728 | let pid4 = exec('default.js', ['b']); 729 | t.is(pid4.status, 0, 'exits without error code'); 730 | t.is(pid4.stdout.toString(), '~> ran "bar" action\n', '~> ran "bar" command'); 731 | t.is(pid4.stderr.length, 0, '~> stderr is empty'); 732 | 733 | t.end(); 734 | }); 735 | 736 | test('(usage) default :: args', t => { 737 | let pid1 = exec('default.js', ['hello']); 738 | t.is(pid1.status, 0, 'exits without error code'); 739 | t.is(pid1.stdout.toString(), '~> ran "foo" action w/ "hello" arg\n'); 740 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 741 | 742 | let pid2 = exec('default.js', ['f', 'hello']); 743 | t.is(pid2.status, 0, 'exits without error code'); 744 | t.is(pid2.stdout.toString(), '~> ran "foo" action w/ "hello" arg\n', '~> ran default command (direct)'); 745 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 746 | 747 | t.end(); 748 | }); 749 | 750 | test('(usage) alias :: default :: help', t => { 751 | let pid1 = exec('default.js', ['--help']); 752 | t.is(pid1.status, 0, 'exits without error code'); 753 | t.true(pid1.stdout.toString().includes('Available Commands\n foo \n bar'), '~> shows global help w/ "Available Commands" text'); 754 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 755 | 756 | let pid2 = exec('default.js', ['f', '-h']); 757 | t.is(pid2.status, 0, 'exits without error code'); 758 | t.true(pid2.stdout.toString().includes('Usage\n $ bin foo [dir] [options]'), '~> shows command help w/ "Usage" text'); 759 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 760 | 761 | let pid3 = exec('default.js', ['b', '-h']); 762 | t.is(pid3.status, 0, 'exits without error code'); 763 | t.true(pid3.stdout.toString().includes('Usage\n $ bin bar [options]'), '~> shows command help w/ "Usage" text'); 764 | t.is(pid3.stderr.length, 0, '~> stderr is empty'); 765 | 766 | t.end(); 767 | }); 768 | 769 | test('(usage) alias :: single :: throws', t => { 770 | let pid = exec('alias1.js'); 771 | t.is(pid.status, 1, 'exits with error code'); 772 | t.is(pid.stdout.length, 0, '~> stdout is empty'); 773 | // throws an error in the process 774 | t.true(pid.stderr.toString().includes('Error: Cannot call `alias()` in "single" mode'), '~> threw Error w/ message'); 775 | t.end(); 776 | }); 777 | 778 | test('(usage) alias :: pre-command :: throws', t => { 779 | let pid = exec('alias2.js'); 780 | t.is(pid.status, 1, 'exits with error code'); 781 | t.is(pid.stdout.length, 0, '~> stdout is empty'); 782 | // throws an error in the process 783 | t.true(pid.stderr.toString().includes('Error: Cannot call `alias()` before defining a command'), '~> threw Error w/ message'); 784 | t.end(); 785 | }); 786 | 787 | 788 | // --- 789 | // Input Order 790 | // --- 791 | 792 | 793 | test('(usage) order :: basic', t => { 794 | let pid1 = exec('basic.js', ['--foo', 'bar', 'f']); 795 | t.is(pid1.status, 0, 'exits without error code'); 796 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 797 | t.is(pid1.stdout.toString(), '~> ran "foo" action\n', '~> command invoked'); 798 | 799 | let pid2 = exec('basic.js', ['--foo', 'bar', 'fo']); 800 | t.is(pid2.status, 0, 'exits without error code'); 801 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 802 | t.is(pid2.stdout.toString(), '~> ran "foo" action\n', '~> command invoked'); 803 | 804 | t.end(); 805 | }); 806 | 807 | test('(usage) order :: basic :: error :: invalid command', t => { 808 | let pid = exec('basic.js', ['--foo', 'bar', 'fff']); 809 | t.is(pid.status, 1, 'exits with error code'); 810 | t.is( 811 | pid.stderr.toString(), 812 | '\n ERROR\n Invalid command: fff\n\n Run `$ bin --help` for more info.\n\n', 813 | '~> stderr has "Invalid command: fff" error message' 814 | ); 815 | t.is(pid.stdout.length, 0, '~> stdout is empty'); 816 | t.end(); 817 | }); 818 | 819 | test('(usage) order :: basic :: help', t => { 820 | let pid1 = exec('basic.js', ['--foo', 'bar', '-h']); 821 | t.is(pid1.status, 0, 'exits with error code'); 822 | t.true(pid1.stdout.toString().includes('Available Commands\n foo'), '~> shows global help w/ "Available Commands" text'); 823 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 824 | 825 | let pid2 = exec('basic.js', ['--foo', 'bar', 'f', '-h']); 826 | t.is(pid2.status, 0, 'exits with error code'); 827 | t.true(pid2.stdout.toString().includes('Usage\n $ bin foo [options]'), '~> shows command help w/ "Usage" text'); 828 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 829 | 830 | t.end(); 831 | }); 832 | 833 | 834 | test('(usage) order :: args.required', t => { 835 | let pid = exec('args.js', ['--foo', 'bar', 'f', 'value']); 836 | t.is(pid.status, 0, 'exits without error code'); 837 | t.is(pid.stdout.toString(), '~> ran "foo" with "value" arg\n', '~> command invoked'); 838 | t.is(pid.stderr.length, 0, '~> stderr is empty'); 839 | t.end(); 840 | }); 841 | 842 | test('(usage) order :: args.required :: error :: missing argument', t => { 843 | let pid = exec('args.js', ['--foo', 'bar', 'f']); 844 | t.is(pid.status, 1, 'exits with error code'); 845 | t.is( 846 | pid.stderr.toString(), 847 | '\n ERROR\n Insufficient arguments!\n\n Run `$ bin foo --help` for more info.\n\n', 848 | '~> stderr has "Insufficient arguments!" error message' 849 | ); 850 | t.is(pid.stdout.length, 0, '~> stdout is empty'); 851 | t.end(); 852 | }); 853 | 854 | test('(usage) order :: args.optional', t => { 855 | let pid = exec('args.js', ['--foo', 'bar', 'b']); 856 | t.is(pid.status, 0, 'exits without error code'); 857 | t.is(pid.stdout.toString(), '~> ran "bar" with "~default~" arg\n', '~> command invoked'); 858 | t.is(pid.stderr.length, 0, '~> stderr is empty'); 859 | t.end(); 860 | }); 861 | 862 | test('(usage) order :: args.optional w/ value', t => { 863 | let pid = exec('args.js', ['--foo', 'bar', 'b', 'value']); 864 | t.is(pid.status, 0, 'exits without error code'); 865 | t.is(pid.stdout.toString(), '~> ran "bar" with "value" arg\n', '~> command invoked'); 866 | t.is(pid.stderr.length, 0, '~> stderr is empty'); 867 | t.end(); 868 | }); 869 | 870 | 871 | 872 | test('(usage) order :: options.long', t => { 873 | let pid1 = exec('options.js', ['--foo', 'bar', 'f', '--long']); 874 | t.is(pid1.status, 0, 'exits without error code'); 875 | t.is(pid1.stdout.toString(), '~> ran "long" option\n', '~> command invoked'); 876 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 877 | 878 | let pid2 = exec('options.js', ['--foo', 'bar', 'f', '-l']); 879 | t.is(pid2.status, 0, 'exits without error code'); 880 | t.is(pid2.stdout.toString(), '~> ran "long" option\n', '~> command invoked'); 881 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 882 | 883 | t.end(); 884 | }); 885 | 886 | test('(usage) order :: options.short', t => { 887 | let pid1 = exec('options.js', ['--foo', 'bar', 'f', '--short']); 888 | t.is(pid1.status, 0, 'exits without error code'); 889 | t.is(pid1.stdout.toString(), '~> ran "short" option\n', '~> command invoked'); 890 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 891 | 892 | let pid2 = exec('options.js', ['--foo', 'bar', 'f', '-s']); 893 | t.is(pid2.status, 0, 'exits without error code'); 894 | t.is(pid2.stdout.toString(), '~> ran "short" option\n', '~> command invoked'); 895 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 896 | 897 | t.end(); 898 | }); 899 | 900 | test('(usage) order :: options.hello', t => { 901 | let pid1 = exec('options.js', ['--foo', 'bar', 'f', '--hello']); 902 | t.is(pid1.status, 0, 'exits without error code'); 903 | t.is(pid1.stdout.toString(), '~> ran "hello" option\n', '~> command invoked'); 904 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 905 | 906 | // shows that '-h' is always reserved 907 | let pid2 = exec('options.js', ['--foo', 'bar', 'f', '-h']); 908 | let stdout = pid2.stdout.toString(); 909 | t.is(pid2.status, 0, 'exits without error code'); 910 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 911 | 912 | t.not(stdout, '~> ran "long" option\n', '~> did NOT run custom "-h" option'); 913 | t.true(stdout.includes('-h, --help Displays this message'), '~~> shows `--help` text'); 914 | 915 | t.end(); 916 | }); 917 | 918 | test('(usage) order :: options.extra', t => { 919 | let pid = exec('options.js', ['--foo', 'bar', 'f', '--extra=opts', '--404']); 920 | t.is(pid.status, 0, 'exits without error code'); 921 | t.is(pid.stdout.toString(), '~> default with {"404":true,"_":[],"foo":"bar","extra":"opts"}\n', '~> command invoked'); 922 | t.is(pid.stderr.length, 0, '~> stderr is empty'); 923 | t.end(); 924 | }); 925 | 926 | test('(usage) order :: options.global', t => { 927 | let pid1 = exec('options.js', ['--foo', 'bar', 'f', '--global']); 928 | t.is(pid1.status, 0, 'exits without error code'); 929 | t.is(pid1.stdout.toString(), '~> default with {"_":[],"foo":"bar","global":true,"g":true}\n', '~> command invoked'); 930 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 931 | 932 | let pid2 = exec('options.js', ['--foo', 'bar', 'f', '-g', 'hello']); 933 | t.is(pid2.status, 0, 'exits without error code'); 934 | t.is(pid2.stdout.toString(), '~> default with {"_":[],"foo":"bar","g":"hello","global":"hello"}\n', '~> command invoked'); 935 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 936 | 937 | t.end(); 938 | }); 939 | 940 | test('(usage) order :: options w/o alias', t => { 941 | let pid1 = exec('options.js', ['--foo', 'bar', 'b', 'hello']); 942 | t.is(pid1.status, 0, 'exits without error code'); 943 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 944 | t.is(pid1.stdout.toString(), '~> "bar" with "hello" value\n', '~> command invoked'); 945 | 946 | let pid2 = exec('options.js', ['--foo', 'bar', 'b', 'hello', '--only']); 947 | t.is(pid2.status, 0, 'exits without error code'); 948 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 949 | t.is(pid2.stdout.toString(), '~> (only) "bar" with "hello" value\n', '~> command invoked'); 950 | 951 | let pid3 = exec('options.js', ['--foo', 'bar', 'b', 'hello', '-o']); 952 | t.is(pid3.status, 0, 'exits without error code'); 953 | t.is(pid3.stderr.length, 0, '~> stderr is empty'); 954 | t.is(pid3.stdout.toString(), '~> "bar" with "hello" value\n', '~> command invoked'); 955 | 956 | t.end(); 957 | }); 958 | 959 | 960 | test('(usage) order :: unknown.custom', t => { 961 | let pid1 = exec('unknown2.js', ['f', '--global', '--local']); 962 | t.is(pid1.status, 0, 'exits without error code'); 963 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 964 | t.is(pid1.stdout.toString(), '~> ran "foo" with {"_":[],"global":true,"local":true,"g":true,"l":true}\n', '~> command invoked'); 965 | 966 | let pid2 = exec('unknown2.js', ['--foo', 'bar', 'f', '--bar']); 967 | t.is(pid2.status, 1, 'exits with error code'); 968 | t.is(pid2.stdout.length, 0, '~> stdout is empty'); 969 | t.is( 970 | pid2.stderr.toString(), 971 | '\n ERROR\n Custom error: --foo\n\n Run `$ bin --help` for more info.\n\n', // came first 972 | '~> stderr has "Custom error: --foo" error message' // came first 973 | ); 974 | 975 | t.end(); 976 | }); 977 | 978 | 979 | test('(usage) order :: unknown.plain', t => { 980 | let pid1 = exec('unknown2.js', ['f', '--flag1', '--flag2']); 981 | t.is(pid1.status, 0, 'exits without error code'); 982 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 983 | t.is(pid1.stdout.toString(), '~> ran "foo" with {"_":[],"flag1":true,"flag2":true}\n', '~> command invoked'); 984 | 985 | let pid2 = exec('unknown2.js', ['--foo', 'bar', 'f', '--flag3']); 986 | t.is(pid2.status, 1, 'exits with error code'); 987 | t.is(pid2.stdout.length, 0, '~> stdout is empty'); 988 | t.is( 989 | pid2.stderr.toString(), 990 | '\n ERROR\n Custom error: --foo\n\n Run `$ bin --help` for more info.\n\n', // came first 991 | '~> stderr has "Custom error: --foo" error message' // came first 992 | ); 993 | 994 | t.end(); 995 | }); 996 | 997 | 998 | 999 | test('(usage) order :: subcommands', t => { 1000 | let pid1 = exec('subs.js', ['--foo', 'bar', 'r']); 1001 | t.is(pid1.status, 0, 'exits without error code'); 1002 | t.is(pid1.stdout.toString(), '~> ran "remote" action\n', '~> ran parent'); 1003 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 1004 | 1005 | let pid2 = exec('subs.js', ['--foo', 'bar', 'rr', 'origin', 'foobar']); 1006 | t.is(pid2.status, 0, 'exits without error code'); 1007 | t.is(pid2.stdout.toString(), '~> ran "remote rename" with "origin" and "foobar" args\n', '~> ran "rename" child'); 1008 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 1009 | 1010 | let pid3 = exec('subs.js', ['--foo', 'bar', 'ra', 'origin', 'foobar']); 1011 | t.is(pid3.status, 0, 'exits without error code'); 1012 | t.is(pid3.stdout.toString(), '~> ran "remote add" with "origin" and "foobar" args\n', '~> ran "add" child'); 1013 | t.is(pid3.stderr.length, 0, '~> stderr is empty'); 1014 | 1015 | let pid4 = exec('subs.js', ['--foo', 'bar', 'remote', 'new', 'origin', 'foobar']); 1016 | t.is(pid4.status, 0, 'exits without error code'); 1017 | t.is(pid4.stdout.toString(), '~> ran "remote add" with "origin" and "foobar" args\n', '~> ran "add" child'); 1018 | t.is(pid4.stderr.length, 0, '~> stderr is empty'); 1019 | 1020 | t.end(); 1021 | }); 1022 | 1023 | test('(usage) order :: subcommands :: help', t => { 1024 | let pid1 = exec('subs.js', ['--foo', 'bar', '--help']); 1025 | t.is(pid1.status, 0, 'exits without error code'); 1026 | t.true(pid1.stdout.toString().includes('Available Commands\n remote \n remote add \n remote rename'), '~> shows global help w/ "Available Commands" text'); 1027 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 1028 | 1029 | let pid2 = exec('subs.js', ['--foo', 'bar', 'r', '--help']); 1030 | t.is(pid2.status, 0, 'exits without error code'); 1031 | t.true(pid2.stdout.toString().includes('Usage\n $ bin remote [options]'), '~> shows "remote" help text'); 1032 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 1033 | 1034 | let pid3 = exec('subs.js', ['--foo', 'bar', 'rr', '--help']); 1035 | t.is(pid3.status, 0, 'exits without error code'); 1036 | t.true(pid3.stdout.toString().includes('Usage\n $ bin remote rename [options]'), '~> shows "remote rename" help text'); 1037 | t.is(pid3.stderr.length, 0, '~> stderr is empty'); 1038 | 1039 | let pid4 = exec('subs.js', ['--foo', 'bar', 'remote', 'new', '--help']); 1040 | t.is(pid4.status, 0, 'exits without error code'); 1041 | 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'); 1042 | t.is(pid4.stderr.length, 0, '~> stderr is empty'); 1043 | 1044 | t.end(); 1045 | }); 1046 | 1047 | 1048 | test('(usage) order :: default', t => { 1049 | let pid1 = exec('default.js', ['--foo', 'bar']); 1050 | t.is(pid1.status, 0, 'exits without error code'); 1051 | t.is(pid1.stdout.toString(), '~> ran "foo" action w/ "~EMPTY~" arg\n', '~> ran default command'); 1052 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 1053 | 1054 | let pid2 = exec('default.js', ['--foo', 'bar', 'f']); 1055 | t.is(pid2.status, 0, 'exits without error code'); 1056 | t.is(pid2.stdout.toString(), '~> ran "foo" action w/ "~EMPTY~" arg\n', '~> ran default command (direct)'); 1057 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 1058 | 1059 | let pid3 = exec('default.js', ['--foo', 'bar', 'b']); 1060 | t.is(pid3.status, 0, 'exits without error code'); 1061 | t.is(pid3.stdout.toString(), '~> ran "bar" action\n', '~> ran "bar" command'); 1062 | t.is(pid3.stderr.length, 0, '~> stderr is empty'); 1063 | 1064 | t.end(); 1065 | }); 1066 | 1067 | test('(usage) default :: args', t => { 1068 | let pid1 = exec('default.js', ['--foo', 'bar', 'hello']); 1069 | t.is(pid1.status, 0, 'exits without error code'); 1070 | t.is(pid1.stdout.toString(), '~> ran "foo" action w/ "hello" arg\n'); 1071 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 1072 | 1073 | let pid2 = exec('default.js', ['--foo', 'bar', 'foo', 'hello']); 1074 | t.is(pid2.status, 0, 'exits without error code'); 1075 | t.is(pid2.stdout.toString(), '~> ran "foo" action w/ "hello" arg\n', '~> ran default command (direct)'); 1076 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 1077 | 1078 | let pid3 = exec('default.js', ['--foo', 'bar', 'foo', 'hello']); 1079 | t.is(pid3.status, 0, 'exits without error code'); 1080 | t.is(pid3.stdout.toString(), '~> ran "foo" action w/ "hello" arg\n', '~> ran default command (direct)'); 1081 | t.is(pid3.stderr.length, 0, '~> stderr is empty'); 1082 | 1083 | t.end(); 1084 | }); 1085 | 1086 | test('(usage) order :: default :: help', t => { 1087 | let pid1 = exec('default.js', ['--foo', 'bar', '--help']); 1088 | t.is(pid1.status, 0, 'exits without error code'); 1089 | t.true(pid1.stdout.toString().includes('Available Commands\n foo \n bar'), '~> shows global help w/ "Available Commands" text'); 1090 | t.is(pid1.stderr.length, 0, '~> stderr is empty'); 1091 | 1092 | let pid2 = exec('default.js', ['--foo', 'bar', 'f', '-h']); 1093 | t.is(pid2.status, 0, 'exits without error code'); 1094 | t.true(pid2.stdout.toString().includes('Usage\n $ bin foo [dir] [options]'), '~> shows command help w/ "Usage" text'); 1095 | t.is(pid2.stderr.length, 0, '~> stderr is empty'); 1096 | 1097 | let pid3 = exec('default.js', ['--foo', 'bar', 'b', '-h']); 1098 | t.is(pid3.status, 0, 'exits without error code'); 1099 | t.true(pid3.stdout.toString().includes('Usage\n $ bin bar [options]'), '~> shows command help w/ "Usage" text'); 1100 | t.is(pid3.stderr.length, 0, '~> stderr is empty'); 1101 | 1102 | t.end(); 1103 | }); 1104 | 1105 | test('(usage) order :: single :: throws', t => { 1106 | let pid = exec('alias1.js', ['--foo', 'bar']); 1107 | t.is(pid.status, 1, 'exits with error code'); 1108 | t.is(pid.stdout.length, 0, '~> stdout is empty'); 1109 | // throws an error in the process 1110 | t.true(pid.stderr.toString().includes('Error: Cannot call `alias()` in "single" mode'), '~> threw Error w/ message'); 1111 | t.end(); 1112 | }); 1113 | 1114 | test('(usage) order :: pre-command :: throws', t => { 1115 | let pid = exec('alias2.js', ['--foo', 'bar']); 1116 | t.is(pid.status, 1, 'exits with error code'); 1117 | t.is(pid.stdout.length, 0, '~> stdout is empty'); 1118 | // throws an error in the process 1119 | t.true(pid.stderr.toString().includes('Error: Cannot call `alias()` before defining a command'), '~> threw Error w/ message'); 1120 | t.end(); 1121 | }); 1122 | --------------------------------------------------------------------------------