├── assets ├── colors.png ├── caporal.png ├── caporal.pxm └── suggest.png ├── .gitignore ├── tests ├── utils │ ├── make-argv.js │ ├── bootstrap.js │ └── callback-logger.js ├── caporal-export.js ├── fatal-error.js ├── verbosity-default.js ├── verbosity-verbose.js ├── verbosity-quiet.js ├── no-command.js ├── command.js ├── suggest.js ├── issues │ └── issue-13-negative-num-as-arg.js ├── predefined-options.js ├── logger.js ├── validator.js ├── help.js ├── completion-command.js ├── actions.js ├── arg-validation.js ├── option-validation.js └── autocomplete.js ├── lib ├── error │ ├── no-action-error.js │ ├── validation-error.js │ ├── wrong-num-of-arg.js │ ├── option-syntax-error.js │ ├── invalid-argument-value.js │ ├── missing-option.js │ ├── invalid-option-value.js │ ├── base-error.js │ └── unknown-option.js ├── constants.js ├── colorful.js ├── utils.js ├── suggest.js ├── logger.js ├── argument.js ├── option.js ├── help.js ├── validator.js ├── autocomplete.js ├── program.js └── command.js ├── .npmignore ├── index.js ├── .idea ├── misc.xml ├── vcs.xml ├── modules.xml ├── jsLibraryMappings.xml ├── caporal.iml └── inspectionProfiles │ └── Project_Default.xml ├── .editorconfig ├── examples └── pizza │ ├── package.json │ └── pizza.js ├── .eslintrc.json ├── .travis.yml ├── release.sh ├── CHANGELOG.md ├── package.json └── README.md /assets/colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phonbopit/Caporal.js/master/assets/colors.png -------------------------------------------------------------------------------- /assets/caporal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phonbopit/Caporal.js/master/assets/caporal.png -------------------------------------------------------------------------------- /assets/caporal.pxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phonbopit/Caporal.js/master/assets/caporal.pxm -------------------------------------------------------------------------------- /assets/suggest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phonbopit/Caporal.js/master/assets/suggest.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | coverage 4 | .idea/workspace.xml 5 | .idea/tasks.xml 6 | .idea/watcherTasks.xml 7 | -------------------------------------------------------------------------------- /tests/utils/make-argv.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function makeArgv(args) { 4 | return ['', ''].concat(args); 5 | }; 6 | -------------------------------------------------------------------------------- /lib/error/no-action-error.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const BaseError = require('./base-error'); 4 | 5 | class NoActionError extends BaseError {} 6 | 7 | module.exports = NoActionError; 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | coverage 4 | .idea 5 | tests 6 | assets 7 | examples 8 | .editorconfig 9 | .eslintrc.json 10 | .gitignore 11 | .travis.yml 12 | release.sh 13 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Program = require('./lib/program'); 4 | const prog = new Program(); 5 | 6 | 7 | /** 8 | * @type {Program} 9 | */ 10 | module.exports = prog; 11 | -------------------------------------------------------------------------------- /lib/error/validation-error.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const BaseError = require('./base-error'); 4 | 5 | class ValidationError extends BaseError {} 6 | 7 | module.exports = ValidationError; 8 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /lib/error/wrong-num-of-arg.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const BaseError = require('./base-error'); 4 | 5 | class WrongNumberOfArgumentError extends BaseError {} 6 | 7 | module.exports = WrongNumberOfArgumentError; 8 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.INTEGER = exports.INT = 1 << 0; 4 | exports.FLOAT = 1 << 1; 5 | exports.BOOL = exports.BOOLEAN = 1 << 2; 6 | exports.LIST = exports.ARRAY = 1 << 3; 7 | exports.REPEATABLE = 1 << 4; 8 | exports.REQUIRED = 1 << 5; 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | end_of_line = lf 4 | insert_final_newline = true 5 | 6 | [*.js] 7 | indent_size = 2 8 | continuation_indent_size = 2 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | indent_size = 4 14 | -------------------------------------------------------------------------------- /tests/caporal-export.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* global Program, should */ 4 | 5 | describe("require('caporal')", () => { 6 | it(`should return {new Program()}`, () => { 7 | const caporal = require('../'); 8 | should(caporal).be.instanceOf(Program); 9 | }); 10 | }); 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/utils/bootstrap.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | process.env.TABTAB_DEBUG = "/tmp/tabtab.log"; 4 | 5 | global.Program = require('../../lib/program'); 6 | global.should = require('should/as-function'); 7 | global.logger = require('./callback-logger').logger; 8 | global.makeArgv = require('./make-argv'); 9 | global.sinon = require('sinon'); 10 | -------------------------------------------------------------------------------- /lib/error/option-syntax-error.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const BaseError = require('./base-error'); 4 | 5 | class OptionSyntaxError extends BaseError { 6 | constructor(synopsis, program) { 7 | let msg = `Syntax error in option synopsis: ${synopsis}`; 8 | super(msg, {synopsis}, program); 9 | } 10 | } 11 | 12 | module.exports = OptionSyntaxError; 13 | -------------------------------------------------------------------------------- /examples/pizza/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pizza", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "pizza.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "dependencies" : { 10 | "caporal": "file:../../" 11 | }, 12 | "bin" : { "fly" : "./pizza.js" }, 13 | "author": "", 14 | "license": "ISC" 15 | } 16 | -------------------------------------------------------------------------------- /lib/error/invalid-argument-value.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const BaseError = require('./base-error'); 4 | const chalk = require('chalk'); 5 | 6 | class InvalidArgumentValueError extends BaseError { 7 | 8 | constructor(arg, value, command, program) { 9 | let msg = `Invalid value '${value}' for argument ${chalk.italic(arg)}.`; 10 | super(msg, {arg, command, value}, program); 11 | } 12 | } 13 | 14 | module.exports = InvalidArgumentValueError; 15 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /lib/error/missing-option.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const BaseError = require('./base-error'); 4 | const chalk = require('chalk'); 5 | const getDashedOption = require('../utils').getDashedOption; 6 | 7 | class MissingOptionError extends BaseError { 8 | constructor(option, command, program) { 9 | let msg = `Missing option ${chalk.italic(getDashedOption(option))}.`; 10 | super(msg, {option, command}, program); 11 | } 12 | } 13 | 14 | module.exports = MissingOptionError; 15 | -------------------------------------------------------------------------------- /lib/colorful.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const chalk = require('chalk'); 4 | 5 | exports.colorize = (text) => { 6 | return text.replace(/<([a-z0-9-_\.]+)>/gi, (match) => { 7 | return chalk.blue(match); 8 | }).replace(//gi, (match) => { 9 | return chalk.magenta(match); 10 | }).replace(/\[([a-z0-9-_\.]+)\]/gi, (match) => { 11 | return chalk.yellow(match); 12 | }).replace(/ --?([a-z0-9-_\.]+)/gi, (match) => { 13 | return chalk.green(match); 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "mocha": true, 5 | "node": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parserOptions": { 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | "indent": [ 13 | "error", 14 | 2 15 | ], 16 | "no-unused-vars": ["error", { "args": "none" }], 17 | "linebreak-style": [ 18 | "error", 19 | "unix" 20 | ], 21 | "no-console": 0, 22 | "quotes": 0, 23 | "semi": [0, "never"] 24 | } 25 | } -------------------------------------------------------------------------------- /lib/error/invalid-option-value.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const BaseError = require('./base-error'); 4 | const chalk = require('chalk'); 5 | const getDashedOption = require('../utils').getDashedOption; 6 | 7 | class InvalidOptionValueError extends BaseError { 8 | constructor(option, value, command, originalError, program) { 9 | let msg = `Invalid value '${value}' for option ${chalk.italic(getDashedOption(option))}.`; 10 | super(msg, {option, command, originalError}, program); 11 | } 12 | } 13 | 14 | module.exports = InvalidOptionValueError; 15 | -------------------------------------------------------------------------------- /lib/error/base-error.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const chalk = require('chalk'); 4 | 5 | class BaseError extends Error { 6 | constructor(message, meta, program) { 7 | const spaces = " ".repeat(3); 8 | const msg = spaces + chalk.red("Error: " + message) + "\n" + spaces + 9 | `Type ${chalk.bold(program.bin() + ' --help')} for help.\n`; 10 | super(msg); 11 | this.name = this.constructor.name; 12 | this.originalMessage = message; 13 | this.meta = meta; 14 | Error.captureStackTrace(this, this.constructor); 15 | } 16 | } 17 | 18 | module.exports = BaseError; 19 | -------------------------------------------------------------------------------- /tests/fatal-error.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* global Program, logger, should, sinon */ 4 | 5 | const program = new Program(); 6 | 7 | program. 8 | logger(logger) 9 | .version('1.0.0'); 10 | 11 | describe("program.fataError()", () => { 12 | 13 | it(`should call logger.error() and exit(2)`, () => { 14 | const error = sinon.stub(logger, 'error').withArgs("\nfoo"); 15 | const exit = sinon.stub(process, 'exit').withArgs(2); 16 | 17 | program.fatalError(new Error("foo")); 18 | 19 | should(error.callCount).eql(1); 20 | should(exit.callCount).eql(1); 21 | }); 22 | 23 | after(function () { 24 | logger.error.restore(); 25 | process.exit.restore(); 26 | }) 27 | 28 | 29 | }); 30 | 31 | 32 | -------------------------------------------------------------------------------- /.idea/caporal.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const camelCase = require('lodash.camelcase'); 4 | 5 | class GetterSetter { 6 | 7 | makeGetterSetter(varname) { 8 | return function(value) { 9 | const key = '_' + varname; 10 | if (value) { 11 | this[key] = value; 12 | return this; 13 | } 14 | return this[key]; 15 | }.bind(this); 16 | } 17 | 18 | getCleanNameFromNotation(str) { 19 | str = str.replace(/([\[\]<>]+)/g, '').replace('...', ''); 20 | return camelCase(str); 21 | } 22 | 23 | } 24 | 25 | exports.GetterSetter = GetterSetter; 26 | 27 | /** 28 | * 29 | * @param {String} option 30 | */ 31 | exports.getDashedOption = function getDashedOption(option) { 32 | if (option.length === 1) { 33 | return '-' + option; 34 | } 35 | return '--' + option; 36 | }; 37 | 38 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | -------------------------------------------------------------------------------- /lib/suggest.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const levenshtein = require('fast-levenshtein'); 4 | const chalk = require('chalk'); 5 | 6 | /** 7 | * 8 | * @param {String} input - User input 9 | * @param {String[]} possibilities - Possibilities to retrieve suggestions from 10 | */ 11 | exports.getSuggestions = function getSuggestions(input, possibilities) { 12 | return possibilities 13 | .map(p => { 14 | return {suggestion: p, distance: levenshtein.get(input, p)}; 15 | }) 16 | .filter(p => p.distance <= 2) 17 | .sort((a, b) => a.distance - b.distance) 18 | .map(p => p.suggestion); 19 | }; 20 | 21 | exports.getBoldDiffString = (from, to) => { 22 | return to.split('').map((char, index) => { 23 | if (char != from.charAt(index)) { 24 | return chalk.bold(char); 25 | } 26 | return char; 27 | }).join('') 28 | }; 29 | 30 | -------------------------------------------------------------------------------- /lib/error/unknown-option.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const BaseError = require('./base-error'); 4 | const getSuggestions = require('../suggest').getSuggestions; 5 | const getBoldDiffString = require('../suggest').getBoldDiffString; 6 | const getDashedOption = require('../utils').getDashedOption; 7 | const chalk = require('chalk'); 8 | 9 | class UnknownOptionError extends BaseError { 10 | constructor(option, command, program) { 11 | const suggestions = getSuggestions(option, command._getLongOptions()); 12 | let msg = `Unknown option ${chalk.italic(getDashedOption(option))}.`; 13 | if (suggestions.length) { 14 | msg += ' Did you mean ' + suggestions.map( 15 | s => '--' + getBoldDiffString(option, s) 16 | ).join(' or maybe ') + ' ?'; 17 | } 18 | super(msg, {option, command}, program); 19 | } 20 | } 21 | 22 | module.exports = UnknownOptionError; 23 | -------------------------------------------------------------------------------- /tests/verbosity-default.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* global Program, logger, should, makeArgv */ 4 | 5 | const program = new Program(); 6 | 7 | program 8 | .logger(logger) 9 | .version('1.0.0') 10 | .reset() 11 | .command('foo', 'My foo') 12 | .action(function() { 13 | this.debug('debug should NOT be displayed'); 14 | this.info("This should be displayed"); 15 | this.warn('This should be displayed'); 16 | }); 17 | 18 | describe('Caporal with default verbosity', () => { 19 | it(`should output at info level`, (done) => { 20 | 21 | let output = 0; 22 | const listener = _ => output++; 23 | logger.on('logging', listener); 24 | 25 | program.parse(makeArgv(['foo'])); 26 | 27 | setImmediate(() => { 28 | should(output).eql(2); 29 | logger.removeListener('logging', listener); 30 | done(); 31 | }) 32 | }); 33 | }); 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/verbosity-verbose.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* global Program, logger, should, makeArgv */ 4 | 5 | const program = new Program(); 6 | 7 | program 8 | .logger(logger) 9 | .version('1.0.0') 10 | .command('foo', 'My foo') 11 | .action(function() { 12 | this.debug('debug should NOT be displayed'); 13 | this.info("This should be displayed"); 14 | this.warn('This should be displayed'); 15 | }); 16 | 17 | 18 | ['-v', '--verbose'].forEach((opt) => { 19 | describe('Passing ' + opt, () => { 20 | it(`should output at debug level`, (done) => { 21 | let output = 0; 22 | const listener = _ => output++; 23 | logger.on('logging', listener); 24 | program.parse(makeArgv(['foo', opt])); 25 | setImmediate(() => { 26 | should(output).eql(3); 27 | logger.removeListener('logging', listener); 28 | done(); 29 | }) 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/verbosity-quiet.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* global Program, logger, should, makeArgv */ 4 | 5 | const program = new Program(); 6 | 7 | program 8 | .logger(logger) 9 | .version('1.0.0') 10 | .reset() 11 | .command('foo', 'My foo') 12 | .action(function() { 13 | this.log("This should NOT be displayed"); 14 | this.debug('debug should NOT be displayed'); 15 | this.warn('This should be displayed'); 16 | }); 17 | 18 | describe('Passing --quiet', () => { 19 | it(`should only output warnings & errors`, (done) => { 20 | let output = 0; 21 | 22 | const listener = function(out, level, txt) { 23 | output++ 24 | }; 25 | 26 | logger.on('logging', listener); 27 | 28 | program.parse(makeArgv(['foo', '--quiet'])); 29 | 30 | setImmediate(() => { 31 | should(output).eql(1); 32 | logger.removeListener('logging', listener); 33 | done(); 34 | }) 35 | }); 36 | }); 37 | 38 | 39 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - '7' 5 | - '6' 6 | - '4' 7 | 8 | script: 9 | - npm test 10 | - npm run coverage 11 | - cat ./coverage/lcov.info | ./node_modules/.bin/codacy-coverage 12 | 13 | cache: 14 | yarn: true 15 | 16 | env: 17 | global: 18 | secure: jt32uur2Bsx44QcPYzUXv5Y0K3TVQxujGB/RqZv4dN/ld/jDjSKGGb6O4l5XawR9UNhVxplKPlcQS8tjDA13N7VE6ocdMNDdwUnkMLmJnokq24CpEgRP7LWgHjIT6HIHzA42+dWAjZt/jf1NKTtYqouMCnLQXmh6Gp+lGMvRgx7Fy408ldHNiGg6glneBlkgLbrqg968oMFAP2M4begeIUds+nwkHX6knQgzBgSk5tjnPjcWEOsN3u6rfcf+WlsvNYbVPhnp+vgWritpZvXjXqP/IlryEPjh9Wr/LZORXVF0Do3/Yad3b3Jc/yWWgqXO8qNbkEmVnG6azByIimBCzoOStCyQIIGzicFlY3HwBY9TuB40MG4OQbGHTi3RjS3YOyglgjxUG24Hdk1jBPbmWitvSOSZC664sQ4/xJZmXhFrG1btyGlRNIgwKwSHC1vBiMQDWdYf4Ef3ALYo1xFlqncRE0F0svcs27ebOunHgS2o7K68f9hcjsxRjyGwxAElf7TcbguS/aPqsWjRbzKDxrB7WEyK9GflEF5hVHw1Nnbh7Z3xOlejsjolONFaYmzQVWe0vQjlnVuDyP7Zsl44KbCSHsPCFZOnuz3xRq8Ox8EnGqbO6kZyqzewjU6c4pZ5K7Y1f6QEJuU4x0ALcS881EER9Q9v+9vnLTPW1lOu6Ns= 19 | -------------------------------------------------------------------------------- /tests/no-command.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* global Program, logger, should, makeArgv, sinon */ 4 | 5 | 6 | 7 | describe('Setting up no command() but an action()', () => { 8 | 9 | const program = new Program(); 10 | 11 | program 12 | .logger(logger) 13 | .version('1.0.0'); 14 | 15 | it(`should execute action()`, () => { 16 | const action = sinon.stub(); 17 | program.action(action); 18 | program.parse([]); 19 | should(action.callCount).eql(1); 20 | }); 21 | }); 22 | 23 | describe('Setting up no command() but an argument() and an action()', () => { 24 | 25 | const program = new Program(); 26 | 27 | program 28 | .logger(logger) 29 | .version('1.0.0') 30 | .argument('', 'My foo arg'); 31 | 32 | it(`should execute action()`, () => { 33 | const action = sinon.stub(); 34 | program.action(action); 35 | program.parse(makeArgv(['myarg'])); 36 | should(action.callCount).eql(1); 37 | }); 38 | }); 39 | 40 | 41 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const winston = require('winston'); 4 | const util = require('util'); 5 | const prettyjson = require('prettyjson'); 6 | 7 | const CaporalTransport = function (options) { 8 | this.name = 'caporal'; 9 | this.level = options.level || 'info'; 10 | }; 11 | 12 | util.inherits(CaporalTransport, winston.Transport); 13 | 14 | CaporalTransport.prototype.log = function (level, msg, meta, callback) { 15 | if (meta !== null && typeof meta !== 'undefined') { 16 | msg += "\n" + prettyjson.render(meta); 17 | } 18 | const levelInt = winston.levels[level]; 19 | const stdio = levelInt <= 1 ? 'stderr' : 'stdout'; 20 | process[stdio].write(msg); 21 | callback(null, true, stdio); 22 | }; 23 | 24 | exports.createLogger = function createLogger(opts) { 25 | 26 | opts = opts || {}; 27 | 28 | const logger = exports.logger = new (winston.Logger)({ 29 | transports: [ 30 | new (CaporalTransport)(opts) 31 | ] 32 | }); 33 | 34 | return logger; 35 | }; 36 | -------------------------------------------------------------------------------- /tests/command.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* global Program, logger, should, makeArgv, sinon */ 4 | 5 | describe('Chaining 2 commands', () => { 6 | 7 | const program = new Program(); 8 | 9 | program 10 | .logger(logger) 11 | .version('1.0.0') 12 | .command('foo') 13 | .action(function() {}) 14 | .command('bar') 15 | .action(function() {}) 16 | 17 | it(`should generate 2 commands`, () => { 18 | program.parse(makeArgv(['foo'])); 19 | should(program._commands.length).eql(2); 20 | }); 21 | 22 | 23 | }); 24 | 25 | describe('Aliasing a command', () => { 26 | 27 | const program = new Program(); 28 | 29 | const action = sinon.stub(); 30 | 31 | program 32 | .logger(logger) 33 | .version('1.0.0') 34 | .command('foo') 35 | .alias('f') 36 | .action(action); 37 | 38 | it(`should allow calling it with alias`, () => { 39 | program.parse(makeArgv(['f'])); 40 | should(action.callCount).be.eql(1); 41 | }); 42 | 43 | 44 | }); 45 | 46 | 47 | -------------------------------------------------------------------------------- /tests/suggest.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* global Program, logger, should, makeArgv, sinon */ 4 | 5 | const program = new Program(); 6 | const stripColor = require('chalk').stripColor; 7 | 8 | program 9 | .logger(logger) 10 | .version('1.0.0'); 11 | 12 | 13 | describe('Passing --foo', () => { 14 | 15 | it(`should suggest --foor and --afoo and --footx`, () => { 16 | program 17 | .option('--foor ') 18 | .option('--afoo ') 19 | .option('--footx ') 20 | .action(function() {}); 21 | 22 | const error = sinon.stub(program, "fatalError", function(err) { 23 | should(err.name).eql('UnknownOptionError'); 24 | should(stripColor(err.originalMessage)).containEql('foor'); 25 | should(stripColor(err.originalMessage)).containEql('afoo'); 26 | should(stripColor(err.originalMessage)).containEql('footx'); 27 | }); 28 | program.parse(makeArgv('--foo')); 29 | should(error.callCount).be.eql(1); 30 | error.restore(); 31 | program.reset(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /tests/issues/issue-13-negative-num-as-arg.js: -------------------------------------------------------------------------------- 1 | /* global Program, logger, should, makeArgv, sinon */ 2 | 3 | 4 | 5 | describe("Issue #13 - Enter negative number as Argument", function() { 6 | 7 | beforeEach(function () { 8 | 9 | this.program = new Program(); 10 | this.action = sinon.spy(); 11 | 12 | this.program 13 | .logger(logger) 14 | .version('1.0.0') 15 | .command('solve', 'Solve quadratic') 16 | .argument('', 'A', this.program.INT) 17 | .argument('', 'B', this.program.INT) 18 | .argument('', 'C', this.program.INT) 19 | .action(this.action); 20 | 21 | this.fatalError = sinon.stub(this.program, "fatalError"); 22 | }); 23 | 24 | afterEach(function () { 25 | this.fatalError.restore(); 26 | this.program.reset(); 27 | }); 28 | 29 | it(`should not throw WrongNumberOfArgumentError with negative number as argument`, function() { 30 | this.program.parse(makeArgv(['solve', '1', '2', '-3'])); 31 | should(this.fatalError.callCount).eql(0); 32 | should(this.action.callCount).eql(1); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/utils/callback-logger.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const winston = require('winston'); 4 | const util = require('util'); 5 | const prettyjson = require('prettyjson'); 6 | 7 | const CaporalTransport = function (options) { 8 | options = options || {}; 9 | this.name = 'caporal'; 10 | this.level = options.level || 'info'; 11 | this._callback = options.callback || function(text, level){ 12 | //console.log("caporal-callback: %s: %s", level, text); 13 | } 14 | }; 15 | 16 | util.inherits(CaporalTransport, winston.Transport); 17 | 18 | CaporalTransport.prototype.setCallback = function(callback) { 19 | this._callback = callback; 20 | }; 21 | 22 | CaporalTransport.prototype.log = function (level, msg, meta, callback) { 23 | if (typeof meta === 'object' && Object.keys(meta).length) { 24 | msg += "\n" + prettyjson.render(meta); 25 | } 26 | const levelInt = winston.levels[level]; 27 | this._callback(msg, levelInt <= 1 ? 'stderr' : 'stdout'); 28 | callback(null, true); 29 | }; 30 | 31 | exports.logger = new (winston.Logger)({ 32 | transports: [ 33 | new (CaporalTransport)() 34 | ] 35 | }); 36 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ############################################### 3 | # 4 | # Usage 5 | # 6 | # ./release.sh `patch`/`minor`/`major`/`` 7 | # 8 | # defaults to conventional-recommended-bump 9 | # 10 | ############################################### 11 | 12 | np() { 13 | travis status --no-interactive && 14 | ./node_modules/.bin/trash node_modules &>/dev/null; 15 | git pull --rebase && 16 | npm install && 17 | npm test && 18 | cp package.json _package.json && 19 | preset=`./node_modules/.bin/conventional-commits-detector` && 20 | echo $preset && 21 | bump=`./node_modules/.bin/conventional-recommended-bump -p angular` && 22 | echo ${1:-$bump} && 23 | npm --no-git-tag-version version ${1:-$bump} &>/dev/null && 24 | ./node_modules/.bin/conventional-changelog -i CHANGELOG.md -s -p ${2:-$preset} && 25 | git add CHANGELOG.md && 26 | version=`cat package.json | ./node_modules/.bin/json version` && 27 | git commit -m"docs(CHANGELOG): $version" && 28 | mv -f _package.json package.json && 29 | npm version ${1:-$bump} -m "chore(release): %s" && 30 | git push --follow-tags && 31 | npm publish 32 | } 33 | 34 | np $@ 35 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # [0.3.0](https://github.com/mattallty/Caporal.js/compare/v0.2.0...v0.3.0) (2017-02-26) 3 | 4 | 5 | ### Bug Fixes 6 | 7 | * _getLongOptions should only return options that have long notations ([d76e3dd](https://github.com/mattallty/Caporal.js/commit/d76e3dd)) 8 | * change the way we handle new lines in logger ([635fa9b](https://github.com/mattallty/Caporal.js/commit/635fa9b)) 9 | 10 | 11 | ### Features 12 | 13 | * Node.js 4.4.5+ compatibility ([fd0fbdc](https://github.com/mattallty/Caporal.js/commit/fd0fbdc)) 14 | 15 | 16 | 17 | 18 | # [0.2.0](https://github.com/mattallty/Caporal.js/compare/v0.1.0...v0.2.0) (2017-02-25) 19 | 20 | 21 | ### Features 22 | 23 | * Implement auto-completion (#1) ([333c968](https://github.com/mattallty/Caporal.js/commit/333c968)) 24 | 25 | 26 | 27 | 28 | # 0.1.0 (2017-02-19) 29 | 30 | 31 | ### Bug Fixes 32 | 33 | * fix eslint errors ([84177db](https://github.com/mattallty/Caporal.js/commit/84177db)) 34 | * Fix passing logger in action callback ([6d35d5f](https://github.com/mattallty/Caporal.js/commit/6d35d5f)) 35 | * fix release script ([20015a3](https://github.com/mattallty/Caporal.js/commit/20015a3)) 36 | 37 | 38 | 39 | 40 | 41 | 42 | # 0.1.0-alpha (2017-02-19) 43 | 44 | ### Bug Fixes 45 | 46 | * fix eslint errors ([84177db](https://github.com/mattallty/Caporal.js/commit/84177db)) 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /tests/predefined-options.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* global Program, logger, should, makeArgv, sinon */ 4 | 5 | const program = new Program(); 6 | 7 | program 8 | .logger(logger) 9 | .version('1.0.0'); 10 | 11 | describe('Predefined options', function() { 12 | 13 | it(`-V should return program version (${program.version()})`, function() { 14 | const version = sinon.stub(program, "version"); 15 | const exit = sinon.stub(process, "exit"); 16 | program.parse(makeArgv('-V')); 17 | should(version.called).be.true(); 18 | should(exit.callCount).eql(1); 19 | exit.restore(); 20 | version.restore(); 21 | }); 22 | 23 | it(`--version should return program version (${program.version()})`, function() { 24 | const version = sinon.stub(program, "version"); 25 | const exit = sinon.stub(process, "exit"); 26 | program.parse(makeArgv('--version')); 27 | should(version.called).be.true(); 28 | should(exit.callCount).eql(1); 29 | exit.restore(); 30 | version.restore(); 31 | }); 32 | 33 | it(`-h should call help() when only one command`, function() { 34 | program 35 | .reset() 36 | .command('foo', 'My foo'); 37 | 38 | const exit = sinon.stub(process, "exit"); 39 | const help = sinon.spy(program, "help"); 40 | program.parse(makeArgv(['foo', '-h'])); 41 | should(help.called).be.ok(); 42 | should(exit.callCount).eql(1); 43 | exit.restore(); 44 | help.restore(); 45 | }); 46 | 47 | it(`-h should call help() when more than one command`, function() { 48 | program 49 | .reset() 50 | .command('foo', 'My foo') 51 | .command('bar', 'My bar'); 52 | 53 | const exit = sinon.stub(process, "exit"); 54 | const help = sinon.spy(program, "help"); 55 | program.parse(makeArgv(['foo', '-h'])); 56 | should(help.called).be.ok(); 57 | should(exit.callCount).eql(1); 58 | exit.restore(); 59 | help.restore(); 60 | 61 | }); 62 | 63 | }); 64 | -------------------------------------------------------------------------------- /lib/argument.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const GetterSetter = require('./utils').GetterSetter; 4 | const Validator = require('./validator'); 5 | 6 | class Argument extends GetterSetter { 7 | 8 | /** 9 | * 10 | * @param {String} synopsis - Option synopsis 11 | * @param {String} description - Option description 12 | * @param {String|RegExp|Function|Number} [validator] - Option validator, used for checking or casting 13 | * @param {*} [defaultValue] - Default value 14 | * @param {Program} program - program instance 15 | */ 16 | constructor(synopsis, description, validator, defaultValue, program) { 17 | super(); 18 | this._type = synopsis.substring(0, 1) === '[' ? Argument.OPTIONAL : Argument.REQUIRED; 19 | this._variadic = synopsis.substr(-4, 3) === '...'; 20 | this._synopsis = synopsis; 21 | this._name = this.getCleanNameFromNotation(synopsis); 22 | this._description = description; 23 | this._validator = validator ? new Validator(validator, program) : null; 24 | this._default = defaultValue; 25 | this.synopsis = this.makeGetterSetter('synopsis'); 26 | this.description = this.makeGetterSetter('description'); 27 | this.default = this.makeGetterSetter('default'); 28 | } 29 | 30 | hasDefault() { 31 | return this.default() !== undefined; 32 | } 33 | 34 | isRequired() { 35 | return this._type === Argument.REQUIRED 36 | } 37 | 38 | getChoices() { 39 | return this._validator ? this._validator.getChoices() : []; 40 | } 41 | 42 | isOptional() { 43 | return this._type === Argument.OPTIONAL 44 | } 45 | 46 | isVariadic() { 47 | return this._variadic === true 48 | } 49 | 50 | name() { 51 | return this._name; 52 | } 53 | 54 | _validate(value) { 55 | if (!this._validator) { 56 | return value; 57 | } 58 | return this._validator.validate(value); 59 | } 60 | } 61 | 62 | Object.defineProperties(Argument, { 63 | "OPTIONAL": { 64 | value: 'optional' 65 | }, 66 | "REQUIRED": { 67 | value: 'required' 68 | } 69 | }); 70 | 71 | module.exports = Argument; 72 | -------------------------------------------------------------------------------- /tests/logger.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* global should, sinon */ 4 | 5 | const myLogger = require('../lib/logger').createLogger(); 6 | const stripColor = require('chalk').stripColor; 7 | 8 | describe('logger', () => { 9 | 10 | it(`should log info() to stdout`, () => { 11 | 12 | const write = process.stdout.write; 13 | let logStr = null; 14 | 15 | sinon.stub(process.stdout, "write", function(str) { 16 | logStr = str; 17 | }); 18 | const callback = sinon.stub().withArgs(null, true, 'stdout'); 19 | 20 | myLogger.log('info', 'foo', {foo:'bar'}, callback); 21 | 22 | const oldWrite = process.stdout.write; 23 | process.stdout.write = write; 24 | 25 | should(stripColor(logStr)).be.eql("foo\nfoo: bar"); 26 | should(callback.called).be.ok(); 27 | should(oldWrite.called).be.ok(); 28 | }); 29 | 30 | 31 | 32 | it(`should log error() to stderr`, () => { 33 | 34 | const write = process.stderr.write; 35 | let logStr = null; 36 | 37 | sinon.stub(process.stderr, "write", function(str) { 38 | logStr = str; 39 | }); 40 | const callback = sinon.stub().withArgs(null, true, 'stderr'); 41 | 42 | myLogger.log('error', 'foo', {foo:'bar'}, callback); 43 | 44 | const oldWrite = process.stderr.write; 45 | process.stderr.write = write; 46 | 47 | should(stripColor(logStr)).be.eql("foo\nfoo: bar"); 48 | should(callback.called).be.ok(); 49 | should(oldWrite.called).be.ok(); 50 | 51 | }); 52 | 53 | it(`should log without meta`, () => { 54 | 55 | const write = process.stdout.write; 56 | let logStr = null; 57 | 58 | sinon.stub(process.stdout, "write", function(str) { 59 | logStr = str; 60 | }); 61 | const callback = sinon.stub().withArgs(null, true, 'stdout'); 62 | 63 | myLogger.log('info', 'foo', null, callback); 64 | 65 | const oldWrite = process.stdout.write; 66 | process.stdout.write = write; 67 | 68 | should(stripColor(logStr)).be.eql("foo"); 69 | should(callback.called).be.ok(); 70 | should(oldWrite.called).be.ok(); 71 | 72 | }); 73 | 74 | }); 75 | 76 | 77 | -------------------------------------------------------------------------------- /tests/validator.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* global Program, logger, should, makeArgv, sinon */ 4 | 5 | const program = new Program(); 6 | 7 | program 8 | .logger(logger) 9 | .version('1.0.0'); 10 | 11 | describe('Setting up an invalid validator flag', () => { 12 | 13 | it(`should throw ValidationError`, () => { 14 | 15 | const error = sinon.stub(program, "fatalError", function(err) { 16 | should(err.name).eql('ValidationError'); 17 | }); 18 | 19 | program 20 | .command('foo') 21 | .option('-t ', 'my option', 256) 22 | .action(function() {}); 23 | 24 | program.parse(makeArgv(['foo', '-t', '2982'])); 25 | should(error.callCount).be.eql(1); 26 | error.restore(); 27 | program.reset(); 28 | }); 29 | }); 30 | 31 | describe('Setting up an invalid validator (boolean)', () => { 32 | 33 | it(`should throw ValidationError`, () => { 34 | 35 | const error = sinon.stub(program, "fatalError", function(err) { 36 | should(err.name).eql('ValidationError'); 37 | }); 38 | 39 | program 40 | .command('foo') 41 | .option('-t ', 'my option', true) 42 | .action(function() {}); 43 | 44 | program.parse(makeArgv(['foo', '-t', '2982'])); 45 | should(error.callCount).be.eql(1); 46 | error.restore(); 47 | program.reset(); 48 | }); 49 | }); 50 | 51 | describe('Setting up an option without validator', () => { 52 | 53 | it(`should return empty array for option.getChoices()`, () => { 54 | 55 | program 56 | .command('foo') 57 | .option('-t ', 'my option') 58 | .action(function() {}); 59 | 60 | program.parse(makeArgv(['foo', '-t', '2982'])); 61 | should(program.getCommands()[0]._options[0].getChoices()).be.eql([]); 62 | program.reset(); 63 | }); 64 | }); 65 | 66 | describe('Setting up an option with an non-Array validator', () => { 67 | 68 | it(`should return empty array for validator.getChoices()`, () => { 69 | 70 | program 71 | .command('foo') 72 | .option('-t ', 'my option', program.INT) 73 | .action(function() {}); 74 | 75 | program.parse(makeArgv(['foo', '-t', '2982'])); 76 | should(program.getCommands()[0]._options[0]._validator.getChoices()).be.eql([]); 77 | program.reset(); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /examples/pizza/pizza.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | 4 | const prog = require('../..'); 5 | 6 | prog 7 | .version('1.0.0') 8 | // the "order" command 9 | .command('order', 'Order a pizza') 10 | .alias('give-it-to-me') 11 | // will be auto-magicaly autocompleted by providing the user with 3 choices 12 | .argument('', 'Kind of pizza', ["margherita", "hawaiian", "fredo"]) 13 | .argument('', 'Which store to order from') 14 | // enable auto-completion for argument using a sync function returning an array 15 | .complete(function() { 16 | return ['store-1', 'store-2', 'store-3', 'store-4', 'store-5']; 17 | }) 18 | 19 | .argument('', 'Which account id to use') 20 | // enable auto-completion for argument using a Promise 21 | .complete(function() { 22 | return Promise.resolve(['account-1', 'account-2']); 23 | }) 24 | 25 | .option('-n, --number ', 'Number of pizza', prog.INT, 1) 26 | .option('-d, --discount ', 'Discount offer', prog.FLOAT) 27 | .option('-p, --pay-by ', 'Pay by option') 28 | // enable auto-completion for -p | --pay-by argument using a Promise 29 | .complete(function() { 30 | return Promise.resolve(['cash', 'credit-card']); 31 | }) 32 | 33 | // --extra will be auto-magicaly autocompleted by providing the user with 3 choices 34 | .option('-e ', 'Add extra ingredients', ['pepperoni', 'onion', 'cheese']) 35 | .action(function(args, options, logger) { 36 | logger.info("Command 'order' called with:"); 37 | logger.info("arguments: %j", args); 38 | logger.info("options: %j", options); 39 | }) 40 | 41 | // the "return" command 42 | .command('return', 'Return an order') 43 | // will be auto-magicaly autocompleted by providing the user with 3 choices 44 | .argument('', 'Order id') 45 | // enable auto-completion for argument using a Promise 46 | .complete(function() { 47 | return Promise.resolve(['#82792', '#71727', '#526Z52']); 48 | }) 49 | .argument('', 'Store id') 50 | .option('--ask-change ', 'Ask for other kind of pizza') 51 | .complete(function() { 52 | return Promise.resolve(["margherita", "hawaiian", "fredo"]); 53 | }) 54 | .option('--say-something ', 'Say something to the manager') 55 | .action(function(args, options, logger) { 56 | return Promise.resolve("wooooo").then(function (ret) { 57 | logger.info("Command 'return' called with:"); 58 | logger.info("arguments: %j", args); 59 | logger.info("options: %j", options); 60 | logger.info("promise succeed with: %s", ret); 61 | }); 62 | }); 63 | 64 | prog.parse(process.argv); 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "caporal", 3 | "version": "0.3.0", 4 | "description": "A full-featured framework for building command line applications (cli) with node.js", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "./node_modules/.bin/eslint lib tests", 8 | "test": "./node_modules/mocha/bin/mocha --require ./tests/utils/bootstrap.js --full-trace --recursive -R mocha-unfunk-reporter tests/", 9 | "test-watch": "npm run lint && ./node_modules/mocha/bin/mocha --require ./tests/utils/bootstrap.js --full-trace --watch --recursive -R mocha-unfunk-reporter tests/", 10 | "coverage": "./node_modules/.bin/istanbul cover --include-all-sources true -x **/examples/**/* --print both ./node_modules/.bin/_mocha -- --require ./tests/utils/bootstrap.js -R spec --recursive tests/", 11 | "commit": "git-cz", 12 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -w -r 0", 13 | "precommit": "npm run lint && npm test", 14 | "prepush": "npm run lint && npm test", 15 | "commitmsg": "validate-commit-msg", 16 | "postinstall": "(test -f ./node_modules/husky/bin/install.js && node ./node_modules/husky/bin/install.js) || exit 0" 17 | }, 18 | "engines": { 19 | "node": ">= 4.4.5" 20 | }, 21 | "homepage": "https://github.com/mattallty/Caporal.js", 22 | "keywords": [ 23 | "cli", 24 | "argument-parser", 25 | "command", 26 | "commander", 27 | "clap", 28 | "cli-app", 29 | "minimist", 30 | "optimist", 31 | "cli-table", 32 | "command line apps" 33 | ], 34 | "author": "Matthias ETIENNE (https://github.com/mattallty)", 35 | "repository": "mattallty/Caporal.js", 36 | "license": "MIT", 37 | "devDependencies": { 38 | "codacy-coverage": "^2.0.1", 39 | "commitizen": "^2.9.5", 40 | "conventional-changelog-cli": "^1.2.0", 41 | "conventional-commits-detector": "^0.1.1", 42 | "conventional-github-releaser": "^1.1.3", 43 | "conventional-recommended-bump": "^0.3.0", 44 | "cz-conventional-changelog": "^1.2.0", 45 | "eslint": "^3.15.0", 46 | "husky": "^0.13.1", 47 | "istanbul": "^0.4.5", 48 | "json": "^9.0.4", 49 | "mocha": "^3.2.0", 50 | "mocha-unfunk-reporter": "^0.4.0", 51 | "should": "^11.2.0", 52 | "sinon": "^1.17.7", 53 | "trash": "^4.0.0", 54 | "validate-commit-msg": "^2.11.1" 55 | }, 56 | "dependencies": { 57 | "bluebird": "^3.4.7", 58 | "chalk": "^1.1.3", 59 | "cli-table2": "^0.2.0", 60 | "fast-levenshtein": "^2.0.6", 61 | "lodash.camelcase": "^4.3.0", 62 | "micromist": "^1.0.1", 63 | "prettyjson": "^1.2.1", 64 | "tabtab": "^2.2.2", 65 | "winston": "^2.3.1" 66 | }, 67 | "config": { 68 | "commitizen": { 69 | "path": "./node_modules/cz-conventional-changelog" 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/help.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* global Program, logger, should, makeArgv, sinon */ 4 | 5 | const program = new Program(); 6 | 7 | program 8 | .logger(logger) 9 | .version('1.0.0'); 10 | 11 | 12 | describe('Calling {program} help', function() { 13 | 14 | it(`should output global help for single command program`, function() { 15 | program 16 | .argument('', 'Required arg') 17 | .argument('[optional]', 'Optional arg') 18 | .option('-f, --foo', 'Foo option') 19 | .action(function() {}); 20 | 21 | const help = sinon.spy(program, "help"); 22 | program.parse(makeArgv('help')); 23 | should(help.callCount).be.eql(1); 24 | help.restore(); 25 | program.reset(); 26 | }); 27 | 28 | it(`should output global help for multiple commands program`, function() { 29 | program 30 | .description('my desc') 31 | .command('command1', '1st command') 32 | .argument('', 'Required arg') 33 | .argument('[optional]', 'Optional arg', null, 2) 34 | .option('-f, --foo', 'Foo option') 35 | .action(function() {}) 36 | .command('command2', '2nd command') 37 | .argument('', 'Required arg') 38 | .argument('[optional]', 'Optional arg') 39 | .option('-f, --foo', 'Foo option') 40 | .action(function() {}) 41 | 42 | 43 | const help = sinon.spy(program, "help"); 44 | program.parse(makeArgv('help')); 45 | should(help.callCount).be.eql(1); 46 | help.restore(); 47 | program.reset(); 48 | }); 49 | 50 | it(`should output command-specific help for multiple commands program`, function() { 51 | program 52 | .description('my desc') 53 | .command('command1', '1st command') 54 | .argument('', 'Required arg') 55 | .argument('[optional]', 'Optional arg', null, 2) 56 | .option('-f, --foo', 'Foo option') 57 | .option('-b, --bar', 'Bar option', null, 1, true) 58 | .action(function() {}) 59 | .command('command2', '2nd command') 60 | .argument('', 'Required arg') 61 | .argument('[optional]', 'Optional arg') 62 | .option('-f, --foo', 'Foo option') 63 | .option('-b, --bar', 'Bar option', null, 1, true) 64 | .action(function() {}) 65 | 66 | 67 | const help = sinon.spy(program, "help"); 68 | program.parse(makeArgv(['help', 'command1'])); 69 | should(help.callCount).be.eql(1); 70 | help.restore(); 71 | program.reset(); 72 | }); 73 | 74 | it(`should output command-specific help for single command program`, function() { 75 | program 76 | .description('my desc') 77 | .command('command1', '1st command') 78 | .argument('', 'Required arg') 79 | .argument('[optional]', 'Optional arg', null, 2) 80 | .option('-f, --foo', 'Foo option') 81 | .option('-b, --bar', 'Bar option', null, 1, true) 82 | .action(function() {}); 83 | 84 | const help = sinon.spy(program, "help"); 85 | program.parse(makeArgv(['help', 'command1'])); 86 | should(help.callCount).be.eql(1); 87 | help.restore(); 88 | program.reset(); 89 | }); 90 | 91 | }); 92 | -------------------------------------------------------------------------------- /tests/completion-command.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* global Program, logger, should, sinon */ 4 | 5 | const program = new Program(); 6 | 7 | program 8 | .logger(logger) 9 | .bin('myapp') 10 | .version('1.0.0') 11 | .command('foo', 'My foo'); 12 | 13 | 14 | const zshComp = `###-begin-myapp-completion-### 15 | if type compdef &>/dev/null; then 16 | _myapp_completion () { 17 | local reply 18 | local si=$IFS 19 | 20 | IFS=$'\\n' reply=($(COMP_CWORD="$((CURRENT-1))" COMP_LINE="$BUFFER" COMP_POINT="$CURSOR" myapp completion -- "\${words[@]}")) 21 | IFS=$si 22 | 23 | _describe 'values' reply 24 | } 25 | compdef _myapp_completion myapp 26 | fi 27 | ###-end-myapp-completion-### 28 | `; 29 | 30 | const bashComp = `###-begin-myapp-completion-### 31 | if type complete &>/dev/null; then 32 | _myapp_completion () { 33 | local words cword 34 | if type _get_comp_words_by_ref &>/dev/null; then 35 | _get_comp_words_by_ref -n = -n @ -w words -i cword 36 | else 37 | cword="$COMP_CWORD" 38 | words=("\${COMP_WORDS[@]}") 39 | fi 40 | 41 | local si="$IFS" 42 | IFS=$'\\n' COMPREPLY=($(COMP_CWORD="$cword" \\ 43 | COMP_LINE="$COMP_LINE" \\ 44 | COMP_POINT="$COMP_POINT" \\ 45 | myapp completion -- "\${words[@]}" \\ 46 | 2>/dev/null)) || return $? 47 | IFS="$si" 48 | } 49 | complete -o default -F _myapp_completion myapp 50 | fi 51 | ###-end-myapp-completion-### 52 | `; 53 | 54 | 55 | const fishComp = `###-begin-myapp-completion-### 56 | function _myapp_completion 57 | set cmd (commandline -opc) 58 | set cursor (commandline -C) 59 | set completions (eval env DEBUG=\\"" \\"" COMP_CWORD=\\""$cmd\\"" COMP_LINE=\\""$cmd \\"" COMP_POINT=\\""$cursor\\"" myapp completion -- $cmd) 60 | 61 | for completion in $completions 62 | echo -e $completion 63 | end 64 | end 65 | 66 | complete -f -d 'myapp' -c myapp -a "(eval _myapp_completion)" 67 | ###-end-myapp-completion-### 68 | `; 69 | 70 | 71 | describe('./myapp completion zsh|bash|fish', () => { 72 | 73 | beforeEach(function () { 74 | this.info = sinon.spy(logger, "info"); 75 | }); 76 | 77 | afterEach(function () { 78 | this.info.restore(); 79 | }); 80 | 81 | it(`should output shell script for zsh`, function() { 82 | program.parse(['node', 'myapp', 'completion', 'zsh']); 83 | should(this.info.called).be.ok(); 84 | should(this.info.args[0][0]).be.eql(zshComp); 85 | }); 86 | 87 | it(`should output shell script for bash`, function() { 88 | program.parse(['node', 'myapp', 'completion', 'bash']); 89 | should(this.info.called).be.ok(); 90 | //console.log(this.info.args[0][0]); 91 | should(this.info.args[0][0]).be.eql(bashComp); 92 | }); 93 | 94 | it(`should output shell script for fish`, function() { 95 | program.parse(['node', 'myapp', 'completion', 'fish']); 96 | should(this.info.called).be.ok(); 97 | should(this.info.args[0][0]).be.eql(fishComp); 98 | }); 99 | 100 | }); 101 | 102 | 103 | -------------------------------------------------------------------------------- /tests/actions.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* global Program, logger, should, makeArgv, sinon */ 4 | 5 | const Promise = require('bluebird'); 6 | 7 | describe('Setting up no action()', () => { 8 | 9 | it(`should throw NoActionError`, () => { 10 | 11 | const program = new Program(); 12 | 13 | program 14 | .logger(logger) 15 | .version('1.0.0') 16 | .command('foo', 'My foo'); 17 | 18 | const error = sinon.stub(program, "fatalError", function(err) { 19 | should(err.name).eql('NoActionError'); 20 | }); 21 | 22 | program.parse(makeArgv('foo')); 23 | 24 | const count = error.callCount; 25 | error.restore(); 26 | should(count).be.eql(1); 27 | program.reset(); 28 | }); 29 | 30 | }); 31 | 32 | 33 | describe('Setting up a sync action', () => { 34 | 35 | it(`should call this action`, () => { 36 | 37 | const program = new Program(); 38 | const action = sinon.spy(); 39 | 40 | program 41 | .logger(logger) 42 | .version('1.0.0') 43 | .command('foo', 'My foo') 44 | .action(action); 45 | 46 | program.parse(makeArgv('foo')); 47 | 48 | should(action.callCount).be.eql(1); 49 | 50 | program.reset(); 51 | }); 52 | 53 | }); 54 | 55 | 56 | describe('Setting up a async action', () => { 57 | 58 | it(`should succeed for a resolved promise`, () => { 59 | 60 | const program = new Program(); 61 | const action = function() { 62 | return Promise.resolve('foo') 63 | }; 64 | const stub = sinon.spy(action); 65 | 66 | program 67 | .logger(logger) 68 | .version('1.0.0') 69 | .command('foo', 'My foo') 70 | .action(stub); 71 | 72 | program.parse(makeArgv('foo')); 73 | 74 | should(stub.callCount).be.eql(1); 75 | program.reset(); 76 | 77 | }); 78 | 79 | it(`should fatalError() for a rejected promise (error string)`, (done) => { 80 | 81 | const program = new Program(); 82 | const action = function() { 83 | return Promise.reject('Failed!') 84 | }; 85 | const stub = sinon.spy(action); 86 | const fatalError = sinon.stub(program, "fatalError"); 87 | 88 | program 89 | .logger(logger) 90 | .version('1.0.0') 91 | .command('foo', 'My foo') 92 | .action(stub); 93 | 94 | program.parse(makeArgv('foo')); 95 | 96 | setImmediate(function () { 97 | should(stub.callCount).be.eql(1); 98 | should(fatalError.callCount).be.eql(1); 99 | done() 100 | }); 101 | 102 | }); 103 | it(`should fatalError() for a rejected promise (error object)`, (done) => { 104 | 105 | const program = new Program(); 106 | const action = function() { 107 | return Promise.reject(new Error('Failed!')) 108 | }; 109 | const stub = sinon.spy(action); 110 | const fatalError = sinon.stub(program, "fatalError"); 111 | 112 | program 113 | .logger(logger) 114 | .version('1.0.0') 115 | .command('foo', 'My foo') 116 | .action(stub); 117 | 118 | program.parse(makeArgv('foo')); 119 | 120 | setImmediate(function () { 121 | should(stub.callCount).be.eql(1); 122 | should(fatalError.callCount).be.eql(1); 123 | done() 124 | }); 125 | 126 | }); 127 | 128 | }); 129 | -------------------------------------------------------------------------------- /lib/option.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const GetterSetter = require('./utils').GetterSetter; 4 | const Validator = require('./validator'); 5 | const OptionSyntaxError = require('./error/option-syntax-error'); 6 | 7 | class Option extends GetterSetter { 8 | 9 | /** 10 | * 11 | * @param {String} synopsis - Option synopsis 12 | * @param {String} description - Option description 13 | * @param {String|RegExp|Function|Number} [validator] - Option validator, used for checking or casting 14 | * @param {*} [defaultValue] - Default value 15 | * @param {Boolean} [required] - Is the option itself required 16 | * @param {Program} [program] - Program instance 17 | */ 18 | constructor(synopsis, description, validator, defaultValue, required, program) { 19 | super(); 20 | this._synopsis = synopsis; 21 | this._description = description; 22 | this._program = program; 23 | this._validator = validator ? new Validator(validator, program) : null; 24 | this._default = defaultValue; 25 | this.synopsis = this.makeGetterSetter('synopsis'); 26 | this.description = this.makeGetterSetter('description'); 27 | this.default = this.makeGetterSetter('default'); 28 | const analysis = this._analyseSynopsis(); 29 | this._valueType = analysis.valueType; 30 | this._required = required || false; 31 | this._varadic = analysis.variadic; 32 | this._longCleanName = analysis.longCleanName; 33 | this._shortCleanName = analysis.shortCleanName; 34 | this._short = analysis.short; 35 | this._long = analysis.long; 36 | this._booleanFlag = analysis.booleanFlag; 37 | this._name = this.getCleanNameFromNotation(this._longCleanName || this._shortCleanName); 38 | } 39 | 40 | hasDefault() { 41 | return typeof this._default !== 'undefined' && this._default !== null; 42 | } 43 | 44 | getChoices() { 45 | return this._validator ? this._validator.getChoices() : []; 46 | } 47 | 48 | isRequired() { 49 | return this._required; 50 | } 51 | 52 | getLongOrShortCleanName() { 53 | return this.getLongCleanName() || this.getShortCleanName(); 54 | } 55 | 56 | getLongOrShortName() { 57 | return this._long || this._short; 58 | } 59 | 60 | getShortName() { 61 | return this._short; 62 | } 63 | 64 | getLongName() { 65 | return this._long; 66 | } 67 | 68 | 69 | getLongCleanName() { 70 | return this._longCleanName; 71 | } 72 | 73 | getShortCleanName() { 74 | return this._shortCleanName; 75 | } 76 | 77 | name() { 78 | return this._name; 79 | } 80 | 81 | _analyseOption(type, value, acc) { 82 | acc.valueType = type; 83 | acc.variadic = value.substr(-4, 3) === '...'; 84 | } 85 | 86 | /** 87 | * 88 | * @returns {*} 89 | * @private 90 | */ 91 | _analyseSynopsis() { 92 | 93 | const infos = this._synopsis.split(/[\s\t,]+/).reduce((acc, value) => { 94 | if (value.substring(0, 2) === '--') { 95 | acc.long = value; 96 | acc.longCleanName = value.substring(2); 97 | } else if (value.substring(0, 1) === '-') { 98 | acc.short = value; 99 | acc.shortCleanName = value.substring(1); 100 | } else if (value.substring(0, 1) === '[') { 101 | this._analyseOption(Option.OPTIONAL_VALUE, value, acc); 102 | } else if (value.substring(0, 1) === '<') { 103 | this._analyseOption(Option.REQUIRED_VALUE, value, acc); 104 | } else { 105 | this._program.fatalError(new OptionSyntaxError(this._synopsis, this._program)); 106 | } 107 | return acc; 108 | }, {}); 109 | 110 | return infos; 111 | } 112 | 113 | _validate(value) { 114 | if (!this._validator) { 115 | return value; 116 | } 117 | return this._validator.validate(value); 118 | } 119 | } 120 | 121 | Object.defineProperties(Option, { 122 | "OPTIONAL_VALUE": { 123 | value: 'optional' 124 | }, 125 | "REQUIRED_VALUE": { 126 | value: 'required' 127 | } 128 | }); 129 | 130 | module.exports = Option; 131 | -------------------------------------------------------------------------------- /lib/help.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Table = require('cli-table2'); 4 | const chalk = require('chalk'); 5 | const colorize = require('./colorful').colorize; 6 | 7 | class Help { 8 | 9 | constructor(program) { 10 | this._program = program; 11 | } 12 | 13 | display(command) { 14 | 15 | if (!command && this._program._commands.length === 1) { 16 | command = this._program._commands[0]; 17 | } 18 | 19 | const description = this._program.description() ? '- ' + this._program.description() : ''; 20 | let help = ` 21 | ${chalk.cyan(this._program.name() || this._program.bin())} ${chalk.dim(this._program.version())} ${description} 22 | 23 | ${this._getUsage(command)} `; 24 | 25 | if (!command && this._program._commands.length > 1) { 26 | help += "\n\n " + this._getCommands(); 27 | } 28 | 29 | help += "\n\n " + this._getGlobalOptions(); 30 | 31 | return this._program.logger().info(help + "\n"); 32 | } 33 | 34 | _getCommands() { 35 | const commandTable = this._getSimpleTable(); 36 | this._program._commands.forEach(cmd => { 37 | commandTable.push( 38 | [chalk.magenta(cmd.getSynopsis()), cmd.description()] 39 | ); 40 | }); 41 | commandTable.push([chalk.magenta('help '), 'Display help for a specific command']); 42 | return chalk.bold('COMMANDS') + "\n\n" + colorize(commandTable.toString()); 43 | } 44 | 45 | _getGlobalOptions() { 46 | const optionsTable = this._getSimpleTable(); 47 | this._getPredefinedOptions().forEach(o => optionsTable.push(o)); 48 | return chalk.bold('GLOBAL OPTIONS') + "\n\n" + colorize(optionsTable.toString()); 49 | } 50 | 51 | _getCommandHelp(cmd) { 52 | const args = cmd.args(); 53 | const options = cmd.options(); 54 | let help = (cmd.name() ? cmd.name() + ' ' : '') + args.map(a => a.synopsis()).join(' '); 55 | 56 | if (args.length) { 57 | help += `\n\n ${chalk.bold('ARGUMENTS')}\n\n`; 58 | const argsTable = this._getSimpleTable(); 59 | args.forEach(a => { 60 | const def = a.hasDefault() ? 'default: ' + JSON.stringify(a.default()) : ''; 61 | const req = a.isRequired() ? chalk.bold('required') : chalk.grey('optional'); 62 | argsTable.push([a.synopsis(), a.description(), req, chalk.grey(def)]) 63 | }); 64 | help += argsTable.toString(); 65 | } 66 | 67 | if (options.length) { 68 | help += `\n\n ${chalk.bold('OPTIONS')}\n\n`; 69 | const optionsTable = this._getSimpleTable(); 70 | options.forEach(a => { 71 | const def = a.hasDefault() ? 'default: ' + JSON.stringify(a.default()) : ''; 72 | const req = a.isRequired() ? chalk.bold('required') : chalk.grey('optional'); 73 | optionsTable.push([a.synopsis(), a.description(), req, chalk.grey(def)]) 74 | }); 75 | help += optionsTable.toString(); 76 | } 77 | 78 | return help; 79 | } 80 | 81 | _getUsage(cmd) { 82 | let help = `${chalk.bold('USAGE')}\n\n ${chalk.italic(this._program.bin())} `; 83 | if (cmd) { 84 | help += colorize(this._getCommandHelp(cmd)); 85 | } else { 86 | help += colorize(' [options]'); 87 | } 88 | return help; 89 | } 90 | 91 | 92 | 93 | _getSimpleTable() { 94 | return new Table({ 95 | chars: { 'top': '' , 'top-mid': '' , 'top-left': '' , 'top-right': '' 96 | , 'bottom': '' , 'bottom-mid': '' , 'bottom-left': '' , 'bottom-right': '' 97 | , 'left': '' , 'left-mid': '' , 'mid': '' , 'mid-mid': '' 98 | , 'right': '' , 'right-mid': '' , 'middle': ' ' }, 99 | style: { 'padding-left': 5, 'padding-right': 0 } 100 | }); 101 | } 102 | 103 | _getPredefinedOptions() { 104 | return [['-h, --help', 'Display help'], 105 | ['-V, --version', 'Display version'], 106 | ['--no-color', 'Disable colors'], 107 | ['--quiet', 'Quiet mode - only displays warn and error messages'], 108 | ['-v, --verbose', 'Verbose mode - will also output debug messages']]; 109 | } 110 | 111 | } 112 | 113 | 114 | module.exports = Help; 115 | -------------------------------------------------------------------------------- /lib/validator.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const constants = require('./constants'); 4 | const ValidationError = require('./error/validation-error'); 5 | 6 | class Validator { 7 | 8 | /** 9 | * 10 | * @param {RegExp|Function|Number|Array} validator 11 | * @param {Program} programr 12 | */ 13 | constructor(validator, program) { 14 | this._validator = validator; 15 | this._program = program; 16 | 17 | if (typeof this._validator === 'number') { 18 | this._checkFlagValidator(); 19 | } else { 20 | this._checkOtherValidator(); 21 | } 22 | } 23 | 24 | /** 25 | * 26 | * @private 27 | */ 28 | _checkOtherValidator() { 29 | if (typeof this._validator !== 'function' && !(this._validator instanceof RegExp) && !Array.isArray(this._validator)) { 30 | const err = new ValidationError( 31 | "Caporal setup error - Invalid validator setup.", 32 | {validator: this._validator}, 33 | this._program 34 | ); 35 | this._program.fatalError(err); 36 | } 37 | } 38 | 39 | /** 40 | * 41 | * @private 42 | */ 43 | _checkFlagValidator() { 44 | const isValidatorInvalid = Object.keys(constants).every(v => { 45 | return ((constants[v] & this._validator) === 0) 46 | }); 47 | if (isValidatorInvalid) { 48 | const err = new ValidationError( 49 | "Caporal setup error - Invalid flag validator setup.", 50 | {validator: this._validator}, 51 | this._program 52 | ); 53 | this._program.fatalError(err); 54 | } 55 | } 56 | 57 | isArrayValidator() { 58 | return Array.isArray(this._validator); 59 | } 60 | 61 | getChoices() { 62 | return this.isArrayValidator() ? this._validator : []; 63 | } 64 | 65 | /** 66 | * 67 | * @param value 68 | * @returns {*} 69 | */ 70 | validate(value) { 71 | 72 | if (typeof this._validator === 'function') { 73 | return this._validateWithFunction(value); 74 | } 75 | else if (this._validator instanceof RegExp) { 76 | return this._validateWithRegExp(value); 77 | } 78 | else if (Array.isArray(this._validator)) { 79 | return this._validateWithArray(value); 80 | } 81 | // Caporal flag validator 82 | else if(typeof this._validator === 'number') { 83 | return this._validateWithFlags(value); 84 | } 85 | } 86 | 87 | /** 88 | * 89 | * @returns {*} 90 | * @private 91 | */ 92 | _validateWithFlags(value, unary) { 93 | if (!unary && this._validator & constants.ARRAY) { 94 | 95 | if (typeof value === 'string') { 96 | value = value.split(','); 97 | } 98 | 99 | return value.map(function (el) { 100 | return this._validateWithFlags(el, true); 101 | }, this); 102 | } 103 | 104 | if (this._validator & constants.INT) { 105 | if (!Validator.isNumber(value)) { 106 | throw new ValidationError("Type (INT) validation failed", {value}, this._program); 107 | } 108 | return parseInt(value); 109 | } 110 | else if (this._validator & constants.FLOAT) { 111 | if (!Validator.isNumber(value)) { 112 | throw new ValidationError("Type (FLOAT) validation failed", {value}, this._program); 113 | } 114 | return parseFloat(value); 115 | } 116 | else if (this._validator & constants.BOOL) { 117 | if (typeof value === 'boolean') { 118 | return value; 119 | } else if (/^(true|false|yes|no|0|1)$/i.test(value) === false) { 120 | throw new ValidationError("Type (BOOL) validation failed", {value}, this._program); 121 | } else { 122 | return !(value === '0' || value === 'no' || value === 'false'); 123 | } 124 | } 125 | } 126 | 127 | /** 128 | * 129 | * @returns {*} 130 | * @private 131 | */ 132 | _validateWithFunction(value) { 133 | try { 134 | return this._validator(value); 135 | } catch(e) { 136 | throw new ValidationError( 137 | "Function validation failed", 138 | {validator: this._validator, value, originalError: e}, 139 | this._program 140 | ); 141 | } 142 | } 143 | 144 | /** 145 | * 146 | * @returns {*} 147 | * @private 148 | */ 149 | _validateWithArray(value) { 150 | if (this._validator.map(String).indexOf(value) === -1) { 151 | throw new ValidationError( 152 | "Array validation failed", 153 | {validator: this._validator, value}, 154 | this._program 155 | ); 156 | } 157 | return value; 158 | } 159 | 160 | /** 161 | * 162 | * @returns {*} 163 | * @private 164 | */ 165 | _validateWithRegExp(value) { 166 | if (!this._validator.test(value)) { 167 | throw new ValidationError( 168 | "RegExp validation failed", 169 | {validator: this._validator, value}, 170 | this._program 171 | ); 172 | } 173 | return value; 174 | } 175 | 176 | static isNumber(obj) { 177 | return !isNaN(parseFloat(obj)); 178 | } 179 | } 180 | 181 | module.exports = Validator; 182 | -------------------------------------------------------------------------------- /lib/autocomplete.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const tabtab = require('tabtab'); 4 | const parseArgs = require('micromist'); 5 | const Promise = require('bluebird'); 6 | 7 | class Autocomplete { 8 | 9 | constructor(program) { 10 | this._program = program; 11 | this._tab = tabtab({ 12 | name: this._program.bin(), 13 | cache: false 14 | }); 15 | this._completions = {}; 16 | } 17 | 18 | listen(options) { 19 | this._tab.on(this._program.bin(), this._complete.bind(this)); 20 | this._tab.handle(options); 21 | } 22 | 23 | /** 24 | * Register a completion handler 25 | * 26 | * @param {Argument|Option} arg_or_opt argument or option to complete 27 | * @param {Function} completer 28 | */ 29 | registerCompletion(arg_or_opt, completer) { 30 | this._completions[arg_or_opt.name()] = completer; 31 | } 32 | 33 | /** 34 | * @param {Argument|Option} arg_or_opt argument or option to complete 35 | */ 36 | getCompletion(arg_or_opt) { 37 | return this._completions[arg_or_opt.name()]; 38 | } 39 | 40 | _findCommand(cmdStr) { 41 | 42 | let cmd; 43 | const commandArr = []; 44 | const args = cmdStr.split(' ').filter(s => s!= ''); 45 | const commands = this._program.getCommands(); 46 | 47 | while(!cmd && args.length) { 48 | commandArr.push(args.shift()); 49 | const cmdStr = commandArr.join(' '); 50 | cmd = commands.filter(c => { 51 | return (c.name() === cmdStr || c.getAlias() === cmdStr) 52 | })[0]; 53 | } 54 | 55 | if (!cmd && this._program._getDefaultCommand()) { 56 | cmd = this._program._getDefaultCommand(); 57 | } 58 | return cmd; 59 | } 60 | 61 | _countArgs(currentCommand, args) { 62 | let realArgsCmdFound = false; 63 | return args._.reduce((acc, value, index, arr) => { 64 | const possibleCmd = arr.slice(0, index + 1).join(' '); 65 | if (currentCommand && (currentCommand.name() === possibleCmd || currentCommand.alias() === possibleCmd)) { 66 | realArgsCmdFound = true; 67 | } else if (realArgsCmdFound) { 68 | acc++; 69 | } 70 | return acc; 71 | }, 0); 72 | } 73 | 74 | _getOptionsAlreadyUsed(args) { 75 | return args.filter(o => o.startsWith('-') || o.startsWith('--')); 76 | } 77 | 78 | _lastPartialIsKnownOption(currentCommand, lastPartialIsOption, lastPartial) { 79 | return lastPartialIsOption && 80 | currentCommand && 81 | currentCommand.options().some(o => (lastPartial === o.getShortName() || lastPartial === o.getLongName())); 82 | } 83 | 84 | _getCurrentOption(currentCommand, lastPartialIsKnownOption, lastPartial) { 85 | return lastPartialIsKnownOption ? 86 | currentCommand.options() 87 | .filter(o => (lastPartial === o.getShortName() || lastPartial === o.getLongName()))[0] 88 | : null; 89 | } 90 | 91 | _getPossibleArgumentValues(currentCommand, lastPartialIsOption, argsCount) { 92 | if (!currentCommand || 93 | lastPartialIsOption || 94 | !currentCommand.args(argsCount)) { 95 | return Promise.resolve([]); 96 | } 97 | 98 | const arg = currentCommand.args(argsCount); 99 | 100 | // Choices 101 | if (arg.getChoices().length) { 102 | return Promise.resolve(arg.getChoices().map(choice => choice + ':Value for argument ' + arg.synopsis())); 103 | } 104 | 105 | // Promise completion 106 | const completion = this.getCompletion(arg); 107 | 108 | if (typeof completion === 'function') { 109 | return this._hanldleCompletionHandler(completion); 110 | } 111 | 112 | return Promise.resolve([]); 113 | } 114 | 115 | _hanldleCompletionHandler(handler) { 116 | const result = handler(); 117 | return Array.isArray(result) ? Promise.resolve(result) : result; 118 | } 119 | 120 | _getPossibleOptionNames(currentCommand, optionsAlreadyUsed, lastPartial, lastPartialIsOption) { 121 | const optNames = currentCommand ? 122 | currentCommand.options().map(o => { 123 | if ((o.getShortName() && o.getShortName() != lastPartial && o.getShortName().startsWith(lastPartial)) || 124 | (o.getLongName() && o.getLongName() != lastPartial && o.getLongName().startsWith(lastPartial)) ) { 125 | return o.getLongOrShortName() + ':' + o.description(); 126 | } else if(!lastPartialIsOption && optionsAlreadyUsed.indexOf(o.getShortName()) === -1 && 127 | optionsAlreadyUsed.indexOf(o.getLongName()) === -1) { 128 | return o.getLongOrShortName() + ':' + o.description(); 129 | } 130 | }).filter(o => typeof o != 'undefined') : []; 131 | 132 | return Promise.resolve(optNames); 133 | } 134 | 135 | _getPossibleOptionValues(currentOption) { 136 | if (!currentOption) { 137 | return Promise.resolve([]); 138 | } 139 | // Choices 140 | if (currentOption.getChoices().length) { 141 | return Promise.resolve(currentOption.getChoices()); 142 | } 143 | 144 | // Promise completion 145 | const completion = this.getCompletion(currentOption); 146 | if (typeof completion === 'function') { 147 | return this._hanldleCompletionHandler(completion); 148 | } 149 | 150 | return Promise.resolve([]); 151 | } 152 | 153 | _getPossibleCommands(currentCommand, cmdStr) { 154 | const commands = this._program 155 | .getCommands() 156 | .filter(c => { 157 | if (!cmdStr) { 158 | return true; 159 | } 160 | return (c.name().startsWith(cmdStr) || 161 | (c.getAlias() && c.getAlias().startsWith(cmdStr))) && 162 | (!currentCommand || !this._isSameCommand(c, currentCommand)) 163 | }) 164 | .filter(c => c.name() !== '') // do not take the default command 165 | .map(c => { 166 | return c.name() + ':' + c.description() 167 | }); 168 | 169 | return Promise.resolve(commands); 170 | } 171 | 172 | _isSameCommand(command, command2) { 173 | return command2.name() === command.name(); 174 | } 175 | 176 | _complete(data, done) { 177 | const args = parseArgs(data.args.slice(1)); 178 | const cmd = args._.join(' '); 179 | const currCommand = this._findCommand(cmd); 180 | 181 | const realArgsCount = this._countArgs(currCommand, args); 182 | const optionsAlreadyUsed = this._getOptionsAlreadyUsed(data.args); 183 | const lastPartial = data.lastPartial; 184 | 185 | const lastPartIsOption = data.lastPartial.startsWith('-'); 186 | const lastPartIsKnownOption = this._lastPartialIsKnownOption(currCommand, lastPartIsOption, lastPartial); 187 | const currOption = this._getCurrentOption(currCommand, lastPartIsKnownOption, lastPartial); 188 | 189 | const possArgValues = this._getPossibleArgumentValues(currCommand, lastPartIsOption, realArgsCount); 190 | const possOptNames = this._getPossibleOptionNames(currCommand, optionsAlreadyUsed, lastPartial, lastPartIsOption); 191 | const possOptValues = this._getPossibleOptionValues(currOption); 192 | const possCommands = this._getPossibleCommands(currCommand, cmd); 193 | 194 | return Promise.all([possCommands, possArgValues, possOptNames, possOptValues]) 195 | .then(function (results) { 196 | const completions = [] 197 | .concat.apply([], results) 198 | .filter(e => typeof e != 'undefined'); 199 | done(null, completions); 200 | return completions; 201 | }) 202 | .catch(err => { 203 | done(err); 204 | return []; 205 | }) 206 | } 207 | 208 | } 209 | 210 | module.exports = Autocomplete; 211 | -------------------------------------------------------------------------------- /tests/arg-validation.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* global Program, logger, should, makeArgv, sinon */ 4 | 5 | const InvalidArgumentValueError = require('../lib/error/invalid-argument-value'); 6 | const WrongNumberOfArgumentError = require('../lib/error/wrong-num-of-arg'); 7 | const program = new Program(); 8 | 9 | program 10 | .logger(logger) 11 | .version('1.0.0') 12 | .reset() 13 | .command('foo', 'Fooooo') 14 | .argument('', 'My bar', /^[a-z]+$/) 15 | .action(function(){}); 16 | 17 | describe("Argument validation", function() { 18 | 19 | beforeEach(function () { 20 | this.fatalError = sinon.stub(program, "fatalError"); 21 | this.action = sinon.spy(); 22 | }); 23 | 24 | afterEach(function () { 25 | this.fatalError.restore(); 26 | program.reset(); 27 | }); 28 | 29 | it(`should throw InvalidArgumentValueError for an invalid required argument value (Regex validator)`, function() { 30 | program.parse(makeArgv(['foo', '827E92'])); 31 | should(this.fatalError.callCount).eql(1); 32 | should(this.fatalError.calledWith(sinon.match.instanceOf(InvalidArgumentValueError))).be.ok(); 33 | }); 34 | 35 | it(`should throw InvalidArgumentValueError for an invalid optional argument value (Regex validator)`, function() { 36 | program 37 | .command('foo', 'Fooooo') 38 | .argument('[foo]', 'My bar', /^[a-z]+$/) 39 | .action(this.action); 40 | 41 | program.parse(makeArgv(['foo', '827E92'])); 42 | should(this.fatalError.callCount).eql(1); 43 | should(this.fatalError.calledWith(sinon.match.instanceOf(InvalidArgumentValueError))).be.ok(); 44 | }); 45 | 46 | it(`should throw InvalidArgumentValueError for an invalid optional argument value (Array validator)`, function() { 47 | program 48 | .command('foo', 'Fooooo') 49 | .argument('[foo]', 'My bar', ["bim", "bam", "boom"]) 50 | .action(this.action); 51 | 52 | program.parse(makeArgv(['foo', '827E92'])); 53 | should(this.fatalError.callCount).eql(1); 54 | should(this.fatalError.calledWith(sinon.match.instanceOf(InvalidArgumentValueError))).be.ok(); 55 | }); 56 | 57 | 58 | it(`should throw InvalidArgumentValueError for an invalid required argument value (Array validator)`, function() { 59 | program 60 | .command('foo', 'Fooooo') 61 | .argument('', 'My bar', ["bim", "bam", "boom"]) 62 | .action(this.action); 63 | 64 | program.parse(makeArgv(['foo', '827E92'])); 65 | should(this.fatalError.callCount).eql(1); 66 | should(this.fatalError.calledWith(sinon.match.instanceOf(InvalidArgumentValueError))).be.ok(); 67 | }); 68 | 69 | it(`should not throw InvalidArgumentValueError for an valid required argument value (Array validator)`, function() { 70 | program 71 | .command('foo', 'Fooooo') 72 | .argument('', 'My bar', ["bim", "bam", "boom"]) 73 | .action(this.action); 74 | 75 | program.parse(makeArgv(['foo', 'bam'])); 76 | should(this.fatalError.callCount).eql(0); 77 | }); 78 | 79 | it(`should take default value if not passed when setting up a default argument value`, function() { 80 | program 81 | .command('foo', 'Fooooo') 82 | .argument('[foo]', 'My bar', /^[a-z]+$/, 'bar') 83 | .action(this.action); 84 | 85 | program.parse(makeArgv(['foo'])); 86 | should(this.action.callCount).eql(1); 87 | should(this.action.calledWith({foo:"bar"})); 88 | should(this.fatalError.callCount).eql(0); 89 | }); 90 | 91 | it(`should throw WrongNumberOfArgumentError when passing an unknown argument for a command that does not accept arguments`, function() { 92 | program 93 | .command('foo', 'Fooooo') 94 | .action(this.action); 95 | 96 | program.parse(makeArgv(['foo', '827E92'])); 97 | should(this.fatalError.callCount).eql(1); 98 | should(this.fatalError.calledWith(sinon.match.instanceOf(WrongNumberOfArgumentError))).be.ok(); 99 | }); 100 | 101 | it(`should throw WrongNumberOfArgumentError for a known command when forgetting an argument`, function() { 102 | 103 | program 104 | .command('foo', 'Fooooo') 105 | .argument('', 'max') 106 | .argument('', 'jiji') 107 | .action(this.action); 108 | 109 | program.parse(makeArgv(['foo'])); 110 | should(this.fatalError.callCount).eql(1); 111 | should(this.fatalError.calledWith(sinon.match.instanceOf(WrongNumberOfArgumentError))).be.ok(); 112 | }); 113 | 114 | it(`should throw WrongNumberOfArgumentError for a default command when forgetting an argument`, function() { 115 | program 116 | .argument('', 'max') 117 | .argument('', 'jiji') 118 | .action(this.action); 119 | 120 | program.parse(makeArgv(['foo'])); 121 | should(this.fatalError.callCount).eql(1); 122 | should(this.fatalError.calledWith(sinon.match.instanceOf(WrongNumberOfArgumentError))).be.ok(); 123 | }); 124 | 125 | it(`should not throw any error when passing an argument without validator`, function() { 126 | program 127 | .command('foo', 'Fooooo') 128 | .argument('', 'My foo') 129 | .action(this.action); 130 | 131 | program.parse(makeArgv(['foo', '827E-Z92'])); 132 | should(this.fatalError.callCount).eql(0); 133 | }); 134 | 135 | it(`should return an array for variadic arguments without validator`, function() { 136 | program 137 | .command('foo', 'Fooooo') 138 | .argument('[foo]', 'My bar', /^[a-z]+$/, 'bar') 139 | .argument('[other-foo...]', 'Other foo') 140 | .action(this.action); 141 | 142 | program.parse(makeArgv(['foo', 'bar', 'im', 'a', 'variadic', 'arg'])); 143 | should(this.fatalError.callCount).eql(0); 144 | should(this.action.calledWith({foo: "bar", otherFoo: ['im', 'a', 'variadic', 'arg']})) 145 | }); 146 | 147 | it(`should handled optional arguments with no default and no validator`, function() { 148 | program 149 | .command('foo', 'Fooooo') 150 | .argument('[foo]', 'My bar', /^[a-z]+$/) 151 | .action(this.action); 152 | 153 | program.parse(makeArgv(['foo'])); 154 | should(this.action.callCount).eql(1); 155 | }); 156 | 157 | it(`should hanldle negative numbers in quoted arguments`, function() { 158 | program 159 | .command('order', 'Order something') 160 | .argument('', 'What to order', ["pizza", "burger", "smoothie"]) 161 | .argument('', 'How much', program.INT) 162 | .action(this.action); 163 | 164 | program.parse(makeArgv(['order', "pizza", '-1'])); 165 | should(this.fatalError.callCount).eql(0); 166 | should(this.action.callCount).eql(1); 167 | should(this.action.args[0][0]).eql({ what: 'pizza', howMuch: -1 }); 168 | }); 169 | 170 | it(`should throw WrongNumberOfArgumentError when no argument is given to completion`, function() { 171 | program.parse(makeArgv(['completion'])); 172 | should(this.fatalError.callCount).eql(1); 173 | should(this.fatalError.calledWith(sinon.match.instanceOf(WrongNumberOfArgumentError))).be.ok(); 174 | }); 175 | 176 | it(`should throw WrongNumberOfArgumentError when wrong argument is given to completion`, function() { 177 | program.parse(makeArgv(['completion', 'nosh'])); 178 | should(this.fatalError.callCount).eql(1); 179 | should(this.fatalError.calledWith(sinon.match.instanceOf(WrongNumberOfArgumentError))).be.ok(); 180 | }); 181 | 182 | it(`should not throw any error when passing an handled argument to completion`, function() { 183 | program.parse(makeArgv(['completion', 'zsh'])); 184 | should(this.fatalError.callCount).eql(0); 185 | }); 186 | 187 | }); 188 | -------------------------------------------------------------------------------- /lib/program.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const GetterSetter = require('./utils').GetterSetter; 4 | const Command = require('./command'); 5 | const parseArgs = require('micromist'); 6 | const Help = require('./help'); 7 | const path = require('path'); 8 | const Autocomplete = require('./autocomplete'); 9 | const createLogger = require('./logger').createLogger; 10 | const tabtabComplete = require('tabtab/src/complete'); 11 | const WrongNumberOfArgumentError = require('./error/wrong-num-of-arg'); 12 | 13 | class Program extends GetterSetter { 14 | 15 | constructor() { 16 | super(); 17 | this._commands = []; 18 | this._help = new Help(this); 19 | this.version = this.makeGetterSetter('version'); 20 | this.name = this.makeGetterSetter('name'); 21 | this.description = this.makeGetterSetter('description'); 22 | this.logger = this.makeGetterSetter('logger'); 23 | this.bin = this.makeGetterSetter('bin'); 24 | this._bin = path.basename(process.argv[1]); 25 | this._autocomplete = new Autocomplete(this); 26 | this._supportedShell = ['bash', 'zsh', 'fish']; 27 | this.logger(createLogger()); 28 | this._defaultCommand = null; 29 | } 30 | 31 | /** 32 | * @private 33 | */ 34 | help(cmdStr) { 35 | const cmd = this._commands.filter(c => (c.name() === cmdStr || c.getAlias() === cmdStr))[0]; 36 | this._help.display(cmd); 37 | } 38 | 39 | /** 40 | * @private 41 | */ 42 | getCommands() { 43 | return this._commands; 44 | } 45 | 46 | /** 47 | * Reset all commands 48 | * 49 | * @private 50 | * @returns {Program} 51 | */ 52 | reset() { 53 | this._commands = []; 54 | this._defaultCommand = null; 55 | return this; 56 | } 57 | 58 | /** 59 | * 60 | * @param synospis 61 | * @param description 62 | * @returns {Command} 63 | */ 64 | command(synospis, description) { 65 | const cmd = new Command(synospis, description, this); 66 | this._commands.push(cmd); 67 | return cmd; 68 | } 69 | 70 | /** 71 | * @param {Error} errObj - Error object 72 | * @private 73 | */ 74 | fatalError(errObj) { 75 | this.logger().error("\n" + errObj.message); 76 | process.exit(2); 77 | } 78 | 79 | /** 80 | * Find the command to run, based on args, performs checks, then run it 81 | * 82 | * @param {Array} args - Command arguments 83 | * @param {Object} options - Options passed 84 | * @returns {*} 85 | * @private 86 | */ 87 | _run(args, options) { 88 | 89 | if (args[0] === 'help') { 90 | return this.help(args.slice(1).join(' ')); 91 | } 92 | 93 | let argsCopy = args.slice(); 94 | const commandArr = []; 95 | /** 96 | * @type {Command} 97 | */ 98 | let cmd; 99 | 100 | while(!cmd && argsCopy.length) { 101 | commandArr.push(argsCopy.shift()); 102 | const cmdStr = commandArr.join(' '); 103 | cmd = this._commands.filter(c => (c.name() === cmdStr || c.getAlias() === cmdStr))[0]; 104 | } 105 | 106 | if (options.V || options.version) { 107 | this.logger().info(this.version()); 108 | return process.exit(0); 109 | } 110 | 111 | if (!cmd && this._getDefaultCommand()) { 112 | cmd = this._getDefaultCommand(); 113 | argsCopy = args.slice(); 114 | } 115 | 116 | if (!cmd || options.help || options.h) { 117 | this.help(); 118 | return process.exit(0); 119 | } 120 | 121 | // If quiet, only output warning & errors 122 | if (options.quiet || options.silent) { 123 | this._changeLogLevel('warn'); 124 | // verbose mode 125 | } else if (options.v || options.verbose) { 126 | this._changeLogLevel('debug'); 127 | } 128 | 129 | let validated; 130 | 131 | try { 132 | validated = cmd._validateCall(argsCopy, options); 133 | } catch(err) { 134 | return this.fatalError(err); 135 | } 136 | 137 | return cmd._run(validated.args, validated.options); 138 | } 139 | 140 | /** 141 | * 142 | * @private 143 | */ 144 | _changeLogLevel(level) { 145 | const logger = this.logger(); 146 | Object.keys(logger.transports).forEach(t => logger.transports[t].level = level) 147 | } 148 | 149 | /** 150 | * Sets a unique action for the program 151 | * 152 | * @param {Function} action - Action to run 153 | */ 154 | action(action) { 155 | this._getDefaultCommand(true).action(action); 156 | return this; 157 | } 158 | 159 | /** 160 | * Set an option on the default command 161 | * 162 | * @param {String} synopsis - Option synopsis like '-f, --force', or '-f, --file ' 163 | * @param {String} description - Option description 164 | * @param {String|RegExp|Function|Number} [validator] - Option validator, used for checking or casting 165 | * @param {*} [defaultValue] - Default value 166 | * @param {Boolean} [required] - Is the option itself required 167 | * @returns {Program} 168 | */ 169 | option(synopsis, description, validator, defaultValue, required) { 170 | const cmd = this._getDefaultCommand(true); 171 | let args = Array.prototype.slice.call(arguments); 172 | cmd.option.apply(cmd, args); 173 | return this; 174 | } 175 | 176 | /** 177 | * Add an argument to the default command 178 | * 179 | * @param {String} synopsis - Argument synopsis like `` or `[my-argument]`. 180 | * Angled brackets (e.g. ``) indicate required input. Square brackets (e.g. `[env]`) indicate optional input. 181 | * @param {String} description - Option description 182 | * @param {String|RegExp|Function|Number} [validator] - Option validator, used for checking or casting 183 | * @param {*} [defaultValue] - Default value 184 | * @public 185 | * @returns {Command} 186 | */ 187 | argument(synopsis, description, validator, defaultValue) { 188 | const cmd = this._getDefaultCommand(true); 189 | let args = Array.prototype.slice.call(arguments); 190 | cmd.argument.apply(cmd, args); 191 | return cmd; 192 | } 193 | 194 | /** 195 | * 196 | * @returns {Command} 197 | * @private 198 | */ 199 | _getDefaultCommand(create) { 200 | if (!this._defaultCommand && create) { 201 | this._defaultCommand = this.command('_default', 'Default command'); 202 | } 203 | return this._defaultCommand; 204 | } 205 | 206 | /** 207 | * @private 208 | * @param args 209 | * @param argv 210 | * @returns {*} 211 | */ 212 | _handleCompletionCommand(args, argv) { 213 | if(argv[1] === '--') { 214 | return this._autocomplete.listen({"_" : args._}); 215 | } 216 | const complete = new tabtabComplete({name: this.bin()}); 217 | 218 | if (typeof argv[1] !== "string" || this._supportedShell.indexOf(argv[1]) === -1) { 219 | this.fatalError(new WrongNumberOfArgumentError(`A valid shell must be passed (${this._supportedShell.join('/')})`, {}, this)); 220 | } else { 221 | this.logger().info(complete.script(this.bin(), this.bin(), argv[1].toLowerCase())); 222 | } 223 | } 224 | 225 | /** 226 | * Parse command line arguments. 227 | * @param {Array} [argv] argv 228 | * @public 229 | */ 230 | parse(argv) { 231 | const argvSlice = argv.slice(2); 232 | const args = parseArgs(argv); 233 | let options = Object.assign({}, args); 234 | delete options._; 235 | 236 | if (args._[0] === 'completion') { 237 | return this._handleCompletionCommand(args, argvSlice); 238 | } 239 | 240 | this._run(args._, options); 241 | } 242 | 243 | } 244 | 245 | const constants = require('./constants'); 246 | 247 | Object.keys(constants).map(c => Program.prototype[c] = constants[c]); 248 | 249 | module.exports = Program; 250 | -------------------------------------------------------------------------------- /tests/option-validation.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* global Program, logger, should, makeArgv, sinon */ 4 | 5 | const program = new Program(); 6 | 7 | program 8 | .logger(logger) 9 | .version('1.0.0'); 10 | 11 | 12 | 13 | describe('Passing --option invalid-value', () => { 14 | 15 | ['regex', 'function', 'INT', 'BOOL', 'FLOAT', 'LIST(int)', 'LIST(bool)', 'LIST(float)', 'LIST(repeated)'].forEach(function(checkType) { 16 | it(`should throw an error for ${checkType} check`, () => { 17 | 18 | const error = sinon.stub(program, "fatalError", function(err) { 19 | should(err.name).eql('InvalidOptionValueError'); 20 | }); 21 | 22 | program.action(function() {}); 23 | 24 | if (checkType === 'regex') { 25 | program.option('-t, --time ', 'Time in seconds', /^\d+$/); 26 | program.parse(makeArgv(['-t', 'i-am-invalid'])); 27 | 28 | } else if(checkType === 'function') { 29 | program.option('-t, --time ', 'Time in seconds, superior to zero', function(val) { 30 | const o = parseInt(val); 31 | if (isNaN(o) || o <= 0) { 32 | throw new Error("FOOOO") 33 | } 34 | return o; 35 | }); 36 | program.parse(makeArgv(['-t', 'i-am-invalid'])); 37 | 38 | } else if(checkType === 'INT') { 39 | program.option('-t, --time ', 'Time in seconds', program.INT); 40 | program.parse(makeArgv(['-t', 'i-am-invalid'])); 41 | 42 | } else if(checkType === 'BOOL') { 43 | program.option('--happy ', 'Am I happy ?', program.BOOLEAN); 44 | program.parse(makeArgv(['--happy', 'i-am-invalid'])); 45 | 46 | } else if(checkType === 'FLOAT') { 47 | program.option('-t, --time ', 'Time in seconds', program.FLOAT); 48 | program.parse(makeArgv(['-t', 'i-am-invalid'])); 49 | 50 | } else if(checkType === 'LIST(int)') { 51 | program.option('-l, --list ', 'My list', program.LIST | program.INT); 52 | program.parse(makeArgv(['--list', '0,1,A'])); 53 | 54 | } else if(checkType === 'LIST(bool)') { 55 | program.option('-l, --list ', 'My list', program.LIST | program.BOOL); 56 | program.parse(makeArgv(['--list', 'true,0,fake'])); 57 | 58 | } else if(checkType === 'LIST(float)') { 59 | program.option('-l, --list ', 'My list', program.LIST | program.FLOAT); 60 | program.parse(makeArgv(['--list', '1.0,0,fake'])); 61 | } 62 | else if(checkType === 'LIST(repeated)') { 63 | program.option('-l, --list ', 'My list', program.LIST | program.FLOAT); 64 | program.parse(makeArgv(['--list', '1.0', '--list','fake'])); 65 | } 66 | 67 | const count = error.callCount; 68 | 69 | should(count).be.eql(1); 70 | 71 | program.reset(); 72 | error.restore(); 73 | }); 74 | }); 75 | }); 76 | 77 | 78 | describe('Passing --option valid-value', () => { 79 | 80 | ['regex', 'function', 'INT', 'BOOL', 'BOOL(implicit)', 'FLOAT', 'LIST(int)', 'LIST(bool)', 'LIST(float)'].forEach(function(checkType) { 81 | it(`should succeed for ${checkType} check`, () => { 82 | 83 | const error = sinon.stub(program, "fatalError"); 84 | 85 | program.action(function() {}); 86 | 87 | if (checkType === 'regex') { 88 | program.option('-t, --time ', 'Time in seconds', /^\d+$/); 89 | program.parse(makeArgv(['-t', '234'])); 90 | 91 | } else if(checkType === 'function') { 92 | program.option('-t, --time ', 'Time in seconds, superior to zero', function(val) { 93 | const o = parseInt(val); 94 | if (isNaN(o) || o <= 0) { 95 | throw new Error("FOOOO") 96 | } 97 | return o; 98 | }); 99 | program.parse(makeArgv(['-t', '2'])); 100 | 101 | } else if(checkType === 'INT') { 102 | program.option('-t, --time ', 'Time in seconds', program.INT); 103 | program.parse(makeArgv(['-t', '282'])); 104 | 105 | } else if(checkType === 'BOOL') { 106 | program.option('--happy ', 'Am I happy ?', program.BOOLEAN); 107 | program.parse(makeArgv(['--happy', 'yes'])); 108 | 109 | } else if(checkType === 'BOOL(implicit)') { 110 | program.option('--happy', 'Am I happy ?', program.BOOLEAN); 111 | program.parse(makeArgv(['--happy'])); 112 | 113 | } else if(checkType === 'FLOAT') { 114 | program.option('-t, --time ', 'Time in seconds', program.FLOAT); 115 | program.parse(makeArgv(['-t', '2.8'])); 116 | 117 | } else if(checkType === 'LIST(int)') { 118 | program.option('-l, --list ', 'My list', program.LIST | program.INT); 119 | program.parse(makeArgv(['--list', '1,8'])); 120 | 121 | } else if(checkType === 'LIST(bool)') { 122 | program.option('-l, --list ', 'My list', program.LIST | program.BOOL); 123 | program.parse(makeArgv(['--list', 'true,0,yes,no,1,false'])); 124 | 125 | } else if(checkType === 'LIST(float)') { 126 | program.option('-l, --list ', 'My list', program.LIST | program.FLOAT); 127 | program.parse(makeArgv(['--list', '1.0,0'])); 128 | } 129 | 130 | const count = error.callCount; 131 | 132 | should(count).be.eql(0); 133 | 134 | program.reset(); 135 | error.restore(); 136 | }); 137 | }); 138 | }); 139 | 140 | 141 | describe('Passing --unknown-option (long)', () => { 142 | 143 | it(`should throw UnknownOptionError`, () => { 144 | program 145 | .option('-t, --time ') 146 | .action(function() {}); 147 | 148 | const error = sinon.stub(program, "fatalError", function(err) { 149 | should(err.name).eql('UnknownOptionError'); 150 | }); 151 | program.parse(makeArgv('--unknown-option')); 152 | should(error.callCount).be.eql(1); 153 | error.restore(); 154 | program.reset(); 155 | }); 156 | }); 157 | 158 | describe('Setting up an option with a default value', () => { 159 | it(`should take default value if nothing is passed`, () => { 160 | 161 | program 162 | .reset() 163 | .command('foo', 'Fooooo') 164 | .option('--foo ', 'My bar', /^[a-z]+$/, 'bar') 165 | .action(function(args, options){ 166 | should(options.foo).eql('bar'); 167 | }); 168 | 169 | const error = sinon.stub(program, "fatalError"); 170 | program.parse(makeArgv(['foo'])); 171 | should(error.callCount).eql(0); 172 | program.reset(); 173 | error.restore(); 174 | }); 175 | }); 176 | 177 | describe('Setting up an option with an optionnal value', () => { 178 | it(`should work when no value is passed`, () => { 179 | 180 | program 181 | .reset() 182 | .command('foo', 'Fooooo') 183 | .option('--with-openssl [path]', 'Compile with openssl') 184 | .action(function(args, options){ 185 | should(options.withOpenssl).eql(true); 186 | }); 187 | 188 | const error = sinon.stub(program, "fatalError"); 189 | program.parse(makeArgv(['foo', '--with-openssl'])); 190 | should(error.callCount).eql(0); 191 | program.reset(); 192 | error.restore(); 193 | }); 194 | }); 195 | 196 | 197 | 198 | describe('Passing an unknown short option', () => { 199 | 200 | it(`should throw an error`, () => { 201 | program 202 | .reset() 203 | .option('-t, --time ') 204 | .action(function() {}); 205 | 206 | const error = sinon.stub(program, "fatalError", function(err) { 207 | should(err.name).eql('UnknownOptionError'); 208 | }); 209 | program.parse(makeArgv('-u')); 210 | should(error.callCount).be.eql(1); 211 | error.restore(); 212 | program.reset(); 213 | }); 214 | }); 215 | 216 | describe('Passing a known short option', () => { 217 | 218 | it(`should succeed`, () => { 219 | program 220 | .reset() 221 | .option('-t ') 222 | .action(function() {}); 223 | 224 | const error = sinon.stub(program, "fatalError"); 225 | program.parse(makeArgv(['-t', '278'])); 226 | should(error.callCount).be.eql(0); 227 | error.restore(); 228 | program.reset(); 229 | }); 230 | }); 231 | 232 | describe('Setting up a required option (long)', () => { 233 | 234 | it(`should throw MissingOptionError if not passed`, () => { 235 | program 236 | .command('foo') 237 | .option('-t, --time ', 'my option', null, null, true) 238 | .action(function() {}); 239 | 240 | const error = sinon.stub(program, "fatalError", function(err) { 241 | should(err.name).eql('MissingOptionError'); 242 | }); 243 | program.parse(makeArgv('foo')); 244 | should(error.callCount).be.eql(1); 245 | error.restore(); 246 | program.reset(); 247 | }); 248 | }); 249 | 250 | describe('Setting up a required option (short)', () => { 251 | 252 | it(`should throw MissingOptionError if not passed`, () => { 253 | program 254 | .command('foo') 255 | .option('-t ', 'my option', null, null, true) 256 | .action(function() {}); 257 | 258 | const error = sinon.stub(program, "fatalError", function(err) { 259 | should(err.name).eql('MissingOptionError'); 260 | }); 261 | program.parse(makeArgv('foo')); 262 | should(error.callCount).be.eql(1); 263 | error.restore(); 264 | program.reset(); 265 | }); 266 | }); 267 | 268 | describe('Setting up a option synopsis containing an error', () => { 269 | 270 | it(`should throw OptionSyntaxError`, () => { 271 | 272 | const error = sinon.stub(program, "fatalError", function(err) { 273 | should(err.name).eql('OptionSyntaxError'); 274 | }); 275 | 276 | program 277 | .command('foo') 278 | .option('-t foo', 'my option', null, null, true) 279 | .action(function() {}); 280 | 281 | should(error.callCount).be.eql(1); 282 | error.restore(); 283 | program.reset(); 284 | }); 285 | }); 286 | -------------------------------------------------------------------------------- /lib/command.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const GetterSetter = require('./utils').GetterSetter; 4 | const Option = require('./option'); 5 | const Argument = require('./argument'); 6 | const UnknownOptionError = require('./error/unknown-option'); 7 | const InvalidOptionValueError = require('./error/invalid-option-value'); 8 | const InvalidArgumentValueError = require('./error/invalid-argument-value'); 9 | const MissingOptionError = require('./error/missing-option'); 10 | const NoActionError = require('./error/no-action-error'); 11 | const WrongNumberOfArgumentError = require('./error/wrong-num-of-arg'); 12 | const Promise = require('bluebird'); 13 | 14 | /** 15 | * Command class 16 | */ 17 | class Command extends GetterSetter { 18 | 19 | /** 20 | * 21 | * @param {String|null} name - Command name. 22 | * @param {String} description - Command description 23 | * @param {Program} program Program instance 24 | */ 25 | constructor(name, description, program) { 26 | super(); 27 | this._name = name; 28 | this._description = description; 29 | this._options = []; 30 | this._program = program; 31 | this._logger = this._program.logger(); 32 | this._alias = null; 33 | this.description = this.makeGetterSetter('description'); 34 | this._args = []; 35 | this._lastAddedArgOrOpt = null; 36 | this._setupLoggerMethods(); 37 | } 38 | 39 | 40 | 41 | /** 42 | * @private 43 | * @returns {Command.command|null|Program.command|string|*} 44 | */ 45 | name() { 46 | return this._name === '_default' ? '' : this._name; 47 | } 48 | 49 | /** 50 | * @private 51 | * @returns {Argument[]} 52 | */ 53 | args(index) { 54 | return typeof index !== 'undefined' ? this._args[index] : this._args; 55 | } 56 | 57 | /** 58 | * @private 59 | * @returns {Option[]} 60 | */ 61 | options() { 62 | return this._options; 63 | } 64 | 65 | /** 66 | * @private 67 | */ 68 | getSynopsis() { 69 | return this.name() + ' ' + (this.args().map(a => a.synopsis()).join(' ')); 70 | } 71 | 72 | /** 73 | * @private 74 | * @returns {null|String} 75 | */ 76 | getAlias() { 77 | return this._alias; 78 | } 79 | 80 | /** 81 | * Add command argument 82 | * 83 | * @param {String} synopsis - Argument synopsis like `` or `[my-argument]`. 84 | * Angled brackets (e.g. ``) indicate required input. Square brackets (e.g. `[env]`) indicate optional input. 85 | * @param {String} description - Option description 86 | * @param {String|RegExp|Function|Number} [validator] - Option validator, used for checking or casting 87 | * @param {*} [defaultValue] - Default value 88 | * @public 89 | * @returns {Command} 90 | */ 91 | argument(synopsis, description, validator, defaultValue) { 92 | const arg = new Argument(synopsis, description, validator, defaultValue, this._program); 93 | this._lastAddedArgOrOpt = arg; 94 | this._args.push(arg); 95 | return this; 96 | } 97 | 98 | /** 99 | * 100 | * @returns {Number} 101 | * @private 102 | */ 103 | _requiredArgsCount() { 104 | return this.args().filter(a => a.isRequired()).length; 105 | } 106 | 107 | /** 108 | * 109 | * @returns {Number} 110 | * @private 111 | */ 112 | _optionalArgsCount() { 113 | return this.args().filter(a => a.isOptional()).length; 114 | } 115 | 116 | /** 117 | * 118 | * @returns {{min: Number, max: *}} 119 | * @private 120 | */ 121 | _acceptedArgsRange() { 122 | const min = this._requiredArgsCount(); 123 | const max = this._hasVariadicArguments() ? Infinity : (this._requiredArgsCount() + this._optionalArgsCount()); 124 | return {min, max}; 125 | } 126 | 127 | /** 128 | * 129 | * @param optName 130 | * @returns {Option|undefined} 131 | * @private 132 | */ 133 | _findOption(optName) { 134 | return this._options.find(o => (o.getShortCleanName() === optName || o.getLongCleanName() === optName)); 135 | } 136 | 137 | 138 | /** 139 | * 140 | * @param {String} name - Argument name 141 | * @returns {Argument|undefined} 142 | * @private 143 | */ 144 | _findArgument(name) { 145 | return this._args.find(a => a.name() === name); 146 | } 147 | 148 | /** 149 | * @private 150 | */ 151 | _getLongOptions() { 152 | return this._options.map(opt => opt.getLongCleanName()).filter(o => typeof o !== 'undefined'); 153 | } 154 | 155 | /** 156 | * Allow chaining command() with other .command() 157 | * @returns {Command} 158 | */ 159 | command() { 160 | return this._program.command.apply(this._program, arguments); 161 | } 162 | 163 | /** 164 | * @private 165 | * @returns {boolean} 166 | */ 167 | _hasVariadicArguments() { 168 | return this.args().find(a => a.isVariadic()) !== undefined; 169 | } 170 | 171 | /** 172 | * 173 | * @param {Array} args 174 | * @return {Object} 175 | * @private 176 | */ 177 | _argsArrayToObject(args) { 178 | return this.args().reduce((acc, arg, index) => { 179 | if (typeof args[index] !== 'undefined') { 180 | acc[arg.name()] = args[index]; 181 | } else if(arg.hasDefault()) { 182 | acc[arg.name()] = arg.default(); 183 | } 184 | return acc; 185 | }, {}); 186 | 187 | } 188 | 189 | /** 190 | * 191 | * @param {Array} args 192 | * @returns {Array} 193 | * @private 194 | */ 195 | _splitArgs(args) { 196 | return this.args().reduce((acc, arg) => { 197 | if (arg.isVariadic()) { 198 | acc.push(args.slice()); 199 | } else { 200 | acc.push(args.shift()); 201 | } 202 | return acc; 203 | }, []); 204 | } 205 | 206 | /** 207 | * 208 | * @param {Array} argsArr 209 | * @private 210 | */ 211 | _checkArgsRange(argsArr) { 212 | const range = this._acceptedArgsRange(); 213 | const argsCount = argsArr.length; 214 | if (argsCount < range.min || argsCount> range.max) { 215 | const expArgsStr = range.min === range.max ? `exactly ${range.min}.` : `between ${range.min} and ${range.max}.`; 216 | throw new WrongNumberOfArgumentError( 217 | "Wrong number of argument(s)" + (this.name() ? ' for command ' + this.name() : '') + 218 | `. Got ${argsCount}, expected ` + expArgsStr, 219 | {}, 220 | this._program 221 | ) 222 | } 223 | } 224 | 225 | /** 226 | * 227 | * @param args 228 | * @returns {*} 229 | * @private 230 | */ 231 | _validateArgs(args) { 232 | return Object.keys(args).reduce((acc, key) => { 233 | const arg = this._findArgument(key); 234 | const value = args[key]; 235 | try { 236 | acc[key] = arg._validate(value); 237 | } catch(e) { 238 | throw new InvalidArgumentValueError(key, value, this, this._program); 239 | } 240 | return acc; 241 | }, {}); 242 | } 243 | 244 | /** 245 | * 246 | * @param options 247 | * @returns {*} 248 | * @private 249 | */ 250 | _checkRequiredOptions(options) { 251 | return this._options.reduce((acc, opt) => { 252 | if (typeof acc[opt.getLongCleanName()] === 'undefined' && typeof acc[opt.getShortCleanName()] === 'undefined') { 253 | if (opt.hasDefault()) { 254 | acc[opt.getLongCleanName()] = opt.default(); 255 | } else if (opt.isRequired()) { 256 | throw new MissingOptionError(opt.getLongOrShortCleanName(), this, this._program); 257 | } 258 | } 259 | return acc; 260 | }, options); 261 | } 262 | 263 | /** 264 | * 265 | * @param options 266 | * @returns {*} 267 | * @private 268 | */ 269 | _validateOptions(options) { 270 | return Object.keys(options).reduce((acc, key) => { 271 | if (Command.NATIVE_OPTIONS.indexOf(key) !== -1) { 272 | return acc; 273 | } 274 | const value = acc[key]; 275 | const opt = this._findOption(key); 276 | if (!opt) { 277 | throw new UnknownOptionError(key, this, this._program); 278 | } 279 | try { 280 | acc[key] = opt._validate(value); 281 | } catch(e) { 282 | throw new InvalidOptionValueError(key, value, this, e, this._program); 283 | } 284 | return acc; 285 | }, options); 286 | } 287 | 288 | /** 289 | * 290 | * @param options 291 | * @returns {*} 292 | * @private 293 | */ 294 | _addLongNotationToOptions(options) { 295 | return Object.keys(options).reduce((acc, key) => { 296 | if (key.length === 1) { 297 | const value = acc[key]; 298 | const opt = this._findOption(key); 299 | if (opt && opt.getLongCleanName()) { 300 | acc[opt.getLongCleanName()] = value; 301 | } 302 | } 303 | return acc; 304 | }, options); 305 | } 306 | 307 | /** 308 | * 309 | * @param options 310 | * @returns {*} 311 | * @private 312 | */ 313 | _camelCaseOptions(options) { 314 | return this._options.reduce((acc, opt) => { 315 | if (typeof options[opt.getLongCleanName()] !== 'undefined') { 316 | acc[opt.name()] = options[opt.getLongCleanName()]; 317 | } 318 | return acc; 319 | }, {}); 320 | } 321 | 322 | /** 323 | * 324 | * @param args 325 | * @param options 326 | * @returns {*} 327 | * @private 328 | */ 329 | _validateCall(args, options) { 330 | // check min & max arguments accepted 331 | this._checkArgsRange(args); 332 | // split args 333 | args = this._splitArgs(args); 334 | // transfrom args array to object, and set defaults for arguments not passed 335 | args = this._argsArrayToObject(args); 336 | // arguments validation 337 | args = this._validateArgs(args); 338 | // check required options 339 | options = this._checkRequiredOptions(options); 340 | // options validation 341 | options = this._validateOptions(options); 342 | // add long notation if exists 343 | options = this._addLongNotationToOptions(options); 344 | // camelcase options 345 | options = this._camelCaseOptions(options); 346 | return {args, options}; 347 | } 348 | 349 | 350 | /** 351 | * Add an option 352 | * 353 | * @param {String} synopsis - Option synopsis like '-f, --force', or '-f, --file ', or '--with-openssl [path]' 354 | * @param {String} description - Option description 355 | * @param {String|RegExp|Function|Number} [validator] - Option validator, used for checking or casting 356 | * @param {*} [defaultValue] - Default value 357 | * @param {Boolean} [required] - Is the option itself required 358 | * @public 359 | * @returns {Command} 360 | */ 361 | option(synopsis, description, validator, defaultValue, required) { 362 | const opt = new Option(synopsis, description, validator, defaultValue, required, this._program); 363 | this._lastAddedArgOrOpt = opt; 364 | this._options.push(opt); 365 | return this; 366 | } 367 | 368 | /** 369 | * Set the corresponding action to execute for this command 370 | * 371 | * @param {Function} action - Action to execute 372 | * @returns {Command} 373 | * @public 374 | */ 375 | action(action) { 376 | this._action = action; 377 | return this; 378 | } 379 | 380 | /** 381 | * Run the command's action 382 | * 383 | * @param {Object} args - Arguments 384 | * @param {Object} options - Options 385 | * @private 386 | */ 387 | _run(args, options) { 388 | if (!this._action) { 389 | return this._program.fatalError(new NoActionError( 390 | "Caporal Setup Error: You don't have defined an action for you program/command. Use .action()", 391 | {}, 392 | this._program 393 | )); 394 | } 395 | const actionResults = this._action.apply(this, [args, options, this._logger]); 396 | const response = Promise.resolve(actionResults); 397 | response 398 | .catch(err => { 399 | err = err instanceof Error ? err : new Error(err); 400 | return this._program.fatalError(err); 401 | }) 402 | } 403 | 404 | /** 405 | * 406 | * @private 407 | */ 408 | _setupLoggerMethods() { 409 | ['error', 'warn', 'info', 'log', 'debug'].forEach(function(lev) { 410 | const overrideLevel = (lev === 'log') ? 'info' : lev; 411 | this[lev] = this._logger[overrideLevel].bind(this._logger); 412 | }, this); 413 | } 414 | 415 | /** 416 | * Set an alias for this command. Only one alias can be set up for a command 417 | * 418 | * @param {String} alias - Alias 419 | * @returns {Command} 420 | * @public 421 | */ 422 | alias(alias) { 423 | this._alias = alias; 424 | return this; 425 | } 426 | 427 | /** 428 | * Autocomplete callabck 429 | */ 430 | complete(callback) { 431 | this._program._autocomplete.registerCompletion(this._lastAddedArgOrOpt, callback); 432 | return this; 433 | } 434 | 435 | } 436 | 437 | Object.defineProperties(Command, { 438 | "NATIVE_OPTIONS": { 439 | value: ['h', 'help', 'V', 'version', 'color', 'quiet', 'silent', 'v', 'verbose'] 440 | } 441 | }); 442 | 443 | module.exports = Command; 444 | -------------------------------------------------------------------------------- /tests/autocomplete.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* global Program, logger, should, sinon */ 4 | 5 | 6 | 7 | describe('Autocomplete', () => { 8 | 9 | 10 | beforeEach(function () { 11 | 12 | this.program = new Program(); 13 | 14 | this.program 15 | .logger(logger) 16 | .version('1.0.0') 17 | // the "order" command 18 | .command('order', 'Order a pizza') 19 | .alias('give-it-to-me') 20 | // will be auto-magicaly autocompleted by providing the user with 3 choices 21 | .argument('', 'Kind of pizza', ["margherita", "hawaiian", "fredo"]) 22 | .argument('', 'Which store to order from') 23 | // enable auto-completion for argument using a sync function returning an array 24 | .complete(function() { 25 | return ['store-1', 'store-2', 'store-3', 'store-4', 'store-5']; 26 | }) 27 | 28 | .argument('', 'Which account id to use') 29 | // enable auto-completion for argument using a Promise 30 | .complete(function() { 31 | return Promise.resolve(['account-1', 'account-2']); 32 | }) 33 | 34 | .option('-n, --number ', 'Number of pizza', this.program.INT, 1) 35 | .option('-d, --discount ', 'Discount offer', this.program.FLOAT) 36 | .option('-p, --pay-by ', 'Pay by option') 37 | // enable auto-completion for -p | --pay-by argument using a Promise 38 | .complete(function() { 39 | return Promise.resolve(['cash', 'credit-card']); 40 | }) 41 | 42 | // --extra will be auto-magicaly autocompleted by providing the user with 3 choices 43 | .option('-e ', 'Add extra ingredients', ['pepperoni', 'onion', 'cheese']) 44 | .action(function(args, options, logger) { 45 | 46 | }) 47 | 48 | // the "return" command 49 | .command('return', 'Return an order') 50 | // will be auto-magicaly autocompleted by providing the user with 3 choices 51 | .argument('', 'Order id') 52 | // enable auto-completion for argument using the `done` callback 53 | .complete(function() { 54 | return Promise.resolve(['#82792', '#71727', '#526Z52']); 55 | }) 56 | .argument('', 'Store id') 57 | .option('--ask-change ', 'Ask for other kind of pizza') 58 | .complete(function() { 59 | return Promise.resolve(["margherita", "hawaiian", "fredo"]); 60 | }) 61 | .option('--say-something ', 'Say something to the manager') 62 | .action(function(args, options, logger) { 63 | 64 | }); 65 | 66 | this._complete = sinon.spy(this.program._autocomplete, "_complete"); 67 | 68 | }); 69 | 70 | afterEach(function () { 71 | this._complete.restore(); 72 | }); 73 | 74 | it(`should complete commands when none provided`, function(done) { 75 | 76 | process.env.COMP_LINE = "_mocha"; 77 | process.env.COMP_POINT = process.env.COMP_LINE.length.toString(); 78 | process.env.COMP_CWORD = process.env.COMP_LINE.trim().split(' ').length.toString(); 79 | 80 | this.program.parse(['node', '_mocha', 'completion', '--'].concat(process.env.COMP_LINE.split(' '))); 81 | 82 | setImmediate(() => { 83 | should(this._complete.called).be.true(); 84 | const response = this._complete.returnValues[0]; 85 | should(response).be.Promise(); 86 | should(response).be.fulfilledWith([ 87 | 'order:Order a pizza', 88 | 'return:Return an order' 89 | ]).then(_ => done()).catch(err => done(err)); 90 | //done(); 91 | }); 92 | }); 93 | 94 | it(`should complete commands when substring provided`, function(done) { 95 | 96 | process.env.COMP_LINE = "_mocha or"; 97 | process.env.COMP_POINT = process.env.COMP_LINE.length.toString(); 98 | process.env.COMP_CWORD = process.env.COMP_LINE.trim().split(' ').length.toString(); 99 | 100 | this.program.parse(['node', '_mocha', 'completion', '--'].concat(process.env.COMP_LINE.split(' '))); 101 | 102 | setImmediate(() => { 103 | should(this._complete.called).be.true(); 104 | const response = this._complete.returnValues[0]; 105 | should(response).be.Promise(); 106 | should(response).be.fulfilledWith([ 107 | 'order:Order a pizza', 108 | ]); 109 | done(); 110 | }); 111 | 112 | 113 | }); 114 | 115 | 116 | it(`should complete commands when substring (alias) provided`, function(done) { 117 | 118 | process.env.COMP_LINE = "_mocha gi"; 119 | process.env.COMP_POINT = process.env.COMP_LINE.length.toString(); 120 | process.env.COMP_CWORD = process.env.COMP_LINE.trim().split(' ').length.toString(); 121 | 122 | this.program.parse(['node', '_mocha', 'completion', '--'].concat(process.env.COMP_LINE.split(' '))); 123 | 124 | setImmediate(() => { 125 | should(this._complete.called).be.true(); 126 | const response = this._complete.returnValues[0]; 127 | should(response).be.Promise(); 128 | should(response).be.fulfilledWith([ 129 | 'order:Order a pizza', 130 | ]).then(_ => done()).catch(err => done(err)); 131 | 132 | }); 133 | }); 134 | 135 | it(`should not complete commands if none available after string`, function(done) { 136 | 137 | process.env.COMP_LINE = "_mocha order"; 138 | process.env.COMP_POINT = process.env.COMP_LINE.length.toString(); 139 | process.env.COMP_CWORD = process.env.COMP_LINE.trim().split(' ').length.toString(); 140 | 141 | this.program.parse(['node', '_mocha', 'completion', '--'].concat(process.env.COMP_LINE.split(' '))); 142 | 143 | setImmediate(() => { 144 | should(this._complete.called).be.true(); 145 | const response = this._complete.returnValues[0]; 146 | should(response) 147 | .be.fulfilledWith([ 148 | 'margherita:Value for argument ', 149 | 'hawaiian:Value for argument ', 150 | 'fredo:Value for argument ', 151 | '--number:Number of pizza', 152 | '--discount:Discount offer', 153 | '--pay-by:Pay by option', 154 | '-e:Add extra ingredients' 155 | ]) 156 | .then(_ => done()) 157 | .catch(err => done(err)); 158 | }); 159 | 160 | }); 161 | 162 | it(`should not suggest argument(s) already provided`, function(done) { 163 | 164 | process.env.COMP_LINE = "_mocha order margherita"; 165 | process.env.COMP_POINT = process.env.COMP_LINE.length.toString(); 166 | process.env.COMP_CWORD = process.env.COMP_LINE.trim().split(' ').length.toString(); 167 | 168 | this.program.parse(['node', '_mocha', 'completion', '--'].concat(process.env.COMP_LINE.split(' '))); 169 | 170 | setImmediate(() => { 171 | should(this._complete.called).be.true(); 172 | const response = this._complete.returnValues[0]; 173 | response.then(results => { 174 | should(results).be.eql([ 175 | 'store-1', 176 | 'store-2', 177 | 'store-3', 178 | 'store-4', 179 | 'store-5', 180 | '--number:Number of pizza', 181 | '--discount:Discount offer', 182 | '--pay-by:Pay by option', 183 | '-e:Add extra ingredients' 184 | ]); 185 | done(); 186 | }).catch(e => { 187 | done(e); 188 | }); 189 | }); 190 | }); 191 | 192 | it(`should not complete current command`, function(done) { 193 | 194 | process.env.COMP_LINE = "_mocha order"; 195 | process.env.COMP_POINT = process.env.COMP_LINE.length.toString(); 196 | process.env.COMP_CWORD = process.env.COMP_LINE.trim().split(' ').length.toString(); 197 | 198 | this.program.parse(['node', '_mocha', 'completion', '--'].concat(process.env.COMP_LINE.split(' '))); 199 | 200 | setImmediate(() => { 201 | should(this._complete.called).be.true(); 202 | const response = this._complete.returnValues[0]; 203 | response.then(results => { 204 | should(results).be.eql([ 205 | "margherita:Value for argument ", 206 | "hawaiian:Value for argument ", 207 | "fredo:Value for argument ", 208 | "--number:Number of pizza", 209 | "--discount:Discount offer", 210 | "--pay-by:Pay by option", 211 | "-e:Add extra ingredients" 212 | ]); 213 | done(); 214 | }).catch(e => { 215 | done(e); 216 | }); 217 | }); 218 | 219 | }); 220 | 221 | it(`should handle command alias`, function(done) { 222 | 223 | process.env.COMP_LINE = "_mocha give-it-to-me"; 224 | process.env.COMP_POINT = process.env.COMP_LINE.length.toString(); 225 | process.env.COMP_CWORD = process.env.COMP_LINE.trim().split(' ').length.toString(); 226 | 227 | this.program.parse(['node', '_mocha', 'completion', '--'].concat(process.env.COMP_LINE.split(' '))); 228 | 229 | setImmediate(() => { 230 | should(this._complete.called).be.true(); 231 | const response = this._complete.returnValues[0]; 232 | response.then(results => { 233 | should(results).be.eql([ 234 | "margherita:Value for argument ", 235 | "hawaiian:Value for argument ", 236 | "fredo:Value for argument ", 237 | "--number:Number of pizza", 238 | "--discount:Discount offer", 239 | "--pay-by:Pay by option", 240 | "-e:Add extra ingredients" 241 | ]); 242 | done(); 243 | }).catch(e => { 244 | done(e); 245 | }); 246 | }); 247 | }); 248 | 249 | it(`should suggest long option names`, function(done) { 250 | 251 | process.env.COMP_LINE = "_mocha order --n"; 252 | process.env.COMP_POINT = process.env.COMP_LINE.length.toString(); 253 | process.env.COMP_CWORD = process.env.COMP_LINE.trim().split(' ').length.toString(); 254 | 255 | this.program.parse(['node', '_mocha', 'completion', '--'].concat(process.env.COMP_LINE.split(' '))); 256 | 257 | setImmediate(() => { 258 | should(this._complete.called).be.true(); 259 | const response = this._complete.returnValues[0]; 260 | response.then(results => { 261 | should(results).be.eql([ 262 | "--number:Number of pizza" 263 | ]); 264 | done(); 265 | }).catch(e => { 266 | done(e); 267 | }); 268 | }); 269 | 270 | }); 271 | 272 | it(`should suggest long option value`, function(done) { 273 | 274 | process.env.COMP_LINE = "_mocha order -e"; 275 | process.env.COMP_POINT = process.env.COMP_LINE.length.toString(); 276 | process.env.COMP_CWORD = process.env.COMP_LINE.trim().split(' ').length.toString(); 277 | 278 | this.program.parse(['node', '_mocha', 'completion', '--'].concat(process.env.COMP_LINE.split(' '))); 279 | 280 | setImmediate(() => { 281 | should(this._complete.called).be.true(); 282 | const response = this._complete.returnValues[0]; 283 | response.then(results => { 284 | should(results).be.eql(['pepperoni', 'onion', 'cheese']); 285 | done(); 286 | }).catch(e => { 287 | done(e); 288 | }); 289 | }); 290 | }); 291 | 292 | it(`should work with _default command`, function(done) { 293 | 294 | this.program = new Program(); 295 | 296 | this.program 297 | .logger(logger) 298 | .version('1.0.0') 299 | // the "deafult" command 300 | // will be auto-magicaly autocompleted by providing the user with 3 choices 301 | .argument('', 'Kind of pizza', ["margherita", "hawaiian", "fredo"]) 302 | .argument('', 'Which store to order from') 303 | // enable auto-completion for argument using the `done` callback 304 | .complete(function() { 305 | return ['store-1', 'store-2', 'store-3', 'store-4', 'store-5']; 306 | }) 307 | 308 | .argument('', 'Which account id to use') 309 | // enable auto-completion for argument using a Promise 310 | .complete(function() { 311 | return Promise.resolve(['account-1', 'account-2']); 312 | }) 313 | 314 | .option('-n, --number ', 'Number of pizza', this.program.INT, 1) 315 | .option('-d, --discount ', 'Discount offer', this.program.FLOAT) 316 | .option('-p, --pay-by ', 'Pay by option') 317 | // enable auto-completion for -p | --pay-by argument using a Promise 318 | .complete(function() { 319 | return Promise.resolve(['cash', 'credit-card']); 320 | }) 321 | 322 | // --extra will be auto-magicaly autocompleted by providing the user with 3 choices 323 | .option('-e ', 'Add extra ingredients', ['pepperoni', 'onion', 'cheese']) 324 | .action(function(args, options, logger) { 325 | 326 | }); 327 | 328 | this._complete = sinon.spy(this.program._autocomplete, "_complete"); 329 | 330 | process.env.COMP_LINE = "_mocha -e"; 331 | process.env.COMP_POINT = process.env.COMP_LINE.length.toString(); 332 | process.env.COMP_CWORD = process.env.COMP_LINE.trim().split(' ').length.toString(); 333 | 334 | this.program.parse(['node', '_mocha', 'completion', '--'].concat(process.env.COMP_LINE.split(' '))); 335 | 336 | setImmediate(() => { 337 | should(this._complete.called).be.true(); 338 | const response = this._complete.returnValues[0]; 339 | response.then(results => { 340 | should(results).be.eql(['pepperoni', 'onion', 'cheese']); 341 | done(); 342 | }).catch(e => { 343 | done(e); 344 | }); 345 | }); 346 | }); 347 | 348 | it(`should not suggest option names if last partial does not match`, function(done) { 349 | 350 | process.env.COMP_LINE = "_mocha order --foo"; 351 | process.env.COMP_POINT = process.env.COMP_LINE.length.toString(); 352 | process.env.COMP_CWORD = process.env.COMP_LINE.trim().split(' ').length.toString(); 353 | 354 | this.program.parse(['node', '_mocha', 'completion', '--'].concat(process.env.COMP_LINE.split(' '))); 355 | 356 | setImmediate(() => { 357 | should(this._complete.called).be.true(); 358 | const response = this._complete.returnValues[0]; 359 | response.then(results => { 360 | should(results).be.eql([]); 361 | done(); 362 | }).catch(e => { 363 | done(e); 364 | }); 365 | }); 366 | }); 367 | 368 | it(`should work with simple completers returning an array`, function(done) { 369 | 370 | process.env.COMP_LINE = "_mocha return"; 371 | process.env.COMP_POINT = process.env.COMP_LINE.length.toString(); 372 | process.env.COMP_CWORD = process.env.COMP_LINE.trim().split(' ').length.toString(); 373 | 374 | this.program.parse(['node', '_mocha', 'completion', '--'].concat(process.env.COMP_LINE.split(' '))); 375 | 376 | setImmediate(() => { 377 | should(this._complete.called).be.true(); 378 | const response = this._complete.returnValues[0]; 379 | response.then(results => { 380 | should(results).containDeepOrdered(['#82792', '#71727', '#526Z52']); 381 | done(); 382 | }).catch(e => { 383 | done(e); 384 | }); 385 | }); 386 | }); 387 | 388 | it(`should handle arguments without completers`, function(done) { 389 | 390 | process.env.COMP_LINE = "_mocha return #82792"; 391 | process.env.COMP_POINT = process.env.COMP_LINE.length.toString(); 392 | process.env.COMP_CWORD = process.env.COMP_LINE.trim().split(' ').length.toString(); 393 | 394 | this.program.parse(['node', '_mocha', 'completion', '--'].concat(process.env.COMP_LINE.split(' '))); 395 | 396 | setImmediate(() => { 397 | should(this._complete.called).be.true(); 398 | const response = this._complete.returnValues[0]; 399 | response.then(results => { 400 | should(results).eql([ 401 | '--ask-change:Ask for other kind of pizza', 402 | '--say-something:Say something to the manager' 403 | ]); 404 | done(); 405 | }).catch(e => { 406 | done(e); 407 | }); 408 | }); 409 | }); 410 | 411 | 412 | it(`should handle options completers`, function(done) { 413 | 414 | process.env.COMP_LINE = "_mocha return --ask-change"; 415 | process.env.COMP_POINT = process.env.COMP_LINE.length.toString(); 416 | process.env.COMP_CWORD = process.env.COMP_LINE.trim().split(' ').length.toString(); 417 | 418 | this.program.parse(['node', '_mocha', 'completion', '--'].concat(process.env.COMP_LINE.split(' '))); 419 | 420 | setImmediate(() => { 421 | should(this._complete.called).be.true(); 422 | const response = this._complete.returnValues[0]; 423 | response.then(results => { 424 | should(results).eql(["margherita", "hawaiian", "fredo"]); 425 | done(); 426 | }).catch(e => { 427 | done(e); 428 | }); 429 | }); 430 | }); 431 | 432 | it(`should handle options without completers`, function(done) { 433 | 434 | process.env.COMP_LINE = "_mocha return --say-something"; 435 | process.env.COMP_POINT = process.env.COMP_LINE.length.toString(); 436 | process.env.COMP_CWORD = process.env.COMP_LINE.trim().split(' ').length.toString(); 437 | 438 | this.program.parse(['node', '_mocha', 'completion', '--'].concat(process.env.COMP_LINE.split(' '))); 439 | 440 | setImmediate(() => { 441 | should(this._complete.called).be.true(); 442 | const response = this._complete.returnValues[0]; 443 | response.then(results => { 444 | should(results).eql([]); 445 | done(); 446 | }).catch(e => { 447 | done(e); 448 | }); 449 | }); 450 | }); 451 | 452 | it(`should handle a failling completion promise`, function(done) { 453 | 454 | this.program = new Program(); 455 | 456 | this.program 457 | .logger(logger) 458 | .version('1.0.0') 459 | .argument('', 'Which store to order from') 460 | .complete(function() { 461 | return Promise.reject(new Error("foo")); 462 | }) 463 | .action(function(args, options, logger) { 464 | 465 | }); 466 | 467 | this._complete = sinon.spy(this.program._autocomplete, "_complete"); 468 | 469 | process.env.COMP_LINE = "_mocha"; 470 | process.env.COMP_POINT = process.env.COMP_LINE.length.toString(); 471 | process.env.COMP_CWORD = process.env.COMP_LINE.trim().split(' ').length.toString(); 472 | 473 | this.program.parse(['node', '_mocha', 'completion', '--'].concat(process.env.COMP_LINE.split(' '))); 474 | 475 | setImmediate(() => { 476 | should(this._complete.called).be.true(); 477 | const response = this._complete.returnValues[0]; 478 | response.then(results => { 479 | should(results).be.eql([]); 480 | done(); 481 | }).catch(e => { 482 | done(e); 483 | }); 484 | }); 485 | }); 486 | 487 | 488 | }); 489 | 490 | 491 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | [![Travis](https://img.shields.io/travis/mattallty/Caporal.js.svg)](https://travis-ci.org/mattallty/Caporal.js) 6 | [![Codacy grade](https://img.shields.io/codacy/grade/6e5459fd36e341d1bd27414cf6b06e5c.svg)](https://www.codacy.com/app/matthiasetienne/Caporal-js/dashboard) 7 | [![Codacy coverage](https://img.shields.io/codacy/coverage/6e5459fd36e341d1bd27414cf6b06e5c.svg)]() 8 | [![npm](https://img.shields.io/npm/v/caporal.svg)](https://www.npmjs.com/package/caporal) 9 | [![npm](https://img.shields.io/npm/dm/caporal.svg)](https://www.npmjs.com/package/caporal) 10 | [![David](https://img.shields.io/david/mattallty/Caporal.js.svg)](https://david-dm.org/mattallty/Caporal.js) 11 | [![GitHub stars](https://img.shields.io/github/stars/mattallty/Caporal.js.svg?style=social&label=Star)](https://github.com/mattallty/Caporal.js/stargazers) 12 | [![GitHub forks](https://img.shields.io/github/forks/mattallty/Caporal.js.svg?style=social&label=Fork)](https://github.com/mattallty/Caporal.js/network) 13 | 14 | # Caporal 15 | 16 | > A full-featured framework for building command line applications (cli) with node.js, 17 | > including help generation, colored output, verbosity control, custom logger, coercion 18 | > and casting, typos suggestions, and auto-complete for bash/zsh/fish. 19 | 20 | ## Install 21 | 22 | Simply add Caporal as a dependency: 23 | ```bash 24 | $ npm install caporal --save 25 | 26 | # Or if you are using yarn (https://yarnpkg.com/lang/en/) 27 | $ yarn add caporal 28 | ``` 29 | ## Glossary 30 | 31 | * **Program**: a cli app that you can build using Caporal 32 | * **Command**: a command within your program. A program may have multiple commands. 33 | * **Argument**: a command may have one or more arguments passed after the command. 34 | * **Options**: a command may have one or more options passed after (or before) arguments. 35 | 36 | Angled brackets (e.g. ``) indicate required input. Square brackets (e.g. `[env]`) indicate optional input. 37 | 38 | ## Examples 39 | 40 | ```javascript 41 | #!/usr/bin/env node 42 | const prog = require('caporal'); 43 | prog 44 | .version('1.0.0') 45 | // you specify arguments using .argument() 46 | // 'app' is required, 'env' is optional 47 | .command('deploy', 'Deploy an application') 48 | .argument('', 'App to deploy', /^myapp|their-app$/) 49 | .argument('[env]', 'Environment to deploy on', /^dev|staging|production$/, 'local') 50 | // you specify options using .option() 51 | // if --tail is passed, its value is required 52 | .option('--tail ', 'Tail lines of logs after deploy', prog.INT) 53 | .action(function(args, options, logger) { 54 | // args and options are objects 55 | // args = {"app": "myapp", "env": "production"} 56 | // options = {"tail" : 100} 57 | }); 58 | 59 | prog.parse(process.argv); 60 | 61 | // ./myprog deploy myapp production --tail 100 62 | ``` 63 | 64 | ### Variadic arguments 65 | 66 | You can use `...` to indicate variadic arguments. In that case, the resulted value will be an array. 67 | **Note:** Only the last argument of a command can be variadic ! 68 | 69 | ```javascript 70 | #!/usr/bin/env node 71 | const prog = require('caporal'); 72 | prog 73 | .version('1.0.0') 74 | .command('deploy', 'Our deploy command') 75 | // 'app' and 'env' are required 76 | // and you can pass additional environments 77 | .argument('', 'App to deploy') 78 | .argument('', 'Environment') 79 | .argument('[other-env...]', 'Other environments') 80 | .action(function(args, options, logger) { 81 | console.log(args); 82 | // { 83 | // "app": "myapp", 84 | // "env": "production", 85 | // "otherEnv": ["google", "azure"] 86 | // } 87 | }); 88 | 89 | prog.parse(process.argv); 90 | 91 | // ./myprog deploy myapp production aws google azure 92 | ``` 93 | 94 | ### Simple program (single command) 95 | 96 | For a very simple program with just one command, you can omit the .command() call: 97 | 98 | ```javascript 99 | #!/usr/bin/env node 100 | const prog = require('caporal'); 101 | prog 102 | .version('1.0.0') 103 | .description('A simple program that says "biiiip"') 104 | .action(function(args, options, logger) { 105 | logger.info("biiiip") 106 | }); 107 | 108 | prog.parse(process.argv); 109 | ``` 110 | 111 | ## Logging 112 | 113 | Inside your action(), use the logger argument (third one) to log informations. 114 | 115 | ```javascript 116 | #!/usr/bin/env node 117 | const prog = require('caporal'); 118 | prog 119 | .version('1.0.0') 120 | .command('deploy', 'The deploy command') 121 | .action((args, options, logger) => { 122 | // Available methods: 123 | // - logger.debug() 124 | // - logger.info() or logger.log() 125 | // - logger.warn() 126 | // - logger.error() 127 | logger.info("Application deployed !"); 128 | }); 129 | 130 | prog.parse(process.argv); 131 | ``` 132 | 133 | ### Logging levels 134 | 135 | The default logging level is 'info'. The predefined options can be used to change the logging level: 136 | 137 | * `-v, --verbose`: Set the logging level to 'debug' so debug() logs will be output. 138 | * `--quiet, --silent`: Set the logging level to 'warn' so only warn() and error() logs will be output. 139 | 140 | ### Custom logger 141 | 142 | Caporal uses `winston` for logging. You can provide your own winston-compatible logger using `.logger()` the following way: 143 | 144 | ```javascript 145 | #!/usr/bin/env node 146 | const prog = require('caporal'); 147 | const myLogger = require('/path/to/my/logger.js'); 148 | prog 149 | .version('1.0.0') 150 | .logger(myLogger) 151 | .command('foo', 'Foo command description') 152 | .action((args, options, logger) => { 153 | logger.info("Foo !!"); 154 | }); 155 | 156 | prog.parse(process.argv); 157 | ``` 158 | 159 | * `-v, --verbose`: Set the logging level to 'debug' so debug() logs will be output. 160 | * `--quiet, --silent`: Set the logging level to 'warn' so only warn() and error() logs will be output. 161 | 162 | 163 | ## Coercion and casting using validators 164 | 165 | You can apply coercion and casting using various *validators*: 166 | 167 | * [Caporal flags](#flag-validator) 168 | * [Functions](#function-validator) 169 | * [Array](#array-validator) 170 | * [RegExp](#regexp-validator) 171 | 172 | ### Flag validator 173 | 174 | * `INT` (or `INTEGER`): Check option looks like an int and cast it with `parseInt()` 175 | * `FLOAT`: Will Check option looks like a float and cast it with `parseFloat()` 176 | * `BOOL` (or `BOOLEAN`): Check for string like `0`, `1`, `true`, `false`, `on`, `off` and cast it 177 | * `LIST` (or `ARRAY`): Transform input to array by splitting it on comma 178 | * `REPEATABLE`: Make the option repeatable, eg `./mycli -f foo -f bar -f joe` 179 | 180 | ```javascript 181 | #!/usr/bin/env node 182 | const prog = require('caporal'); 183 | prog 184 | .version('1.0.0') 185 | .command('order pizza') 186 | .option('--number ', 'Number of pizza', prog.INT, 1) 187 | .option('--kind ', 'Kind of pizza', /^margherita|hawaiian$/) 188 | .option('--discount ', 'Discount offer', prog.FLOAT) 189 | .option('--add-ingredients ', prog.LIST) 190 | .action(function(args, options) { 191 | // options.kind = 'margherita' 192 | // options.number = 1 193 | // options.addIngredients = ['pepperoni', 'onion'] 194 | // options.discount = 1.25 195 | }); 196 | 197 | prog.parse(process.argv); 198 | 199 | // ./myprog order pizza --kind margherita --discount=1.25 --add-ingredients=pepperoni,onion 200 | ``` 201 | 202 | ```javascript 203 | #!/usr/bin/env node 204 | const prog = require('caporal'); 205 | prog 206 | .version('1.0.0') 207 | .command('concat') // concat files 208 | .option('-f ', 'File to concat', prog.REPEATABLE) 209 | .action(function(args, options) { 210 | 211 | }); 212 | 213 | prog.parse(process.argv); 214 | 215 | // Usage: 216 | // ./myprog concat -f file1.txt -f file2.txt -f file3.txt 217 | ``` 218 | 219 | ### Function validator 220 | 221 | Using this method, you can check and cast user input. Make the check fail by throwing an `Error`, 222 | and cast input by returning a new value from your function. 223 | 224 | 225 | ```javascript 226 | #!/usr/bin/env node 227 | const prog = require('caporal'); 228 | prog 229 | .version('1.0.0') 230 | .command('order pizza') 231 | .option('--kind ', 'Kind of pizza', function(opt) { 232 | if (['margherita', 'hawaiian'].includes(opt) === false) { 233 | throw new Error("You can only order margherita or hawaiian pizza!"); 234 | } 235 | return opt.toUpperCase(); 236 | }) 237 | .action(function(args, options) { 238 | // options = { "kind" : "MARGHERITA" } 239 | }); 240 | 241 | prog.parse(process.argv); 242 | 243 | // ./myprog order pizza --kind margherita 244 | ``` 245 | 246 | ### Array validator 247 | 248 | Using an `Array`, Caporal will check that it contains the argument/option passed. 249 | 250 | **Note**: It is not possible to cast user input with this method, only checking it, 251 | so it's basically only interesting for strings, but a major advantage is that this method 252 | will allow autocompletion of arguments and option *values*. 253 | 254 | ```javascript 255 | #!/usr/bin/env node 256 | const prog = require('caporal'); 257 | prog 258 | .version('1.0.0') 259 | .command('order pizza') 260 | .option('--kind ', 'Kind of pizza', ["margherita", "hawaiian"]) 261 | .action(function(args, options) { 262 | 263 | }); 264 | 265 | prog.parse(process.argv); 266 | 267 | // ./myprog order pizza --kind margherita 268 | ``` 269 | 270 | ### RegExp validator 271 | 272 | Simply pass a RegExp object to test against it. 273 | **Note**: It is not possible to cast user input with this method, only checking it, 274 | so it's basically only interesting for strings. 275 | 276 | ```javascript 277 | #!/usr/bin/env node 278 | const prog = require('caporal'); 279 | prog 280 | .version('1.0.0') 281 | .command('order pizza') 282 | .option('--kind ', 'Kind of pizza', /^margherita|hawaiian$/) 283 | .action(function(args, options) { 284 | 285 | }); 286 | 287 | prog.parse(process.argv); 288 | 289 | // ./myprog order pizza --kind margherita 290 | ``` 291 | 292 | ## Colors 293 | 294 | By default, Caporal will output colors for help and errors. 295 | This behaviour can be disabled by passing `--no-colors`. 296 | 297 | ## Auto-generated help 298 | 299 | Caporal automatically generates help/usage instructions for you. 300 | Help can be displayed using `-h` or `--help` options, or with the default `help` command. 301 | 302 |

303 | 304 |

305 | 306 | 307 | ## Typo suggestions 308 | 309 | Caporal will automatically make suggestions for option typos. 310 | 311 |

312 | 313 |

314 | 315 | 316 | ## Shell auto-completion 317 | 318 | Caporal comes with an auto-completion feature out of the box for `bash`, `zsh`, and `fish`, 319 | thanks to [tabtab](https://github.com/mklabs/node-tabtab). 320 | 321 | For this feature to work, you will have to: 322 | 323 | - Put your cli app in your `$PATH` (this is the case if your app is installed globally using `npm install -g `) 324 | - Setup auto-completion for your shell, like bellow. 325 | 326 | #### If you are using bash 327 | 328 | ```bash 329 | # For bash 330 | source <(myapp completion bash) 331 | 332 | # or add it to your .bashrc to make it persist 333 | echo "source <(myapp completion bash)" >> ~/.bashrc 334 | ``` 335 | 336 | #### If you are using zsh 337 | 338 | ```bash 339 | # For zsh 340 | source <(myapp completion zsh) 341 | 342 | # or add it to your .zshrc to make it persist 343 | echo "source <(myapp completion zsh)" >> ~/.zshrc 344 | ``` 345 | 346 | #### If you are using fish 347 | 348 | ```bash 349 | # For fish 350 | source <(myapp completion fish) 351 | 352 | # or add it to your config.fish to make it persist 353 | echo "source <(myapp completion fish)" >> ~/.config/fish/config.fish 354 | ``` 355 | 356 | ### Basic auto-completion 357 | 358 | By default, it will autocomplete *commands* and *option names*. 359 | Also, *options* having an *Array validator* will be autocompleted. 360 | 361 | ### Auto-completion setup example 362 | 363 | ```javascript 364 | #!/usr/bin/env node 365 | 366 | const prog = require('caporal'); 367 | 368 | prog 369 | .version('1.0.0') 370 | // the "order" command 371 | .command('order', 'Order a pizza') 372 | .alias('give-it-to-me') 373 | // will be auto-magicaly autocompleted by providing the user with 3 choices 374 | .argument('', 'Kind of pizza', ["margherita", "hawaiian", "fredo"]) 375 | .argument('', 'Which store to order from') 376 | // enable auto-completion for argument using a sync function returning an array 377 | .complete(function() { 378 | return ['store-1', 'store-2', 'store-3', 'store-4', 'store-5']; 379 | }) 380 | 381 | .argument('', 'Which account id to use') 382 | // enable auto-completion for argument using a Promise 383 | .complete(function() { 384 | return Promise.resolve(['account-1', 'account-2']); 385 | }) 386 | 387 | .option('-n, --number ', 'Number of pizza', prog.INT, 1) 388 | .option('-d, --discount ', 'Discount offer', prog.FLOAT) 389 | .option('-p, --pay-by ', 'Pay by option') 390 | // enable auto-completion for -p | --pay-by argument using a Promise 391 | .complete(function() { 392 | return Promise.resolve(['cash', 'credit-card']); 393 | }) 394 | 395 | // --extra will be auto-magicaly autocompleted by providing the user with 3 choices 396 | .option('-e ', 'Add extra ingredients', ['pepperoni', 'onion', 'cheese']) 397 | .action(function(args, options, logger) { 398 | logger.info("Command 'order' called with:"); 399 | logger.info("arguments: %j", args); 400 | logger.info("options: %j", options); 401 | }) 402 | 403 | // the "return" command 404 | .command('return', 'Return an order') 405 | // will be auto-magicaly autocompleted by providing the user with 3 choices 406 | .argument('', 'Order id') 407 | // enable auto-completion for argument using a Promise 408 | .complete(function() { 409 | return Promise.resolve(['#82792', '#71727', '#526Z52']); 410 | }) 411 | .argument('', 'Store id') 412 | .option('--ask-change ', 'Ask for other kind of pizza') 413 | .complete(function() { 414 | return Promise.resolve(["margherita", "hawaiian", "fredo"]); 415 | }) 416 | .option('--say-something ', 'Say something to the manager') 417 | .action(function(args, options, logger) { 418 | logger.info("Command 'return' called with:"); 419 | logger.info("arguments: %j", args); 420 | logger.info("options: %j", options); 421 | }); 422 | 423 | prog.parse(process.argv); 424 | ``` 425 | 426 | ## API 427 | 428 | #### `require('caporal)` 429 | 430 | Returns a `Program` instance. 431 | 432 | ### Program API 433 | 434 | #### `.version(version) : Program` 435 | 436 | Set the version of your program. You may want to use your `package.json` version to fill it: 437 | 438 | ```javascript 439 | const myProgVersion = require('./package.json').version; 440 | const prog = require('caporal'); 441 | prog 442 | .version(myProgVersion) 443 | // [...] 444 | 445 | prog.parse(process.argv); 446 | ``` 447 | 448 | Your program will then automaticaly handle `-V` and `--version` options: 449 | 450 | matt@mb:~$ ./my-program --version 451 | 1.0.0 452 | 453 | #### `.command(name, description) -> Command` 454 | 455 | Set up a new command with name and description. Multiple commands can be added to one program. 456 | Returns a {Command}. 457 | 458 | ```javascript 459 | const prog = require('caporal'); 460 | prog 461 | .version('1.0.0') 462 | // one command 463 | .command('walk', 'Make the player walk') 464 | .action((args, options, logger) => { logger.log("I'm walking !")}) // you must attach an action for your command 465 | // a second command 466 | .command('run', 'Make the player run') 467 | .action((args, options, logger) => { logger.log("I'm running !")}) 468 | // a command may have multiple words 469 | .command('cook pizza', 'Make the player cook a pizza') 470 | .argument('', 'Kind of pizza') 471 | .action((args, options, logger) => { logger.log("I'm cooking a pizza !")}) 472 | // [...] 473 | 474 | prog.parse(process.argv); 475 | ``` 476 | 477 | #### `.logger([logger]) -> Program | winston` 478 | 479 | Get or set the logger instance. Without argument, it returns the logger instance (*winston* by default). 480 | With the *logger* argument, it sets a new logger. 481 | 482 | ### Command API 483 | 484 | #### `.argument(synopsis, description, [validator, [defaultValue]]) -> Command` 485 | 486 | Add an argument to the command. Can be called multiple times to add several arguments. 487 | 488 | * **synopsis** (*String*): something like `` or `` 489 | * **description** (*String*): argument description 490 | * **validator** (*Caporal Flag | Function | Array | RegExp*): optional validator, see [Coercion and casting ](#coercion-and-casting) 491 | * **defaultValue** (*): optional default value 492 | 493 | #### `.option(synopsis, description, [validator, [defaultValue, [required]]) -> Command` 494 | 495 | Add an option to the command. Can be called multiple times to add several options. 496 | 497 | * **synopsis** (*String*): You can pass short or long notation here, or both. See examples. 498 | * **description** (*String*): option description 499 | * **validator** (*Caporal Flag | Function | Array | RegExp*): optional validator, see [Coercion and casting ](#coercion-and-casting) 500 | * **defaultValue** (*): optional default value 501 | * **required** (*Bool*): Is the option itself required ? Default to `false` 502 | 503 | #### `.action(action) -> Command` 504 | 505 | Define the action, e.g a *Function*, for the current command. 506 | 507 | The *action* callback will be called with 3 arguments: 508 | * *args* (Object): Passed arguments 509 | * *options* (Object): Passed options 510 | * *logger* (winston): Winston logger instance 511 | 512 | ```javascript 513 | // sync action 514 | const prog = require('caporal'); 515 | prog 516 | .version('1.0.0') 517 | .command('walk', 'Make the player walk') 518 | .action((args, options, logger) => { 519 | logger.log("I'm walking !") 520 | }); 521 | ``` 522 | 523 | You can make your actions async by using Promises: 524 | 525 | ```javascript 526 | // async action 527 | const prog = require('caporal'); 528 | prog 529 | .version('1.0.0') 530 | .command('walk', 'Make the player walk') 531 | .action((args, options, logger) => { 532 | return new Promise(/* ... */); 533 | }); 534 | ``` 535 | 536 | #### `.alias(alias) -> Command` 537 | 538 | Define an alias for the current command. A command can only have one alias. 539 | 540 | ```javascript 541 | const prog = require('caporal'); 542 | prog 543 | .version('1.0.0') 544 | // one command 545 | .command('walk', 'Make the player walk') 546 | .alias('w') 547 | .action((args, options, logger) => { logger.log("I'm walking !")}); 548 | 549 | prog.parse(process.argv); 550 | 551 | // ./myapp w 552 | // same as 553 | // ./myapp walk 554 | ``` 555 | 556 | #### `.complete(completer) -> Command` 557 | 558 | Define an auto-completion handler for the latest argument or option added to the command. 559 | 560 | * **completer** (*Function*): The completer function has to return either an `Array` or a `Promise` which resolves to an `Array`. 561 | 562 | 563 | ## Credits 564 | 565 | Caporal is strongly inspired by [commander.js](https://github.com/tj/commander.js) and [Symfony Console](http://symfony.com/doc/current/components/console.html). 566 | Caporal make use of the following npm packages: 567 | * [chalk](https://www.npmjs.com/package/chalk) for colors 568 | * [cli-table2](https://www.npmjs.com/package/cli-table2) for cli tables 569 | * [fast-levenshtein](https://www.npmjs.com/package/fast-levenshtein) for suggestions 570 | * [tabtab](https://www.npmjs.com/package/tabtab) for auto-completion 571 | * [minimist](https://www.npmjs.com/package/minimist) for argument parsing 572 | * [prettyjson](https://www.npmjs.com/package/prettyjson) to output json 573 | * [winston](https://www.npmjs.com/package/winston) for logging 574 | 575 | 576 | ## License 577 | 578 | Copyright © Matthias ETIENNE 579 | 580 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 581 | 582 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 583 | 584 | The Software is provided “as is”, without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the software or the use or other dealings in the Software. 585 | --------------------------------------------------------------------------------