├── .npmrc ├── .gitignore ├── .travis.yml ├── appveyor.yml ├── .editorconfig ├── lib ├── example.js ├── examples.js ├── options.js ├── command.js ├── version.js ├── index.js ├── option.js ├── help.js ├── parse.js └── utils.js ├── test ├── _fixture.js └── index.js ├── LICENSE ├── package.json └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact = true 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # log 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "8" 5 | - "node" 6 | cache: 7 | directories: 8 | - node_modules 9 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | branches: 2 | only: 3 | - master 4 | 5 | environment: 6 | matrix: 7 | - nodejs_version: "6" 8 | 9 | install: 10 | - ps: Install-Product node $env:nodejs_version 11 | - npm install 12 | 13 | test_script: 14 | - node --version 15 | - npm --version 16 | - npm run test 17 | 18 | build: off 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # More details: editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | 17 | [*.{json,yml}] 18 | indent_style = space 19 | indent_size = 2 20 | -------------------------------------------------------------------------------- /lib/example.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function(usage, description) { 4 | if (typeof usage !== 'string' || typeof description !== 'string') { 5 | throw new TypeError( 6 | 'Usage for adding an Example: args.example("usage", "description")' 7 | ) 8 | } 9 | 10 | this.details.examples.push({ usage, description }) 11 | 12 | return this 13 | } 14 | -------------------------------------------------------------------------------- /lib/examples.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function(list) { 4 | if (list.constructor !== Array) { 5 | throw new Error('Item passed to .examples is not an array') 6 | } 7 | 8 | for (const item of list) { 9 | const usage = item.usage || false 10 | const description = item.description || false 11 | this.example(usage, description) 12 | } 13 | 14 | return this 15 | } 16 | -------------------------------------------------------------------------------- /lib/options.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function(list) { 4 | if (list.constructor !== Array) { 5 | throw new Error('Item passed to .options is not an array') 6 | } 7 | 8 | for (const item of list) { 9 | const preset = item.defaultValue 10 | const init = item.init || false 11 | 12 | this.option(item.name, item.description, preset, init) 13 | } 14 | 15 | return this 16 | } 17 | -------------------------------------------------------------------------------- /lib/command.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function(usage, description, init, aliases) { 4 | if (Array.isArray(init)) { 5 | aliases = init 6 | init = undefined 7 | } 8 | 9 | if (aliases && Array.isArray(aliases)) { 10 | usage = [].concat([usage], aliases) 11 | } 12 | 13 | // Register command to global scope 14 | this.details.commands.push({ 15 | usage, 16 | description, 17 | init: typeof init === 'function' ? init : false 18 | }) 19 | 20 | // Allow chaining of .command() 21 | return this 22 | } 23 | -------------------------------------------------------------------------------- /test/_fixture.js: -------------------------------------------------------------------------------- 1 | const args = require('../lib') 2 | 3 | args.command( 4 | 'install', 5 | 'desc here', 6 | () => { 7 | console.log('install') 8 | }, 9 | ['i'] 10 | ) 11 | 12 | args.command( 13 | 'uninstall', 14 | 'another desc here', 15 | () => { 16 | console.log('uninstall') 17 | }, 18 | ['u', 'rm', 'remove'] 19 | ) 20 | 21 | args.command('cmd', 'cmd desc', () => { 22 | console.log('^~^') 23 | }) 24 | 25 | args.command('binary', 'some desc', ['b']) 26 | args.option(['a', 'abc'], 'something', 'def value') 27 | args.examples([ 28 | { 29 | usage: 'args install -d', 30 | description: 'Run the args command with the option -d' 31 | }, 32 | { 33 | usage: 'args uninstall -d', 34 | description: 'Another description here' 35 | } 36 | ]) 37 | args.parse(process.argv) 38 | -------------------------------------------------------------------------------- /lib/version.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | 6 | /** 7 | * Retrieves the main module package.json information. 8 | * 9 | * @param {string} directory 10 | * The directory to start looking in. 11 | * 12 | * @return {Object|null} 13 | * An object containing the package.json contents or NULL if it could not be found. 14 | */ 15 | function findPackage(directory) { 16 | const file = path.resolve(directory, 'package.json') 17 | if (fs.existsSync(file) && fs.statSync(file).isFile()) { 18 | return require(file) 19 | } 20 | 21 | const parent = path.resolve(directory, '..') 22 | return parent === directory ? null : findPackage(parent) 23 | } 24 | 25 | module.exports = function() { 26 | const pkg = findPackage(path.dirname(process.mainModule.filename)) 27 | const version = (pkg && pkg.version) || '-/-' 28 | 29 | console.log(version) 30 | 31 | if (this.config.exit && this.config.exit.version) { 32 | // eslint-disable-next-line unicorn/no-process-exit 33 | process.exit() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Leonard Lamprecht 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "args", 3 | "version": "5.0.3", 4 | "description": "Minimal toolkit for building CLIs", 5 | "files": [ 6 | "lib" 7 | ], 8 | "main": "lib/index.js", 9 | "scripts": { 10 | "precommit": "lint-staged", 11 | "test": "npm run lint && ava", 12 | "lint": "xo" 13 | }, 14 | "repository": "leo/args", 15 | "engines": { 16 | "node": ">= 6.0.0" 17 | }, 18 | "keywords": [ 19 | "cli", 20 | "command", 21 | "arguments", 22 | "util", 23 | "bin", 24 | "commander", 25 | "nanomist" 26 | ], 27 | "xo": { 28 | "extends": "prettier" 29 | }, 30 | "lint-staged": { 31 | "*.js": [ 32 | "npm run lint", 33 | "prettier --single-quote --write --no-semi", 34 | "git add" 35 | ] 36 | }, 37 | "author": "leo", 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/leo/args/issues" 41 | }, 42 | "homepage": "https://github.com/leo/args#readme", 43 | "devDependencies": { 44 | "ava": "1.2.1", 45 | "eslint-config-prettier": "4.1.0", 46 | "execa": "1.0.0", 47 | "husky": "1.3.1", 48 | "lint-staged": "8.1.5", 49 | "prettier": "1.16.4", 50 | "xo": "0.24.0" 51 | }, 52 | "dependencies": { 53 | "camelcase": "5.0.0", 54 | "chalk": "2.4.2", 55 | "leven": "2.1.0", 56 | "mri": "1.1.4" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const chalk = require('chalk') 4 | const utils = require('./utils') 5 | 6 | const publicMethods = { 7 | option: require('./option'), 8 | options: require('./options'), 9 | command: require('./command'), 10 | parse: require('./parse'), 11 | example: require('./example'), 12 | examples: require('./examples'), 13 | showHelp: require('./help'), 14 | showVersion: require('./version') 15 | } 16 | 17 | function Args() { 18 | this.details = { 19 | options: [], 20 | commands: [], 21 | examples: [] 22 | } 23 | 24 | // Configuration defaults 25 | this.config = { 26 | exit: { help: true, version: true }, 27 | help: true, 28 | version: true, 29 | usageFilter: null, 30 | value: null, 31 | name: null, 32 | mainColor: 'yellow', 33 | subColor: 'dim' 34 | } 35 | 36 | this.printMainColor = chalk 37 | this.printSubColor = chalk 38 | } 39 | 40 | // Assign internal helpers 41 | for (const util in utils) { 42 | if (!{}.hasOwnProperty.call(utils, util)) { 43 | continue 44 | } 45 | 46 | Args.prototype[util] = utils[util] 47 | } 48 | 49 | // Assign public methods 50 | for (const method in publicMethods) { 51 | if (!{}.hasOwnProperty.call(publicMethods, method)) { 52 | continue 53 | } 54 | 55 | Args.prototype[method] = publicMethods[method] 56 | } 57 | 58 | module.exports = new Args() 59 | module.exports.Args = Args; 60 | -------------------------------------------------------------------------------- /lib/option.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function(name, description, defaultValue, init) { 4 | let usage = [] 5 | 6 | const assignShort = (name, options, short) => { 7 | if (options.find(flagName => flagName.usage[0] === short)) { 8 | short = name.charAt(0).toUpperCase() 9 | } 10 | 11 | return [short, name] 12 | } 13 | 14 | // If name is an array, pick the values 15 | // Otherwise just use the whole thing 16 | switch (name.constructor) { 17 | case String: 18 | usage = assignShort(name, this.details.options, name.charAt(0)) 19 | break 20 | case Array: 21 | usage = usage.concat(name) 22 | break 23 | default: 24 | throw new Error('Invalid name for option') 25 | } 26 | 27 | // Throw error if short option is too long 28 | if (usage.length > 0 && usage[0].length > 1) { 29 | throw new Error('Short version of option is longer than 1 char') 30 | } 31 | 32 | const optionDetails = { 33 | defaultValue, 34 | usage, 35 | description 36 | } 37 | 38 | let defaultIsWrong 39 | 40 | switch (defaultValue) { 41 | case false: 42 | defaultIsWrong = true 43 | break 44 | case null: 45 | defaultIsWrong = true 46 | break 47 | case undefined: 48 | defaultIsWrong = true 49 | break 50 | default: 51 | defaultIsWrong = false 52 | } 53 | 54 | if (typeof init === 'function') { 55 | optionDetails.init = init 56 | } else if (!defaultIsWrong) { 57 | // Set initializer depending on type of default value 58 | optionDetails.init = this.handleType(defaultValue)[1] 59 | } 60 | 61 | // Register option to global scope 62 | this.details.options.push(optionDetails) 63 | 64 | // Allow chaining of .option() 65 | return this 66 | } 67 | -------------------------------------------------------------------------------- /lib/help.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function() { 4 | const name = this.config.name || this.binary.replace('-', ' ') 5 | const capitalize = word => word.charAt(0).toUpperCase() + word.substr(1) 6 | 7 | const parts = [] 8 | 9 | const groups = { 10 | commands: true, 11 | options: true, 12 | examples: true 13 | } 14 | 15 | for (const group in groups) { 16 | if (this.details[group].length > 0) { 17 | continue 18 | } 19 | 20 | groups[group] = false 21 | } 22 | 23 | const optionHandle = groups.options ? '[options] ' : '' 24 | const cmdHandle = groups.commands ? '[command]' : '' 25 | const value = 26 | typeof this.config.value === 'string' ? ' ' + this.config.value : '' 27 | 28 | parts.push([ 29 | ` Usage: ${this.printMainColor(name)} ${this.printSubColor( 30 | optionHandle + cmdHandle + value 31 | )}`, 32 | '' 33 | ]) 34 | 35 | for (const group in groups) { 36 | if (!groups[group]) { 37 | continue 38 | } 39 | 40 | parts.push(['', capitalize(group) + ':', '']) 41 | 42 | if (group === 'examples') { 43 | parts.push(this.generateExamples()) 44 | } else { 45 | parts.push(this.generateDetails(group)) 46 | } 47 | 48 | parts.push(['', '']) 49 | } 50 | 51 | let output = '' 52 | 53 | // And finally, merge and output them 54 | for (const part of parts) { 55 | output += part.join('\n ') 56 | } 57 | 58 | if (!groups.commands && !groups.options) { 59 | output = 'No sub commands or options available' 60 | } 61 | 62 | const { usageFilter } = this.config 63 | 64 | // If filter is available, pass usage information through 65 | if (typeof usageFilter === 'function') { 66 | output = usageFilter(output) || output 67 | } 68 | 69 | console.log(output) 70 | 71 | if (this.config.exit && this.config.exit.help) { 72 | // eslint-disable-next-line unicorn/no-process-exit 73 | process.exit() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/parse.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const parser = require('mri') 5 | 6 | module.exports = function(argv, options) { 7 | // Override default option values 8 | Object.assign(this.config, options) 9 | 10 | if (Array.isArray(this.config.mainColor)) { 11 | for (const item in this.config.mainColor) { 12 | if (!{}.hasOwnProperty.call(this.config.mainColor, item)) { 13 | continue 14 | } 15 | 16 | // Chain all colors to our print method 17 | this.printMainColor = this.printMainColor[this.config.mainColor[item]] 18 | } 19 | } else { 20 | this.printMainColor = this.printMainColor[this.config.mainColor] 21 | } 22 | 23 | if (Array.isArray(this.config.subColor)) { 24 | for (const item in this.config.subColor) { 25 | if (!{}.hasOwnProperty.call(this.config.subColor, item)) { 26 | continue 27 | } 28 | 29 | // Chain all colors to our print method 30 | this.printSubColor = this.printSubColor[this.config.subColor[item]] 31 | } 32 | } else { 33 | this.printSubColor = this.printSubColor[this.config.subColor] 34 | } 35 | 36 | // Parse arguments using mri 37 | this.raw = parser(argv.slice(1), this.config.mri || this.config.minimist) 38 | this.binary = path.basename(this.raw._[0]) 39 | 40 | // If default version is allowed, check for it 41 | if (this.config.version) { 42 | this.checkVersion() 43 | } 44 | 45 | // If default help is allowed, check for it 46 | if (this.config.help) { 47 | this.checkHelp() 48 | } 49 | 50 | const subCommand = this.raw._[1] 51 | const args = {} 52 | const defined = this.isDefined(subCommand, 'commands') 53 | const optionList = this.getOptions(defined) 54 | 55 | Object.assign(args, this.raw) 56 | args._.shift() 57 | 58 | // Export sub arguments of command 59 | this.sub = args._ 60 | 61 | // If sub command is defined, run it 62 | if (defined) { 63 | this.runCommand(defined, optionList) 64 | return {} 65 | } 66 | 67 | // Hand back list of options 68 | return optionList 69 | } 70 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | // Native 2 | import path from 'path' 3 | 4 | // Packages 5 | import test from 'ava' 6 | import execa from 'execa' 7 | 8 | // Ours 9 | import args from '../lib' 10 | import { version } from '../package' 11 | 12 | // Provide a reset function during testing. 13 | args.reset = function() { 14 | this.details = { 15 | options: [], 16 | commands: [], 17 | examples: [] 18 | } 19 | return this 20 | } 21 | 22 | // Provide a helper function for suppressing any output. 23 | args.suppressOutput = function(fn) { 24 | const original = process.stdout.write 25 | process.stdout.write = () => () => {} 26 | const result = fn.call(this) 27 | process.stdout.write = original 28 | return result 29 | } 30 | 31 | const port = 8000 32 | 33 | const argv = [ 34 | 'node', 35 | 'foo', 36 | '-p', 37 | port.toString(), 38 | '--data', 39 | '--D', 40 | 'D', 41 | '-a', 42 | 'anotheroptionvalue', 43 | '-l', 44 | 10 45 | ] 46 | 47 | // Reset args after each test. 48 | test.afterEach.always(() => { 49 | args.reset() 50 | }) 51 | 52 | // @todo Each of these options should be broken out into separate tests. 53 | function setupOptions() { 54 | args 55 | .option('port', 'The port on which the site will run') 56 | .option('true', 'Boolean', true) 57 | .option('list', 'List', []) 58 | .option(['d', 'data'], 'The data that shall be used') 59 | .option('duplicated', 'Duplicated first char in option') 60 | .options([{ name: 'anotheroption', description: 'another option' }]) 61 | return args 62 | } 63 | 64 | // @todo Each of these options should be broken out into separate tests. 65 | test('options', t => { 66 | const args = setupOptions() 67 | 68 | const config = args.parse(argv) 69 | 70 | for (const property in config) { 71 | if (!{}.hasOwnProperty.call(config, property)) { 72 | continue 73 | } 74 | 75 | const content = config[property] 76 | 77 | switch (content) { 78 | case 'D': 79 | t.is(content, 'D') 80 | break 81 | case version: 82 | t.is(content, version) 83 | break 84 | case 8000: 85 | t.is(content, port) 86 | break 87 | case 'anotheroptionvalue': 88 | if (property === 'a') { 89 | t.is(property, 'a') 90 | } else { 91 | t.is(property, 'anotheroption') 92 | } 93 | 94 | break 95 | default: 96 | if (content.constructor === Array) { 97 | t.deepEqual(content, [10]) 98 | } else { 99 | t.true(content) 100 | } 101 | } 102 | } 103 | }) 104 | 105 | test('help/host: only host is triggered', t => { 106 | args.option('host', 'The host address') 107 | const config = args.parse(['node', 'foo', '-h', 'http://example.com']) 108 | t.is(config.h, 'http://example.com') 109 | t.is(config.host, 'http://example.com') 110 | t.falsy(config.H) 111 | t.falsy(config.help) 112 | }) 113 | 114 | test('help/host: only help is triggered', t => { 115 | args.option('host', 'The host address') 116 | const config = args.suppressOutput(() => 117 | args.parse(['node', 'foo', '-H'], { exit: { help: false } }) 118 | ) 119 | t.falsy(config.h) 120 | t.falsy(config.host) 121 | t.true(config.H) 122 | t.true(config.help) 123 | }) 124 | 125 | test('version/verbose: only verbose is triggered', t => { 126 | args.option('verbose', 'Verbose output') 127 | const config = args.parse(['node', 'foo', '-v']) 128 | t.true(config.v) 129 | t.true(config.verbose) 130 | t.falsy(config.H) 131 | t.falsy(config.help) 132 | }) 133 | 134 | test('version/verbose: only version is triggered', t => { 135 | args.option('verbose', 'Verbose output') 136 | const config = args.suppressOutput(() => 137 | args.parse(['node', 'foo', '-V'], { exit: { version: false } }) 138 | ) 139 | t.falsy(config.v) 140 | t.falsy(config.verbose) 141 | t.true(config.V) 142 | t.true(config.version) 143 | }) 144 | 145 | test('usage information', t => { 146 | const args = setupOptions() 147 | const filter = data => data 148 | 149 | args.parse(argv, { 150 | value: '', 151 | usageFilter: filter 152 | }) 153 | 154 | const runner = args.config.usageFilter 155 | const value = 'a test' 156 | 157 | t.is(runner(value), value) 158 | }) 159 | 160 | test('config', t => { 161 | const args = setupOptions() 162 | args.parse(argv, { 163 | help: false, 164 | errors: false 165 | }) 166 | 167 | t.true(args.config.version) 168 | t.false(args.config.help) 169 | t.false(args.config.errors) 170 | }) 171 | 172 | function run(command) { 173 | return execa.stdout('node', [path.join(__dirname, '_fixture'), command]) 174 | } 175 | 176 | test('command aliases', async t => { 177 | let result = await run('install') 178 | t.is(result, 'install') 179 | 180 | result = await run('i') 181 | t.is(result, 'install') 182 | 183 | result = await run('rm') 184 | t.is(result, 'uninstall') 185 | 186 | result = await run('cmd') 187 | t.is(result, '^~^') 188 | 189 | try { 190 | await run('b') 191 | } catch (error) { 192 | t.regex(error.message, /_fixture-binary/gm) 193 | } 194 | 195 | result = await run('help') 196 | const regexes = [/binary, b/, /cmd/, /-a, --abc \[value]/] 197 | for (const regex of regexes) { 198 | t.regex(result, regex) 199 | } 200 | }) 201 | 202 | test('options propogated to mri', t => { 203 | const args = setupOptions() 204 | args.option('port', 'The port on which the site will run') 205 | 206 | const config = args.parse(argv, { mri: { string: 'p' } }) 207 | 208 | t.is(config.port, port.toString()) 209 | }) 210 | 211 | test('short form option works with mri default', t => { 212 | const args = setupOptions() 213 | args.option('port', 'The port on which the site will run') 214 | 215 | const config = args.parse(argv, { mri: { default: { port: 3000 } } }) 216 | 217 | t.is(config.port, port) 218 | }) 219 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # args 2 | 3 | [![Build Status](https://travis-ci.org/leo/args.svg?branch=master)](https://travis-ci.org/leo/args) 4 | [![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo) 5 | 6 | This package makes creating command line interfaces a breeze. 7 | 8 | ## Features 9 | 10 | - Git-style sub commands (e.g. `pizza cheese` executes the "pizza-cheese" binary) 11 | - Auto-generated usage information 12 | - Determines type of option by checking type of default value (e.g. `['hi']` => ``) 13 | - Clean [syntax](#usage) for defining options and commands 14 | - Easily [retrieve](#usage) values of options 15 | - Automatically suggests a similar option, if the user entered an unknown one 16 | 17 | ## Usage 18 | 19 | Install the package (you'll need at least version 6.0.0 of [Node](https://nodejs.org/en/)): 20 | 21 | ```bash 22 | npm install --save args 23 | ``` 24 | 25 | Once you're done, you can start using it within your binaries: 26 | 27 | ```js 28 | #!/usr/bin/env node 29 | 30 | const args = require('args') 31 | 32 | args 33 | .option('port', 'The port on which the app will be running', 3000) 34 | .option('reload', 'Enable/disable livereloading') 35 | .command('serve', 'Serve your static site', ['s']) 36 | 37 | const flags = args.parse(process.argv) 38 | ``` 39 | 40 | The upper code defines two options called "port" and "reload" for the current binary, as well as a new sub command named "serve". So if you want to check for the value of the "port" option, just do this: 41 | 42 | ```js 43 | // This also works with "flags.p", because the short name of the "port" option is "p" 44 | 45 | if (flags.port) { 46 | console.log(`I'll be running on port ${flags.port}`) 47 | } 48 | ``` 49 | 50 | In turn, this is how the auto-generated usage information will look like: 51 | 52 | ``` 53 | 54 | Usage: haha [options] [command] 55 | 56 | 57 | Commands: 58 | 59 | serve, s Serve your static site 60 | help Display help 61 | 62 | Options: 63 | 64 | -v, --version Output the version number 65 | -r, --reload Enable/disable livereloading 66 | -h, --help Output usage information 67 | -p, --port The port on which the app will be running 68 | 69 | ``` 70 | 71 | ## API 72 | 73 | ### .option(name, description, default, init) 74 | 75 | Register a new option for the binary in which it's being called. 76 | 77 | - **name:** Takes a string which defines the name of the option. In this case, the first letter will be used as the short version (`port` => `-p, --port`). However, it can also be an array in which the first value defines the short version (`p` => `-p`) and the second one the long version (`packages` => `--packages`). 78 | - **description:** A short explanation of what the option shall be used for. Will be outputted along with help. 79 | - **default:** If it's defined, args will not only use it as a default value for the property, but it will also determine the type and append it to the usage info when the help gets outputted. For example: If the default param of an option named "package" contains an array, the usage information will look like this: `-p, --package `. 80 | - **init:** A function through which the option's value will be passed when used. The first paramater within said function will contain the option's value. If the parameter "default" is defined, args will provide a default initializer depending on the type of its value. For example: If "default" contains an integer, "init" will be `parseInt`. 81 | 82 | ### .options(list) 83 | 84 | Takes in an array of objects that are each defining an option that shall be registered. This is basically a minimalistic way to register a huge list of options at once. Here's what each option object needs to look like: 85 | 86 | ```js 87 | { 88 | name: 'port', 89 | description: 'The port on which the app runs', 90 | init: content => content, 91 | defaultValue: 3000 92 | } 93 | ``` 94 | 95 | However, the keys `init` and `defaultValue` are not strictly required. 96 | 97 | ### .command(name, description, init, aliases) 98 | 99 | Register a new sub command. Args requires all binaries to be defined in the style of git's. That means each sub command should be a separate binary called "<parent-command>-<sub-command>". 100 | 101 | For example: If your main binary is called "muffin", the binary of the subcommand "muffin list" should be called "muffin-list". And all of them should be defined as such in your [package.json](https://docs.npmjs.com/files/package.json#bin). 102 | 103 | - **name:** Takes a string which defines the name of the command. This value will be used when outputting the help. 104 | - **description:** A short explanation of what the command shall be used for. Will be outputted along with help. 105 | - **init:** If a function was passed through at this parameter, args will call it instead of running the binary related to that command. The function receives three arguments: 106 | 107 | ```js 108 | function aCommand (name, sub, options) { 109 | name // The name of the command 110 | sub // The output of .sub 111 | options // An object containing the options that have been used 112 | } 113 | ``` 114 | 115 | Using an initializer is currently only recommended if your command doesn't need special/different options than the binary in which you're defining it. The reason for this is that the "options" argument of the upper function will contain the options registered within the current binary. 116 | 117 | - **aliases:** Takes in an array of aliases which can be used to run the command. 118 | 119 | ### .example(usage, description) 120 | 121 | Register an example which will be shown when calling `help` 122 | 123 | - **usage:** Takes a string which defines your usage example command 124 | - **description:** A short explanation of what the example shall be used for. Will be outputted along with help. 125 | 126 | ### .examples(list) 127 | Takes in an array of objects that are each defining an example that shall be registered. This is basically a minimalistic way to register a huge list of examples at once. Here's what each option object needs to look like: 128 | 129 | ```js 130 | { 131 | usage: 'args command -d', 132 | description: 'Run the args command with the option -d' 133 | } 134 | ``` 135 | 136 | ### .parse(argv, options) 137 | 138 | This method takes the process' command line arguments (command and options) and uses the internal methods to get their values and assign them to the current instance of args. It needs to be run after all of the `.option` and `.command` calls. If you run it before them, the method calls after it won't take effect. 139 | 140 | The methods also returns all options that have been used and their respective values. 141 | 142 | - **argv:** Should be the process' argv: `process.argv`, for example. 143 | - **options:** This parameter accepts an object containing several [configuration options](#configuration). 144 | 145 | ### .sub 146 | 147 | This property exposes all sub arguments that have been parsed by [mri](https://npmjs.com/mri). This is useful when trying to get the value after the command, for example: 148 | 149 | ```bash 150 | pizza ./directory 151 | ``` 152 | 153 | The upper path can now be loaded by doing: 154 | 155 | ```js 156 | // Contains "./directory" 157 | const path = args.sub[0] 158 | ``` 159 | 160 | This also works completely fine with sub commands: After you've registered a new command using `.command()`, you can easily check the following sub argument within its binary like mentioned above: 161 | 162 | ```bash 163 | pizza eat ./directory 164 | ``` 165 | 166 | ### .showHelp() 167 | 168 | Outputs the usage information based on the options and comments you've registered so far and exits, if configured to do so. 169 | 170 | ### .showVersion() 171 | 172 | Outputs the version and exits, if configured to do so. 173 | 174 | ## Configuration 175 | 176 | 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: 177 | 178 | | Property | Description | Default value | Type | 179 | | -------- | ----------- | ------------------ | ---- | 180 | | exit | Automatically exits when help or version is rendered | `{ help: true, version: true }` | Object | 181 | | help | Automatically render the usage information when running `help`, `-h` or `--help` | true | Boolean | 182 | | name | The name of your program to display in help | Name of script file | String | 183 | | version | Outputs the version tag of your package.json | true | Boolean | 184 | | usageFilter | Allows you to specify a filter through which the usage information will be passed before it gets outputted | null | Function | 185 | | value | Suffix for the "Usage" section of the usage information ([example](https://github.com/leo/args/issues/13)) | null | String | 186 | | mri | Additional parsing options to pass to mri, see [mri docs](https://github.com/lukeed/mri) for details | undefined | Object | 187 | | mainColor | Specify the main color for the output when running the `help` command. See [chalk docs](https://github.com/chalk/chalk) for available colors / modifiers. You can specify multiple colors / modifiers with an array. For example: `{mainColor: ['red', 'bold', 'underline']}` | yellow | String[Array] | 188 | | subColor | Specify the sub color for the output when running the `help` command. See [chalk docs](https://github.com/chalk/chalk) for available colors / modifiers. You can specify multiple colors / modifiers with an array. For example: `{subColor: ['dim', 'blue']}` | dim | String[Array] | 189 | 190 | You can pass the configuration object as the second paramater of [.parse()](#parseargv-options). 191 | 192 | ## Contribute 193 | 194 | 1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device 195 | 2. Link the package to the global module directory: `npm link` 196 | 3. Within the module you want to test your local development instance of args, just link it to the dependencies: `npm link args`. Instead of the default one from npm, node will now use your clone of args! 197 | 198 | As always, you can run the [AVA](https://github.com/sindresorhus/ava) and [ESLint](http://eslint.org) tests using: `npm test` 199 | 200 | ## Special thanks 201 | 202 | ... to [Dmitry Smolin](https://github.com/dimsmol) who donated the package name. If you're looking for the old content (before I've added my stuff) of the package, you can find it [here](https://github.com/dimsmol/args). 203 | 204 | ## Authors 205 | 206 | - Leo Lamprecht ([@notquiteleo](https://twitter.com/notquiteleo)) 207 | - Marvin Mieth ([@ntwcklng](https://twitter.com/ntwcklng)) 208 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { spawn } = require('child_process') 4 | const path = require('path') 5 | const camelcase = require('camelcase') 6 | const leven = require('leven') 7 | 8 | function similarityBestMatch(mainString, targetStrings) { 9 | let bestMatch 10 | const ratings = targetStrings.map(targetString => { 11 | const score = leven(mainString, targetString) 12 | 13 | const res = { 14 | target: targetString, 15 | rating: leven(mainString, targetString) 16 | } 17 | 18 | if (!bestMatch || score < bestMatch.rating) bestMatch = res 19 | 20 | return res 21 | }) 22 | 23 | return { 24 | ratings, 25 | bestMatch 26 | } 27 | } 28 | 29 | module.exports = { 30 | handleType(value) { 31 | let type = value 32 | if (typeof value !== 'function') { 33 | type = value.constructor 34 | } 35 | 36 | // Depending on the type of the default value, 37 | // select a default initializer function 38 | switch (type) { 39 | case String: 40 | return ['[value]'] 41 | case Array: 42 | return [''] 43 | case Number: 44 | case parseInt: 45 | return ['', parseInt] 46 | default: 47 | return [''] 48 | } 49 | }, 50 | 51 | readOption(option) { 52 | let value = option.defaultValue 53 | const contents = {} 54 | 55 | // If option has been used, get its value 56 | for (const name of option.usage) { 57 | const fromArgs = this.raw[name] 58 | if (typeof fromArgs !== 'undefined') { 59 | value = fromArgs 60 | break 61 | } 62 | } 63 | 64 | // Process the option's value 65 | for (let name of option.usage) { 66 | let propVal = value 67 | 68 | // Convert the value to an array when the option is called just once 69 | if ( 70 | Array.isArray(option.defaultValue) && 71 | typeof propVal !== typeof option.defaultValue 72 | ) { 73 | propVal = [propVal] 74 | } 75 | 76 | if ( 77 | typeof option.defaultValue !== 'undefined' && 78 | typeof propVal !== typeof option.defaultValue 79 | ) { 80 | propVal = option.defaultValue 81 | } 82 | 83 | let condition = true 84 | 85 | if (option.init) { 86 | // Only use the toString initializer if value is a number 87 | if (option.init === toString) { 88 | condition = propVal.constructor === Number 89 | } 90 | 91 | if (condition) { 92 | // Pass it through the initializer 93 | propVal = option.init(propVal) 94 | } 95 | } 96 | 97 | // Camelcase option name (skip short flag) 98 | if (name.length > 1) { 99 | name = camelcase(name) 100 | } 101 | 102 | // Add option to list 103 | contents[name] = propVal 104 | } 105 | 106 | return contents 107 | }, 108 | 109 | getOptions(definedSubcommand) { 110 | const options = {} 111 | const args = {} 112 | 113 | // Copy over the arguments 114 | Object.assign(args, this.raw) 115 | delete args._ 116 | 117 | // Set option defaults 118 | for (const option of this.details.options) { 119 | if (typeof option.defaultValue === 'undefined') { 120 | continue 121 | } 122 | 123 | Object.assign(options, this.readOption(option)) 124 | } 125 | 126 | // Override defaults if used in command line 127 | for (const option in args) { 128 | if (!{}.hasOwnProperty.call(args, option)) { 129 | continue 130 | } 131 | 132 | const related = this.isDefined(option, 'options') 133 | 134 | if (related) { 135 | const details = this.readOption(related) 136 | Object.assign(options, details) 137 | } 138 | 139 | if (!related && !definedSubcommand) { 140 | // Unknown Option 141 | const availableOptions = [] 142 | this.details.options.forEach(opt => { 143 | availableOptions.push(...opt.usage) 144 | }) 145 | 146 | const suggestOption = similarityBestMatch(option, availableOptions) 147 | 148 | process.stdout.write(`The option "${option}" is unknown.`) 149 | 150 | if (suggestOption.bestMatch.rating >= 0.5) { 151 | process.stdout.write(' Did you mean the following one?\n') 152 | 153 | const suggestion = this.details.options.filter(item => { 154 | for (const flag of item.usage) { 155 | if (flag === suggestOption.bestMatch.target) { 156 | return true 157 | } 158 | } 159 | 160 | return false 161 | }) 162 | 163 | process.stdout.write( 164 | this.generateDetails(suggestion)[0].trim() + '\n' 165 | ) 166 | 167 | // eslint-disable-next-line unicorn/no-process-exit 168 | process.exit() 169 | } else { 170 | process.stdout.write(` Here's a list of all available options: \n`) 171 | this.showHelp() 172 | } 173 | } 174 | } 175 | 176 | return options 177 | }, 178 | 179 | generateExamples() { 180 | const { examples } = this.details 181 | const parts = [] 182 | 183 | for (const item in examples) { 184 | if (!{}.hasOwnProperty.call(examples, item)) { 185 | continue 186 | } 187 | 188 | const usage = this.printSubColor('$ ' + examples[item].usage) 189 | const description = this.printMainColor('- ' + examples[item].description) 190 | parts.push(` ${description}\n ${usage}\n`) 191 | } 192 | 193 | return parts 194 | }, 195 | 196 | generateDetails(kind) { 197 | // Get all properties of kind from global scope 198 | const items = [] 199 | 200 | // Clone passed objects so changing them here doesn't affect real data. 201 | const passed = [].concat( 202 | typeof kind === 'string' ? this.details[kind] : kind 203 | ) 204 | for (let i = 0, l = passed.length; i < l; i++) { 205 | items.push(Object.assign({}, passed[i])) 206 | } 207 | 208 | const parts = [] 209 | const isCmd = kind === 'commands' 210 | 211 | // Sort items alphabetically 212 | items.sort((a, b) => { 213 | const first = isCmd ? a.usage : a.usage[1] 214 | const second = isCmd ? b.usage : b.usage[1] 215 | 216 | switch (true) { 217 | case first < second: 218 | return -1 219 | case first > second: 220 | return 1 221 | default: 222 | return 0 223 | } 224 | }) 225 | 226 | for (const item in items) { 227 | if (!{}.hasOwnProperty.call(items, item)) { 228 | continue 229 | } 230 | 231 | let { usage } = items[item] 232 | let initial = items[item].defaultValue 233 | 234 | // If usage is an array, show its contents 235 | if (usage.constructor === Array) { 236 | if (isCmd) { 237 | usage = usage.join(', ') 238 | } else { 239 | const isVersion = usage.indexOf('v') 240 | usage = `-${usage[0]}, --${usage[1]}` 241 | 242 | if (!initial) { 243 | initial = items[item].init 244 | } 245 | 246 | usage += 247 | initial && isVersion === -1 ? ' ' + this.handleType(initial)[0] : '' 248 | } 249 | } 250 | 251 | // Overwrite usage with readable syntax 252 | items[item].usage = usage 253 | } 254 | 255 | // Find length of longest option or command 256 | // Before doing that, make a copy of the original array 257 | const longest = items.slice().sort((a, b) => { 258 | return b.usage.length - a.usage.length 259 | })[0].usage.length 260 | 261 | for (const item of items) { 262 | let { usage, description, defaultValue } = item 263 | const difference = longest - usage.length 264 | 265 | // Compensate the difference to longest property with spaces 266 | usage += ' '.repeat(difference) 267 | 268 | // Add some space around it as well 269 | if (typeof defaultValue !== 'undefined') { 270 | if (typeof defaultValue === 'boolean') { 271 | description += ` (${ 272 | defaultValue ? 'enabled' : 'disabled' 273 | } by default)` 274 | } else { 275 | description += ` (defaults to ${JSON.stringify(defaultValue)})` 276 | } 277 | } 278 | 279 | parts.push( 280 | ' ' + 281 | this.printMainColor(usage) + 282 | ' ' + 283 | this.printSubColor(description) 284 | ) 285 | } 286 | 287 | return parts 288 | }, 289 | 290 | runCommand(details, options) { 291 | // If help is disabled, remove initializer 292 | if (details.usage === 'help' && !this.config.help) { 293 | details.init = false 294 | } 295 | 296 | // If version is disabled, remove initializer 297 | if (details.usage === 'version' && !this.config.version) { 298 | details.init = false 299 | } 300 | 301 | // If command has initializer, call it 302 | if (details.init) { 303 | const sub = [].concat(this.sub) 304 | sub.shift() 305 | 306 | return details.init.bind(this)(details.usage, sub, options) 307 | } 308 | 309 | // Generate full name of binary 310 | const subCommand = Array.isArray(details.usage) 311 | ? details.usage[0] 312 | : details.usage 313 | let full = this.binary + '-' + subCommand 314 | 315 | // Remove node and original command. 316 | const args = process.argv.slice(2) 317 | 318 | // Remove the first occurance of subCommand from the args. 319 | for (let i = 0, l = args.length; i < l; i++) { 320 | if (args[i] === subCommand) { 321 | args.splice(i, 1) 322 | break 323 | } 324 | } 325 | 326 | if (process.platform === 'win32') { 327 | const binaryExt = path.extname(this.binary) 328 | const mainModule = process.env.APPVEYOR 329 | ? '_fixture' 330 | : process.mainModule.filename 331 | 332 | full = `${mainModule}-${subCommand}` 333 | 334 | if (path.extname(this.binary)) { 335 | full = `${mainModule.replace(binaryExt, '')}-${subCommand}${binaryExt}` 336 | } 337 | 338 | // Run binary of sub command on windows 339 | args.unshift(full) 340 | this.child = spawn(process.execPath, args, { 341 | stdio: 'inherit' 342 | }) 343 | } else { 344 | // Run binary of sub command 345 | this.child = spawn(full, args, { 346 | stdio: 'inherit' 347 | }) 348 | } 349 | 350 | // Throw an error if something fails within that binary 351 | this.child.on('error', err => { 352 | throw err 353 | }) 354 | 355 | this.child.on('exit', (code, signal) => { 356 | process.on('exit', () => { 357 | this.child = null 358 | if (signal) { 359 | process.kill(process.pid, signal) 360 | } else { 361 | process.exit(code) 362 | } 363 | }) 364 | }) 365 | 366 | // Proxy SIGINT to child process 367 | process.on('SIGINT', () => { 368 | if (this.child) { 369 | this.child.kill('SIGINT') 370 | this.child.kill('SIGTERM') // If that didn't work, we're probably in an infinite loop, so make it die 371 | } 372 | }) 373 | }, 374 | 375 | checkHelp() { 376 | // Register default option and command. 377 | this.option('help', 'Output usage information') 378 | this.command('help', 'Display help', this.showHelp) 379 | 380 | // Immediately output if option was provided. 381 | if (this.optionWasProvided('help')) { 382 | this.showHelp() 383 | } 384 | }, 385 | 386 | checkVersion() { 387 | // Register default option and command. 388 | this.option('version', 'Output the version number') 389 | this.command('version', 'Display version', this.showVersion) 390 | 391 | // Immediately output if option was provided. 392 | if (this.optionWasProvided('version')) { 393 | this.showVersion() 394 | } 395 | }, 396 | 397 | isDefined(name, list) { 398 | // Get all items of kind 399 | const children = this.details[list] 400 | 401 | // Check if a child matches the requested name 402 | for (const child of children) { 403 | const { usage } = child 404 | const type = usage.constructor 405 | 406 | if (type === Array && usage.indexOf(name) > -1) { 407 | return child 408 | } 409 | 410 | if (type === String && usage === name) { 411 | return child 412 | } 413 | } 414 | 415 | // If nothing matches, item is not defined 416 | return false 417 | }, 418 | 419 | optionWasProvided(name) { 420 | const option = this.isDefined(name, 'options') 421 | return option && (this.raw[option.usage[0]] || this.raw[option.usage[1]]) 422 | } 423 | } 424 | --------------------------------------------------------------------------------