diff --git a/lib/help.js b/lib/help.js index dace7fe..2a34b64 100644 --- a/lib/help.js +++ b/lib/help.js @@ -22,13 +22,14 @@ module.exports = function() { const optionHandle = groups.options ? '[options] ' : ''; const cmdHandle = groups.commands ? '[command]' : ''; - const value = typeof this.config.value === 'string' - ? ' ' + this.config.value - : ''; + const value = + typeof this.config.value === 'string' ? ' ' + this.config.value : ''; parts.push([ '', - `Usage: ${this.printMainColor(name)} ${this.printSubColor(optionHandle + cmdHandle + value)}`, + `Usage: ${this.printMainColor(name)} ${this.printSubColor( + optionHandle + cmdHandle + value + )}`, '' ]); @@ -68,6 +69,8 @@ module.exports = function() { console.log(output); - // eslint-disable-next-line unicorn/no-process-exit - process.exit(); + if (this.config.exit && this.config.exit.help) { + // eslint-disable-next-line unicorn/no-process-exit + process.exit(); + } }; diff --git a/lib/index.js b/lib/index.js index 0890343..8046d4d 100644 --- a/lib/index.js +++ b/lib/index.js @@ -10,7 +10,8 @@ const publicMethods = { parse: require('./parse'), example: require('./example'), examples: require('./examples'), - showHelp: require('./help') + showHelp: require('./help'), + showVersion: require('./version') }; function Args() { @@ -22,6 +23,7 @@ function Args() { // Configuration defaults this.config = { + exit: { help: true, version: true }, help: true, version: true, usageFilter: null, @@ -33,8 +35,6 @@ function Args() { this.printMainColor = chalk; this.printSubColor = chalk; - - this.parent = module.parent; } // Assign internal helpers diff --git a/lib/parse.js b/lib/parse.js index 3c32519..47e7ce6 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -33,24 +33,21 @@ module.exports = function(argv, options) { this.printSubColor = this.printSubColor[this.config.subColor]; } - if (this.config.help) { - // Register default options and commands - this.option('help', 'Output usage information'); - this.command('help', 'Display help', this.showHelp); - } - // Parse arguments using mri this.raw = parser(argv.slice(1), this.config.mri || this.config.minimist); this.binary = path.basename(this.raw._[0]); // If default version is allowed, check for it if (this.config.version) { - this.checkVersion(this.parent); + this.checkVersion(); } - const subCommand = this.raw._[1]; - const helpTriggered = this.raw.h || this.raw.help; + // If default help is allowed, check for it + if (this.config.help) { + this.checkHelp(); + } + const subCommand = this.raw._[1]; const args = {}; const defined = this.isDefined(subCommand, 'commands'); const optionList = this.getOptions(defined); @@ -67,12 +64,6 @@ module.exports = function(argv, options) { return {}; } - // Show usage information if "help" or "h" option was used - // And respect the option related to it - if (this.config.help && helpTriggered) { - this.showHelp(); - } - // Hand back list of options return optionList; }; diff --git a/lib/utils.js b/lib/utils.js index afc61d2..10d1a60 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -177,7 +177,16 @@ module.exports = { generateDetails(kind) { // Get all properties of kind from global scope - const items = typeof kind === 'string' ? this.details[kind] : kind; + const items = []; + + // Clone passed objects so changing them here doesn't affect real data. + const passed = [].concat( + typeof kind === 'string' ? this.details[kind] : kind + ); + for (let i = 0, l = passed.length; i < l; i++) { + items.push(Object.assign({}, passed[i])); + } + const parts = []; const isCmd = kind === 'commands'; @@ -269,6 +278,11 @@ module.exports = { details.init = false; } + // If version is disabled, remove initializer + if (details.usage === 'version' && !this.config.version) { + details.init = false; + } + // If command has initializer, call it if (details.init) { const sub = [].concat(this.sub); @@ -283,17 +297,22 @@ module.exports = { : details.usage; let full = this.binary + '-' + subCommand; - const args = process.argv; - let i = 0; + // Remove node and original command. + const args = process.argv.slice(2); - while (i < 3) { - args.shift(); - i++; + // Remove the first occurance of subCommand from the args. + for (let i = 0, l = args.length; i < l; i++) { + if (args[i] === subCommand) { + args.splice(i, 1); + break; + } } if (process.platform === 'win32') { const binaryExt = path.extname(this.binary); - const mainModule = process.env.APPVEYOR ? '_fixture' : process.mainModule.filename; + const mainModule = process.env.APPVEYOR + ? '_fixture' + : process.mainModule.filename; full = `${mainModule}-${subCommand}`; @@ -337,29 +356,25 @@ module.exports = { }); }, - checkVersion(parent) { - // Load parent module - try { - const pkginfo = require('pkginfo'); - pkginfo(parent); - } catch (err) { - // Do nothing, but version could not be aquired - } + checkHelp() { + // Register default option and command. + this.option('help', 'Output usage information'); + this.command('help', 'Display help', this.showHelp); - // And get its version property - const version = parent.exports.version || '-/-'; - - if (version) { - // If it exists, register it as a default option - this.option('version', 'Output the version number'); + // Immediately output if option was provided. + if (this.optionWasProvided('help')) { + this.showHelp(); + } + }, - // And immediately output it if used in command line - if (this.raw.v || this.raw.version) { - console.log(version); + checkVersion() { + // Register default option and command. + this.option('version', 'Output the version number'); + this.command('version', 'Display version', this.showVersion); - // eslint-disable-next-line unicorn/no-process-exit - process.exit(); - } + // Immediately output if option was provided. + if (this.optionWasProvided('version')) { + this.showVersion(); } }, @@ -383,5 +398,10 @@ module.exports = { // If nothing matches, item is not defined return false; + }, + + optionWasProvided(name) { + const option = this.isDefined(name, 'options'); + return option && (this.raw[option.usage[0]] || this.raw[option.usage[1]]); } }; diff --git a/lib/version.js b/lib/version.js new file mode 100644 index 0000000..311f956 --- /dev/null +++ b/lib/version.js @@ -0,0 +1,34 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +/** + * Retrieves the main module package.json information. + * + * @param {string} directory + * The directory to start looking in. + * + * @return {Object|null} + * An object containing the package.json contents or NULL if it could not be found. + */ +function findPackage(directory) { + const file = path.resolve(directory, 'package.json'); + if (fs.existsSync(file) && fs.statSync(file).isFile()) { + return require(file); + } + const parent = path.resolve(directory, '..'); + return parent === directory ? null : findPackage(parent); +} + +module.exports = function() { + const pkg = findPackage(path.dirname(process.mainModule.filename)); + const version = (pkg && pkg.version) || '-/-'; + + console.log(version); + + if (this.config.exit && this.config.exit.version) { + // eslint-disable-next-line unicorn/no-process-exit + process.exit(); + } +}; diff --git a/package-lock.json b/package-lock.json index b0afbec..ff41da4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -340,9 +340,27 @@ "dev": true, "requires": { "arr-exclude": "1.0.0", + "execa": "0.7.0", "has-yarn": "1.0.0", "read-pkg-up": "2.0.0", "write-pkg": "3.1.0" + }, + "dependencies": { + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "dev": true, + "requires": { + "cross-spawn": "5.1.0", + "get-stream": "3.0.0", + "is-stream": "1.1.0", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", + "strip-eof": "1.0.0" + } + } } }, "babel-code-frame": { @@ -4995,11 +5013,6 @@ "find-up": "2.1.0" } }, - "pkginfo": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.4.1.tgz", - "integrity": "sha1-tUGO8EOd5UJfxJlQQtztFPsqhP8=" - }, "plur": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/plur/-/plur-2.1.2.tgz", @@ -5751,7 +5764,27 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", - "dev": true + "dev": true, + "requires": { + "execa": "0.7.0" + }, + "dependencies": { + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "dev": true, + "requires": { + "cross-spawn": "5.1.0", + "get-stream": "3.0.0", + "is-stream": "1.1.0", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", + "strip-eof": "1.0.0" + } + } + } }, "text-table": { "version": "0.2.0", diff --git a/package.json b/package.json index b8a780c..983e88f 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,6 @@ "camelcase": "4.1.0", "chalk": "2.1.0", "mri": "1.1.0", - "pkginfo": "0.4.1", "string-similarity": "1.2.0" } } diff --git a/readme.md b/readme.md index 21e5e32..1d04776 100644 --- a/readme.md +++ b/readme.md @@ -165,14 +165,19 @@ pizza eat ./directory ### .showHelp() -Outputs the usage information based on the options and comments you've registered so far. +Outputs the usage information based on the options and comments you've registered so far and exits, if configured to do so. + +### .showVersion() + +Outputs the version and exits, if configured to do so. ## Configuration -By default, the module already registers some default options (e.g. "version" and "help"), as well as a command named "help". These things have been implemented to make creating CLIs easier for beginners. However, they can also be disabled by taking advantage of the following properties: +By default, the module already registers some default options and commands (e.g. "version" and "help"). These things have been implemented to make creating CLIs easier for beginners. However, they can also be disabled by taking advantage of the following properties: | Property | Description | Default value | Type | | -------- | ----------- | ------------------ | ---- | +| exit | Automatically exits when help or version is rendered | `{ help: true, version: true }` | Object | | help | Automatically render the usage information when running `help`, `-h` or `--help` | true | Boolean | | name | The name of your program to display in help | Name of script file | String | | version | Outputs the version tag of your package.json | true | Boolean | diff --git a/test/index.js b/test/index.js index 540def7..4d7b229 100644 --- a/test/index.js +++ b/test/index.js @@ -9,6 +9,25 @@ import execa from 'execa'; import args from '../lib'; import { version } from '../package'; +// Provide a reset function during testing. +args.reset = function() { + this.details = { + options: [], + commands: [], + examples: [] + }; + return this; +}; + +// Provide a helper function for suppressing any output. +args.suppressOutput = function(fn) { + const original = process.stdout.write; + process.stdout.write = () => () => {}; + const result = fn.call(this); + process.stdout.write = original; + return result; +}; + const port = 8000; const argv = [ @@ -25,7 +44,13 @@ const argv = [ 10 ]; -test('options', t => { +// Reset args after each test. +test.afterEach.always(() => { + args.reset(); +}); + +// @todo Each of these options should be broken out into separate tests. +function setupOptions() { args .option('port', 'The port on which the site will run') .option('true', 'Boolean', true) @@ -33,6 +58,12 @@ test('options', t => { .option(['d', 'data'], 'The data that shall be used') .option('duplicated', 'Duplicated first char in option') .options([{ name: 'anotheroption', description: 'another option' }]); + return args; +} + +// @todo Each of these options should be broken out into separate tests. +test('options', t => { + const args = setupOptions(); const config = args.parse(argv); @@ -70,7 +101,48 @@ test('options', t => { } }); +test('help/host: only host is triggered', t => { + args.option('host', 'The host address'); + const config = args.parse(['node', 'foo', '-h', 'http://example.com']); + t.is(config.h, 'http://example.com'); + t.is(config.host, 'http://example.com'); + t.falsy(config.H); + t.falsy(config.help); +}); + +test('help/host: only help is triggered', t => { + args.option('host', 'The host address'); + const config = args.suppressOutput(() => + args.parse(['node', 'foo', '-H'], { exit: { help: false } }) + ); + t.falsy(config.h); + t.falsy(config.host); + t.true(config.H); + t.true(config.help); +}); + +test('version/verbose: only verbose is triggered', t => { + args.option('verbose', 'Verbose output'); + const config = args.parse(['node', 'foo', '-v']); + t.true(config.v); + t.true(config.verbose); + t.falsy(config.H); + t.falsy(config.help); +}); + +test('version/verbose: only version is triggered', t => { + args.option('verbose', 'Verbose output'); + const config = args.suppressOutput(() => + args.parse(['node', 'foo', '-V'], { exit: { version: false } }) + ); + t.falsy(config.v); + t.falsy(config.verbose); + t.true(config.V); + t.true(config.version); +}); + test('usage information', t => { + const args = setupOptions(); const filter = data => data; args.parse(argv, { @@ -85,6 +157,7 @@ test('usage information', t => { }); test('config', t => { + const args = setupOptions(); args.parse(argv, { help: false, errors: false @@ -126,6 +199,7 @@ test('command aliases', async t => { }); test('options propogated to mri', t => { + const args = setupOptions(); args.option('port', 'The port on which the site will run'); const config = args.parse(argv, { mri: { string: 'p' } });