├── lib ├── index.js ├── shell.js ├── arg.js ├── completion.sh ├── coaobject.js ├── coaparam.js ├── opt.js ├── completion.js └── cmd.js ├── index.mjs ├── DisclosureTriangleRole.js ├── LICENSE.txt ├── unique.js ├── anyOf.jst ├── link-rel-prerender.js ├── geolocation.js ├── anySeries.js ├── package.json ├── README.md └── CHANGELOG.md /lib/index.js: -------------------------------------------------------------------------------- 1 | const 2 | Cmd = require('./cmd'), 3 | Opt = require('./opt'), 4 | Arg = require('./arg'), 5 | shell = require('./shell'); 6 | 7 | module.exports = { 8 | Cmd : Cmd.create, 9 | Opt : Opt.create, 10 | Arg : Arg.create, 11 | classes : { Cmd, Opt, Arg }, 12 | shell, 13 | require 14 | }; 15 | -------------------------------------------------------------------------------- /index.mjs: -------------------------------------------------------------------------------- 1 | // Bootstrap cliui with CommonJS dependencies: 2 | import { cliui } from './build/lib/index.js' 3 | import { wrap, stripAnsi } from './build/lib/string-utils.js' 4 | 5 | export default function ui (opts) { 6 | return cliui(opts, { 7 | stringWidth: (str) => { 8 | return [...str].length 9 | }, 10 | stripAnsi, 11 | wrap 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /lib/shell.js: -------------------------------------------------------------------------------- 1 | module.exports = { escape, unescape }; 2 | 3 | function unescape(w) { 4 | w = w.charAt(0) === '"' 5 | ? w.replace(/^"|([^\\])"$/g, '$1') 6 | : w.replace(/\\ /g, ' '); 7 | 8 | return w.replace(/\\("|'|\$|`|\\)/g, '$1'); 9 | } 10 | 11 | function escape(w) { 12 | w = w.replace(/(["'$`\\])/g,'\\$1'); 13 | return w.match(/\s+/) ? `"${w}"` : w; 14 | } 15 | -------------------------------------------------------------------------------- /DisclosureTriangleRole.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports["default"] = void 0; 7 | var DisclosureTriangleRole = { 8 | relatedConcepts: [{ 9 | module: 'HTML', 10 | concept: { 11 | name: 'summary' 12 | } 13 | }], 14 | type: 'widget' 15 | }; 16 | var _default = DisclosureTriangleRole; 17 | exports["default"] = _default; -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Contributors 2 | 3 | Permission to use, copy, modify, and/or distribute this software 4 | for any purpose with or without fee is hereby granted, provided 5 | that the above copyright notice and this permission notice 6 | appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES 10 | OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE 11 | LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES 12 | OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, 13 | WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 14 | ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /unique.js: -------------------------------------------------------------------------------- 1 | var uniqueExec = require('../vendor/unique'); 2 | /** 3 | * 4 | * @namespace faker.unique 5 | */ 6 | function Unique (faker) { 7 | 8 | // initialize unique module class variables 9 | 10 | // maximum time unique.exec will attempt to run before aborting 11 | var maxTime = 10; 12 | 13 | // maximum retries unique.exec will recurse before abortings ( max loop depth ) 14 | var maxRetries = 10; 15 | 16 | // time the script started 17 | // var startTime = 0; 18 | 19 | /** 20 | * unique 21 | * 22 | * @method unique 23 | */ 24 | this.unique = function unique (method, args, opts) { 25 | opts = opts || {}; 26 | opts.startTime = new Date().getTime(); 27 | if (typeof opts.maxTime !== 'number') { 28 | opts.maxTime = maxTime; 29 | } 30 | if (typeof opts.maxRetries !== 'number') { 31 | opts.maxRetries = maxRetries; 32 | } 33 | opts.currentIterations = 0; 34 | return uniqueExec.exec(method, args, opts); 35 | } 36 | } 37 | 38 | module['exports'] = Unique; -------------------------------------------------------------------------------- /anyOf.jst: -------------------------------------------------------------------------------- 1 | {{# def.definitions }} 2 | {{# def.errors }} 3 | {{# def.setupKeyword }} 4 | {{# def.setupNextLevel }} 5 | 6 | {{ 7 | var $noEmptySchema = $schema.every(function($sch) { 8 | return {{# def.nonEmptySchema:$sch }}; 9 | }); 10 | }} 11 | {{? $noEmptySchema }} 12 | {{ var $currentBaseId = $it.baseId; }} 13 | var {{=$errs}} = errors; 14 | var {{=$valid}} = false; 15 | 16 | {{# def.setCompositeRule }} 17 | 18 | {{~ $schema:$sch:$i }} 19 | {{ 20 | $it.schema = $sch; 21 | $it.schemaPath = $schemaPath + '[' + $i + ']'; 22 | $it.errSchemaPath = $errSchemaPath + '/' + $i; 23 | }} 24 | 25 | {{# def.insertSubschemaCode }} 26 | 27 | {{=$valid}} = {{=$valid}} || {{=$nextValid}}; 28 | 29 | if (!{{=$valid}}) { 30 | {{ $closingBraces += '}'; }} 31 | {{~}} 32 | 33 | {{# def.resetCompositeRule }} 34 | 35 | {{= $closingBraces }} 36 | 37 | if (!{{=$valid}}) { 38 | {{# def.extraError:'anyOf' }} 39 | } else { 40 | {{# def.resetErrors }} 41 | {{? it.opts.allErrors }} } {{?}} 42 | {{??}} 43 | {{? $breakOnError }} 44 | if (true) { 45 | {{?}} 46 | {{?}} 47 | -------------------------------------------------------------------------------- /link-rel-prerender.js: -------------------------------------------------------------------------------- 1 | module.exports={A:{A:{"1":"B","2":"J D E F A uB"},B:{"1":"P Q R S T U V W Z a b c d e f g h i j k X H","2":"C K L G M N O"},C:{"2":"0 1 2 3 4 5 6 7 8 9 vB jB I l J D E F A B C K L G M N O m n o p q r s t u v w x y z AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB kB QB lB RB SB Y TB UB VB WB XB YB ZB aB bB cB dB eB fB gB P Q R mB S T U V W Z a b c d e f g h i j k X H nB wB xB"},D:{"1":"0 1 2 3 4 5 6 7 8 9 K L G M N O m n o p q r s t u v w x y z AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB kB QB lB RB SB Y TB UB VB WB XB YB ZB aB bB cB dB eB fB gB P Q R S T U V W Z a b c d e f g h i j k X H nB yB zB","2":"I l J D E F A B C"},E:{"2":"I l J D E F A B C K L G 0B oB 1B 2B 3B 4B pB hB iB 5B 6B 7B qB rB 8B"},F:{"1":"0 1 2 3 4 5 6 7 8 9 G M N O m n o p q r s t u v w x y z AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB QB RB SB Y TB UB VB WB XB YB ZB aB bB cB dB eB fB gB P Q R mB S T U V W","2":"F B C 9B AC BC CC hB sB DC iB"},G:{"2":"E oB EC tB FC GC HC IC JC KC LC MC NC OC PC QC RC SC TC UC VC WC XC qB rB"},H:{"2":"YC"},I:{"2":"jB I H ZC aC bC cC tB dC eC"},J:{"2":"D A"},K:{"2":"A B C Y hB sB iB"},L:{"1":"H"},M:{"2":"X"},N:{"1":"B","2":"A"},O:{"2":"fC"},P:{"1":"I gC hC iC jC kC pB lC mC nC oC pC qC"},Q:{"2":"rC"},R:{"1":"sC"},S:{"2":"tC"}},B:5,C:"Resource Hints: prerender"}; 2 | -------------------------------------------------------------------------------- /geolocation.js: -------------------------------------------------------------------------------- 1 | module.exports={A:{A:{"1":"F A B","2":"uB","8":"J D E"},B:{"1":"C K L G M N O","129":"P Q R S T U V W Z a b c d e f g h i j k X H"},C:{"1":"0 1 2 3 4 5 6 7 8 9 I l J D E F A B C K L G M N O m n o p q r s t u v w x y z AB BB CB DB EB FB GB HB IB JB KB LB wB xB","8":"vB jB","129":"MB NB OB PB kB QB lB RB SB Y TB UB VB WB XB YB ZB aB bB cB dB eB fB gB P Q R mB S T U V W Z a b c d e f g h i j k X H nB"},D:{"1":"0 1 2 3 4 5 6 7 8 9 l J D E F A B C K L G M N O m n o p q r s t u v w x y z AB BB CB DB EB FB GB","4":"I","129":"HB IB JB KB LB MB NB OB PB kB QB lB RB SB Y TB UB VB WB XB YB ZB aB bB cB dB eB fB gB P Q R S T U V W Z a b c d e f g h i j k X H nB yB zB"},E:{"1":"l J D E F B C K L G 1B 2B 3B 4B pB hB iB 5B 6B 7B qB rB 8B","8":"I 0B oB","129":"A"},F:{"1":"0 1 2 3 4 5 B C M N O m n o p q r s t u v w x y z CC hB sB DC iB","2":"F G 9B","8":"AC BC","129":"6 7 8 9 AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB QB RB SB Y TB UB VB WB XB YB ZB aB bB cB dB eB fB gB P Q R mB S T U V W"},G:{"1":"E oB EC tB FC GC HC IC JC KC","129":"LC MC NC OC PC QC RC SC TC UC VC WC XC qB rB"},H:{"2":"YC"},I:{"1":"jB I ZC aC bC cC tB dC eC","129":"H"},J:{"1":"D A"},K:{"1":"B C hB sB iB","8":"A","129":"Y"},L:{"129":"H"},M:{"129":"X"},N:{"1":"A B"},O:{"1":"fC"},P:{"1":"I","129":"gC hC iC jC kC pB lC mC nC oC pC qC"},Q:{"129":"rC"},R:{"129":"sC"},S:{"1":"tC"}},B:2,C:"Geolocation"}; 2 | -------------------------------------------------------------------------------- /lib/arg.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const 4 | CoaParam = require('./coaparam'), 5 | chalk = require('chalk'); 6 | 7 | /** 8 | * Argument 9 | * 10 | * Unnamed entity. From command line arguments passed as list of unnamed values. 11 | * 12 | * @class Arg 13 | * @extends CoaParam 14 | */ 15 | module.exports = class Arg extends CoaParam { 16 | /** 17 | * @constructs 18 | * @param {COA.Cmd} cmd - parent command 19 | */ 20 | constructor(cmd) { 21 | super(cmd); 22 | 23 | this._cmd._args.push(this); 24 | } 25 | 26 | _saveVal(args, val) { 27 | this._val && (val = this._val(val)); 28 | 29 | const name = this._name; 30 | this._arr 31 | ? (args[name] || (args[name] = [])).push(val) 32 | : (args[name] = val); 33 | 34 | return val; 35 | } 36 | 37 | _parse(arg, args) { 38 | return this._saveVal(args, arg); 39 | } 40 | 41 | _checkParsed(opts, args) { 42 | return !args.hasOwnProperty(this._name); 43 | } 44 | 45 | _usage() { 46 | const res = []; 47 | 48 | res.push(chalk.magentaBright(this._name.toUpperCase()), ' : ', this._title); 49 | 50 | this._req && res.push(' ', chalk.redBright('(required)')); 51 | 52 | return res.join(''); 53 | } 54 | 55 | _requiredText() { 56 | return `Missing required argument:\n ${this._usage()}`; 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /lib/completion.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ###-begin-{{cmd}}-completion-### 3 | # 4 | # {{cmd}} command completion script 5 | # 6 | # Installation: {{cmd}} completion >> ~/.bashrc (or ~/.zshrc) 7 | # Or, maybe: {{cmd}} completion > /usr/local/etc/bash_completion.d/{{cmd}} 8 | # 9 | 10 | COMP_WORDBREAKS=${COMP_WORDBREAKS/=/} 11 | COMP_WORDBREAKS=${COMP_WORDBREAKS/@/} 12 | export COMP_WORDBREAKS 13 | 14 | if complete &>/dev/null; then 15 | _{{cmd}}_completion () { 16 | local si="$IFS" 17 | IFS=$'\n' COMPREPLY=($(COMP_CWORD="$COMP_CWORD" \ 18 | COMP_LINE="$COMP_LINE" \ 19 | COMP_POINT="$COMP_POINT" \ 20 | {{cmd}} completion -- "${COMP_WORDS[@]}" \ 21 | 2>/dev/null)) || return $? 22 | IFS="$si" 23 | } 24 | complete -F _{{cmd}}_completion {{cmd}} 25 | elif compctl &>/dev/null; then 26 | _{{cmd}}_completion () { 27 | local cword line point words si 28 | read -Ac words 29 | read -cn cword 30 | let cword-=1 31 | read -l line 32 | read -ln point 33 | si="$IFS" 34 | IFS=$'\n' reply=($(COMP_CWORD="$cword" \ 35 | COMP_LINE="$line" \ 36 | COMP_POINT="$point" \ 37 | {{cmd}} completion -- "${words[@]}" \ 38 | 2>/dev/null)) || return $? 39 | IFS="$si" 40 | } 41 | compctl -K _{{cmd}}_completion {{cmd}} 42 | fi 43 | ###-end-{{cmd}}-completion-### 44 | -------------------------------------------------------------------------------- /anySeries.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createTester = require('./internal/createTester.js'); 8 | 9 | var _createTester2 = _interopRequireDefault(_createTester); 10 | 11 | var _eachOfSeries = require('./eachOfSeries.js'); 12 | 13 | var _eachOfSeries2 = _interopRequireDefault(_eachOfSeries); 14 | 15 | var _awaitify = require('./internal/awaitify.js'); 16 | 17 | var _awaitify2 = _interopRequireDefault(_awaitify); 18 | 19 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 20 | 21 | /** 22 | * The same as [`some`]{@link module:Collections.some} but runs only a single async operation at a time. 23 | * 24 | * @name someSeries 25 | * @static 26 | * @memberOf module:Collections 27 | * @method 28 | * @see [async.some]{@link module:Collections.some} 29 | * @alias anySeries 30 | * @category Collection 31 | * @param {Array|Iterable|AsyncIterable|Object} coll - A collection to iterate over. 32 | * @param {AsyncFunction} iteratee - An async truth test to apply to each item 33 | * in the collections in series. 34 | * The iteratee should complete with a boolean `result` value. 35 | * Invoked with (item, callback). 36 | * @param {Function} [callback] - A callback which is called as soon as any 37 | * iteratee returns `true`, or after all the iteratee functions have finished. 38 | * Result will be either `true` or `false` depending on the values of the async 39 | * tests. Invoked with (err, result). 40 | * @returns {Promise} a promise, if no callback provided 41 | */ 42 | function someSeries(coll, iteratee, callback) { 43 | return (0, _createTester2.default)(Boolean, res => res)(_eachOfSeries2.default, coll, iteratee, callback); 44 | } 45 | exports.default = (0, _awaitify2.default)(someSeries, 3); 46 | module.exports = exports['default']; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cliui", 3 | "version": "7.0.4", 4 | "description": "easily create complex multi-column command-line-interfaces", 5 | "main": "build/index.cjs", 6 | "exports": { 7 | ".": [ 8 | { 9 | "import": "./index.mjs", 10 | "require": "./build/index.cjs" 11 | }, 12 | "./build/index.cjs" 13 | ] 14 | }, 15 | "type": "module", 16 | "module": "./index.mjs", 17 | "scripts": { 18 | "check": "standardx '**/*.ts' && standardx '**/*.js' && standardx '**/*.cjs'", 19 | "fix": "standardx --fix '**/*.ts' && standardx --fix '**/*.js' && standardx --fix '**/*.cjs'", 20 | "pretest": "rimraf build && tsc -p tsconfig.test.json && cross-env NODE_ENV=test npm run build:cjs", 21 | "test": "c8 mocha ./test/*.cjs", 22 | "test:esm": "c8 mocha ./test/esm/cliui-test.mjs", 23 | "postest": "check", 24 | "coverage": "c8 report --check-coverage", 25 | "precompile": "rimraf build", 26 | "compile": "tsc", 27 | "postcompile": "npm run build:cjs", 28 | "build:cjs": "rollup -c", 29 | "prepare": "npm run compile" 30 | }, 31 | "repository": "yargs/cliui", 32 | "standard": { 33 | "ignore": [ 34 | "**/example/**" 35 | ], 36 | "globals": [ 37 | "it" 38 | ] 39 | }, 40 | "keywords": [ 41 | "cli", 42 | "command-line", 43 | "layout", 44 | "design", 45 | "console", 46 | "wrap", 47 | "table" 48 | ], 49 | "author": "Ben Coe ", 50 | "license": "ISC", 51 | "dependencies": { 52 | "string-width": "^4.2.0", 53 | "strip-ansi": "^6.0.0", 54 | "wrap-ansi": "^7.0.0" 55 | }, 56 | "devDependencies": { 57 | "@types/node": "^14.0.27", 58 | "@typescript-eslint/eslint-plugin": "^4.0.0", 59 | "@typescript-eslint/parser": "^4.0.0", 60 | "@wessberg/rollup-plugin-ts": "^1.3.2", 61 | "c8": "^7.3.0", 62 | "chai": "^4.2.0", 63 | "chalk": "^4.1.0", 64 | "cross-env": "^7.0.2", 65 | "eslint": "^7.6.0", 66 | "eslint-plugin-import": "^2.22.0", 67 | "eslint-plugin-node": "^11.1.0", 68 | "gts": "^3.0.0", 69 | "mocha": "^8.1.1", 70 | "rimraf": "^3.0.2", 71 | "rollup": "^2.23.1", 72 | "standardx": "^7.0.0", 73 | "typescript": "^4.0.0" 74 | }, 75 | "files": [ 76 | "build", 77 | "index.mjs", 78 | "!*.d.ts" 79 | ], 80 | "engine": { 81 | "node": ">=10" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/coaobject.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | 'use strict'; 3 | 4 | const Q = require('q'); 5 | 6 | /** 7 | * COA Object 8 | * 9 | * Base class for all COA-related objects 10 | * 11 | * --------|-----|-----|----- 12 | * | Cmd | Opt | Arg 13 | * --------|-----|-----|----- 14 | * name | ✓ | ✓ | ✓ 15 | * title | ✓ | ✓ | ✓ 16 | * comp | ✓ | ✓ | ✓ 17 | * reject | ✓ | ✓ | ✓ 18 | * end | ✓ | ✓ | ✓ 19 | * apply | ✓ | ✓ | ✓ 20 | * 21 | * @class CoaObject 22 | */ 23 | module.exports = class CoaObject { 24 | constructor(cmd) { 25 | this._cmd = cmd; 26 | this._name = null; 27 | this._title = null; 28 | this._comp = null; 29 | } 30 | 31 | /** 32 | * Set a canonical identifier to be used anywhere in the API. 33 | * 34 | * @param {String} name - command, option or argument name 35 | * @returns {COA.CoaObject} - this instance (for chainability) 36 | */ 37 | name(name) { 38 | this._name = name; 39 | return this; 40 | } 41 | 42 | /** 43 | * Set a long description to be used anywhere in text messages. 44 | * @param {String} title - human readable entity title 45 | * @returns {COA.CoaObject} - this instance (for chainability) 46 | */ 47 | title(title) { 48 | this._title = title; 49 | return this; 50 | } 51 | 52 | /** 53 | * Set custom additional completion for current object. 54 | * 55 | * @param {Function} comp - completion generation function, 56 | * invoked in the context of object instance. 57 | * Accepts parameters: 58 | * - {Object} opts - completion options 59 | * It can return promise or any other value threated as a result. 60 | * @returns {COA.CoaObject} - this instance (for chainability) 61 | */ 62 | comp(comp) { 63 | this._comp = comp; 64 | return this; 65 | } 66 | 67 | /** 68 | * Apply function with arguments in a context of object instance. 69 | * 70 | * @param {Function} fn - body 71 | * @param {Array.<*>} args... - arguments 72 | * @returns {COA.CoaObject} - this instance (for chainability) 73 | */ 74 | apply(fn) { 75 | arguments.length > 1? 76 | fn.apply(this, [].slice.call(arguments, 1)) 77 | : fn.call(this); 78 | 79 | return this; 80 | } 81 | 82 | /** 83 | * Return reject of actions results promise with error code. 84 | * Use in .act() for return with error. 85 | * @param {Object} reason - reject reason 86 | * You can customize toString() method and exitCode property 87 | * of reason object. 88 | * @returns {Q.promise} rejected promise 89 | */ 90 | reject(reason) { 91 | return Q.reject(reason); 92 | } 93 | 94 | /** 95 | * Finish chain for current subcommand and return parent command instance. 96 | * @returns {COA.Cmd} parent command 97 | */ 98 | end() { 99 | return this._cmd; 100 | } 101 | }; 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cliui 2 | 3 | ![ci](https://github.com/yargs/cliui/workflows/ci/badge.svg) 4 | [![NPM version](https://img.shields.io/npm/v/cliui.svg)](https://www.npmjs.com/package/cliui) 5 | [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org) 6 | ![nycrc config on GitHub](https://img.shields.io/nycrc/yargs/cliui) 7 | 8 | easily create complex multi-column command-line-interfaces. 9 | 10 | ## Example 11 | 12 | ```js 13 | const ui = require('cliui')() 14 | 15 | ui.div('Usage: $0 [command] [options]') 16 | 17 | ui.div({ 18 | text: 'Options:', 19 | padding: [2, 0, 1, 0] 20 | }) 21 | 22 | ui.div( 23 | { 24 | text: "-f, --file", 25 | width: 20, 26 | padding: [0, 4, 0, 4] 27 | }, 28 | { 29 | text: "the file to load." + 30 | chalk.green("(if this description is long it wraps).") 31 | , 32 | width: 20 33 | }, 34 | { 35 | text: chalk.red("[required]"), 36 | align: 'right' 37 | } 38 | ) 39 | 40 | console.log(ui.toString()) 41 | ``` 42 | 43 | ## Deno/ESM Support 44 | 45 | As of `v7` `cliui` supports [Deno](https://github.com/denoland/deno) and 46 | [ESM](https://nodejs.org/api/esm.html#esm_ecmascript_modules): 47 | 48 | ```typescript 49 | import cliui from "https://deno.land/x/cliui/deno.ts"; 50 | 51 | const ui = cliui({}) 52 | 53 | ui.div('Usage: $0 [command] [options]') 54 | 55 | ui.div({ 56 | text: 'Options:', 57 | padding: [2, 0, 1, 0] 58 | }) 59 | 60 | ui.div({ 61 | text: "-f, --file", 62 | width: 20, 63 | padding: [0, 4, 0, 4] 64 | }) 65 | 66 | console.log(ui.toString()) 67 | ``` 68 | 69 | 70 | 71 | ## Layout DSL 72 | 73 | cliui exposes a simple layout DSL: 74 | 75 | If you create a single `ui.div`, passing a string rather than an 76 | object: 77 | 78 | * `\n`: characters will be interpreted as new rows. 79 | * `\t`: characters will be interpreted as new columns. 80 | * `\s`: characters will be interpreted as padding. 81 | 82 | **as an example...** 83 | 84 | ```js 85 | var ui = require('./')({ 86 | width: 60 87 | }) 88 | 89 | ui.div( 90 | 'Usage: node ./bin/foo.js\n' + 91 | ' \t provide a regex\n' + 92 | ' \t provide a glob\t [required]' 93 | ) 94 | 95 | console.log(ui.toString()) 96 | ``` 97 | 98 | **will output:** 99 | 100 | ```shell 101 | Usage: node ./bin/foo.js 102 | provide a regex 103 | provide a glob [required] 104 | ``` 105 | 106 | ## Methods 107 | 108 | ```js 109 | cliui = require('cliui') 110 | ``` 111 | 112 | ### cliui({width: integer}) 113 | 114 | Specify the maximum width of the UI being generated. 115 | If no width is provided, cliui will try to get the current window's width and use it, and if that doesn't work, width will be set to `80`. 116 | 117 | ### cliui({wrap: boolean}) 118 | 119 | Enable or disable the wrapping of text in a column. 120 | 121 | ### cliui.div(column, column, column) 122 | 123 | Create a row with any number of columns, a column 124 | can either be a string, or an object with the following 125 | options: 126 | 127 | * **text:** some text to place in the column. 128 | * **width:** the width of a column. 129 | * **align:** alignment, `right` or `center`. 130 | * **padding:** `[top, right, bottom, left]`. 131 | * **border:** should a border be placed around the div? 132 | 133 | ### cliui.span(column, column, column) 134 | 135 | Similar to `div`, except the next row will be appended without 136 | a new line being created. 137 | 138 | ### cliui.resetOutput() 139 | 140 | Resets the UI elements of the current cliui instance, maintaining the values 141 | set for `width` and `wrap`. 142 | -------------------------------------------------------------------------------- /lib/coaparam.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | 5 | const CoaObject = require('./coaobject'); 6 | 7 | /** 8 | * COA Parameter 9 | * 10 | * Base class for options and arguments 11 | * 12 | * --------|-----|-----|----- 13 | * | Cmd | Opt | Arg 14 | * --------|-----|-----|----- 15 | * arr | | ✓ | ✓ 16 | * req | | ✓ | ✓ 17 | * val | | ✓ | ✓ 18 | * def | | ✓ | ✓ 19 | * input | | ✓ | ✓ 20 | * output | | ✓ | ✓ 21 | * 22 | * @class CoaParam 23 | * @extends CoaObject 24 | */ 25 | module.exports = class CoaParam extends CoaObject { 26 | constructor(cmd) { 27 | super(cmd); 28 | 29 | this._arr = false; 30 | this._req = false; 31 | this._val = undefined; 32 | this._def = undefined; 33 | } 34 | 35 | /** 36 | * Makes a param accepts multiple values. 37 | * Otherwise, the value will be used by the latter passed. 38 | * 39 | * @returns {COA.CoaParam} - this instance (for chainability) 40 | */ 41 | arr() { 42 | this._arr = true; 43 | return this; 44 | } 45 | 46 | /** 47 | * Makes a param required. 48 | * 49 | * @returns {COA.CoaParam} - this instance (for chainability) 50 | */ 51 | req() { 52 | this._req = true; 53 | return this; 54 | } 55 | 56 | /** 57 | * Set a validation (or value) function for param. 58 | * Value from command line passes through before becoming available from API. 59 | * Using for validation and convertion simple types to any values. 60 | * 61 | * @param {Function} val - validating function, 62 | * invoked in the context of option instance 63 | * and has one parameter with value from command line. 64 | * @returns {COA.CoaParam} - this instance (for chainability) 65 | */ 66 | val(val) { 67 | this._val = val; 68 | return this; 69 | } 70 | 71 | /** 72 | * Set a default value for param. 73 | * Default value passed through validation function as ordinary value. 74 | * 75 | * @param {*} def - default value of function generator 76 | * @returns {COA.CoaParam} - this instance (for chainability) 77 | */ 78 | def(def) { 79 | this._def = def; 80 | return this; 81 | } 82 | 83 | /** 84 | * Make option value inputting stream. 85 | * It's add useful validation and shortcut for STDIN. 86 | * 87 | * @returns {COA.CoaParam} - this instance (for chainability) 88 | */ 89 | input() { 90 | process.stdin.pause(); 91 | return this 92 | .def(process.stdin) 93 | .val(function(v) { 94 | if(typeof v !== 'string') 95 | return v; 96 | 97 | if(v === '-') 98 | return process.stdin; 99 | 100 | const s = fs.createReadStream(v, { encoding : 'utf8' }); 101 | s.pause(); 102 | return s; 103 | }); 104 | } 105 | 106 | /** 107 | * Make option value outputing stream. 108 | * It's add useful validation and shortcut for STDOUT. 109 | * 110 | * @returns {COA.CoaParam} - this instance (for chainability) 111 | */ 112 | output() { 113 | return this 114 | .def(process.stdout) 115 | .val(function(v) { 116 | if(typeof v !== 'string') 117 | return v; 118 | 119 | if(v === '-') 120 | return process.stdout; 121 | 122 | return fs.createWriteStream(v, { encoding : 'utf8' }); 123 | }); 124 | } 125 | }; 126 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [7.0.4](https://www.github.com/yargs/cliui/compare/v7.0.3...v7.0.4) (2020-11-08) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * **deno:** import UIOptions from definitions ([#97](https://www.github.com/yargs/cliui/issues/97)) ([f04f343](https://www.github.com/yargs/cliui/commit/f04f3439bc78114c7e90f82ff56f5acf16268ea8)) 11 | 12 | ### [7.0.3](https://www.github.com/yargs/cliui/compare/v7.0.2...v7.0.3) (2020-10-16) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * **exports:** node 13.0 and 13.1 require the dotted object form _with_ a string fallback ([#93](https://www.github.com/yargs/cliui/issues/93)) ([eca16fc](https://www.github.com/yargs/cliui/commit/eca16fc05d26255df3280906c36d7f0e5b05c6e9)) 18 | 19 | ### [7.0.2](https://www.github.com/yargs/cliui/compare/v7.0.1...v7.0.2) (2020-10-14) 20 | 21 | 22 | ### Bug Fixes 23 | 24 | * **exports:** node 13.0-13.6 require a string fallback ([#91](https://www.github.com/yargs/cliui/issues/91)) ([b529d7e](https://www.github.com/yargs/cliui/commit/b529d7e432901af1af7848b23ed6cf634497d961)) 25 | 26 | ### [7.0.1](https://www.github.com/yargs/cliui/compare/v7.0.0...v7.0.1) (2020-08-16) 27 | 28 | 29 | ### Bug Fixes 30 | 31 | * **build:** main should be build/index.cjs ([dc29a3c](https://www.github.com/yargs/cliui/commit/dc29a3cc617a410aa850e06337b5954b04f2cb4d)) 32 | 33 | ## [7.0.0](https://www.github.com/yargs/cliui/compare/v6.0.0...v7.0.0) (2020-08-16) 34 | 35 | 36 | ### ⚠ BREAKING CHANGES 37 | 38 | * tsc/ESM/Deno support (#82) 39 | * modernize deps and build (#80) 40 | 41 | ### Build System 42 | 43 | * modernize deps and build ([#80](https://www.github.com/yargs/cliui/issues/80)) ([339d08d](https://www.github.com/yargs/cliui/commit/339d08dc71b15a3928aeab09042af94db2f43743)) 44 | 45 | 46 | ### Code Refactoring 47 | 48 | * tsc/ESM/Deno support ([#82](https://www.github.com/yargs/cliui/issues/82)) ([4b777a5](https://www.github.com/yargs/cliui/commit/4b777a5fe01c5d8958c6708695d6aab7dbe5706c)) 49 | 50 | ## [6.0.0](https://www.github.com/yargs/cliui/compare/v5.0.0...v6.0.0) (2019-11-10) 51 | 52 | 53 | ### ⚠ BREAKING CHANGES 54 | 55 | * update deps, drop Node 6 56 | 57 | ### Code Refactoring 58 | 59 | * update deps, drop Node 6 ([62056df](https://www.github.com/yargs/cliui/commit/62056df)) 60 | 61 | ## [5.0.0](https://github.com/yargs/cliui/compare/v4.1.0...v5.0.0) (2019-04-10) 62 | 63 | 64 | ### Bug Fixes 65 | 66 | * Update wrap-ansi to fix compatibility with latest versions of chalk. ([#60](https://github.com/yargs/cliui/issues/60)) ([7bf79ae](https://github.com/yargs/cliui/commit/7bf79ae)) 67 | 68 | 69 | ### BREAKING CHANGES 70 | 71 | * Drop support for node < 6. 72 | 73 | 74 | 75 | 76 | ## [4.1.0](https://github.com/yargs/cliui/compare/v4.0.0...v4.1.0) (2018-04-23) 77 | 78 | 79 | ### Features 80 | 81 | * add resetOutput method ([#57](https://github.com/yargs/cliui/issues/57)) ([7246902](https://github.com/yargs/cliui/commit/7246902)) 82 | 83 | 84 | 85 | 86 | ## [4.0.0](https://github.com/yargs/cliui/compare/v3.2.0...v4.0.0) (2017-12-18) 87 | 88 | 89 | ### Bug Fixes 90 | 91 | * downgrades strip-ansi to version 3.0.1 ([#54](https://github.com/yargs/cliui/issues/54)) ([5764c46](https://github.com/yargs/cliui/commit/5764c46)) 92 | * set env variable FORCE_COLOR. ([#56](https://github.com/yargs/cliui/issues/56)) ([7350e36](https://github.com/yargs/cliui/commit/7350e36)) 93 | 94 | 95 | ### Chores 96 | 97 | * drop support for node < 4 ([#53](https://github.com/yargs/cliui/issues/53)) ([b105376](https://github.com/yargs/cliui/commit/b105376)) 98 | 99 | 100 | ### Features 101 | 102 | * add fallback for window width ([#45](https://github.com/yargs/cliui/issues/45)) ([d064922](https://github.com/yargs/cliui/commit/d064922)) 103 | 104 | 105 | ### BREAKING CHANGES 106 | 107 | * officially drop support for Node < 4 108 | 109 | 110 | 111 | 112 | ## [3.2.0](https://github.com/yargs/cliui/compare/v3.1.2...v3.2.0) (2016-04-11) 113 | 114 | 115 | ### Bug Fixes 116 | 117 | * reduces tarball size ([acc6c33](https://github.com/yargs/cliui/commit/acc6c33)) 118 | 119 | ### Features 120 | 121 | * adds standard-version for release management ([ff84e32](https://github.com/yargs/cliui/commit/ff84e32)) 122 | -------------------------------------------------------------------------------- /lib/opt.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const 4 | Q = require('q'), 5 | 6 | CoaParam = require('./coaparam'), 7 | chalk = require('chalk'); 8 | 9 | /** 10 | * Option 11 | * 12 | * Named entity. Options may have short and long keys for use from command line. 13 | * 14 | * @namespace 15 | * @class Opt 16 | * @extends CoaParam 17 | */ 18 | module.exports = class Opt extends CoaParam { 19 | /** 20 | * @constructs 21 | * @param {COA.Cmd} cmd - parent command 22 | */ 23 | constructor(cmd) { 24 | super(cmd); 25 | 26 | this._short = null; 27 | this._long = null; 28 | this._flag = false; 29 | this._only = false; 30 | this._cmd._opts.push(this); 31 | } 32 | 33 | /** 34 | * Set a short key for option to be used with one hyphen from command line. 35 | * 36 | * @param {String} short - short name 37 | * @returns {COA.Opt} - this instance (for chainability) 38 | */ 39 | short(short) { 40 | this._short = short; 41 | this._cmd._optsByKey[`-${short}`] = this; 42 | return this; 43 | } 44 | 45 | /** 46 | * Set a short key for option to be used with double hyphens from command line. 47 | * 48 | * @param {String} long - long name 49 | * @returns {COA.Opt} - this instance (for chainability) 50 | */ 51 | long(long) { 52 | this._long = long; 53 | this._cmd._optsByKey[`--${long}`] = this; 54 | return this; 55 | } 56 | 57 | /** 58 | * Make an option boolean, i.e. option without value. 59 | * 60 | * @returns {COA.Opt} - this instance (for chainability) 61 | */ 62 | flag() { 63 | this._flag = true; 64 | return this; 65 | } 66 | 67 | /** 68 | * Makes an option to act as a command, 69 | * i.e. program will exit just after option action. 70 | * 71 | * @returns {COA.Opt} - this instance (for chainability) 72 | */ 73 | only() { 74 | this._only = true; 75 | return this; 76 | } 77 | 78 | /** 79 | * Add action for current option command. 80 | * This action is performed if the current option 81 | * is present in parsed options (with any value). 82 | * 83 | * @param {Function} act - action function, 84 | * invoked in the context of command instance 85 | * and has the parameters: 86 | * - {Object} opts - parsed options 87 | * - {Array} args - parsed arguments 88 | * - {Object} res - actions result accumulator 89 | * It can return rejected promise by Cmd.reject (in case of error) 90 | * or any other value treated as result. 91 | * @returns {COA.Opt} - this instance (for chainability) 92 | */ 93 | act(act) { 94 | // Need function here for arguments 95 | const opt = this; 96 | this._cmd.act(function(opts) { 97 | if(!opts.hasOwnProperty(opt._name)) return; 98 | 99 | const res = act.apply(this, arguments); 100 | if(!opt._only) return res; 101 | 102 | return Q.when(res, out => this.reject({ 103 | toString : () => out.toString(), 104 | exitCode : 0 105 | })); 106 | }); 107 | 108 | return this; 109 | } 110 | 111 | _saveVal(opts, val) { 112 | this._val && (val = this._val(val)); 113 | 114 | const name = this._name; 115 | this._arr 116 | ? (opts[name] || (opts[name] = [])).push(val) 117 | : (opts[name] = val); 118 | 119 | return val; 120 | } 121 | 122 | _parse(argv, opts) { 123 | return this._saveVal(opts, this._flag ? true : argv.shift()); 124 | } 125 | 126 | _checkParsed(opts) { 127 | return !opts.hasOwnProperty(this._name); 128 | } 129 | 130 | _usage() { 131 | const res = [], 132 | nameStr = this._name.toUpperCase(); 133 | 134 | if(this._short) { 135 | res.push('-', chalk.greenBright(this._short)); 136 | this._flag || res.push(' ' + nameStr); 137 | res.push(', '); 138 | } 139 | 140 | if(this._long) { 141 | res.push('--', chalk.green(this._long)); 142 | this._flag || res.push('=' + nameStr); 143 | } 144 | 145 | res.push(' : ', this._title); 146 | 147 | this._req && res.push(' ', chalk.redBright('(required)')); 148 | 149 | return res.join(''); 150 | } 151 | 152 | _requiredText() { 153 | return `Missing required option:\n ${this._usage()}`; 154 | } 155 | }; 156 | -------------------------------------------------------------------------------- /lib/completion.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const constants = require('constants'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | 7 | const Q = require('q'); 8 | 9 | const shell = require('./shell'); 10 | const escape = shell.escape; 11 | const unescape = shell.unescape; 12 | 13 | /** 14 | * Most of the code adopted from the npm package shell completion code. 15 | * See https://github.com/isaacs/npm/blob/master/lib/completion.js 16 | * 17 | * @returns {COA.CoaObject} 18 | */ 19 | module.exports = function completion() { 20 | return this 21 | .title('Shell completion') 22 | .helpful() 23 | .arg() 24 | .name('raw') 25 | .title('Completion words') 26 | .arr() 27 | .end() 28 | .act((opts, args) => { 29 | if(process.platform === 'win32') { 30 | const e = new Error('shell completion not supported on windows'); 31 | e.code = 'ENOTSUP'; 32 | e.errno = constants.ENOTSUP; 33 | return this.reject(e); 34 | } 35 | 36 | // if the COMP_* isn't in the env, then just dump the script 37 | if((process.env.COMP_CWORD == null) 38 | || (process.env.COMP_LINE == null) 39 | || (process.env.COMP_POINT == null)) { 40 | return dumpScript(this._cmd._name); 41 | } 42 | 43 | console.error('COMP_LINE: %s', process.env.COMP_LINE); 44 | console.error('COMP_CWORD: %s', process.env.COMP_CWORD); 45 | console.error('COMP_POINT: %s', process.env.COMP_POINT); 46 | console.error('args: %j', args.raw); 47 | 48 | // completion opts 49 | opts = getOpts(args.raw); 50 | 51 | // cmd 52 | const parsed = this._cmd._parseCmd(opts.partialWords); 53 | return Q.when(complete(parsed.cmd, parsed.opts), compls => { 54 | console.error('filtered: %j', compls); 55 | return console.log(compls.map(escape).join('\n')); 56 | }); 57 | }); 58 | }; 59 | 60 | function dumpScript(name) { 61 | const defer = Q.defer(); 62 | 63 | fs.readFile(path.resolve(__dirname, 'completion.sh'), 'utf8', function(err, d) { 64 | if(err) return defer.reject(err); 65 | d = d.replace(/{{cmd}}/g, path.basename(name)).replace(/^#!.*?\n/, ''); 66 | 67 | process.stdout.on('error', onError); 68 | process.stdout.write(d, () => defer.resolve()); 69 | }); 70 | 71 | return defer.promise; 72 | 73 | function onError(err) { 74 | // Darwin is a real dick sometimes. 75 | // 76 | // This is necessary because the "source" or "." program in 77 | // bash on OS X closes its file argument before reading 78 | // from it, meaning that you get exactly 1 write, which will 79 | // work most of the time, and will always raise an EPIPE. 80 | // 81 | // Really, one should not be tossing away EPIPE errors, or any 82 | // errors, so casually. But, without this, `. <(cmd completion)` 83 | // can never ever work on OS X. 84 | if(err.errno !== constants.EPIPE) return defer.reject(err); 85 | process.stdout.removeListener('error', onError); 86 | return defer.resolve(); 87 | } 88 | } 89 | 90 | function getOpts(argv) { 91 | // get the partial line and partial word, if the point isn't at the end 92 | // ie, tabbing at: cmd foo b|ar 93 | const line = process.env.COMP_LINE; 94 | const w = +process.env.COMP_CWORD; 95 | const point = +process.env.COMP_POINT; 96 | const words = argv.map(unescape); 97 | const word = words[w]; 98 | const partialLine = line.substr(0, point); 99 | const partialWords = words.slice(0, w); 100 | 101 | // figure out where in that last word the point is 102 | let partialWord = argv[w] || ''; 103 | let i = partialWord.length; 104 | while(partialWord.substr(0, i) !== partialLine.substr(-1 * i) && i > 0) i--; 105 | 106 | partialWord = unescape(partialWord.substr(0, i)); 107 | partialWord && partialWords.push(partialWord); 108 | 109 | return { 110 | line, 111 | w, 112 | point, 113 | words, 114 | word, 115 | partialLine, 116 | partialWords, 117 | partialWord 118 | }; 119 | } 120 | 121 | function complete(cmd, opts) { 122 | let optWord, optPrefix, 123 | compls = []; 124 | 125 | // Complete on cmds 126 | if(opts.partialWord.indexOf('-')) 127 | compls = Object.keys(cmd._cmdsByName); 128 | // Complete on required opts without '-' in last partial word 129 | // (if required not already specified) 130 | // 131 | // Commented out because of uselessness: 132 | // -b, --block suggest results in '-' on cmd line; 133 | // next completion suggest all options, because of '-' 134 | //.concat Object.keys(cmd._optsByKey).filter (v) -> cmd._optsByKey[v]._req 135 | else { 136 | // complete on opt values: --opt=| case 137 | const m = opts.partialWord.match(/^(--\w[\w-_]*)=(.*)$/); 138 | if(m) { 139 | optWord = m[1]; 140 | optPrefix = optWord + '='; 141 | } else 142 | // complete on opts 143 | // don't complete on opts in case of --opt=val completion 144 | // TODO: don't complete on opts in case of unknown arg after commands 145 | // TODO: complete only on opts with arr() or not already used 146 | // TODO: complete only on full opts? 147 | compls = Object.keys(cmd._optsByKey); 148 | } 149 | 150 | // complete on opt values: next arg case 151 | opts.partialWords[opts.w - 1].indexOf('-') || (optWord = opts.partialWords[opts.w - 1]); 152 | 153 | // complete on opt values: completion 154 | let opt; 155 | optWord 156 | && (opt = cmd._optsByKey[optWord]) 157 | && !opt._flag 158 | && opt._comp 159 | && (compls = Q.join(compls, 160 | Q.when(opt._comp(opts), 161 | (c, o) => c.concat(o.map(v => (optPrefix || '') + v))))); 162 | 163 | // TODO: complete on args values (context aware, custom completion?) 164 | 165 | // custom completion on cmds 166 | cmd._comp && (compls = Q.join(compls, Q.when(cmd._comp(opts)), (c, o) => c.concat(o))); 167 | 168 | // TODO: context aware custom completion on cmds, opts and args 169 | // (can depend on already entered values, especially options) 170 | 171 | return Q.when(compls, complitions => { 172 | console.error('partialWord: %s', opts.partialWord); 173 | console.error('compls: %j', complitions); 174 | return compls.filter(c => c.indexOf(opts.partialWord) === 0); 175 | }); 176 | } 177 | -------------------------------------------------------------------------------- /lib/cmd.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | 'use strict'; 3 | 4 | const 5 | UTIL = require('util'), 6 | PATH = require('path'), 7 | EOL = require('os').EOL, 8 | 9 | Q = require('q'), 10 | chalk = require('chalk'), 11 | 12 | CoaObject = require('./coaobject'), 13 | Opt = require('./opt'), 14 | Arg = require('./arg'), 15 | completion = require('./completion'); 16 | 17 | /** 18 | * Command 19 | * 20 | * Top level entity. Commands may have options and arguments. 21 | * 22 | * @namespace 23 | * @class Cmd 24 | * @extends CoaObject 25 | */ 26 | class Cmd extends CoaObject { 27 | /** 28 | * @constructs 29 | * @param {COA.Cmd} [cmd] parent command 30 | */ 31 | constructor(cmd) { 32 | super(cmd); 33 | 34 | this._parent(cmd); 35 | this._cmds = []; 36 | this._cmdsByName = {}; 37 | this._opts = []; 38 | this._optsByKey = {}; 39 | this._args = []; 40 | this._api = null; 41 | this._ext = false; 42 | } 43 | 44 | static create(cmd) { 45 | return new Cmd(cmd); 46 | } 47 | 48 | /** 49 | * Returns object containing all its subcommands as methods 50 | * to use from other programs. 51 | * 52 | * @returns {Object} 53 | */ 54 | get api() { 55 | // Need _this here because of passed arguments into _api 56 | const _this = this; 57 | this._api || (this._api = function () { 58 | return _this.invoke.apply(_this, arguments); 59 | }); 60 | 61 | const cmds = this._cmdsByName; 62 | Object.keys(cmds).forEach(cmd => { this._api[cmd] = cmds[cmd].api; }); 63 | 64 | return this._api; 65 | } 66 | 67 | _parent(cmd) { 68 | this._cmd = cmd || this; 69 | 70 | this.isRootCmd || 71 | cmd._cmds.push(this) && 72 | this._name && 73 | (this._cmd._cmdsByName[this._name] = this); 74 | 75 | return this; 76 | } 77 | 78 | get isRootCmd() { 79 | return this._cmd === this; 80 | } 81 | 82 | /** 83 | * Set a canonical command identifier to be used anywhere in the API. 84 | * 85 | * @param {String} name - command name 86 | * @returns {COA.Cmd} - this instance (for chainability) 87 | */ 88 | name(name) { 89 | super.name(name); 90 | 91 | this.isRootCmd || 92 | (this._cmd._cmdsByName[name] = this); 93 | 94 | return this; 95 | } 96 | 97 | /** 98 | * Create new or add existing subcommand for current command. 99 | * 100 | * @param {COA.Cmd} [cmd] existing command instance 101 | * @returns {COA.Cmd} new subcommand instance 102 | */ 103 | cmd(cmd) { 104 | return cmd? 105 | cmd._parent(this) 106 | : new Cmd(this); 107 | } 108 | 109 | /** 110 | * Create option for current command. 111 | * 112 | * @returns {COA.Opt} new option instance 113 | */ 114 | opt() { 115 | return new Opt(this); 116 | } 117 | 118 | /** 119 | * Create argument for current command. 120 | * 121 | * @returns {COA.Opt} new argument instance 122 | */ 123 | arg() { 124 | return new Arg(this); 125 | } 126 | 127 | /** 128 | * Add (or set) action for current command. 129 | * 130 | * @param {Function} act - action function, 131 | * invoked in the context of command instance 132 | * and has the parameters: 133 | * - {Object} opts - parsed options 134 | * - {String[]} args - parsed arguments 135 | * - {Object} res - actions result accumulator 136 | * It can return rejected promise by Cmd.reject (in case of error) 137 | * or any other value treated as result. 138 | * @param {Boolean} [force=false] flag for set action instead add to existings 139 | * @returns {COA.Cmd} - this instance (for chainability) 140 | */ 141 | act(act, force) { 142 | if(!act) return this; 143 | 144 | (!this._act || force) && (this._act = []); 145 | this._act.push(act); 146 | 147 | return this; 148 | } 149 | 150 | /** 151 | * Make command "helpful", i.e. add -h --help flags for print usage. 152 | * 153 | * @returns {COA.Cmd} - this instance (for chainability) 154 | */ 155 | helpful() { 156 | return this.opt() 157 | .name('help') 158 | .title('Help') 159 | .short('h') 160 | .long('help') 161 | .flag() 162 | .only() 163 | .act(function() { 164 | return this.usage(); 165 | }) 166 | .end(); 167 | } 168 | 169 | /** 170 | * Adds shell completion to command, adds "completion" subcommand, 171 | * that makes all the magic. 172 | * Must be called only on root command. 173 | * 174 | * @returns {COA.Cmd} - this instance (for chainability) 175 | */ 176 | completable() { 177 | return this.cmd() 178 | .name('completion') 179 | .apply(completion) 180 | .end(); 181 | } 182 | 183 | /** 184 | * Allow command to be extendable by external node.js modules. 185 | * 186 | * @param {String} [pattern] Pattern of node.js module to find subcommands at. 187 | * @returns {COA.Cmd} - this instance (for chainability) 188 | */ 189 | extendable(pattern) { 190 | this._ext = pattern || true; 191 | return this; 192 | } 193 | 194 | _exit(msg, code) { 195 | return process.once('exit', function(exitCode) { 196 | msg && console[code === 0 ? 'log' : 'error'](msg); 197 | process.exit(code || exitCode || 0); 198 | }); 199 | } 200 | 201 | /** 202 | * Build full usage text for current command instance. 203 | * 204 | * @returns {String} usage text 205 | */ 206 | usage() { 207 | const res = []; 208 | 209 | this._title && res.push(this._fullTitle()); 210 | 211 | res.push('', 'Usage:'); 212 | 213 | this._cmds.length 214 | && res.push([ 215 | '', '', chalk.redBright(this._fullName()), chalk.blueBright('COMMAND'), 216 | chalk.greenBright('[OPTIONS]'), chalk.magentaBright('[ARGS]') 217 | ].join(' ')); 218 | 219 | (this._opts.length + this._args.length) 220 | && res.push([ 221 | '', '', chalk.redBright(this._fullName()), 222 | chalk.greenBright('[OPTIONS]'), chalk.magentaBright('[ARGS]') 223 | ].join(' ')); 224 | 225 | res.push( 226 | this._usages(this._cmds, 'Commands'), 227 | this._usages(this._opts, 'Options'), 228 | this._usages(this._args, 'Arguments') 229 | ); 230 | 231 | return res.join(EOL); 232 | } 233 | 234 | _usage() { 235 | return chalk.blueBright(this._name) + ' : ' + this._title; 236 | } 237 | 238 | _usages(os, title) { 239 | if(!os.length) return; 240 | 241 | return ['', title + ':'] 242 | .concat(os.map(o => ` ${o._usage()}`)) 243 | .join(EOL); 244 | } 245 | 246 | _fullTitle() { 247 | return `${this.isRootCmd? '' : this._cmd._fullTitle() + EOL}${this._title}`; 248 | } 249 | 250 | _fullName() { 251 | return `${this.isRootCmd? '' : this._cmd._fullName() + ' '}${PATH.basename(this._name)}`; 252 | } 253 | 254 | _ejectOpt(opts, opt) { 255 | const pos = opts.indexOf(opt); 256 | if(pos === -1) return; 257 | 258 | return opts[pos]._arr? 259 | opts[pos] : 260 | opts.splice(pos, 1)[0]; 261 | } 262 | 263 | _checkRequired(opts, args) { 264 | if(this._opts.some(opt => opt._only && opts.hasOwnProperty(opt._name))) return; 265 | 266 | const all = this._opts.concat(this._args); 267 | let i; 268 | while(i = all.shift()) 269 | if(i._req && i._checkParsed(opts, args)) 270 | return this.reject(i._requiredText()); 271 | } 272 | 273 | _parseCmd(argv, unparsed) { 274 | unparsed || (unparsed = []); 275 | 276 | let i, 277 | optSeen = false; 278 | while(i = argv.shift()) { 279 | i.indexOf('-') || (optSeen = true); 280 | 281 | if(optSeen || !/^\w[\w-_]*$/.test(i)) { 282 | unparsed.push(i); 283 | continue; 284 | } 285 | 286 | let pkg, cmd = this._cmdsByName[i]; 287 | if(!cmd && this._ext) { 288 | if(this._ext === true) { 289 | pkg = i; 290 | let c = this; 291 | while(true) { // eslint-disable-line 292 | pkg = c._name + '-' + pkg; 293 | if(c.isRootCmd) break; 294 | c = c._cmd; 295 | } 296 | } else if(typeof this._ext === 'string') 297 | pkg = ~this._ext.indexOf('%s')? 298 | UTIL.format(this._ext, i) : 299 | this._ext + i; 300 | 301 | let cmdDesc; 302 | try { 303 | cmdDesc = require(pkg); 304 | } catch(e) { 305 | // Dummy 306 | } 307 | 308 | if(cmdDesc) { 309 | if(typeof cmdDesc === 'function') { 310 | this.cmd().name(i).apply(cmdDesc).end(); 311 | } else if(typeof cmdDesc === 'object') { 312 | this.cmd(cmdDesc); 313 | cmdDesc.name(i); 314 | } else throw new Error('Error: Unsupported command declaration type, ' 315 | + 'should be a function or COA.Cmd() object'); 316 | 317 | cmd = this._cmdsByName[i]; 318 | } 319 | } 320 | 321 | if(cmd) return cmd._parseCmd(argv, unparsed); 322 | 323 | unparsed.push(i); 324 | } 325 | 326 | return { cmd : this, argv : unparsed }; 327 | } 328 | 329 | _parseOptsAndArgs(argv) { 330 | const opts = {}, 331 | args = {}, 332 | nonParsedOpts = this._opts.concat(), 333 | nonParsedArgs = this._args.concat(); 334 | 335 | let res, i; 336 | while(i = argv.shift()) { 337 | if(i !== '--' && i[0] === '-') { 338 | const m = i.match(/^(--\w[\w-_]*)=(.*)$/); 339 | if(m) { 340 | i = m[1]; 341 | this._optsByKey[i]._flag || argv.unshift(m[2]); 342 | } 343 | 344 | const opt = this._ejectOpt(nonParsedOpts, this._optsByKey[i]); 345 | if(!opt) return this.reject(`Unknown option: ${i}`); 346 | 347 | if(Q.isRejected(res = opt._parse(argv, opts))) return res; 348 | 349 | continue; 350 | } 351 | 352 | i === '--' && (i = argv.splice(0)); 353 | Array.isArray(i) || (i = [i]); 354 | 355 | let a; 356 | while(a = i.shift()) { 357 | let arg = nonParsedArgs.shift(); 358 | if(!arg) return this.reject(`Unknown argument: ${a}`); 359 | 360 | arg._arr && nonParsedArgs.unshift(arg); 361 | if(Q.isRejected(res = arg._parse(a, args))) return res; 362 | } 363 | } 364 | 365 | return { 366 | opts : this._setDefaults(opts, nonParsedOpts), 367 | args : this._setDefaults(args, nonParsedArgs) 368 | }; 369 | } 370 | 371 | _setDefaults(params, desc) { 372 | for(const item of desc) 373 | item._def !== undefined && 374 | !params.hasOwnProperty(item._name) && 375 | item._saveVal(params, item._def); 376 | 377 | return params; 378 | } 379 | 380 | _processParams(params, desc) { 381 | const notExists = []; 382 | 383 | for(const item of desc) { 384 | const n = item._name; 385 | 386 | if(!params.hasOwnProperty(n)) { 387 | notExists.push(item); 388 | continue; 389 | } 390 | 391 | const vals = Array.isArray(params[n])? params[n] : [params[n]]; 392 | delete params[n]; 393 | 394 | let res; 395 | for(const v of vals) 396 | if(Q.isRejected(res = item._saveVal(params, v))) 397 | return res; 398 | } 399 | 400 | return this._setDefaults(params, notExists); 401 | } 402 | 403 | _parseArr(argv) { 404 | return Q.when(this._parseCmd(argv), p => 405 | Q.when(p.cmd._parseOptsAndArgs(p.argv), r => ({ 406 | cmd : p.cmd, 407 | opts : r.opts, 408 | args : r.args 409 | }))); 410 | } 411 | 412 | _do(inputPromise) { 413 | return Q.when(inputPromise, input => { 414 | return [this._checkRequired] 415 | .concat(input.cmd._act || []) 416 | .reduce((res, act) => 417 | Q.when(res, prev => act.call(input.cmd, input.opts, input.args, prev)), 418 | undefined); 419 | }); 420 | } 421 | 422 | /** 423 | * Parse arguments from simple format like NodeJS process.argv 424 | * and run ahead current program, i.e. call process.exit when all actions done. 425 | * 426 | * @param {String[]} argv - arguments 427 | * @returns {COA.Cmd} - this instance (for chainability) 428 | */ 429 | run(argv) { 430 | argv || (argv = process.argv.slice(2)); 431 | 432 | const cb = code => 433 | res => res? 434 | this._exit(res.stack || res.toString(), (res.hasOwnProperty('exitCode')? res.exitCode : code) || 0) : 435 | this._exit(); 436 | 437 | Q.when(this.do(argv), cb(0), cb(1)).done(); 438 | 439 | return this; 440 | } 441 | 442 | /** 443 | * Invoke specified (or current) command using provided 444 | * options and arguments. 445 | * 446 | * @param {String|String[]} [cmds] - subcommand to invoke (optional) 447 | * @param {Object} [opts] - command options (optional) 448 | * @param {Object} [args] - command arguments (optional) 449 | * @returns {Q.Promise} 450 | */ 451 | invoke(cmds, opts, args) { 452 | cmds || (cmds = []); 453 | opts || (opts = {}); 454 | args || (args = {}); 455 | typeof cmds === 'string' && (cmds = cmds.split(' ')); 456 | 457 | if(arguments.length < 3 && !Array.isArray(cmds)) { 458 | args = opts; 459 | opts = cmds; 460 | cmds = []; 461 | } 462 | 463 | return Q.when(this._parseCmd(cmds), p => { 464 | if(p.argv.length) 465 | return this.reject(`Unknown command: ${cmds.join(' ')}`); 466 | 467 | return Q.all([ 468 | this._processParams(opts, this._opts), 469 | this._processParams(args, this._args) 470 | ]).spread((_opts, _args) => 471 | this._do({ 472 | cmd : p.cmd, 473 | opts : _opts, 474 | args : _args 475 | }) 476 | .fail(res => (res && res.exitCode === 0)? 477 | res.toString() : 478 | this.reject(res))); 479 | }); 480 | } 481 | } 482 | 483 | /** 484 | * Convenient function to run command from tests. 485 | * 486 | * @param {String[]} argv - arguments 487 | * @returns {Q.Promise} 488 | */ 489 | Cmd.prototype.do = function(argv) { 490 | return this._do(this._parseArr(argv || [])); 491 | }; 492 | 493 | module.exports = Cmd; 494 | --------------------------------------------------------------------------------