├── 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 |
5 |
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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
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 | [](https://travis-ci.org/mattallty/Caporal.js)
6 | [](https://www.codacy.com/app/matthiasetienne/Caporal-js/dashboard)
7 | []()
8 | [](https://www.npmjs.com/package/caporal)
9 | [](https://www.npmjs.com/package/caporal)
10 | [](https://david-dm.org/mattallty/Caporal.js)
11 | [](https://github.com/mattallty/Caporal.js/stargazers)
12 | [](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 |
--------------------------------------------------------------------------------