├── .npmignore ├── .nycrc ├── .eslintrc ├── internal ├── util.js ├── errors.js ├── validators.js └── primordials.js ├── test ├── utils.js ├── find-long-option-for-short.js ├── is-lone-short-option.js ├── dash.js ├── short-option-groups.js ├── is-lone-long-option.js ├── is-option-value.js ├── is-long-option-and-value.js ├── is-short-option-and-value.js ├── store-user-intent.js ├── is-option-like-value.js ├── is-short-option-group.js ├── prototype-pollution.js ├── short-option-combined-with-value.js ├── allow-negative.js ├── default-values.js └── index.js ├── .editorconfig ├── examples ├── simple-hard-coded.js ├── is-default-value.js ├── no-repeated-options.js ├── limit-long-syntax.js ├── negate.js ├── ordered-options.mjs └── optional-value.mjs ├── package.json ├── .github └── workflows │ ├── ci.yaml │ └── release.yml ├── LICENSE ├── CONTRIBUTING.md ├── .gitignore ├── utils.js ├── CHANGELOG.md ├── index.js └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | coverage 2 | test 3 | .nycrc 4 | .eslintrc 5 | .github 6 | CONTRIBUTING.md 7 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "reporter": [ 3 | "html", 4 | "text" 5 | ], 6 | "lines": 95, 7 | "branches": "80", 8 | "statements": "95" 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:eslint-plugin-node-core/recommended"], 3 | "env": { 4 | "node": true, 5 | "es6": true 6 | }, 7 | "rules": { 8 | "linebreak-style": 0 9 | }, 10 | "ignorePatterns": ["README.md"] 11 | } 12 | -------------------------------------------------------------------------------- /internal/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a placeholder for util.js in node.js land. 4 | 5 | const { 6 | ObjectCreate, 7 | ObjectFreeze, 8 | } = require('./primordials'); 9 | 10 | const kEmptyObject = ObjectFreeze(ObjectCreate(null)); 11 | 12 | module.exports = { 13 | kEmptyObject, 14 | }; 15 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tape = require('tape'); 4 | 5 | module.exports = { 6 | test: (description, body) => { 7 | tape(description, (t) => { 8 | t.deepStrictEqual = t.deepEqual; 9 | global.assert = t; 10 | body(); 11 | t.end(); 12 | }); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Copied from Node.js to ease compatibility in PR. 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | indent_size = 2 11 | indent_style = space 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | quote_type = single 15 | -------------------------------------------------------------------------------- /examples/simple-hard-coded.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This example is used in the documentation. 4 | 5 | // 1. const { parseArgs } = require('node:util'); // from node 6 | // 2. const { parseArgs } = require('@pkgjs/parseargs'); // from package 7 | const { parseArgs } = require('..'); // in repo 8 | 9 | const args = ['-f', '--bar', 'b']; 10 | const options = { 11 | foo: { 12 | type: 'boolean', 13 | short: 'f' 14 | }, 15 | bar: { 16 | type: 'string' 17 | } 18 | }; 19 | const { 20 | values, 21 | positionals 22 | } = parseArgs({ args, options }); 23 | console.log(values, positionals); 24 | 25 | // Try the following: 26 | // node simple-hard-coded.js 27 | -------------------------------------------------------------------------------- /test/find-long-option-for-short.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint max-len: 0 */ 3 | 4 | const test = require('tape'); 5 | const { findLongOptionForShort } = require('../utils.js'); 6 | 7 | test('findLongOptionForShort: when passed empty options then returns short', (t) => { 8 | t.equal(findLongOptionForShort('a', {}), 'a'); 9 | t.end(); 10 | }); 11 | 12 | test('findLongOptionForShort: when passed short not present in options then returns short', (t) => { 13 | t.equal(findLongOptionForShort('a', { foo: { short: 'f', type: 'string' } }), 'a'); 14 | t.end(); 15 | }); 16 | 17 | test('findLongOptionForShort: when passed short present in options then returns long', (t) => { 18 | t.equal(findLongOptionForShort('a', { alpha: { short: 'a' } }), 'alpha'); 19 | t.end(); 20 | }); 21 | -------------------------------------------------------------------------------- /examples/is-default-value.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This example shows how to understand if a default value is used or not. 4 | 5 | // 1. const { parseArgs } = require('node:util'); // from node 6 | // 2. const { parseArgs } = require('@pkgjs/parseargs'); // from package 7 | const { parseArgs } = require('..'); // in repo 8 | 9 | const options = { 10 | file: { short: 'f', type: 'string', default: 'FOO' }, 11 | }; 12 | 13 | const { values, tokens } = parseArgs({ options, tokens: true }); 14 | 15 | const isFileDefault = !tokens.some((token) => token.kind === 'option' && 16 | token.name === 'file' 17 | ); 18 | 19 | console.log(values); 20 | console.log(`Is the file option [${values.file}] the default value? ${isFileDefault}`); 21 | 22 | // Try the following: 23 | // node is-default-value.js 24 | // node is-default-value.js -f FILE 25 | // node is-default-value.js --file FILE 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pkgjs/parseargs", 3 | "version": "0.11.0", 4 | "description": "Polyfill of future proposal for `util.parseArgs()`", 5 | "engines": { 6 | "node": ">=14" 7 | }, 8 | "main": "index.js", 9 | "exports": { 10 | ".": "./index.js", 11 | "./package.json": "./package.json" 12 | }, 13 | "scripts": { 14 | "coverage": "c8 --check-coverage tape 'test/*.js'", 15 | "test": "c8 tape 'test/*.js'", 16 | "posttest": "eslint .", 17 | "fix": "npm run posttest -- --fix" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git@github.com:pkgjs/parseargs.git" 22 | }, 23 | "keywords": [], 24 | "author": "", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/pkgjs/parseargs/issues" 28 | }, 29 | "homepage": "https://github.com/pkgjs/parseargs#readme", 30 | "devDependencies": { 31 | "c8": "^7.10.0", 32 | "eslint": "^8.2.0", 33 | "eslint-plugin-node-core": "iansu/eslint-plugin-node-core", 34 | "tape": "^5.2.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/no-repeated-options.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is an example of using tokens to add a custom behaviour. 4 | // 5 | // Throw an error if an option is used more than once. 6 | 7 | // 1. const { parseArgs } = require('node:util'); // from node 8 | // 2. const { parseArgs } = require('@pkgjs/parseargs'); // from package 9 | const { parseArgs } = require('..'); // in repo 10 | 11 | const options = { 12 | ding: { type: 'boolean', short: 'd' }, 13 | beep: { type: 'boolean', short: 'b' } 14 | }; 15 | const { values, tokens } = parseArgs({ options, tokens: true }); 16 | 17 | const seenBefore = new Set(); 18 | tokens.forEach((token) => { 19 | if (token.kind !== 'option') return; 20 | if (seenBefore.has(token.name)) { 21 | throw new Error(`option '${token.name}' used multiple times`); 22 | } 23 | seenBefore.add(token.name); 24 | }); 25 | 26 | console.log(values); 27 | 28 | // Try the following: 29 | // node no-repeated-options --ding --beep 30 | // node no-repeated-options --beep -b 31 | // node no-repeated-options -ddd 32 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | types: [ assigned, opened, synchronize, reopened, labeled ] 7 | name: ci 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | node: [14, 16, 18, 20, 22] 14 | steps: 15 | - uses: actions/checkout@v1 16 | - uses: actions/setup-node@v1 17 | with: 18 | node-version: ${{ matrix.node }} 19 | - run: node --version 20 | - run: npm install --engine-strict 21 | - run: npm test 22 | windows: 23 | runs-on: windows-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions/setup-node@v1 27 | with: 28 | node-version: 16 29 | - run: npm install 30 | - run: npm test 31 | coverage: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v2 35 | - uses: actions/setup-node@v1 36 | with: 37 | node-version: 16 38 | - run: npm install 39 | - run: npm run coverage 40 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | name: release-please 6 | jobs: 7 | release-please: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: google-github-actions/release-please-action@v3 11 | id: release 12 | with: 13 | token: ${{ secrets.GITHUB_TOKEN }} 14 | release-type: node 15 | bump-minor-pre-major: true 16 | # The logic below handles the npm publication: 17 | - uses: actions/checkout@v2 18 | # these if statements ensure that a publication only occurs when 19 | # a new release is created: 20 | if: ${{ steps.release.outputs.release_created }} 21 | - uses: actions/setup-node@v1 22 | with: 23 | node-version: 16 24 | registry-url: 'https://external-dot-oss-automation.appspot.com' 25 | if: ${{ steps.release.outputs.release_created }} 26 | - run: npm publish --access=public 27 | env: 28 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 29 | if: ${{ steps.release.outputs.release_created }} 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Darcy Clarke and contributors 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 | -------------------------------------------------------------------------------- /examples/limit-long-syntax.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is an example of using tokens to add a custom behaviour. 4 | // 5 | // Require the use of `=` for long options and values by blocking 6 | // the use of space separated values. 7 | // So allow `--foo=bar`, and not allow `--foo bar`. 8 | // 9 | // Note: this is not a common behaviour, most CLIs allow both forms. 10 | 11 | // 1. const { parseArgs } = require('node:util'); // from node 12 | // 2. const { parseArgs } = require('@pkgjs/parseargs'); // from package 13 | const { parseArgs } = require('..'); // in repo 14 | 15 | const options = { 16 | file: { short: 'f', type: 'string' }, 17 | log: { type: 'string' }, 18 | }; 19 | 20 | const { values, tokens } = parseArgs({ options, tokens: true }); 21 | 22 | const badToken = tokens.find((token) => token.kind === 'option' && 23 | token.value != null && 24 | token.rawName.startsWith('--') && 25 | !token.inlineValue 26 | ); 27 | if (badToken) { 28 | throw new Error(`Option value for '${badToken.rawName}' must be inline, like '${badToken.rawName}=VALUE'`); 29 | } 30 | 31 | console.log(values); 32 | 33 | // Try the following: 34 | // node limit-long-syntax.js -f FILE --log=LOG 35 | // node limit-long-syntax.js --file FILE 36 | -------------------------------------------------------------------------------- /test/is-lone-short-option.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint max-len: 0 */ 3 | 4 | const test = require('tape'); 5 | const { isLoneShortOption } = require('../utils.js'); 6 | 7 | test('isLoneShortOption: when passed short option then returns true', (t) => { 8 | t.true(isLoneShortOption('-s')); 9 | t.end(); 10 | }); 11 | 12 | test('isLoneShortOption: when passed short option group (or might be short and value) then returns false', (t) => { 13 | t.false(isLoneShortOption('-abc')); 14 | t.end(); 15 | }); 16 | 17 | test('isLoneShortOption: when passed long option then returns false', (t) => { 18 | t.false(isLoneShortOption('--foo')); 19 | t.end(); 20 | }); 21 | 22 | test('isLoneShortOption: when passed long option with value then returns false', (t) => { 23 | t.false(isLoneShortOption('--foo=bar')); 24 | t.end(); 25 | }); 26 | 27 | test('isLoneShortOption: when passed empty string then returns false', (t) => { 28 | t.false(isLoneShortOption('')); 29 | t.end(); 30 | }); 31 | 32 | test('isLoneShortOption: when passed plain text then returns false', (t) => { 33 | t.false(isLoneShortOption('foo')); 34 | t.end(); 35 | }); 36 | 37 | test('isLoneShortOption: when passed single dash then returns false', (t) => { 38 | t.false(isLoneShortOption('-')); 39 | t.end(); 40 | }); 41 | 42 | test('isLoneShortOption: when passed double dash then returns false', (t) => { 43 | t.false(isLoneShortOption('--')); 44 | t.end(); 45 | }); 46 | -------------------------------------------------------------------------------- /test/dash.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint max-len: 0 */ 3 | 4 | const test = require('tape'); 5 | const { parseArgs } = require('../index.js'); 6 | 7 | // The use of `-` as a positional is specifically mentioned in the Open Group Utility Conventions. 8 | // The interpretation is up to the utility, and for a file positional (operand) the examples are 9 | // '-' may stand for standard input (or standard output), or for a file named -. 10 | // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html 11 | // 12 | // A different usage and example is `git switch -` to switch back to the previous branch. 13 | 14 | test("dash: when args include '-' used as positional then result has '-' in positionals", (t) => { 15 | const args = ['-']; 16 | const expected = { values: { __proto__: null }, positionals: ['-'] }; 17 | 18 | const result = parseArgs({ allowPositionals: true, args }); 19 | 20 | t.deepEqual(result, expected); 21 | t.end(); 22 | }); 23 | 24 | // If '-' is a valid positional, it is symmetrical to allow it as an option value too. 25 | test("dash: when args include '-' used as space-separated option value then result has '-' in option value", (t) => { 26 | const args = ['-v', '-']; 27 | const options = { v: { type: 'string' } }; 28 | const expected = { values: { __proto__: null, v: '-' }, positionals: [] }; 29 | 30 | const result = parseArgs({ args, options }); 31 | 32 | t.deepEqual(result, expected); 33 | t.end(); 34 | }); 35 | -------------------------------------------------------------------------------- /examples/negate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This example is used in the documentation. 4 | 5 | // How might I add my own support for --no-foo? 6 | 7 | // 1. const { parseArgs } = require('node:util'); // from node 8 | // 2. const { parseArgs } = require('@pkgjs/parseargs'); // from package 9 | const { parseArgs } = require('..'); // in repo 10 | 11 | const options = { 12 | 'color': { type: 'boolean' }, 13 | 'no-color': { type: 'boolean' }, 14 | 'logfile': { type: 'string' }, 15 | 'no-logfile': { type: 'boolean' }, 16 | }; 17 | const { values, tokens } = parseArgs({ options, tokens: true }); 18 | 19 | // Reprocess the option tokens and overwrite the returned values. 20 | tokens 21 | .filter((token) => token.kind === 'option') 22 | .forEach((token) => { 23 | if (token.name.startsWith('no-')) { 24 | // Store foo:false for --no-foo 25 | const positiveName = token.name.slice(3); 26 | values[positiveName] = false; 27 | delete values[token.name]; 28 | } else { 29 | // Resave value so last one wins if both --foo and --no-foo. 30 | values[token.name] = token.value ?? true; 31 | } 32 | }); 33 | 34 | const color = values.color; 35 | const logfile = values.logfile ?? 'default.log'; 36 | 37 | console.log({ logfile, color }); 38 | 39 | // Try the following: 40 | // node negate.js 41 | // node negate.js --no-logfile --no-color 42 | // negate.js --logfile=test.log --color 43 | // node negate.js --no-logfile --logfile=test.log --color --no-color 44 | -------------------------------------------------------------------------------- /examples/ordered-options.mjs: -------------------------------------------------------------------------------- 1 | // This is an example of using tokens to add a custom behaviour. 2 | // 3 | // This adds a option order check so that --some-unstable-option 4 | // may only be used after --enable-experimental-options 5 | // 6 | // Note: this is not a common behaviour, the order of different options 7 | // does not usually matter. 8 | 9 | import { parseArgs } from '../index.js'; 10 | 11 | function findTokenIndex(tokens, target) { 12 | return tokens.findIndex((token) => token.kind === 'option' && 13 | token.name === target 14 | ); 15 | } 16 | 17 | const experimentalName = 'enable-experimental-options'; 18 | const unstableName = 'some-unstable-option'; 19 | 20 | const options = { 21 | [experimentalName]: { type: 'boolean' }, 22 | [unstableName]: { type: 'boolean' }, 23 | }; 24 | 25 | const { values, tokens } = parseArgs({ options, tokens: true }); 26 | 27 | const experimentalIndex = findTokenIndex(tokens, experimentalName); 28 | const unstableIndex = findTokenIndex(tokens, unstableName); 29 | if (unstableIndex !== -1 && 30 | ((experimentalIndex === -1) || (unstableIndex < experimentalIndex))) { 31 | throw new Error(`'--${experimentalName}' must be specified before '--${unstableName}'`); 32 | } 33 | 34 | console.log(values); 35 | 36 | /* eslint-disable max-len */ 37 | // Try the following: 38 | // node ordered-options.mjs 39 | // node ordered-options.mjs --some-unstable-option 40 | // node ordered-options.mjs --some-unstable-option --enable-experimental-options 41 | // node ordered-options.mjs --enable-experimental-options --some-unstable-option 42 | -------------------------------------------------------------------------------- /internal/errors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class ERR_INVALID_ARG_TYPE extends TypeError { 4 | constructor(name, expected, actual) { 5 | super(`${name} must be ${expected} got ${actual}`); 6 | this.code = 'ERR_INVALID_ARG_TYPE'; 7 | } 8 | } 9 | 10 | class ERR_INVALID_ARG_VALUE extends TypeError { 11 | constructor(arg1, arg2, expected) { 12 | super(`The property ${arg1} ${expected}. Received '${arg2}'`); 13 | this.code = 'ERR_INVALID_ARG_VALUE'; 14 | } 15 | } 16 | 17 | class ERR_PARSE_ARGS_INVALID_OPTION_VALUE extends Error { 18 | constructor(message) { 19 | super(message); 20 | this.code = 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE'; 21 | } 22 | } 23 | 24 | class ERR_PARSE_ARGS_UNKNOWN_OPTION extends Error { 25 | constructor(option, allowPositionals) { 26 | const suggestDashDash = allowPositionals ? `. To specify a positional argument starting with a '-', place it at the end of the command after '--', as in '-- ${JSON.stringify(option)}` : ''; 27 | super(`Unknown option '${option}'${suggestDashDash}`); 28 | this.code = 'ERR_PARSE_ARGS_UNKNOWN_OPTION'; 29 | } 30 | } 31 | 32 | class ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL extends Error { 33 | constructor(positional) { 34 | super(`Unexpected argument '${positional}'. This command does not take positional arguments`); 35 | this.code = 'ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL'; 36 | } 37 | } 38 | 39 | module.exports = { 40 | codes: { 41 | ERR_INVALID_ARG_TYPE, 42 | ERR_INVALID_ARG_VALUE, 43 | ERR_PARSE_ARGS_INVALID_OPTION_VALUE, 44 | ERR_PARSE_ARGS_UNKNOWN_OPTION, 45 | ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL, 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | ## 🚀 Getting Started 4 | 5 | * Fork the [parseArgs repo](https://github.com/pkgjs/parseargs) using the Fork button on the rop right 6 | 7 | * Clone your fork using SSH, GitHub CLI, or HTTPS 8 | 9 | ```bash 10 | git clone git@github.com:/parseargs.git # SSH 11 | gh repo clone /parseargs # GitHub CLI 12 | git clone https://github.com//parseargs.git # HTTPS 13 | ``` 14 | 15 | * Change into your local parseargs project directory created from the step above 16 | 17 | ```bash 18 | cd parseargs 19 | ``` 20 | 21 | * Add pkgjs/parseargs as your upstream remote branch 22 | 23 | ```bash 24 | git remote add upstream git@github.com:pkgjs/parseargs.git 25 | ``` 26 | 27 | * Create a new branch for your awesome work 28 | 29 | ```bash 30 | git checkout -b 31 | ``` 32 | 33 | * Confirm tests are passing 34 | 35 | ```bash 36 | npm test 37 | ``` 38 | 39 | * Commit your work. See the [`Pull Request and Commit Guidelines`](#-pull-request-and-commit-guidelines) section 40 | 41 | * Push to your branch 42 | 43 | ```bash 44 | git push -u origin 45 | ``` 46 | 47 | * Open a pull request 48 | 49 | ---- 50 | 51 | ## 📝 Pull Request and Commit Guidelines 52 | 53 | * Multiple Commits would ideally be [squashed](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/managing-commits/squashing-commits) and merged into one commit wherever possible 54 | 55 | * Do not use the merge button 56 | 57 | * Commit messages would ideally reference the associated changed subsystem or behavior. 58 | 59 | * Pull Requests for a new feature or bug fix must include tests. Additional tests or changes to the existing ones can made in the following file: [test/index.js](test/index.js) 60 | -------------------------------------------------------------------------------- /test/short-option-groups.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint max-len: 0 */ 3 | 4 | const test = require('tape'); 5 | const { parseArgs } = require('../index.js'); 6 | 7 | test('when pass zero-config group of booleans then parsed as booleans', (t) => { 8 | const args = ['-rf', 'p']; 9 | const options = { }; 10 | const expected = { values: { __proto__: null, r: true, f: true }, positionals: ['p'] }; 11 | 12 | const result = parseArgs({ strict: false, args, options }); 13 | 14 | t.deepEqual(result, expected); 15 | t.end(); 16 | }); 17 | 18 | test('when pass full-config group of booleans then parsed as booleans', (t) => { 19 | const args = ['-rf', 'p']; 20 | const options = { r: { type: 'boolean' }, f: { type: 'boolean' } }; 21 | const expected = { values: { __proto__: null, r: true, f: true }, positionals: ['p'] }; 22 | 23 | const result = parseArgs({ allowPositionals: true, args, options }); 24 | 25 | t.deepEqual(result, expected); 26 | t.end(); 27 | }); 28 | 29 | test('when pass group with string option on end then parsed as booleans and string option', (t) => { 30 | const args = ['-rf', 'p']; 31 | const options = { r: { type: 'boolean' }, f: { type: 'string' } }; 32 | const expected = { values: { __proto__: null, r: true, f: 'p' }, positionals: [] }; 33 | 34 | const result = parseArgs({ args, options }); 35 | 36 | t.deepEqual(result, expected); 37 | t.end(); 38 | }); 39 | 40 | test('when pass group with string option in middle and strict:false then parsed as booleans and string option with trailing value', (t) => { 41 | const args = ['-afb', 'p']; 42 | const options = { f: { type: 'string' } }; 43 | const expected = { values: { __proto__: null, a: true, f: 'b' }, positionals: ['p'] }; 44 | 45 | const result = parseArgs({ args, options, strict: false }); 46 | 47 | t.deepEqual(result, expected); 48 | t.end(); 49 | }); 50 | -------------------------------------------------------------------------------- /examples/optional-value.mjs: -------------------------------------------------------------------------------- 1 | // This is an example of adding support for an option with an optional value, 2 | // which can be used like a boolean-type or a string-type. 3 | 4 | import { parseArgs } from 'node:util'; 5 | import process from 'node:process'; 6 | 7 | const options = { 8 | 'host': { 9 | type: 'string', 10 | short: 'h', 11 | default: 'default.com', 12 | preset: 'localhost' 13 | }, 14 | 'debug': { type: 'boolean', short: 'd' }, 15 | }; 16 | 17 | const args = process.argv.slice(2); 18 | 19 | do { 20 | const { tokens } = parseArgs({ args, options, strict: false, tokens: true }); 21 | // Insert preset if: 22 | // - missing value, like: --host 23 | // - value came from following option argument, like: --host --debug 24 | // An empty string is a valid value for a string-type option. 25 | const needsPreset = tokens.find((token) => 26 | token.kind === 'option' && 27 | options[token.name] && 28 | options[token.name].type === 'string' && 29 | options[token.name].preset !== undefined && 30 | ( 31 | token.value === undefined || 32 | (token.value.startsWith('-') && !token.inlineValue) 33 | )); 34 | 35 | if (!needsPreset) break; 36 | 37 | // Add preset value as an inline value to the original argument. 38 | const joiner = args[needsPreset.index].startsWith('--') ? '=' : ''; 39 | args[needsPreset.index] = `${args[needsPreset.index]}${joiner}${options[needsPreset.name].preset}`; 40 | 41 | } while (true); 42 | 43 | 44 | const { values } = parseArgs({ args, options }); 45 | console.log(values); 46 | 47 | // Try the following: 48 | // node optional-value.mjs 49 | // node optional-value.mjs -h 50 | // node optional-value.mjs --host 51 | // node optional-value.mjs -hHOSTNAME 52 | // node optional-value.mjs --host=HOSTNAME 53 | // node optional-value.mjs --host= 54 | // node optional-value.mjs -h -d 55 | // node optional-value.mjs -dh 56 | // node optional-value.mjs --host --debug 57 | // node optional-value.mjs --host -- POSITIONAL 58 | -------------------------------------------------------------------------------- /test/is-lone-long-option.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint max-len: 0 */ 3 | 4 | const test = require('tape'); 5 | const { isLoneLongOption } = require('../utils.js'); 6 | 7 | test('isLoneLongOption: when passed short option then returns false', (t) => { 8 | t.false(isLoneLongOption('-s')); 9 | t.end(); 10 | }); 11 | 12 | test('isLoneLongOption: when passed short option group then returns false', (t) => { 13 | t.false(isLoneLongOption('-abc')); 14 | t.end(); 15 | }); 16 | 17 | test('isLoneLongOption: when passed lone long option then returns true', (t) => { 18 | t.true(isLoneLongOption('--foo')); 19 | t.end(); 20 | }); 21 | 22 | test('isLoneLongOption: when passed single character long option then returns true', (t) => { 23 | t.true(isLoneLongOption('--f')); 24 | t.end(); 25 | }); 26 | 27 | test('isLoneLongOption: when passed long option and value then returns false', (t) => { 28 | t.false(isLoneLongOption('--foo=bar')); 29 | t.end(); 30 | }); 31 | 32 | test('isLoneLongOption: when passed empty string then returns false', (t) => { 33 | t.false(isLoneLongOption('')); 34 | t.end(); 35 | }); 36 | 37 | test('isLoneLongOption: when passed plain text then returns false', (t) => { 38 | t.false(isLoneLongOption('foo')); 39 | t.end(); 40 | }); 41 | 42 | test('isLoneLongOption: when passed single dash then returns false', (t) => { 43 | t.false(isLoneLongOption('-')); 44 | t.end(); 45 | }); 46 | 47 | test('isLoneLongOption: when passed double dash then returns false', (t) => { 48 | t.false(isLoneLongOption('--')); 49 | t.end(); 50 | }); 51 | 52 | // This is a bit bogus, but simple consistent behaviour: long option follows double dash. 53 | test('isLoneLongOption: when passed arg starting with triple dash then returns true', (t) => { 54 | t.true(isLoneLongOption('---foo')); 55 | t.end(); 56 | }); 57 | 58 | // This is a bit bogus, but simple consistent behaviour: long option follows double dash. 59 | test("isLoneLongOption: when passed '--=' then returns true", (t) => { 60 | t.true(isLoneLongOption('--=')); 61 | t.end(); 62 | }); 63 | -------------------------------------------------------------------------------- /test/is-option-value.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint max-len: 0 */ 3 | 4 | const test = require('tape'); 5 | const { isOptionValue } = require('../utils.js'); 6 | 7 | // Options are greedy so simple behaviour, but run through the interesting possibilities. 8 | 9 | test('isOptionValue: when passed plain text then returns true', (t) => { 10 | t.true(isOptionValue('abc')); 11 | t.end(); 12 | }); 13 | 14 | test('isOptionValue: when passed digits then returns true', (t) => { 15 | t.true(isOptionValue(123)); 16 | t.end(); 17 | }); 18 | 19 | test('isOptionValue: when passed empty string then returns true', (t) => { 20 | t.true(isOptionValue('')); 21 | t.end(); 22 | }); 23 | 24 | // Special case, used as stdin/stdout et al and not reason to reject 25 | test('isOptionValue: when passed dash then returns true', (t) => { 26 | t.true(isOptionValue('-')); 27 | t.end(); 28 | }); 29 | 30 | test('isOptionValue: when passed -- then returns true', (t) => { 31 | t.true(isOptionValue('--')); 32 | t.end(); 33 | }); 34 | 35 | // Checking undefined so can pass element off end of array. 36 | test('isOptionValue: when passed undefined then returns false', (t) => { 37 | t.false(isOptionValue(undefined)); 38 | t.end(); 39 | }); 40 | 41 | test('isOptionValue: when passed short option then returns true', (t) => { 42 | t.true(isOptionValue('-a')); 43 | t.end(); 44 | }); 45 | 46 | test('isOptionValue: when passed short option digit then returns true', (t) => { 47 | t.true(isOptionValue('-1')); 48 | t.end(); 49 | }); 50 | 51 | test('isOptionValue: when passed negative number then returns true', (t) => { 52 | t.true(isOptionValue('-123')); 53 | t.end(); 54 | }); 55 | 56 | test('isOptionValue: when passed short option group of short option with value then returns true', (t) => { 57 | t.true(isOptionValue('-abd')); 58 | t.end(); 59 | }); 60 | 61 | test('isOptionValue: when passed long option then returns true', (t) => { 62 | t.true(isOptionValue('--foo')); 63 | t.end(); 64 | }); 65 | 66 | test('isOptionValue: when passed long option with value then returns true', (t) => { 67 | t.true(isOptionValue('--foo=bar')); 68 | t.end(); 69 | }); 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /test/is-long-option-and-value.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint max-len: 0 */ 3 | 4 | const test = require('tape'); 5 | const { isLongOptionAndValue } = require('../utils.js'); 6 | 7 | test('isLongOptionAndValue: when passed short option then returns false', (t) => { 8 | t.false(isLongOptionAndValue('-s')); 9 | t.end(); 10 | }); 11 | 12 | test('isLongOptionAndValue: when passed short option group then returns false', (t) => { 13 | t.false(isLongOptionAndValue('-abc')); 14 | t.end(); 15 | }); 16 | 17 | test('isLongOptionAndValue: when passed lone long option then returns false', (t) => { 18 | t.false(isLongOptionAndValue('--foo')); 19 | t.end(); 20 | }); 21 | 22 | test('isLongOptionAndValue: when passed long option and value then returns true', (t) => { 23 | t.true(isLongOptionAndValue('--foo=bar')); 24 | t.end(); 25 | }); 26 | 27 | test('isLongOptionAndValue: when passed single character long option and value then returns true', (t) => { 28 | t.true(isLongOptionAndValue('--f=bar')); 29 | t.end(); 30 | }); 31 | 32 | test('isLongOptionAndValue: when passed empty string then returns false', (t) => { 33 | t.false(isLongOptionAndValue('')); 34 | t.end(); 35 | }); 36 | 37 | test('isLongOptionAndValue: when passed plain text then returns false', (t) => { 38 | t.false(isLongOptionAndValue('foo')); 39 | t.end(); 40 | }); 41 | 42 | test('isLongOptionAndValue: when passed single dash then returns false', (t) => { 43 | t.false(isLongOptionAndValue('-')); 44 | t.end(); 45 | }); 46 | 47 | test('isLongOptionAndValue: when passed double dash then returns false', (t) => { 48 | t.false(isLongOptionAndValue('--')); 49 | t.end(); 50 | }); 51 | 52 | // This is a bit bogus, but simple consistent behaviour: long option follows double dash. 53 | test('isLongOptionAndValue: when passed arg starting with triple dash and value then returns true', (t) => { 54 | t.true(isLongOptionAndValue('---foo=bar')); 55 | t.end(); 56 | }); 57 | 58 | // This is a bit bogus, but simple consistent behaviour: long option follows double dash. 59 | test("isLongOptionAndValue: when passed '--=' then returns false", (t) => { 60 | t.false(isLongOptionAndValue('--=')); 61 | t.end(); 62 | }); 63 | -------------------------------------------------------------------------------- /test/is-short-option-and-value.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint max-len: 0 */ 3 | 4 | const test = require('tape'); 5 | const { isShortOptionAndValue } = require('../utils.js'); 6 | 7 | test('isShortOptionAndValue: when passed lone short option then returns false', (t) => { 8 | t.false(isShortOptionAndValue('-s', {})); 9 | t.end(); 10 | }); 11 | 12 | test('isShortOptionAndValue: when passed group with leading zero-config boolean then returns false', (t) => { 13 | t.false(isShortOptionAndValue('-ab', {})); 14 | t.end(); 15 | }); 16 | 17 | test('isShortOptionAndValue: when passed group with leading configured implicit boolean then returns false', (t) => { 18 | t.false(isShortOptionAndValue('-ab', { aaa: { short: 'a' } })); 19 | t.end(); 20 | }); 21 | 22 | test('isShortOptionAndValue: when passed group with leading configured explicit boolean then returns false', (t) => { 23 | t.false(isShortOptionAndValue('-ab', { aaa: { short: 'a', type: 'boolean' } })); 24 | t.end(); 25 | }); 26 | 27 | test('isShortOptionAndValue: when passed group with leading configured string then returns true', (t) => { 28 | t.true(isShortOptionAndValue('-ab', { aaa: { short: 'a', type: 'string' } })); 29 | t.end(); 30 | }); 31 | 32 | test('isShortOptionAndValue: when passed long option then returns false', (t) => { 33 | t.false(isShortOptionAndValue('--foo', {})); 34 | t.end(); 35 | }); 36 | 37 | test('isShortOptionAndValue: when passed long option with value then returns false', (t) => { 38 | t.false(isShortOptionAndValue('--foo=bar', {})); 39 | t.end(); 40 | }); 41 | 42 | test('isShortOptionAndValue: when passed empty string then returns false', (t) => { 43 | t.false(isShortOptionAndValue('', {})); 44 | t.end(); 45 | }); 46 | 47 | test('isShortOptionAndValue: when passed plain text then returns false', (t) => { 48 | t.false(isShortOptionAndValue('foo', {})); 49 | t.end(); 50 | }); 51 | 52 | test('isShortOptionAndValue: when passed single dash then returns false', (t) => { 53 | t.false(isShortOptionAndValue('-', {})); 54 | t.end(); 55 | }); 56 | 57 | test('isShortOptionAndValue: when passed double dash then returns false', (t) => { 58 | t.false(isShortOptionAndValue('--', {})); 59 | t.end(); 60 | }); 61 | -------------------------------------------------------------------------------- /test/store-user-intent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint max-len: 0 */ 3 | 4 | const test = require('tape'); 5 | const { parseArgs } = require('../index.js'); 6 | 7 | 8 | // Rationale 9 | // 10 | // John Gee: 11 | // - Looks like a boolean option, stored like a boolean option. 12 | // - Looks like a string option, stored like a string option. 13 | // No loss of information. No new pattern to learn in result. 14 | // 15 | // Jordan Harband: In other words, the way they're stored matches the intention of the user, 16 | // not the configurer, which will ensure the configurer can most accurately respond to the 17 | // user's intentions. 18 | 19 | test('when use string short option used as boolean then result as if boolean', (t) => { 20 | const args = ['-o']; 21 | const stringOptions = { opt: { short: 'o', type: 'string' } }; 22 | const booleanOptions = { opt: { short: 'o', type: 'boolean' } }; 23 | 24 | const stringConfigResult = parseArgs({ args, options: stringOptions, strict: false }); 25 | const booleanConfigResult = parseArgs({ args, options: booleanOptions, strict: false }); 26 | 27 | t.deepEqual(stringConfigResult, booleanConfigResult); 28 | t.end(); 29 | }); 30 | 31 | test('when use string long option used as boolean then result as if boolean', (t) => { 32 | const args = ['--opt']; 33 | const stringOptions = { opt: { short: 'o', type: 'string' } }; 34 | const booleanOptions = { opt: { short: 'o', type: 'boolean' } }; 35 | 36 | const stringConfigResult = parseArgs({ args, options: stringOptions, strict: false }); 37 | const booleanConfigResult = parseArgs({ args, options: booleanOptions, strict: false }); 38 | 39 | t.deepEqual(stringConfigResult, booleanConfigResult); 40 | t.end(); 41 | }); 42 | 43 | test('when use boolean long option used as string then result as if string', (t) => { 44 | const args = ['--bool=OOPS']; 45 | const stringOptions = { bool: { type: 'string' } }; 46 | const booleanOptions = { bool: { type: 'boolean' } }; 47 | 48 | const stringConfigResult = parseArgs({ args, options: stringOptions, strict: false }); 49 | const booleanConfigResult = parseArgs({ args, options: booleanOptions, strict: false }); 50 | 51 | t.deepEqual(booleanConfigResult, stringConfigResult); 52 | t.end(); 53 | }); 54 | -------------------------------------------------------------------------------- /test/is-option-like-value.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint max-len: 0 */ 3 | 4 | const test = require('tape'); 5 | const { isOptionLikeValue } = require('../utils.js'); 6 | 7 | // Basically rejecting values starting with a dash, but run through the interesting possibilities. 8 | 9 | test('isOptionLikeValue: when passed plain text then returns false', (t) => { 10 | t.false(isOptionLikeValue('abc')); 11 | t.end(); 12 | }); 13 | 14 | test('isOptionLikeValue: when passed digits then returns false', (t) => { 15 | t.false(isOptionLikeValue(123)); 16 | t.end(); 17 | }); 18 | 19 | test('isOptionLikeValue: when passed empty string then returns false', (t) => { 20 | t.false(isOptionLikeValue('')); 21 | t.end(); 22 | }); 23 | 24 | // Special case, used as stdin/stdout et al and not reason to reject 25 | test('isOptionLikeValue: when passed dash then returns false', (t) => { 26 | t.false(isOptionLikeValue('-')); 27 | t.end(); 28 | }); 29 | 30 | test('isOptionLikeValue: when passed -- then returns true', (t) => { 31 | // Not strictly option-like, but is supect 32 | t.true(isOptionLikeValue('--')); 33 | t.end(); 34 | }); 35 | 36 | // Supporting undefined so can pass element off end of array without checking 37 | test('isOptionLikeValue: when passed undefined then returns false', (t) => { 38 | t.false(isOptionLikeValue(undefined)); 39 | t.end(); 40 | }); 41 | 42 | test('isOptionLikeValue: when passed short option then returns true', (t) => { 43 | t.true(isOptionLikeValue('-a')); 44 | t.end(); 45 | }); 46 | 47 | test('isOptionLikeValue: when passed short option digit then returns true', (t) => { 48 | t.true(isOptionLikeValue('-1')); 49 | t.end(); 50 | }); 51 | 52 | test('isOptionLikeValue: when passed negative number then returns true', (t) => { 53 | t.true(isOptionLikeValue('-123')); 54 | t.end(); 55 | }); 56 | 57 | test('isOptionLikeValue: when passed short option group of short option with value then returns true', (t) => { 58 | t.true(isOptionLikeValue('-abd')); 59 | t.end(); 60 | }); 61 | 62 | test('isOptionLikeValue: when passed long option then returns true', (t) => { 63 | t.true(isOptionLikeValue('--foo')); 64 | t.end(); 65 | }); 66 | 67 | test('isOptionLikeValue: when passed long option with value then returns true', (t) => { 68 | t.true(isOptionLikeValue('--foo=bar')); 69 | t.end(); 70 | }); 71 | -------------------------------------------------------------------------------- /internal/validators.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This file is a proxy of the original file located at: 4 | // https://github.com/nodejs/node/blob/main/lib/internal/validators.js 5 | // Every addition or modification to this file must be evaluated 6 | // during the PR review. 7 | 8 | const { 9 | ArrayIsArray, 10 | ArrayPrototypeIncludes, 11 | ArrayPrototypeJoin, 12 | } = require('./primordials'); 13 | 14 | const { 15 | codes: { 16 | ERR_INVALID_ARG_TYPE 17 | } 18 | } = require('./errors'); 19 | 20 | function validateString(value, name) { 21 | if (typeof value !== 'string') { 22 | throw new ERR_INVALID_ARG_TYPE(name, 'String', value); 23 | } 24 | } 25 | 26 | function validateUnion(value, name, union) { 27 | if (!ArrayPrototypeIncludes(union, value)) { 28 | throw new ERR_INVALID_ARG_TYPE(name, `('${ArrayPrototypeJoin(union, '|')}')`, value); 29 | } 30 | } 31 | 32 | function validateBoolean(value, name) { 33 | if (typeof value !== 'boolean') { 34 | throw new ERR_INVALID_ARG_TYPE(name, 'Boolean', value); 35 | } 36 | } 37 | 38 | function validateArray(value, name) { 39 | if (!ArrayIsArray(value)) { 40 | throw new ERR_INVALID_ARG_TYPE(name, 'Array', value); 41 | } 42 | } 43 | 44 | function validateStringArray(value, name) { 45 | validateArray(value, name); 46 | for (let i = 0; i < value.length; i++) { 47 | validateString(value[i], `${name}[${i}]`); 48 | } 49 | } 50 | 51 | function validateBooleanArray(value, name) { 52 | validateArray(value, name); 53 | for (let i = 0; i < value.length; i++) { 54 | validateBoolean(value[i], `${name}[${i}]`); 55 | } 56 | } 57 | 58 | /** 59 | * @param {unknown} value 60 | * @param {string} name 61 | * @param {{ 62 | * allowArray?: boolean, 63 | * allowFunction?: boolean, 64 | * nullable?: boolean 65 | * }} [options] 66 | */ 67 | function validateObject(value, name, options) { 68 | const useDefaultOptions = options == null; 69 | const allowArray = useDefaultOptions ? false : options.allowArray; 70 | const allowFunction = useDefaultOptions ? false : options.allowFunction; 71 | const nullable = useDefaultOptions ? false : options.nullable; 72 | if ((!nullable && value === null) || 73 | (!allowArray && ArrayIsArray(value)) || 74 | (typeof value !== 'object' && ( 75 | !allowFunction || typeof value !== 'function' 76 | ))) { 77 | throw new ERR_INVALID_ARG_TYPE(name, 'Object', value); 78 | } 79 | } 80 | 81 | module.exports = { 82 | validateArray, 83 | validateObject, 84 | validateString, 85 | validateStringArray, 86 | validateUnion, 87 | validateBoolean, 88 | validateBooleanArray, 89 | }; 90 | -------------------------------------------------------------------------------- /test/is-short-option-group.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint max-len: 0 */ 3 | 4 | const test = require('tape'); 5 | const { isShortOptionGroup } = require('../utils.js'); 6 | 7 | test('isShortOptionGroup: when passed lone short option then returns false', (t) => { 8 | t.false(isShortOptionGroup('-s', {})); 9 | t.end(); 10 | }); 11 | 12 | test('isShortOptionGroup: when passed group with leading zero-config boolean then returns true', (t) => { 13 | t.true(isShortOptionGroup('-ab', {})); 14 | t.end(); 15 | }); 16 | 17 | test('isShortOptionGroup: when passed group with leading configured implicit boolean then returns true', (t) => { 18 | t.true(isShortOptionGroup('-ab', { aaa: { short: 'a' } })); 19 | t.end(); 20 | }); 21 | 22 | test('isShortOptionGroup: when passed group with leading configured explicit boolean then returns true', (t) => { 23 | t.true(isShortOptionGroup('-ab', { aaa: { short: 'a', type: 'boolean' } })); 24 | t.end(); 25 | }); 26 | 27 | test('isShortOptionGroup: when passed group with leading configured string then returns false', (t) => { 28 | t.false(isShortOptionGroup('-ab', { aaa: { short: 'a', type: 'string' } })); 29 | t.end(); 30 | }); 31 | 32 | test('isShortOptionGroup: when passed group with trailing configured string then returns true', (t) => { 33 | t.true(isShortOptionGroup('-ab', { bbb: { short: 'b', type: 'string' } })); 34 | t.end(); 35 | }); 36 | 37 | // This one is dubious, but leave it to caller to handle. 38 | test('isShortOptionGroup: when passed group with middle configured string then returns true', (t) => { 39 | t.true(isShortOptionGroup('-abc', { bbb: { short: 'b', type: 'string' } })); 40 | t.end(); 41 | }); 42 | 43 | test('isShortOptionGroup: when passed long option then returns false', (t) => { 44 | t.false(isShortOptionGroup('--foo', {})); 45 | t.end(); 46 | }); 47 | 48 | test('isShortOptionGroup: when passed long option with value then returns false', (t) => { 49 | t.false(isShortOptionGroup('--foo=bar', {})); 50 | t.end(); 51 | }); 52 | 53 | test('isShortOptionGroup: when passed empty string then returns false', (t) => { 54 | t.false(isShortOptionGroup('', {})); 55 | t.end(); 56 | }); 57 | 58 | test('isShortOptionGroup: when passed plain text then returns false', (t) => { 59 | t.false(isShortOptionGroup('foo', {})); 60 | t.end(); 61 | }); 62 | 63 | test('isShortOptionGroup: when passed single dash then returns false', (t) => { 64 | t.false(isShortOptionGroup('-', {})); 65 | t.end(); 66 | }); 67 | 68 | test('isShortOptionGroup: when passed double dash then returns false', (t) => { 69 | t.false(isShortOptionGroup('--', {})); 70 | t.end(); 71 | }); 72 | -------------------------------------------------------------------------------- /test/prototype-pollution.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint max-len: 0 */ 3 | 4 | const test = require('tape'); 5 | const { parseArgs } = require('../index.js'); 6 | 7 | // These tests are not synced upstream with node, in case of possible side-effects. 8 | // See index.js for tests shared with upstream. 9 | 10 | function setObjectPrototype(prop, value) { 11 | const oldDescriptor = Object.getOwnPropertyDescriptor(Object.prototype, prop); 12 | Object.prototype[prop] = value; 13 | return oldDescriptor; 14 | } 15 | 16 | function restoreObjectPrototype(prop, oldDescriptor) { 17 | if (oldDescriptor == null) { 18 | delete Object.prototype[prop]; 19 | } else { 20 | Object.defineProperty(Object.prototype, prop, oldDescriptor); 21 | } 22 | } 23 | 24 | test('should not allow __proto__ key to be set on object', (t) => { 25 | const args = ['--__proto__=hello']; 26 | const expected = { values: { __proto__: null }, positionals: [] }; 27 | 28 | const result = parseArgs({ strict: false, args }); 29 | 30 | t.deepEqual(result, expected); 31 | t.end(); 32 | }); 33 | 34 | test('when prototype has multiple then ignored', (t) => { 35 | const args = ['--foo', '1', '--foo', '2']; 36 | const options = { foo: { type: 'string' } }; 37 | const expectedResult = { values: { __proto__: null, foo: '2' }, positionals: [] }; 38 | 39 | const holdDescriptor = setObjectPrototype('multiple', true); 40 | const result = parseArgs({ args, options }); 41 | restoreObjectPrototype('multiple', holdDescriptor); 42 | t.deepEqual(result, expectedResult); 43 | t.end(); 44 | }); 45 | 46 | test('when prototype has type then ignored', (t) => { 47 | const args = ['--foo', '1']; 48 | const options = { foo: { } }; 49 | 50 | const holdDescriptor = setObjectPrototype('type', 'string'); 51 | t.throws(() => { 52 | parseArgs({ args, options }); 53 | }); 54 | restoreObjectPrototype('type', holdDescriptor); 55 | t.end(); 56 | }); 57 | 58 | test('when prototype has short then ignored', (t) => { 59 | const args = ['-f', '1']; 60 | const options = { foo: { type: 'string' } }; 61 | 62 | const holdDescriptor = setObjectPrototype('short', 'f'); 63 | t.throws(() => { 64 | parseArgs({ args, options }); 65 | }); 66 | restoreObjectPrototype('short', holdDescriptor); 67 | t.end(); 68 | }); 69 | 70 | test('when prototype has strict then ignored', (t) => { 71 | const args = ['-f']; 72 | 73 | const holdDescriptor = setObjectPrototype('strict', false); 74 | t.throws(() => { 75 | parseArgs({ args }); 76 | }); 77 | restoreObjectPrototype('strict', holdDescriptor); 78 | t.end(); 79 | }); 80 | 81 | test('when prototype has args then ignored', (t) => { 82 | const holdDescriptor = setObjectPrototype('args', ['--foo']); 83 | const result = parseArgs({ strict: false }); 84 | restoreObjectPrototype('args', holdDescriptor); 85 | t.false(result.values.foo); 86 | t.end(); 87 | }); 88 | -------------------------------------------------------------------------------- /test/short-option-combined-with-value.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint max-len: 0 */ 3 | 4 | const test = require('tape'); 5 | const { parseArgs } = require('../index.js'); 6 | 7 | test('when combine string short with plain text then parsed as value', (t) => { 8 | const args = ['-aHELLO']; 9 | const options = { alpha: { short: 'a', type: 'string' } }; 10 | const expected = { values: { __proto__: null, alpha: 'HELLO' }, positionals: [] }; 11 | 12 | const result = parseArgs({ args, options }); 13 | 14 | t.deepEqual(result, expected); 15 | t.end(); 16 | }); 17 | 18 | test('when combine low-config string short with plain text then parsed as value', (t) => { 19 | const args = ['-aHELLO']; 20 | const options = { a: { type: 'string' } }; 21 | const expected = { values: { __proto__: null, a: 'HELLO' }, positionals: [] }; 22 | 23 | const result = parseArgs({ args, options }); 24 | 25 | t.deepEqual(result, expected); 26 | t.end(); 27 | }); 28 | 29 | test('when combine string short with value like short option then parsed as value', (t) => { 30 | const args = ['-a-b']; 31 | const options = { alpha: { short: 'a', type: 'string' } }; 32 | const expected = { values: { __proto__: null, alpha: '-b' }, positionals: [] }; 33 | 34 | const result = parseArgs({ args, options }); 35 | 36 | t.deepEqual(result, expected); 37 | t.end(); 38 | }); 39 | 40 | test('when combine string short with value like long option then parsed as value', (t) => { 41 | const args = ['-a--bar']; 42 | const options = { alpha: { short: 'a', type: 'string' } }; 43 | const expected = { values: { __proto__: null, alpha: '--bar' }, positionals: [] }; 44 | 45 | const result = parseArgs({ args, options }); 46 | 47 | t.deepEqual(result, expected); 48 | t.end(); 49 | }); 50 | 51 | test('when combine string short with value like negative number then parsed as value', (t) => { 52 | const args = ['-a-5']; 53 | const options = { alpha: { short: 'a', type: 'string' } }; 54 | const expected = { values: { __proto__: null, alpha: '-5' }, positionals: [] }; 55 | 56 | const result = parseArgs({ args, options }); 57 | 58 | t.deepEqual(result, expected); 59 | t.end(); 60 | }); 61 | 62 | 63 | test('when combine string short with value which matches configured flag then parsed as value', (t) => { 64 | const args = ['-af']; 65 | const options = { alpha: { short: 'a', type: 'string' }, file: { short: 'f', type: 'boolean' } }; 66 | const expected = { values: { __proto__: null, alpha: 'f' }, positionals: [] }; 67 | const result = parseArgs({ args, options }); 68 | 69 | t.deepEqual(result, expected); 70 | t.end(); 71 | }); 72 | 73 | test('when combine string short with value including equals then parsed with equals in value', (t) => { 74 | const args = ['-a=5']; 75 | const options = { alpha: { short: 'a', type: 'string' } }; 76 | const expected = { values: { __proto__: null, alpha: '=5' }, positionals: [] }; 77 | 78 | const result = parseArgs({ args, options }); 79 | 80 | t.deepEqual(result, expected); 81 | t.end(); 82 | }); 83 | -------------------------------------------------------------------------------- /test/allow-negative.js: -------------------------------------------------------------------------------- 1 | /* global assert */ 2 | /* eslint max-len: 0 */ 3 | 'use strict'; 4 | 5 | const { test } = require('./utils'); 6 | const { parseArgs } = require('../index'); 7 | 8 | test('disable negative options and args are started with "--no-" prefix', () => { 9 | const args = ['--no-alpha']; 10 | const options = { alpha: { type: 'boolean' } }; 11 | assert.throws(() => { 12 | parseArgs({ args, options }); 13 | }, { 14 | code: 'ERR_PARSE_ARGS_UNKNOWN_OPTION' 15 | }); 16 | }); 17 | 18 | test('args are passed `type: "string"` and allow negative options', () => { 19 | const args = ['--no-alpha', 'value']; 20 | const options = { alpha: { type: 'string' } }; 21 | assert.throws(() => { 22 | parseArgs({ args, options, allowNegative: true }); 23 | }, { 24 | code: 'ERR_PARSE_ARGS_UNKNOWN_OPTION' 25 | }); 26 | }); 27 | 28 | test('args are passed `type: "boolean"` and allow negative options', () => { 29 | const args = ['--no-alpha']; 30 | const options = { alpha: { type: 'boolean' } }; 31 | const expected = { values: { __proto__: null, alpha: false }, positionals: [] }; 32 | assert.deepStrictEqual(parseArgs({ args, options, allowNegative: true }), expected); 33 | }); 34 | 35 | test('args are passed `default: "true"` and allow negative options', () => { 36 | const args = ['--no-alpha']; 37 | const options = { alpha: { type: 'boolean', default: true } }; 38 | const expected = { values: { __proto__: null, alpha: false }, positionals: [] }; 39 | assert.deepStrictEqual(parseArgs({ args, options, allowNegative: true }), expected); 40 | }); 41 | 42 | test('args are passed `default: "false" and allow negative options', () => { 43 | const args = ['--no-alpha']; 44 | const options = { alpha: { type: 'boolean', default: false } }; 45 | const expected = { values: { __proto__: null, alpha: false }, positionals: [] }; 46 | assert.deepStrictEqual(parseArgs({ args, options, allowNegative: true }), expected); 47 | }); 48 | 49 | test('allow negative options and multiple as true', () => { 50 | const args = ['--no-alpha', '--alpha', '--no-alpha']; 51 | const options = { alpha: { type: 'boolean', multiple: true } }; 52 | const expected = { values: { __proto__: null, alpha: [false, true, false] }, positionals: [] }; 53 | assert.deepStrictEqual(parseArgs({ args, options, allowNegative: true }), expected); 54 | }); 55 | 56 | test('allow negative options and passed multiple arguments', () => { 57 | const args = ['--no-alpha', '--alpha']; 58 | const options = { alpha: { type: 'boolean' } }; 59 | const expected = { values: { __proto__: null, alpha: true }, positionals: [] }; 60 | assert.deepStrictEqual(parseArgs({ args, options, allowNegative: true }), expected); 61 | }); 62 | 63 | test('auto-detect --no-foo as negated when strict:false and allowNegative', () => { 64 | const holdArgv = process.argv; 65 | process.argv = [process.argv0, 'script.js', '--no-foo']; 66 | const holdExecArgv = process.execArgv; 67 | process.execArgv = []; 68 | const result = parseArgs({ strict: false, allowNegative: true }); 69 | 70 | const expected = { values: { __proto__: null, foo: false }, 71 | positionals: [] }; 72 | assert.deepStrictEqual(result, expected); 73 | process.argv = holdArgv; 74 | process.execArgv = holdExecArgv; 75 | }); 76 | -------------------------------------------------------------------------------- /test/default-values.js: -------------------------------------------------------------------------------- 1 | /* global assert */ 2 | /* eslint max-len: 0 */ 3 | 'use strict'; 4 | 5 | const { test } = require('./utils'); 6 | const { parseArgs } = require('../index.js'); 7 | 8 | test('default must be a boolean when option type is boolean', () => { 9 | const args = []; 10 | const options = { alpha: { type: 'boolean', default: 'not a boolean' } }; 11 | assert.throws(() => { 12 | parseArgs({ args, options }); 13 | }, /options\.alpha\.default must be Boolean/ 14 | ); 15 | }); 16 | 17 | test('default must accept undefined value', () => { 18 | const args = []; 19 | const options = { alpha: { type: 'boolean', default: undefined } }; 20 | const result = parseArgs({ args, options }); 21 | const expected = { 22 | values: { 23 | __proto__: null, 24 | }, 25 | positionals: [] 26 | }; 27 | assert.deepStrictEqual(result, expected); 28 | }); 29 | 30 | test('default must be a boolean array when option type is boolean and multiple', () => { 31 | const args = []; 32 | const options = { alpha: { type: 'boolean', multiple: true, default: 'not an array' } }; 33 | assert.throws(() => { 34 | parseArgs({ args, options }); 35 | }, /options\.alpha\.default must be Array/ 36 | ); 37 | }); 38 | 39 | test('default must be a boolean array when option type is string and multiple is true', () => { 40 | const args = []; 41 | const options = { alpha: { type: 'boolean', multiple: true, default: [true, true, 42] } }; 42 | assert.throws(() => { 43 | parseArgs({ args, options }); 44 | }, /options\.alpha\.default\[2\] must be Boolean/ 45 | ); 46 | }); 47 | 48 | test('default must be a string when option type is string', () => { 49 | const args = []; 50 | const options = { alpha: { type: 'string', default: true } }; 51 | assert.throws(() => { 52 | parseArgs({ args, options }); 53 | }, /options\.alpha\.default must be String/ 54 | ); 55 | }); 56 | 57 | test('default must be an array when option type is string and multiple is true', () => { 58 | const args = []; 59 | const options = { alpha: { type: 'string', multiple: true, default: 'not an array' } }; 60 | assert.throws(() => { 61 | parseArgs({ args, options }); 62 | }, /options\.alpha\.default must be Array/ 63 | ); 64 | }); 65 | 66 | test('default must be a string array when option type is string and multiple is true', () => { 67 | const args = []; 68 | const options = { alpha: { type: 'string', multiple: true, default: ['str', 42] } }; 69 | assert.throws(() => { 70 | parseArgs({ args, options }); 71 | }, /options\.alpha\.default\[1\] must be String/ 72 | ); 73 | }); 74 | 75 | test('default accepted input when multiple is true', () => { 76 | const args = ['--inputStringArr', 'c', '--inputStringArr', 'd', '--inputBoolArr', '--inputBoolArr']; 77 | const options = { 78 | inputStringArr: { type: 'string', multiple: true, default: ['a', 'b'] }, 79 | emptyStringArr: { type: 'string', multiple: true, default: [] }, 80 | fullStringArr: { type: 'string', multiple: true, default: ['a', 'b'] }, 81 | inputBoolArr: { type: 'boolean', multiple: true, default: [false, true, false] }, 82 | emptyBoolArr: { type: 'boolean', multiple: true, default: [] }, 83 | fullBoolArr: { type: 'boolean', multiple: true, default: [false, true, false] }, 84 | }; 85 | const expected = { values: { __proto__: null, 86 | inputStringArr: ['c', 'd'], 87 | inputBoolArr: [true, true], 88 | emptyStringArr: [], 89 | fullStringArr: ['a', 'b'], 90 | emptyBoolArr: [], 91 | fullBoolArr: [false, true, false] }, 92 | positionals: [] }; 93 | const result = parseArgs({ args, options }); 94 | assert.deepStrictEqual(result, expected); 95 | }); 96 | 97 | test('when default is set, the option must be added as result', () => { 98 | const args = []; 99 | const options = { 100 | a: { type: 'string', default: 'HELLO' }, 101 | b: { type: 'boolean', default: false }, 102 | c: { type: 'boolean', default: true } 103 | }; 104 | const expected = { values: { __proto__: null, a: 'HELLO', b: false, c: true }, positionals: [] }; 105 | 106 | const result = parseArgs({ args, options }); 107 | assert.deepStrictEqual(result, expected); 108 | }); 109 | 110 | test('when default is set, the args value takes precedence', () => { 111 | const args = ['--a', 'WORLD', '--b', '-c']; 112 | const options = { 113 | a: { type: 'string', default: 'HELLO' }, 114 | b: { type: 'boolean', default: false }, 115 | c: { type: 'boolean', default: true } 116 | }; 117 | const expected = { values: { __proto__: null, a: 'WORLD', b: true, c: true }, positionals: [] }; 118 | 119 | const result = parseArgs({ args, options }); 120 | assert.deepStrictEqual(result, expected); 121 | }); 122 | 123 | test('tokens should not include the default options', () => { 124 | const args = []; 125 | const options = { 126 | a: { type: 'string', default: 'HELLO' }, 127 | b: { type: 'boolean', default: false }, 128 | c: { type: 'boolean', default: true } 129 | }; 130 | 131 | const expectedTokens = []; 132 | 133 | const { tokens } = parseArgs({ args, options, tokens: true }); 134 | assert.deepStrictEqual(tokens, expectedTokens); 135 | }); 136 | 137 | test('tokens:true should not include the default options after the args input', () => { 138 | const args = ['--z', 'zero', 'positional-item']; 139 | const options = { 140 | z: { type: 'string' }, 141 | a: { type: 'string', default: 'HELLO' }, 142 | b: { type: 'boolean', default: false }, 143 | c: { type: 'boolean', default: true } 144 | }; 145 | 146 | const expectedTokens = [ 147 | { kind: 'option', name: 'z', rawName: '--z', index: 0, value: 'zero', inlineValue: false }, 148 | { kind: 'positional', index: 2, value: 'positional-item' }, 149 | ]; 150 | 151 | const { tokens } = parseArgs({ args, options, tokens: true, allowPositionals: true }); 152 | assert.deepStrictEqual(tokens, expectedTokens); 153 | }); 154 | 155 | test('proto as default value must be ignored', () => { 156 | const args = []; 157 | const options = Object.create(null); 158 | 159 | // eslint-disable-next-line no-proto 160 | options.__proto__ = { type: 'string', default: 'HELLO' }; 161 | 162 | const result = parseArgs({ args, options, allowPositionals: true }); 163 | const expected = { values: { __proto__: null }, positionals: [] }; 164 | assert.deepStrictEqual(result, expected); 165 | }); 166 | 167 | 168 | test('multiple as false should expect a String', () => { 169 | const args = []; 170 | const options = { alpha: { type: 'string', multiple: false, default: ['array'] } }; 171 | assert.throws(() => { 172 | parseArgs({ args, options }); 173 | }, / must be String got array/); 174 | }); 175 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { 4 | ArrayPrototypeFind, 5 | ObjectEntries, 6 | ObjectPrototypeHasOwnProperty: ObjectHasOwn, 7 | StringPrototypeCharAt, 8 | StringPrototypeIncludes, 9 | StringPrototypeStartsWith, 10 | } = require('./internal/primordials'); 11 | 12 | const { 13 | validateObject, 14 | } = require('./internal/validators'); 15 | 16 | // These are internal utilities to make the parsing logic easier to read, and 17 | // add lots of detail for the curious. They are in a separate file to allow 18 | // unit testing, although that is not essential (this could be rolled into 19 | // main file and just tested implicitly via API). 20 | // 21 | // These routines are for internal use, not for export to client. 22 | 23 | /** 24 | * Return the named property, but only if it is an own property. 25 | */ 26 | function objectGetOwn(obj, prop) { 27 | if (ObjectHasOwn(obj, prop)) 28 | return obj[prop]; 29 | } 30 | 31 | /** 32 | * Return the named options property, but only if it is an own property. 33 | */ 34 | function optionsGetOwn(options, longOption, prop) { 35 | if (ObjectHasOwn(options, longOption)) 36 | return objectGetOwn(options[longOption], prop); 37 | } 38 | 39 | /** 40 | * Determines if the argument may be used as an option value. 41 | * @example 42 | * isOptionValue('V') // returns true 43 | * isOptionValue('-v') // returns true (greedy) 44 | * isOptionValue('--foo') // returns true (greedy) 45 | * isOptionValue(undefined) // returns false 46 | */ 47 | function isOptionValue(value) { 48 | if (value == null) return false; 49 | 50 | // Open Group Utility Conventions are that an option-argument 51 | // is the argument after the option, and may start with a dash. 52 | return true; // greedy! 53 | } 54 | 55 | /** 56 | * Detect whether there is possible confusion and user may have omitted 57 | * the option argument, like `--port --verbose` when `port` of type:string. 58 | * In strict mode we throw errors if value is option-like. 59 | */ 60 | function isOptionLikeValue(value) { 61 | if (value == null) return false; 62 | 63 | return value.length > 1 && StringPrototypeCharAt(value, 0) === '-'; 64 | } 65 | 66 | /** 67 | * Determines if `arg` is just a short option. 68 | * @example '-f' 69 | */ 70 | function isLoneShortOption(arg) { 71 | return arg.length === 2 && 72 | StringPrototypeCharAt(arg, 0) === '-' && 73 | StringPrototypeCharAt(arg, 1) !== '-'; 74 | } 75 | 76 | /** 77 | * Determines if `arg` is a lone long option. 78 | * @example 79 | * isLoneLongOption('a') // returns false 80 | * isLoneLongOption('-a') // returns false 81 | * isLoneLongOption('--foo') // returns true 82 | * isLoneLongOption('--foo=bar') // returns false 83 | */ 84 | function isLoneLongOption(arg) { 85 | return arg.length > 2 && 86 | StringPrototypeStartsWith(arg, '--') && 87 | !StringPrototypeIncludes(arg, '=', 3); 88 | } 89 | 90 | /** 91 | * Determines if `arg` is a long option and value in the same argument. 92 | * @example 93 | * isLongOptionAndValue('--foo') // returns false 94 | * isLongOptionAndValue('--foo=bar') // returns true 95 | */ 96 | function isLongOptionAndValue(arg) { 97 | return arg.length > 2 && 98 | StringPrototypeStartsWith(arg, '--') && 99 | StringPrototypeIncludes(arg, '=', 3); 100 | } 101 | 102 | /** 103 | * Determines if `arg` is a short option group. 104 | * 105 | * See Guideline 5 of the [Open Group Utility Conventions](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html). 106 | * One or more options without option-arguments, followed by at most one 107 | * option that takes an option-argument, should be accepted when grouped 108 | * behind one '-' delimiter. 109 | * @example 110 | * isShortOptionGroup('-a', {}) // returns false 111 | * isShortOptionGroup('-ab', {}) // returns true 112 | * // -fb is an option and a value, not a short option group 113 | * isShortOptionGroup('-fb', { 114 | * options: { f: { type: 'string' } } 115 | * }) // returns false 116 | * isShortOptionGroup('-bf', { 117 | * options: { f: { type: 'string' } } 118 | * }) // returns true 119 | * // -bfb is an edge case, return true and caller sorts it out 120 | * isShortOptionGroup('-bfb', { 121 | * options: { f: { type: 'string' } } 122 | * }) // returns true 123 | */ 124 | function isShortOptionGroup(arg, options) { 125 | if (arg.length <= 2) return false; 126 | if (StringPrototypeCharAt(arg, 0) !== '-') return false; 127 | if (StringPrototypeCharAt(arg, 1) === '-') return false; 128 | 129 | const firstShort = StringPrototypeCharAt(arg, 1); 130 | const longOption = findLongOptionForShort(firstShort, options); 131 | return optionsGetOwn(options, longOption, 'type') !== 'string'; 132 | } 133 | 134 | /** 135 | * Determine if arg is a short string option followed by its value. 136 | * @example 137 | * isShortOptionAndValue('-a', {}); // returns false 138 | * isShortOptionAndValue('-ab', {}); // returns false 139 | * isShortOptionAndValue('-fFILE', { 140 | * options: { foo: { short: 'f', type: 'string' }} 141 | * }) // returns true 142 | */ 143 | function isShortOptionAndValue(arg, options) { 144 | validateObject(options, 'options'); 145 | 146 | if (arg.length <= 2) return false; 147 | if (StringPrototypeCharAt(arg, 0) !== '-') return false; 148 | if (StringPrototypeCharAt(arg, 1) === '-') return false; 149 | 150 | const shortOption = StringPrototypeCharAt(arg, 1); 151 | const longOption = findLongOptionForShort(shortOption, options); 152 | return optionsGetOwn(options, longOption, 'type') === 'string'; 153 | } 154 | 155 | /** 156 | * Find the long option associated with a short option. Looks for a configured 157 | * `short` and returns the short option itself if a long option is not found. 158 | * @example 159 | * findLongOptionForShort('a', {}) // returns 'a' 160 | * findLongOptionForShort('b', { 161 | * options: { bar: { short: 'b' } } 162 | * }) // returns 'bar' 163 | */ 164 | function findLongOptionForShort(shortOption, options) { 165 | validateObject(options, 'options'); 166 | const longOptionEntry = ArrayPrototypeFind( 167 | ObjectEntries(options), 168 | ({ 1: optionConfig }) => objectGetOwn(optionConfig, 'short') === shortOption 169 | ); 170 | return longOptionEntry?.[0] ?? shortOption; 171 | } 172 | 173 | /** 174 | * Check if the given option includes a default value 175 | * and that option has not been set by the input args. 176 | * 177 | * @param {string} longOption - long option name e.g. 'foo' 178 | * @param {object} optionConfig - the option configuration properties 179 | * @param {object} values - option values returned in `values` by parseArgs 180 | */ 181 | function useDefaultValueOption(longOption, optionConfig, values) { 182 | return objectGetOwn(optionConfig, 'default') !== undefined && 183 | values[longOption] === undefined; 184 | } 185 | 186 | module.exports = { 187 | findLongOptionForShort, 188 | isLoneLongOption, 189 | isLoneShortOption, 190 | isLongOptionAndValue, 191 | isOptionValue, 192 | isOptionLikeValue, 193 | isShortOptionAndValue, 194 | isShortOptionGroup, 195 | useDefaultValueOption, 196 | objectGetOwn, 197 | optionsGetOwn, 198 | }; 199 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.11.0](https://github.com/pkgjs/parseargs/compare/v0.10.0...v0.11.0) (2022-10-08) 4 | 5 | 6 | ### Features 7 | 8 | * add `default` option parameter ([#142](https://github.com/pkgjs/parseargs/issues/142)) ([cd20847](https://github.com/pkgjs/parseargs/commit/cd20847a00b2f556aa9c085ac83b942c60868ec1)) 9 | 10 | ## [0.10.0](https://github.com/pkgjs/parseargs/compare/v0.9.1...v0.10.0) (2022-07-21) 11 | 12 | 13 | ### Features 14 | 15 | * add parsed meta-data to returned properties ([#129](https://github.com/pkgjs/parseargs/issues/129)) ([91bfb4d](https://github.com/pkgjs/parseargs/commit/91bfb4d3f7b6937efab1b27c91c45d1205f1497e)) 16 | 17 | ## [0.9.1](https://github.com/pkgjs/parseargs/compare/v0.9.0...v0.9.1) (2022-06-20) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * **runtime:** support node 14+ ([#135](https://github.com/pkgjs/parseargs/issues/135)) ([6a1c5a6](https://github.com/pkgjs/parseargs/commit/6a1c5a6f7cadf2f035e004027e2742e3c4ce554b)) 23 | 24 | ## [0.9.0](https://github.com/pkgjs/parseargs/compare/v0.8.0...v0.9.0) (2022-05-23) 25 | 26 | 27 | ### ⚠ BREAKING CHANGES 28 | 29 | * drop handling of electron arguments (#121) 30 | 31 | ### Code Refactoring 32 | 33 | * drop handling of electron arguments ([#121](https://github.com/pkgjs/parseargs/issues/121)) ([a2ffd53](https://github.com/pkgjs/parseargs/commit/a2ffd537c244a062371522b955acb45a404fc9f2)) 34 | 35 | ## [0.8.0](https://github.com/pkgjs/parseargs/compare/v0.7.1...v0.8.0) (2022-05-16) 36 | 37 | 38 | ### ⚠ BREAKING CHANGES 39 | 40 | * switch type:string option arguments to greedy, but with error for suspect cases in strict mode (#88) 41 | * positionals now opt-in when strict:true (#116) 42 | * create result.values with null prototype (#111) 43 | 44 | ### Features 45 | 46 | * create result.values with null prototype ([#111](https://github.com/pkgjs/parseargs/issues/111)) ([9d539c3](https://github.com/pkgjs/parseargs/commit/9d539c3d57f269c160e74e0656ad4fa84ff92ec2)) 47 | * positionals now opt-in when strict:true ([#116](https://github.com/pkgjs/parseargs/issues/116)) ([3643338](https://github.com/pkgjs/parseargs/commit/364333826b746e8a7dc5505b4b22fd19ac51df3b)) 48 | * switch type:string option arguments to greedy, but with error for suspect cases in strict mode ([#88](https://github.com/pkgjs/parseargs/issues/88)) ([c2b5e72](https://github.com/pkgjs/parseargs/commit/c2b5e72161991dfdc535909f1327cc9b970fe7e8)) 49 | 50 | ### [0.7.1](https://github.com/pkgjs/parseargs/compare/v0.7.0...v0.7.1) (2022-04-15) 51 | 52 | 53 | ### Bug Fixes 54 | 55 | * resist pollution ([#106](https://github.com/pkgjs/parseargs/issues/106)) ([ecf2dec](https://github.com/pkgjs/parseargs/commit/ecf2dece0a9f2a76d789384d5d71c68ffe64022a)) 56 | 57 | ## [0.7.0](https://github.com/pkgjs/parseargs/compare/v0.6.0...v0.7.0) (2022-04-13) 58 | 59 | 60 | ### Features 61 | 62 | * Add strict mode to parser ([#74](https://github.com/pkgjs/parseargs/issues/74)) ([8267d02](https://github.com/pkgjs/parseargs/commit/8267d02083a87b8b8a71fcce08348d1e031ea91c)) 63 | 64 | ## [0.6.0](https://github.com/pkgjs/parseargs/compare/v0.5.0...v0.6.0) (2022-04-11) 65 | 66 | 67 | ### ⚠ BREAKING CHANGES 68 | 69 | * rework results to remove redundant `flags` property and store value true for boolean options (#83) 70 | * switch to existing ERR_INVALID_ARG_VALUE (#97) 71 | 72 | ### Code Refactoring 73 | 74 | * rework results to remove redundant `flags` property and store value true for boolean options ([#83](https://github.com/pkgjs/parseargs/issues/83)) ([be153db](https://github.com/pkgjs/parseargs/commit/be153dbed1d488cb7b6e27df92f601ba7337713d)) 75 | * switch to existing ERR_INVALID_ARG_VALUE ([#97](https://github.com/pkgjs/parseargs/issues/97)) ([084a23f](https://github.com/pkgjs/parseargs/commit/084a23f9fde2da030b159edb1c2385f24579ce40)) 76 | 77 | ## [0.5.0](https://github.com/pkgjs/parseargs/compare/v0.4.0...v0.5.0) (2022-04-10) 78 | 79 | 80 | ### ⚠ BREAKING CHANGES 81 | 82 | * Require type to be specified for each supplied option (#95) 83 | 84 | ### Features 85 | 86 | * Require type to be specified for each supplied option ([#95](https://github.com/pkgjs/parseargs/issues/95)) ([02cd018](https://github.com/pkgjs/parseargs/commit/02cd01885b8aaa59f2db8308f2d4479e64340068)) 87 | 88 | ## [0.4.0](https://github.com/pkgjs/parseargs/compare/v0.3.0...v0.4.0) (2022-03-12) 89 | 90 | 91 | ### ⚠ BREAKING CHANGES 92 | 93 | * parsing, revisit short option groups, add support for combined short and value (#75) 94 | * restructure configuration to take options bag (#63) 95 | 96 | ### Code Refactoring 97 | 98 | * parsing, revisit short option groups, add support for combined short and value ([#75](https://github.com/pkgjs/parseargs/issues/75)) ([a92600f](https://github.com/pkgjs/parseargs/commit/a92600fa6c214508ab1e016fa55879a314f541af)) 99 | * restructure configuration to take options bag ([#63](https://github.com/pkgjs/parseargs/issues/63)) ([b412095](https://github.com/pkgjs/parseargs/commit/b4120957d90e809ee8b607b06e747d3e6a6b213e)) 100 | 101 | ## [0.3.0](https://github.com/pkgjs/parseargs/compare/v0.2.0...v0.3.0) (2022-02-06) 102 | 103 | 104 | ### Features 105 | 106 | * **parser:** support short-option groups ([#59](https://github.com/pkgjs/parseargs/issues/59)) ([882067b](https://github.com/pkgjs/parseargs/commit/882067bc2d7cbc6b796f8e5a079a99bc99d4e6ba)) 107 | 108 | ## [0.2.0](https://github.com/pkgjs/parseargs/compare/v0.1.1...v0.2.0) (2022-02-05) 109 | 110 | 111 | ### Features 112 | 113 | * basic support for shorts ([#50](https://github.com/pkgjs/parseargs/issues/50)) ([a2f36d7](https://github.com/pkgjs/parseargs/commit/a2f36d7da4145af1c92f76806b7fe2baf6beeceb)) 114 | 115 | 116 | ### Bug Fixes 117 | 118 | * always store value for a=b ([#43](https://github.com/pkgjs/parseargs/issues/43)) ([a85e8dc](https://github.com/pkgjs/parseargs/commit/a85e8dc06379fd2696ee195cc625de8fac6aee42)) 119 | * support single dash as positional ([#49](https://github.com/pkgjs/parseargs/issues/49)) ([d795bf8](https://github.com/pkgjs/parseargs/commit/d795bf877d068fd67aec381f30b30b63f97109ad)) 120 | 121 | ### [0.1.1](https://github.com/pkgjs/parseargs/compare/v0.1.0...v0.1.1) (2022-01-25) 122 | 123 | 124 | ### Bug Fixes 125 | 126 | * only use arrays in results for multiples ([#42](https://github.com/pkgjs/parseargs/issues/42)) ([c357584](https://github.com/pkgjs/parseargs/commit/c357584847912506319ed34a0840080116f4fd65)) 127 | 128 | ## 0.1.0 (2022-01-22) 129 | 130 | 131 | ### Features 132 | 133 | * expand scenarios covered by default arguments for environments ([#20](https://github.com/pkgjs/parseargs/issues/20)) ([582ada7](https://github.com/pkgjs/parseargs/commit/582ada7be0eca3a73d6e0bd016e7ace43449fa4c)) 134 | * update readme and include contributing guidelines ([8edd6fc](https://github.com/pkgjs/parseargs/commit/8edd6fc863cd705f6fac732724159ebe8065a2b0)) 135 | 136 | 137 | ### Bug Fixes 138 | 139 | * do not strip excess leading dashes on long option names ([#21](https://github.com/pkgjs/parseargs/issues/21)) ([f848590](https://github.com/pkgjs/parseargs/commit/f848590ebf3249ed5979ff47e003fa6e1a8ec5c0)) 140 | * name & readme ([3f057c1](https://github.com/pkgjs/parseargs/commit/3f057c1b158a1bdbe878c64b57460c58e56e465f)) 141 | * package.json values ([9bac300](https://github.com/pkgjs/parseargs/commit/9bac300e00cd76c77076bf9e75e44f8929512da9)) 142 | * update readme name ([957d8d9](https://github.com/pkgjs/parseargs/commit/957d8d96e1dcb48297c0a14345d44c0123b2883e)) 143 | 144 | 145 | ### Build System 146 | 147 | * first release as minor ([421c6e2](https://github.com/pkgjs/parseargs/commit/421c6e2569a8668ad14fac5a5af5be60479a7571)) 148 | -------------------------------------------------------------------------------- /internal/primordials.js: -------------------------------------------------------------------------------- 1 | /* 2 | This file is copied from https://github.com/nodejs/node/blob/v14.19.3/lib/internal/per_context/primordials.js 3 | under the following license: 4 | 5 | Copyright Node.js contributors. All rights reserved. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to 9 | deal in the Software without restriction, including without limitation the 10 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | sell copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 23 | IN THE SOFTWARE. 24 | 25 | A modification is made to always reference `globalThis` instead of referencing 26 | both `global` and `globalThis`. See #157. 27 | */ 28 | 29 | 'use strict'; 30 | 31 | /* eslint-disable node-core/prefer-primordials */ 32 | 33 | // This file subclasses and stores the JS builtins that come from the VM 34 | // so that Node.js's builtin modules do not need to later look these up from 35 | // the global proxy, which can be mutated by users. 36 | 37 | // Use of primordials have sometimes a dramatic impact on performance, please 38 | // benchmark all changes made in performance-sensitive areas of the codebase. 39 | // See: https://github.com/nodejs/node/pull/38248 40 | 41 | const primordials = {}; 42 | 43 | const { 44 | defineProperty: ReflectDefineProperty, 45 | getOwnPropertyDescriptor: ReflectGetOwnPropertyDescriptor, 46 | ownKeys: ReflectOwnKeys, 47 | } = Reflect; 48 | 49 | // `uncurryThis` is equivalent to `func => Function.prototype.call.bind(func)`. 50 | // It is using `bind.bind(call)` to avoid using `Function.prototype.bind` 51 | // and `Function.prototype.call` after it may have been mutated by users. 52 | const { apply, bind, call } = Function.prototype; 53 | const uncurryThis = bind.bind(call); 54 | primordials.uncurryThis = uncurryThis; 55 | 56 | // `applyBind` is equivalent to `func => Function.prototype.apply.bind(func)`. 57 | // It is using `bind.bind(apply)` to avoid using `Function.prototype.bind` 58 | // and `Function.prototype.apply` after it may have been mutated by users. 59 | const applyBind = bind.bind(apply); 60 | primordials.applyBind = applyBind; 61 | 62 | // Methods that accept a variable number of arguments, and thus it's useful to 63 | // also create `${prefix}${key}Apply`, which uses `Function.prototype.apply`, 64 | // instead of `Function.prototype.call`, and thus doesn't require iterator 65 | // destructuring. 66 | const varargsMethods = [ 67 | // 'ArrayPrototypeConcat' is omitted, because it performs the spread 68 | // on its own for arrays and array-likes with a truthy 69 | // @@isConcatSpreadable symbol property. 70 | 'ArrayOf', 71 | 'ArrayPrototypePush', 72 | 'ArrayPrototypeUnshift', 73 | // 'FunctionPrototypeCall' is omitted, since there's 'ReflectApply' 74 | // and 'FunctionPrototypeApply'. 75 | 'MathHypot', 76 | 'MathMax', 77 | 'MathMin', 78 | 'StringPrototypeConcat', 79 | 'TypedArrayOf', 80 | ]; 81 | 82 | function getNewKey(key) { 83 | return typeof key === 'symbol' ? 84 | `Symbol${key.description[7].toUpperCase()}${key.description.slice(8)}` : 85 | `${key[0].toUpperCase()}${key.slice(1)}`; 86 | } 87 | 88 | function copyAccessor(dest, prefix, key, { enumerable, get, set }) { 89 | ReflectDefineProperty(dest, `${prefix}Get${key}`, { 90 | value: uncurryThis(get), 91 | enumerable 92 | }); 93 | if (set !== undefined) { 94 | ReflectDefineProperty(dest, `${prefix}Set${key}`, { 95 | value: uncurryThis(set), 96 | enumerable 97 | }); 98 | } 99 | } 100 | 101 | function copyPropsRenamed(src, dest, prefix) { 102 | for (const key of ReflectOwnKeys(src)) { 103 | const newKey = getNewKey(key); 104 | const desc = ReflectGetOwnPropertyDescriptor(src, key); 105 | if ('get' in desc) { 106 | copyAccessor(dest, prefix, newKey, desc); 107 | } else { 108 | const name = `${prefix}${newKey}`; 109 | ReflectDefineProperty(dest, name, desc); 110 | if (varargsMethods.includes(name)) { 111 | ReflectDefineProperty(dest, `${name}Apply`, { 112 | // `src` is bound as the `this` so that the static `this` points 113 | // to the object it was defined on, 114 | // e.g.: `ArrayOfApply` gets a `this` of `Array`: 115 | value: applyBind(desc.value, src), 116 | }); 117 | } 118 | } 119 | } 120 | } 121 | 122 | function copyPropsRenamedBound(src, dest, prefix) { 123 | for (const key of ReflectOwnKeys(src)) { 124 | const newKey = getNewKey(key); 125 | const desc = ReflectGetOwnPropertyDescriptor(src, key); 126 | if ('get' in desc) { 127 | copyAccessor(dest, prefix, newKey, desc); 128 | } else { 129 | const { value } = desc; 130 | if (typeof value === 'function') { 131 | desc.value = value.bind(src); 132 | } 133 | 134 | const name = `${prefix}${newKey}`; 135 | ReflectDefineProperty(dest, name, desc); 136 | if (varargsMethods.includes(name)) { 137 | ReflectDefineProperty(dest, `${name}Apply`, { 138 | value: applyBind(value, src), 139 | }); 140 | } 141 | } 142 | } 143 | } 144 | 145 | function copyPrototype(src, dest, prefix) { 146 | for (const key of ReflectOwnKeys(src)) { 147 | const newKey = getNewKey(key); 148 | const desc = ReflectGetOwnPropertyDescriptor(src, key); 149 | if ('get' in desc) { 150 | copyAccessor(dest, prefix, newKey, desc); 151 | } else { 152 | const { value } = desc; 153 | if (typeof value === 'function') { 154 | desc.value = uncurryThis(value); 155 | } 156 | 157 | const name = `${prefix}${newKey}`; 158 | ReflectDefineProperty(dest, name, desc); 159 | if (varargsMethods.includes(name)) { 160 | ReflectDefineProperty(dest, `${name}Apply`, { 161 | value: applyBind(value), 162 | }); 163 | } 164 | } 165 | } 166 | } 167 | 168 | // Create copies of configurable value properties of the global object 169 | [ 170 | 'Proxy', 171 | 'globalThis', 172 | ].forEach((name) => { 173 | // eslint-disable-next-line no-restricted-globals 174 | primordials[name] = globalThis[name]; 175 | }); 176 | 177 | // Create copies of URI handling functions 178 | [ 179 | decodeURI, 180 | decodeURIComponent, 181 | encodeURI, 182 | encodeURIComponent, 183 | ].forEach((fn) => { 184 | primordials[fn.name] = fn; 185 | }); 186 | 187 | // Create copies of the namespace objects 188 | [ 189 | 'JSON', 190 | 'Math', 191 | 'Proxy', 192 | 'Reflect', 193 | ].forEach((name) => { 194 | // eslint-disable-next-line no-restricted-globals 195 | copyPropsRenamed(globalThis[name], primordials, name); 196 | }); 197 | 198 | // Create copies of intrinsic objects 199 | [ 200 | 'Array', 201 | 'ArrayBuffer', 202 | 'BigInt', 203 | 'BigInt64Array', 204 | 'BigUint64Array', 205 | 'Boolean', 206 | 'DataView', 207 | 'Date', 208 | 'Error', 209 | 'EvalError', 210 | 'Float32Array', 211 | 'Float64Array', 212 | 'Function', 213 | 'Int16Array', 214 | 'Int32Array', 215 | 'Int8Array', 216 | 'Map', 217 | 'Number', 218 | 'Object', 219 | 'RangeError', 220 | 'ReferenceError', 221 | 'RegExp', 222 | 'Set', 223 | 'String', 224 | 'Symbol', 225 | 'SyntaxError', 226 | 'TypeError', 227 | 'URIError', 228 | 'Uint16Array', 229 | 'Uint32Array', 230 | 'Uint8Array', 231 | 'Uint8ClampedArray', 232 | 'WeakMap', 233 | 'WeakSet', 234 | ].forEach((name) => { 235 | // eslint-disable-next-line no-restricted-globals 236 | const original = globalThis[name]; 237 | primordials[name] = original; 238 | copyPropsRenamed(original, primordials, name); 239 | copyPrototype(original.prototype, primordials, `${name}Prototype`); 240 | }); 241 | 242 | // Create copies of intrinsic objects that require a valid `this` to call 243 | // static methods. 244 | // Refs: https://www.ecma-international.org/ecma-262/#sec-promise.all 245 | [ 246 | 'Promise', 247 | ].forEach((name) => { 248 | // eslint-disable-next-line no-restricted-globals 249 | const original = globalThis[name]; 250 | primordials[name] = original; 251 | copyPropsRenamedBound(original, primordials, name); 252 | copyPrototype(original.prototype, primordials, `${name}Prototype`); 253 | }); 254 | 255 | // Create copies of abstract intrinsic objects that are not directly exposed 256 | // on the global object. 257 | // Refs: https://tc39.es/ecma262/#sec-%typedarray%-intrinsic-object 258 | [ 259 | { name: 'TypedArray', original: Reflect.getPrototypeOf(Uint8Array) }, 260 | { name: 'ArrayIterator', original: { 261 | prototype: Reflect.getPrototypeOf(Array.prototype[Symbol.iterator]()), 262 | } }, 263 | { name: 'StringIterator', original: { 264 | prototype: Reflect.getPrototypeOf(String.prototype[Symbol.iterator]()), 265 | } }, 266 | ].forEach(({ name, original }) => { 267 | primordials[name] = original; 268 | // The static %TypedArray% methods require a valid `this`, but can't be bound, 269 | // as they need a subclass constructor as the receiver: 270 | copyPrototype(original, primordials, name); 271 | copyPrototype(original.prototype, primordials, `${name}Prototype`); 272 | }); 273 | 274 | /* eslint-enable node-core/prefer-primordials */ 275 | 276 | const { 277 | ArrayPrototypeForEach, 278 | FunctionPrototypeCall, 279 | Map, 280 | ObjectFreeze, 281 | ObjectSetPrototypeOf, 282 | Set, 283 | SymbolIterator, 284 | WeakMap, 285 | WeakSet, 286 | } = primordials; 287 | 288 | // Because these functions are used by `makeSafe`, which is exposed 289 | // on the `primordials` object, it's important to use const references 290 | // to the primordials that they use: 291 | const createSafeIterator = (factory, next) => { 292 | class SafeIterator { 293 | constructor(iterable) { 294 | this._iterator = factory(iterable); 295 | } 296 | next() { 297 | return next(this._iterator); 298 | } 299 | [SymbolIterator]() { 300 | return this; 301 | } 302 | } 303 | ObjectSetPrototypeOf(SafeIterator.prototype, null); 304 | ObjectFreeze(SafeIterator.prototype); 305 | ObjectFreeze(SafeIterator); 306 | return SafeIterator; 307 | }; 308 | 309 | primordials.SafeArrayIterator = createSafeIterator( 310 | primordials.ArrayPrototypeSymbolIterator, 311 | primordials.ArrayIteratorPrototypeNext 312 | ); 313 | primordials.SafeStringIterator = createSafeIterator( 314 | primordials.StringPrototypeSymbolIterator, 315 | primordials.StringIteratorPrototypeNext 316 | ); 317 | 318 | const copyProps = (src, dest) => { 319 | ArrayPrototypeForEach(ReflectOwnKeys(src), (key) => { 320 | if (!ReflectGetOwnPropertyDescriptor(dest, key)) { 321 | ReflectDefineProperty( 322 | dest, 323 | key, 324 | ReflectGetOwnPropertyDescriptor(src, key)); 325 | } 326 | }); 327 | }; 328 | 329 | const makeSafe = (unsafe, safe) => { 330 | if (SymbolIterator in unsafe.prototype) { 331 | const dummy = new unsafe(); 332 | let next; // We can reuse the same `next` method. 333 | 334 | ArrayPrototypeForEach(ReflectOwnKeys(unsafe.prototype), (key) => { 335 | if (!ReflectGetOwnPropertyDescriptor(safe.prototype, key)) { 336 | const desc = ReflectGetOwnPropertyDescriptor(unsafe.prototype, key); 337 | if ( 338 | typeof desc.value === 'function' && 339 | desc.value.length === 0 && 340 | SymbolIterator in (FunctionPrototypeCall(desc.value, dummy) ?? {}) 341 | ) { 342 | const createIterator = uncurryThis(desc.value); 343 | next = next ?? uncurryThis(createIterator(dummy).next); 344 | const SafeIterator = createSafeIterator(createIterator, next); 345 | desc.value = function() { 346 | return new SafeIterator(this); 347 | }; 348 | } 349 | ReflectDefineProperty(safe.prototype, key, desc); 350 | } 351 | }); 352 | } else { 353 | copyProps(unsafe.prototype, safe.prototype); 354 | } 355 | copyProps(unsafe, safe); 356 | 357 | ObjectSetPrototypeOf(safe.prototype, null); 358 | ObjectFreeze(safe.prototype); 359 | ObjectFreeze(safe); 360 | return safe; 361 | }; 362 | primordials.makeSafe = makeSafe; 363 | 364 | // Subclass the constructors because we need to use their prototype 365 | // methods later. 366 | // Defining the `constructor` is necessary here to avoid the default 367 | // constructor which uses the user-mutable `%ArrayIteratorPrototype%.next`. 368 | primordials.SafeMap = makeSafe( 369 | Map, 370 | class SafeMap extends Map { 371 | constructor(i) { super(i); } // eslint-disable-line no-useless-constructor 372 | } 373 | ); 374 | primordials.SafeWeakMap = makeSafe( 375 | WeakMap, 376 | class SafeWeakMap extends WeakMap { 377 | constructor(i) { super(i); } // eslint-disable-line no-useless-constructor 378 | } 379 | ); 380 | primordials.SafeSet = makeSafe( 381 | Set, 382 | class SafeSet extends Set { 383 | constructor(i) { super(i); } // eslint-disable-line no-useless-constructor 384 | } 385 | ); 386 | primordials.SafeWeakSet = makeSafe( 387 | WeakSet, 388 | class SafeWeakSet extends WeakSet { 389 | constructor(i) { super(i); } // eslint-disable-line no-useless-constructor 390 | } 391 | ); 392 | 393 | ObjectSetPrototypeOf(primordials, null); 394 | ObjectFreeze(primordials); 395 | 396 | module.exports = primordials; 397 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint max-len: ["error", {"code": 120}], */ 4 | 5 | const { 6 | ArrayPrototypeForEach, 7 | ArrayPrototypeIncludes, 8 | ArrayPrototypeMap, 9 | ArrayPrototypePush, 10 | ArrayPrototypePushApply, 11 | ArrayPrototypeShift, 12 | ArrayPrototypeSlice, 13 | ArrayPrototypeUnshiftApply, 14 | ObjectEntries, 15 | ObjectPrototypeHasOwnProperty: ObjectHasOwn, 16 | StringPrototypeCharAt, 17 | StringPrototypeIndexOf, 18 | StringPrototypeSlice, 19 | StringPrototypeStartsWith, 20 | } = require('./internal/primordials'); 21 | 22 | const { 23 | validateArray, 24 | validateBoolean, 25 | validateBooleanArray, 26 | validateObject, 27 | validateString, 28 | validateStringArray, 29 | validateUnion, 30 | } = require('./internal/validators'); 31 | 32 | const { 33 | kEmptyObject, 34 | } = require('./internal/util'); 35 | 36 | const { 37 | findLongOptionForShort, 38 | isLoneLongOption, 39 | isLoneShortOption, 40 | isLongOptionAndValue, 41 | isOptionValue, 42 | isOptionLikeValue, 43 | isShortOptionAndValue, 44 | isShortOptionGroup, 45 | useDefaultValueOption, 46 | objectGetOwn, 47 | optionsGetOwn, 48 | } = require('./utils'); 49 | 50 | const { 51 | codes: { 52 | ERR_INVALID_ARG_VALUE, 53 | ERR_PARSE_ARGS_INVALID_OPTION_VALUE, 54 | ERR_PARSE_ARGS_UNKNOWN_OPTION, 55 | ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL, 56 | }, 57 | } = require('./internal/errors'); 58 | 59 | function getMainArgs() { 60 | // Work out where to slice process.argv for user supplied arguments. 61 | 62 | // Check node options for scenarios where user CLI args follow executable. 63 | const execArgv = process.execArgv; 64 | if (ArrayPrototypeIncludes(execArgv, '-e') || 65 | ArrayPrototypeIncludes(execArgv, '--eval') || 66 | ArrayPrototypeIncludes(execArgv, '-p') || 67 | ArrayPrototypeIncludes(execArgv, '--print')) { 68 | return ArrayPrototypeSlice(process.argv, 1); 69 | } 70 | 71 | // Normally first two arguments are executable and script, then CLI arguments 72 | return ArrayPrototypeSlice(process.argv, 2); 73 | } 74 | 75 | /** 76 | * In strict mode, throw for possible usage errors like --foo --bar 77 | * 78 | * @param {object} token - from tokens as available from parseArgs 79 | */ 80 | function checkOptionLikeValue(token) { 81 | if (!token.inlineValue && isOptionLikeValue(token.value)) { 82 | // Only show short example if user used short option. 83 | const example = StringPrototypeStartsWith(token.rawName, '--') ? 84 | `'${token.rawName}=-XYZ'` : 85 | `'--${token.name}=-XYZ' or '${token.rawName}-XYZ'`; 86 | const errorMessage = `Option '${token.rawName}' argument is ambiguous. 87 | Did you forget to specify the option argument for '${token.rawName}'? 88 | To specify an option argument starting with a dash use ${example}.`; 89 | throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(errorMessage); 90 | } 91 | } 92 | 93 | /** 94 | * In strict mode, throw for usage errors. 95 | * 96 | * @param {object} config - from config passed to parseArgs 97 | * @param {object} token - from tokens as available from parseArgs 98 | */ 99 | function checkOptionUsage(config, token) { 100 | let tokenName = token.name; 101 | if (!ObjectHasOwn(config.options, tokenName)) { 102 | // Check for negated boolean option. 103 | if (config.allowNegative && StringPrototypeStartsWith(tokenName, 'no-')) { 104 | tokenName = StringPrototypeSlice(tokenName, 3); 105 | if (!ObjectHasOwn(config.options, tokenName) || optionsGetOwn(config.options, tokenName, 'type') !== 'boolean') { 106 | throw new ERR_PARSE_ARGS_UNKNOWN_OPTION( 107 | token.rawName, config.allowPositionals); 108 | } 109 | } else { 110 | throw new ERR_PARSE_ARGS_UNKNOWN_OPTION( 111 | token.rawName, config.allowPositionals); 112 | } 113 | } 114 | 115 | const short = optionsGetOwn(config.options, tokenName, 'short'); 116 | const shortAndLong = `${short ? `-${short}, ` : ''}--${tokenName}`; 117 | const type = optionsGetOwn(config.options, tokenName, 'type'); 118 | if (type === 'string' && typeof token.value !== 'string') { 119 | throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(`Option '${shortAndLong} ' argument missing`); 120 | } 121 | // (Idiomatic test for undefined||null, expecting undefined.) 122 | if (type === 'boolean' && token.value != null) { 123 | throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(`Option '${shortAndLong}' does not take an argument`); 124 | } 125 | } 126 | 127 | 128 | /** 129 | * Store the option value in `values`. 130 | * @param {object} token - from tokens as available from parseArgs 131 | * @param {object} options - option configs, from parseArgs({ options }) 132 | * @param {object} values - option values returned in `values` by parseArgs 133 | * @param {boolean} allowNegative - allow negative optinons if true 134 | */ 135 | function storeOption(token, options, values, allowNegative) { 136 | let longOption = token.name; 137 | let optionValue = token.value; 138 | if (longOption === '__proto__') { 139 | return; // No. Just no. 140 | } 141 | 142 | if (allowNegative && StringPrototypeStartsWith(longOption, 'no-') && optionValue === undefined) { 143 | // Boolean option negation: --no-foo 144 | longOption = StringPrototypeSlice(longOption, 3); 145 | token.name = longOption; 146 | optionValue = false; 147 | } 148 | 149 | // We store based on the option value rather than option type, 150 | // preserving the users intent for author to deal with. 151 | const newValue = optionValue ?? true; 152 | if (optionsGetOwn(options, longOption, 'multiple')) { 153 | // Always store value in array, including for boolean. 154 | // values[longOption] starts out not present, 155 | // first value is added as new array [newValue], 156 | // subsequent values are pushed to existing array. 157 | // (note: values has null prototype, so simpler usage) 158 | if (values[longOption]) { 159 | ArrayPrototypePush(values[longOption], newValue); 160 | } else { 161 | values[longOption] = [newValue]; 162 | } 163 | } else { 164 | values[longOption] = newValue; 165 | } 166 | } 167 | 168 | /** 169 | * Store the default option value in `values`. 170 | * 171 | * @param {string} longOption - long option name e.g. 'foo' 172 | * @param {string 173 | * | boolean 174 | * | string[] 175 | * | boolean[]} optionValue - default value from option config 176 | * @param {object} values - option values returned in `values` by parseArgs 177 | */ 178 | function storeDefaultOption(longOption, optionValue, values) { 179 | if (longOption === '__proto__') { 180 | return; // No. Just no. 181 | } 182 | 183 | values[longOption] = optionValue; 184 | } 185 | 186 | /** 187 | * Process args and turn into identified tokens: 188 | * - option (along with value, if any) 189 | * - positional 190 | * - option-terminator 191 | * 192 | * @param {string[]} args - from parseArgs({ args }) or mainArgs 193 | * @param {object} options - option configs, from parseArgs({ options }) 194 | */ 195 | function argsToTokens(args, options) { 196 | const tokens = []; 197 | let index = -1; 198 | let groupCount = 0; 199 | 200 | const remainingArgs = ArrayPrototypeSlice(args); 201 | while (remainingArgs.length > 0) { 202 | const arg = ArrayPrototypeShift(remainingArgs); 203 | const nextArg = remainingArgs[0]; 204 | if (groupCount > 0) 205 | groupCount--; 206 | else 207 | index++; 208 | 209 | // Check if `arg` is an options terminator. 210 | // Guideline 10 in https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html 211 | if (arg === '--') { 212 | // Everything after a bare '--' is considered a positional argument. 213 | ArrayPrototypePush(tokens, { kind: 'option-terminator', index }); 214 | ArrayPrototypePushApply( 215 | tokens, ArrayPrototypeMap(remainingArgs, (arg) => { 216 | return { kind: 'positional', index: ++index, value: arg }; 217 | }) 218 | ); 219 | break; // Finished processing args, leave while loop. 220 | } 221 | 222 | if (isLoneShortOption(arg)) { 223 | // e.g. '-f' 224 | const shortOption = StringPrototypeCharAt(arg, 1); 225 | const longOption = findLongOptionForShort(shortOption, options); 226 | let value; 227 | let inlineValue; 228 | if (optionsGetOwn(options, longOption, 'type') === 'string' && 229 | isOptionValue(nextArg)) { 230 | // e.g. '-f', 'bar' 231 | value = ArrayPrototypeShift(remainingArgs); 232 | inlineValue = false; 233 | } 234 | ArrayPrototypePush( 235 | tokens, 236 | { kind: 'option', name: longOption, rawName: arg, 237 | index, value, inlineValue }); 238 | if (value != null) ++index; 239 | continue; 240 | } 241 | 242 | if (isShortOptionGroup(arg, options)) { 243 | // Expand -fXzy to -f -X -z -y 244 | const expanded = []; 245 | for (let index = 1; index < arg.length; index++) { 246 | const shortOption = StringPrototypeCharAt(arg, index); 247 | const longOption = findLongOptionForShort(shortOption, options); 248 | if (optionsGetOwn(options, longOption, 'type') !== 'string' || 249 | index === arg.length - 1) { 250 | // Boolean option, or last short in group. Well formed. 251 | ArrayPrototypePush(expanded, `-${shortOption}`); 252 | } else { 253 | // String option in middle. Yuck. 254 | // Expand -abfFILE to -a -b -fFILE 255 | ArrayPrototypePush(expanded, `-${StringPrototypeSlice(arg, index)}`); 256 | break; // finished short group 257 | } 258 | } 259 | ArrayPrototypeUnshiftApply(remainingArgs, expanded); 260 | groupCount = expanded.length; 261 | continue; 262 | } 263 | 264 | if (isShortOptionAndValue(arg, options)) { 265 | // e.g. -fFILE 266 | const shortOption = StringPrototypeCharAt(arg, 1); 267 | const longOption = findLongOptionForShort(shortOption, options); 268 | const value = StringPrototypeSlice(arg, 2); 269 | ArrayPrototypePush( 270 | tokens, 271 | { kind: 'option', name: longOption, rawName: `-${shortOption}`, 272 | index, value, inlineValue: true }); 273 | continue; 274 | } 275 | 276 | if (isLoneLongOption(arg)) { 277 | // e.g. '--foo' 278 | const longOption = StringPrototypeSlice(arg, 2); 279 | let value; 280 | let inlineValue; 281 | if (optionsGetOwn(options, longOption, 'type') === 'string' && 282 | isOptionValue(nextArg)) { 283 | // e.g. '--foo', 'bar' 284 | value = ArrayPrototypeShift(remainingArgs); 285 | inlineValue = false; 286 | } 287 | ArrayPrototypePush( 288 | tokens, 289 | { kind: 'option', name: longOption, rawName: arg, 290 | index, value, inlineValue }); 291 | if (value != null) ++index; 292 | continue; 293 | } 294 | 295 | if (isLongOptionAndValue(arg)) { 296 | // e.g. --foo=bar 297 | const equalIndex = StringPrototypeIndexOf(arg, '='); 298 | const longOption = StringPrototypeSlice(arg, 2, equalIndex); 299 | const value = StringPrototypeSlice(arg, equalIndex + 1); 300 | ArrayPrototypePush( 301 | tokens, 302 | { kind: 'option', name: longOption, rawName: `--${longOption}`, 303 | index, value, inlineValue: true }); 304 | continue; 305 | } 306 | 307 | ArrayPrototypePush(tokens, { kind: 'positional', index, value: arg }); 308 | } 309 | 310 | return tokens; 311 | } 312 | 313 | const parseArgs = (config = kEmptyObject) => { 314 | const args = objectGetOwn(config, 'args') ?? getMainArgs(); 315 | const strict = objectGetOwn(config, 'strict') ?? true; 316 | const allowPositionals = objectGetOwn(config, 'allowPositionals') ?? !strict; 317 | const returnTokens = objectGetOwn(config, 'tokens') ?? false; 318 | const allowNegative = objectGetOwn(config, 'allowNegative') ?? false; 319 | const options = objectGetOwn(config, 'options') ?? { __proto__: null }; 320 | // Bundle these up for passing to strict-mode checks. 321 | const parseConfig = { args, strict, options, allowPositionals, allowNegative }; 322 | 323 | // Validate input configuration. 324 | validateArray(args, 'args'); 325 | validateBoolean(strict, 'strict'); 326 | validateBoolean(allowPositionals, 'allowPositionals'); 327 | validateBoolean(returnTokens, 'tokens'); 328 | validateBoolean(allowNegative, 'allowNegative'); 329 | validateObject(options, 'options'); 330 | ArrayPrototypeForEach( 331 | ObjectEntries(options), 332 | ({ 0: longOption, 1: optionConfig }) => { 333 | validateObject(optionConfig, `options.${longOption}`); 334 | 335 | // type is required 336 | const optionType = objectGetOwn(optionConfig, 'type'); 337 | validateUnion(optionType, `options.${longOption}.type`, ['string', 'boolean']); 338 | 339 | if (ObjectHasOwn(optionConfig, 'short')) { 340 | const shortOption = optionConfig.short; 341 | validateString(shortOption, `options.${longOption}.short`); 342 | if (shortOption.length !== 1) { 343 | throw new ERR_INVALID_ARG_VALUE( 344 | `options.${longOption}.short`, 345 | shortOption, 346 | 'must be a single character' 347 | ); 348 | } 349 | } 350 | 351 | const multipleOption = objectGetOwn(optionConfig, 'multiple'); 352 | if (ObjectHasOwn(optionConfig, 'multiple')) { 353 | validateBoolean(multipleOption, `options.${longOption}.multiple`); 354 | } 355 | 356 | const defaultValue = objectGetOwn(optionConfig, 'default'); 357 | if (defaultValue !== undefined) { 358 | let validator; 359 | switch (optionType) { 360 | case 'string': 361 | validator = multipleOption ? validateStringArray : validateString; 362 | break; 363 | 364 | case 'boolean': 365 | validator = multipleOption ? validateBooleanArray : validateBoolean; 366 | break; 367 | } 368 | validator(defaultValue, `options.${longOption}.default`); 369 | } 370 | } 371 | ); 372 | 373 | // Phase 1: identify tokens 374 | const tokens = argsToTokens(args, options); 375 | 376 | // Phase 2: process tokens into parsed option values and positionals 377 | const result = { 378 | values: { __proto__: null }, 379 | positionals: [], 380 | }; 381 | if (returnTokens) { 382 | result.tokens = tokens; 383 | } 384 | ArrayPrototypeForEach(tokens, (token) => { 385 | if (token.kind === 'option') { 386 | if (strict) { 387 | checkOptionUsage(parseConfig, token); 388 | checkOptionLikeValue(token); 389 | } 390 | storeOption(token, options, result.values, parseConfig.allowNegative); 391 | } else if (token.kind === 'positional') { 392 | if (!allowPositionals) { 393 | throw new ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL(token.value); 394 | } 395 | ArrayPrototypePush(result.positionals, token.value); 396 | } 397 | }); 398 | 399 | // Phase 3: fill in default values for missing args 400 | ArrayPrototypeForEach(ObjectEntries(options), ({ 0: longOption, 401 | 1: optionConfig }) => { 402 | const mustSetDefault = useDefaultValueOption(longOption, 403 | optionConfig, 404 | result.values); 405 | if (mustSetDefault) { 406 | storeDefaultOption(longOption, 407 | objectGetOwn(optionConfig, 'default'), 408 | result.values); 409 | } 410 | }); 411 | 412 | 413 | return result; 414 | }; 415 | 416 | module.exports = { 417 | parseArgs, 418 | }; 419 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # parseArgs 3 | 4 | [![Coverage][coverage-image]][coverage-url] 5 | 6 | Polyfill of `util.parseArgs()` 7 | 8 | ## `util.parseArgs([config])` 9 | 10 | 18 | 19 | > Stability: 2 - Stable 20 | 21 | * `config` {Object} Used to provide arguments for parsing and to configure 22 | the parser. `config` supports the following properties: 23 | * `args` {string\[]} array of argument strings. **Default:** `process.argv` 24 | with `execPath` and `filename` removed. 25 | * `options` {Object} Used to describe arguments known to the parser. 26 | Keys of `options` are the long names of options and values are an 27 | {Object} accepting the following properties: 28 | * `type` {string} Type of argument, which must be either `boolean` or `string`. 29 | * `multiple` {boolean} Whether this option can be provided multiple 30 | times. If `true`, all values will be collected in an array. If 31 | `false`, values for the option are last-wins. **Default:** `false`. 32 | * `short` {string} A single character alias for the option. 33 | * `default` {string | boolean | string\[] | boolean\[]} The default option 34 | value when it is not set by args. It must be of the same type as the 35 | the `type` property. When `multiple` is `true`, it must be an array. 36 | * `strict` {boolean} Should an error be thrown when unknown arguments 37 | are encountered, or when arguments are passed that do not match the 38 | `type` configured in `options`. 39 | **Default:** `true`. 40 | * `allowPositionals` {boolean} Whether this command accepts positional 41 | arguments. 42 | **Default:** `false` if `strict` is `true`, otherwise `true`. 43 | * `allowNegative` {boolean} If `true`, allows explicitly setting boolean 44 | options to `false` by prefixing the option name with `--no-`. 45 | **Default:** `false`. 46 | * `tokens` {boolean} Return the parsed tokens. This is useful for extending 47 | the built-in behavior, from adding additional checks through to reprocessing 48 | the tokens in different ways. 49 | **Default:** `false`. 50 | 51 | * Returns: {Object} The parsed command line arguments: 52 | * `values` {Object} A mapping of parsed option names with their {string} 53 | or {boolean} values. 54 | * `positionals` {string\[]} Positional arguments. 55 | * `tokens` {Object\[] | undefined} See [parseArgs tokens](#parseargs-tokens) 56 | section. Only returned if `config` includes `tokens: true`. 57 | 58 | Provides a higher level API for command-line argument parsing than interacting 59 | with `process.argv` directly. Takes a specification for the expected arguments 60 | and returns a structured object with the parsed options and positionals. 61 | 62 | ```mjs 63 | import { parseArgs } from 'node:util'; 64 | const args = ['-f', '--bar', 'b']; 65 | const options = { 66 | foo: { 67 | type: 'boolean', 68 | short: 'f' 69 | }, 70 | bar: { 71 | type: 'string' 72 | } 73 | }; 74 | const { 75 | values, 76 | positionals 77 | } = parseArgs({ args, options }); 78 | console.log(values, positionals); 79 | // Prints: [Object: null prototype] { foo: true, bar: 'b' } [] 80 | ``` 81 | 82 | ```cjs 83 | const { parseArgs } = require('node:util'); 84 | const args = ['-f', '--bar', 'b']; 85 | const options = { 86 | foo: { 87 | type: 'boolean', 88 | short: 'f' 89 | }, 90 | bar: { 91 | type: 'string' 92 | } 93 | }; 94 | const { 95 | values, 96 | positionals 97 | } = parseArgs({ args, options }); 98 | console.log(values, positionals); 99 | // Prints: [Object: null prototype] { foo: true, bar: 'b' } [] 100 | ``` 101 | 102 | `util.parseArgs` is experimental and behavior may change. Join the 103 | conversation in [pkgjs/parseargs][] to contribute to the design. 104 | 105 | ### `parseArgs` `tokens` 106 | 107 | Detailed parse information is available for adding custom behaviours by 108 | specifying `tokens: true` in the configuration. 109 | The returned tokens have properties describing: 110 | 111 | * all tokens 112 | * `kind` {string} One of 'option', 'positional', or 'option-terminator'. 113 | * `index` {number} Index of element in `args` containing token. So the 114 | source argument for a token is `args[token.index]`. 115 | * option tokens 116 | * `name` {string} Long name of option. 117 | * `rawName` {string} How option used in args, like `-f` of `--foo`. 118 | * `value` {string | undefined} Option value specified in args. 119 | Undefined for boolean options. 120 | * `inlineValue` {boolean | undefined} Whether option value specified inline, 121 | like `--foo=bar`. 122 | * positional tokens 123 | * `value` {string} The value of the positional argument in args (i.e. `args[index]`). 124 | * option-terminator token 125 | 126 | The returned tokens are in the order encountered in the input args. Options 127 | that appear more than once in args produce a token for each use. Short option 128 | groups like `-xy` expand to a token for each option. So `-xxx` produces 129 | three tokens. 130 | 131 | For example, to add support for a negated option like `--no-color` (which 132 | `allowNegative` supports when the option is of `boolean` type), the returned 133 | tokens can be reprocessed to change the value stored for the negated option. 134 | 135 | ```mjs 136 | import { parseArgs } from 'node:util'; 137 | 138 | const options = { 139 | 'color': { type: 'boolean' }, 140 | 'no-color': { type: 'boolean' }, 141 | 'logfile': { type: 'string' }, 142 | 'no-logfile': { type: 'boolean' }, 143 | }; 144 | const { values, tokens } = parseArgs({ options, tokens: true }); 145 | 146 | // Reprocess the option tokens and overwrite the returned values. 147 | tokens 148 | .filter((token) => token.kind === 'option') 149 | .forEach((token) => { 150 | if (token.name.startsWith('no-')) { 151 | // Store foo:false for --no-foo 152 | const positiveName = token.name.slice(3); 153 | values[positiveName] = false; 154 | delete values[token.name]; 155 | } else { 156 | // Resave value so last one wins if both --foo and --no-foo. 157 | values[token.name] = token.value ?? true; 158 | } 159 | }); 160 | 161 | const color = values.color; 162 | const logfile = values.logfile ?? 'default.log'; 163 | 164 | console.log({ logfile, color }); 165 | ``` 166 | 167 | ```cjs 168 | const { parseArgs } = require('node:util'); 169 | 170 | const options = { 171 | 'color': { type: 'boolean' }, 172 | 'no-color': { type: 'boolean' }, 173 | 'logfile': { type: 'string' }, 174 | 'no-logfile': { type: 'boolean' }, 175 | }; 176 | const { values, tokens } = parseArgs({ options, tokens: true }); 177 | 178 | // Reprocess the option tokens and overwrite the returned values. 179 | tokens 180 | .filter((token) => token.kind === 'option') 181 | .forEach((token) => { 182 | if (token.name.startsWith('no-')) { 183 | // Store foo:false for --no-foo 184 | const positiveName = token.name.slice(3); 185 | values[positiveName] = false; 186 | delete values[token.name]; 187 | } else { 188 | // Resave value so last one wins if both --foo and --no-foo. 189 | values[token.name] = token.value ?? true; 190 | } 191 | }); 192 | 193 | const color = values.color; 194 | const logfile = values.logfile ?? 'default.log'; 195 | 196 | console.log({ logfile, color }); 197 | ``` 198 | 199 | Example usage showing negated options, and when an option is used 200 | multiple ways then last one wins. 201 | 202 | ```console 203 | $ node negate.js 204 | { logfile: 'default.log', color: undefined } 205 | $ node negate.js --no-logfile --no-color 206 | { logfile: false, color: false } 207 | $ node negate.js --logfile=test.log --color 208 | { logfile: 'test.log', color: true } 209 | $ node negate.js --no-logfile --logfile=test.log --color --no-color 210 | { logfile: 'test.log', color: false } 211 | ``` 212 | 213 | ----- 214 | 215 | 216 | ## Table of Contents 217 | - [`util.parseArgs([config])`](#utilparseargsconfig) 218 | - [Scope](#scope) 219 | - [Version Matchups](#version-matchups) 220 | - [🚀 Getting Started](#-getting-started) 221 | - [🙌 Contributing](#-contributing) 222 | - [💡 `process.mainArgs` Proposal](#-processmainargs-proposal) 223 | - [Implementation:](#implementation) 224 | - [📃 Examples](#-examples) 225 | - [F.A.Qs](#faqs) 226 | - [Links & Resources](#links--resources) 227 | 228 | ----- 229 | 230 | ## Scope 231 | 232 | It is already possible to build great arg parsing modules on top of what Node.js provides; the prickly API is abstracted away by these modules. Thus, process.parseArgs() is not necessarily intended for library authors; it is intended for developers of simple CLI tools, ad-hoc scripts, deployed Node.js applications, and learning materials. 233 | 234 | It is exceedingly difficult to provide an API which would both be friendly to these Node.js users while being extensible enough for libraries to build upon. We chose to prioritize these use cases because these are currently not well-served by Node.js' API. 235 | 236 | ---- 237 | 238 | ## Version Matchups 239 | 240 | | Node.js | @pkgjs/parseArgs | Changes | 241 | | -- | -- | -- | 242 | | [v18.11.0](https://nodejs.org/docs/latest-v18.x/api/util.html#utilparseargsconfig) | [0.11.0](https://github.com/pkgjs/parseargs/tree/v0.11.0#utilparseargsconfig) | Add support for default values in input `config`. | 243 | | [v16.17.0](https://nodejs.org/dist/latest-v16.x/docs/api/util.html#utilparseargsconfig), [v18.7.0](https://nodejs.org/docs/latest-v18.x/api/util.html#utilparseargsconfig) | [0.10.0](https://github.com/pkgjs/parseargs/tree/v0.10.0#utilparseargsconfig) | Add support for returning detailed parse information using `tokens` in input config and returned properties. | 244 | | [v18.3.0](https://nodejs.org/docs/latest-v18.x/api/util.html#utilparseargsconfig) | [v0.9.1](https://github.com/pkgjs/parseargs/tree/v0.9.1#utilparseargsconfig) | 245 | 246 | ---- 247 | 248 | ## 🚀 Getting Started 249 | 250 | 1. **Install dependencies.** 251 | 252 | ```bash 253 | npm install 254 | ``` 255 | 256 | 2. **Open the index.js file and start editing!** 257 | 258 | 3. **Test your code by calling parseArgs through our test file** 259 | 260 | ```bash 261 | npm test 262 | ``` 263 | 264 | ---- 265 | 266 | ## 🙌 Contributing 267 | 268 | Any person who wants to contribute to the initiative is welcome! Please first read the [Contributing Guide](CONTRIBUTING.md) 269 | 270 | Additionally, reading the [`Examples w/ Output`](#-examples-w-output) section of this document will be the best way to familiarize yourself with the target expected behavior for parseArgs() once it is fully implemented. 271 | 272 | This package was implemented using [tape](https://www.npmjs.com/package/tape) as its test harness. 273 | 274 | ---- 275 | 276 | ## 💡 `process.mainArgs` Proposal 277 | 278 | > Note: This can be moved forward independently of the `util.parseArgs()` proposal/work. 279 | 280 | ### Implementation: 281 | 282 | ```javascript 283 | process.mainArgs = process.argv.slice(process._exec ? 1 : 2) 284 | ``` 285 | 286 | ---- 287 | 288 | ## 📃 Examples 289 | 290 | ```js 291 | const { parseArgs } = require('@pkgjs/parseargs'); 292 | ``` 293 | 294 | ```js 295 | const { parseArgs } = require('@pkgjs/parseargs'); 296 | // specify the options that may be used 297 | const options = { 298 | foo: { type: 'string'}, 299 | bar: { type: 'boolean' }, 300 | }; 301 | const args = ['--foo=a', '--bar']; 302 | const { values, positionals } = parseArgs({ args, options }); 303 | // values = { foo: 'a', bar: true } 304 | // positionals = [] 305 | ``` 306 | 307 | ```js 308 | const { parseArgs } = require('@pkgjs/parseargs'); 309 | // type:string & multiple 310 | const options = { 311 | foo: { 312 | type: 'string', 313 | multiple: true, 314 | }, 315 | }; 316 | const args = ['--foo=a', '--foo', 'b']; 317 | const { values, positionals } = parseArgs({ args, options }); 318 | // values = { foo: [ 'a', 'b' ] } 319 | // positionals = [] 320 | ``` 321 | 322 | ```js 323 | const { parseArgs } = require('@pkgjs/parseargs'); 324 | // shorts 325 | const options = { 326 | foo: { 327 | short: 'f', 328 | type: 'boolean' 329 | }, 330 | }; 331 | const args = ['-f', 'b']; 332 | const { values, positionals } = parseArgs({ args, options, allowPositionals: true }); 333 | // values = { foo: true } 334 | // positionals = ['b'] 335 | ``` 336 | 337 | ```js 338 | const { parseArgs } = require('@pkgjs/parseargs'); 339 | // unconfigured 340 | const options = {}; 341 | const args = ['-f', '--foo=a', '--bar', 'b']; 342 | const { values, positionals } = parseArgs({ strict: false, args, options, allowPositionals: true }); 343 | // values = { f: true, foo: 'a', bar: true } 344 | // positionals = ['b'] 345 | ``` 346 | 347 | ---- 348 | 349 | ## F.A.Qs 350 | 351 | - Is `cmd --foo=bar baz` the same as `cmd baz --foo=bar`? 352 | - yes 353 | - Does the parser execute a function? 354 | - no 355 | - Does the parser execute one of several functions, depending on input? 356 | - no 357 | - Can subcommands take options that are distinct from the main command? 358 | - no 359 | - Does it output generated help when no options match? 360 | - no 361 | - Does it generated short usage? Like: `usage: ls [-ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1] [file ...]` 362 | - no (no usage/help at all) 363 | - Does the user provide the long usage text? For each option? For the whole command? 364 | - no 365 | - Do subcommands (if implemented) have their own usage output? 366 | - no 367 | - Does usage print if the user runs `cmd --help`? 368 | - no 369 | - Does it set `process.exitCode`? 370 | - no 371 | - Does usage print to stderr or stdout? 372 | - N/A 373 | - Does it check types? (Say, specify that an option is a boolean, number, etc.) 374 | - no 375 | - Can an option have more than one type? (string or false, for example) 376 | - no 377 | - Can the user define a type? (Say, `type: path` to call `path.resolve()` on the argument.) 378 | - no 379 | - Does a `--foo=0o22` mean 0, 22, 18, or "0o22"? 380 | - `"0o22"` 381 | - Does it coerce types? 382 | - no 383 | - Does `--no-foo` coerce to `--foo=false`? For all options? Only boolean options? 384 | - no, it sets `{values:{'no-foo': true}}` 385 | - Is `--foo` the same as `--foo=true`? Only for known booleans? Only at the end? 386 | - no, they are not the same. There is no special handling of `true` as a value so it is just another string. 387 | - Does it read environment variables? Ie, is `FOO=1 cmd` the same as `cmd --foo=1`? 388 | - no 389 | - Do unknown arguments raise an error? Are they parsed? Are they treated as positional arguments? 390 | - no, they are parsed, not treated as positionals 391 | - Does `--` signal the end of options? 392 | - yes 393 | - Is `--` included as a positional? 394 | - no 395 | - Is `program -- foo` the same as `program foo`? 396 | - yes, both store `{positionals:['foo']}` 397 | - Does the API specify whether a `--` was present/relevant? 398 | - no 399 | - Is `-bar` the same as `--bar`? 400 | - no, `-bar` is a short option or options, with expansion logic that follows the 401 | [Utility Syntax Guidelines in POSIX.1-2017](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html). `-bar` expands to `-b`, `-a`, `-r`. 402 | - Is `---foo` the same as `--foo`? 403 | - no 404 | - the first is a long option named `'-foo'` 405 | - the second is a long option named `'foo'` 406 | - Is `-` a positional? ie, `bash some-test.sh | tap -` 407 | - yes 408 | 409 | ## Links & Resources 410 | 411 | * [Initial Tooling Issue](https://github.com/nodejs/tooling/issues/19) 412 | * [Initial Proposal](https://github.com/nodejs/node/pull/35015) 413 | * [parseArgs Proposal](https://github.com/nodejs/node/pull/42675) 414 | 415 | [coverage-image]: https://img.shields.io/nycrc/pkgjs/parseargs 416 | [coverage-url]: https://github.com/pkgjs/parseargs/blob/main/.nycrc 417 | [pkgjs/parseargs]: https://github.com/pkgjs/parseargs 418 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* global assert */ 2 | /* eslint max-len: ["error", {"code": 120}], */ 3 | 'use strict'; 4 | 5 | const { test } = require('./utils'); 6 | const { parseArgs } = require('../index'); 7 | 8 | // Test results are as we expect 9 | 10 | test('when short option used as flag then stored as flag', () => { 11 | const args = ['-f']; 12 | const expected = { values: { __proto__: null, f: true }, positionals: [] }; 13 | const result = parseArgs({ strict: false, args }); 14 | assert.deepStrictEqual(result, expected); 15 | }); 16 | 17 | test('when short option used as flag before positional then stored as flag and positional (and not value)', () => { 18 | const args = ['-f', 'bar']; 19 | const expected = { values: { __proto__: null, f: true }, positionals: [ 'bar' ] }; 20 | const result = parseArgs({ strict: false, args }); 21 | assert.deepStrictEqual(result, expected); 22 | }); 23 | 24 | test('when short option `type: "string"` used with value then stored as value', () => { 25 | const args = ['-f', 'bar']; 26 | const options = { f: { type: 'string' } }; 27 | const expected = { values: { __proto__: null, f: 'bar' }, positionals: [] }; 28 | const result = parseArgs({ args, options }); 29 | assert.deepStrictEqual(result, expected); 30 | }); 31 | 32 | test('when short option listed in short used as flag then long option stored as flag', () => { 33 | const args = ['-f']; 34 | const options = { foo: { short: 'f', type: 'boolean' } }; 35 | const expected = { values: { __proto__: null, foo: true }, positionals: [] }; 36 | const result = parseArgs({ args, options }); 37 | assert.deepStrictEqual(result, expected); 38 | }); 39 | 40 | test('when short option listed in short and long listed in `type: "string"` and ' + 41 | 'used with value then long option stored as value', () => { 42 | const args = ['-f', 'bar']; 43 | const options = { foo: { short: 'f', type: 'string' } }; 44 | const expected = { values: { __proto__: null, foo: 'bar' }, positionals: [] }; 45 | const result = parseArgs({ args, options }); 46 | assert.deepStrictEqual(result, expected); 47 | }); 48 | 49 | test('when short option `type: "string"` used without value then stored as flag', () => { 50 | const args = ['-f']; 51 | const options = { f: { type: 'string' } }; 52 | const expected = { values: { __proto__: null, f: true }, positionals: [] }; 53 | const result = parseArgs({ strict: false, args, options }); 54 | assert.deepStrictEqual(result, expected); 55 | }); 56 | 57 | test('short option group behaves like multiple short options', () => { 58 | const args = ['-rf']; 59 | const options = { }; 60 | const expected = { values: { __proto__: null, r: true, f: true }, positionals: [] }; 61 | const result = parseArgs({ strict: false, args, options }); 62 | assert.deepStrictEqual(result, expected); 63 | }); 64 | 65 | test('short option group does not consume subsequent positional', () => { 66 | const args = ['-rf', 'foo']; 67 | const options = { }; 68 | const expected = { values: { __proto__: null, r: true, f: true }, positionals: ['foo'] }; 69 | const result = parseArgs({ strict: false, args, options }); 70 | assert.deepStrictEqual(result, expected); 71 | }); 72 | 73 | // See: Guideline 5 https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html 74 | test('if terminal of short-option group configured `type: "string"`, subsequent positional is stored', () => { 75 | const args = ['-rvf', 'foo']; 76 | const options = { f: { type: 'string' } }; 77 | const expected = { values: { __proto__: null, r: true, v: true, f: 'foo' }, positionals: [] }; 78 | const result = parseArgs({ strict: false, args, options }); 79 | assert.deepStrictEqual(result, expected); 80 | }); 81 | 82 | test('handles short-option groups in conjunction with long-options', () => { 83 | const args = ['-rf', '--foo', 'foo']; 84 | const options = { foo: { type: 'string' } }; 85 | const expected = { values: { __proto__: null, r: true, f: true, foo: 'foo' }, positionals: [] }; 86 | const result = parseArgs({ strict: false, args, options }); 87 | assert.deepStrictEqual(result, expected); 88 | }); 89 | 90 | test('handles short-option groups with "short" alias configured', () => { 91 | const args = ['-rf']; 92 | const options = { remove: { short: 'r', type: 'boolean' } }; 93 | const expected = { values: { __proto__: null, remove: true, f: true }, positionals: [] }; 94 | const result = parseArgs({ strict: false, args, options }); 95 | assert.deepStrictEqual(result, expected); 96 | }); 97 | 98 | test('handles short-option followed by its value', () => { 99 | const args = ['-fFILE']; 100 | const options = { foo: { short: 'f', type: 'string' } }; 101 | const expected = { values: { __proto__: null, foo: 'FILE' }, positionals: [] }; 102 | const result = parseArgs({ strict: false, args, options }); 103 | assert.deepStrictEqual(result, expected); 104 | }); 105 | 106 | test('Everything after a bare `--` is considered a positional argument', () => { 107 | const args = ['--', 'barepositionals', 'mopositionals']; 108 | const expected = { values: { __proto__: null }, positionals: ['barepositionals', 'mopositionals'] }; 109 | const result = parseArgs({ allowPositionals: true, args }); 110 | assert.deepStrictEqual(result, expected); 111 | }); 112 | 113 | test('args are true', () => { 114 | const args = ['--foo', '--bar']; 115 | const expected = { values: { __proto__: null, foo: true, bar: true }, positionals: [] }; 116 | const result = parseArgs({ strict: false, args }); 117 | assert.deepStrictEqual(result, expected); 118 | }); 119 | 120 | test('arg is true and positional is identified', () => { 121 | const args = ['--foo=a', '--foo', 'b']; 122 | const expected = { values: { __proto__: null, foo: true }, positionals: ['b'] }; 123 | const result = parseArgs({ strict: false, args }); 124 | assert.deepStrictEqual(result, expected); 125 | }); 126 | 127 | test('args equals are passed `type: "string"`', () => { 128 | const args = ['--so=wat']; 129 | const options = { so: { type: 'string' } }; 130 | const expected = { values: { __proto__: null, so: 'wat' }, positionals: [] }; 131 | const result = parseArgs({ args, options }); 132 | assert.deepStrictEqual(result, expected); 133 | }); 134 | 135 | test('when args include single dash then result stores dash as positional', () => { 136 | const args = ['-']; 137 | const expected = { values: { __proto__: null }, positionals: ['-'] }; 138 | const result = parseArgs({ allowPositionals: true, args }); 139 | assert.deepStrictEqual(result, expected); 140 | }); 141 | 142 | test('zero config args equals are parsed as if `type: "string"`', () => { 143 | const args = ['--so=wat']; 144 | const options = { }; 145 | const expected = { values: { __proto__: null, so: 'wat' }, positionals: [] }; 146 | const result = parseArgs({ strict: false, args, options }); 147 | assert.deepStrictEqual(result, expected); 148 | }); 149 | 150 | test('same arg is passed twice `type: "string"` and last value is recorded', () => { 151 | const args = ['--foo=a', '--foo', 'b']; 152 | const options = { foo: { type: 'string' } }; 153 | const expected = { values: { __proto__: null, foo: 'b' }, positionals: [] }; 154 | const result = parseArgs({ args, options }); 155 | assert.deepStrictEqual(result, expected); 156 | }); 157 | 158 | test('args equals pass string including more equals', () => { 159 | const args = ['--so=wat=bing']; 160 | const options = { so: { type: 'string' } }; 161 | const expected = { values: { __proto__: null, so: 'wat=bing' }, positionals: [] }; 162 | const result = parseArgs({ args, options }); 163 | assert.deepStrictEqual(result, expected); 164 | }); 165 | 166 | test('first arg passed for `type: "string"` and "multiple" is in array', () => { 167 | const args = ['--foo=a']; 168 | const options = { foo: { type: 'string', multiple: true } }; 169 | const expected = { values: { __proto__: null, foo: ['a'] }, positionals: [] }; 170 | const result = parseArgs({ args, options }); 171 | assert.deepStrictEqual(result, expected); 172 | }); 173 | 174 | test('args are passed `type: "string"` and "multiple"', () => { 175 | const args = ['--foo=a', '--foo', 'b']; 176 | const options = { 177 | foo: { 178 | type: 'string', 179 | multiple: true, 180 | }, 181 | }; 182 | const expected = { values: { __proto__: null, foo: ['a', 'b'] }, positionals: [] }; 183 | const result = parseArgs({ args, options }); 184 | assert.deepStrictEqual(result, expected); 185 | }); 186 | 187 | test('when expecting `multiple:true` boolean option and option used multiple times then result includes array of ' + 188 | 'booleans matching usage', () => { 189 | const args = ['--foo', '--foo']; 190 | const options = { 191 | foo: { 192 | type: 'boolean', 193 | multiple: true, 194 | }, 195 | }; 196 | const expected = { values: { __proto__: null, foo: [true, true] }, positionals: [] }; 197 | const result = parseArgs({ args, options }); 198 | assert.deepStrictEqual(result, expected); 199 | }); 200 | 201 | test('order of option and positional does not matter (per README)', () => { 202 | const args1 = ['--foo=bar', 'baz']; 203 | const args2 = ['baz', '--foo=bar']; 204 | const options = { foo: { type: 'string' } }; 205 | const expected = { values: { __proto__: null, foo: 'bar' }, positionals: ['baz'] }; 206 | let result = parseArgs({ allowPositionals: true, args: args1, options }); 207 | assert.deepStrictEqual(result, expected, Error('option then positional')); 208 | result = parseArgs({ allowPositionals: true, args: args2, options }); 209 | assert.deepStrictEqual(result, expected, Error('positional then option')); 210 | }); 211 | 212 | test('correct default args when use node -p', () => { 213 | const holdArgv = process.argv; 214 | process.argv = [process.argv0, '--foo']; 215 | const holdExecArgv = process.execArgv; 216 | process.execArgv = ['-p', '0']; 217 | const result = parseArgs({ strict: false }); 218 | 219 | const expected = { values: { __proto__: null, foo: true }, 220 | positionals: [] }; 221 | assert.deepStrictEqual(result, expected); 222 | process.argv = holdArgv; 223 | process.execArgv = holdExecArgv; 224 | }); 225 | 226 | test('correct default args when use node --print', () => { 227 | const holdArgv = process.argv; 228 | process.argv = [process.argv0, '--foo']; 229 | const holdExecArgv = process.execArgv; 230 | process.execArgv = ['--print', '0']; 231 | const result = parseArgs({ strict: false }); 232 | 233 | const expected = { values: { __proto__: null, foo: true }, 234 | positionals: [] }; 235 | assert.deepStrictEqual(result, expected); 236 | process.argv = holdArgv; 237 | process.execArgv = holdExecArgv; 238 | }); 239 | 240 | test('correct default args when use node -e', () => { 241 | const holdArgv = process.argv; 242 | process.argv = [process.argv0, '--foo']; 243 | const holdExecArgv = process.execArgv; 244 | process.execArgv = ['-e', '0']; 245 | const result = parseArgs({ strict: false }); 246 | 247 | const expected = { values: { __proto__: null, foo: true }, 248 | positionals: [] }; 249 | assert.deepStrictEqual(result, expected); 250 | process.argv = holdArgv; 251 | process.execArgv = holdExecArgv; 252 | }); 253 | 254 | test('correct default args when use node --eval', () => { 255 | const holdArgv = process.argv; 256 | process.argv = [process.argv0, '--foo']; 257 | const holdExecArgv = process.execArgv; 258 | process.execArgv = ['--eval', '0']; 259 | const result = parseArgs({ strict: false }); 260 | const expected = { values: { __proto__: null, foo: true }, 261 | positionals: [] }; 262 | assert.deepStrictEqual(result, expected); 263 | process.argv = holdArgv; 264 | process.execArgv = holdExecArgv; 265 | }); 266 | 267 | test('correct default args when normal arguments', () => { 268 | const holdArgv = process.argv; 269 | process.argv = [process.argv0, 'script.js', '--foo']; 270 | const holdExecArgv = process.execArgv; 271 | process.execArgv = []; 272 | const result = parseArgs({ strict: false }); 273 | 274 | const expected = { values: { __proto__: null, foo: true }, 275 | positionals: [] }; 276 | assert.deepStrictEqual(result, expected); 277 | process.argv = holdArgv; 278 | process.execArgv = holdExecArgv; 279 | }); 280 | 281 | test('excess leading dashes on options are retained', () => { 282 | // Enforce a design decision for an edge case. 283 | const args = ['---triple']; 284 | const options = { }; 285 | const expected = { 286 | values: { '__proto__': null, '-triple': true }, 287 | positionals: [] 288 | }; 289 | const result = parseArgs({ strict: false, args, options }); 290 | assert.deepStrictEqual(result, expected); 291 | }); 292 | 293 | test('positional arguments are allowed by default in strict:false', () => { 294 | const args = ['foo']; 295 | const options = { }; 296 | const expected = { 297 | values: { __proto__: null }, 298 | positionals: ['foo'] 299 | }; 300 | const result = parseArgs({ strict: false, args, options }); 301 | assert.deepStrictEqual(result, expected); 302 | }); 303 | 304 | test('positional arguments may be explicitly disallowed in strict:false', () => { 305 | const args = ['foo']; 306 | const options = { }; 307 | assert.throws(() => { parseArgs({ strict: false, allowPositionals: false, args, options }); }, { 308 | code: 'ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL' 309 | }); 310 | }); 311 | 312 | // Test bad inputs 313 | 314 | test('invalid argument passed for options', () => { 315 | const args = ['--so=wat']; 316 | const options = 'bad value'; 317 | assert.throws(() => { parseArgs({ args, options }); }, { 318 | code: 'ERR_INVALID_ARG_TYPE' 319 | }); 320 | }); 321 | 322 | test('type property missing for option then throw', () => { 323 | const knownOptions = { foo: { } }; 324 | assert.throws(() => { parseArgs({ options: knownOptions }); }, { 325 | code: 'ERR_INVALID_ARG_TYPE' 326 | }); 327 | }); 328 | 329 | test('boolean passed to "type" option', () => { 330 | const args = ['--so=wat']; 331 | const options = { foo: { type: true } }; 332 | assert.throws(() => { parseArgs({ args, options }); }, { 333 | code: 'ERR_INVALID_ARG_TYPE' 334 | }); 335 | }); 336 | 337 | test('invalid union value passed to "type" option', () => { 338 | const args = ['--so=wat']; 339 | const options = { foo: { type: 'str' } }; 340 | assert.throws(() => { parseArgs({ args, options }); }, { 341 | code: 'ERR_INVALID_ARG_TYPE' 342 | }); 343 | }); 344 | 345 | // Test strict mode 346 | 347 | test('unknown long option --bar', () => { 348 | const args = ['--foo', '--bar']; 349 | const options = { foo: { type: 'boolean' } }; 350 | assert.throws(() => { parseArgs({ args, options }); }, { 351 | code: 'ERR_PARSE_ARGS_UNKNOWN_OPTION' 352 | }); 353 | }); 354 | 355 | test('unknown short option -b', () => { 356 | const args = ['--foo', '-b']; 357 | const options = { foo: { type: 'boolean' } }; 358 | assert.throws(() => { parseArgs({ args, options }); }, { 359 | code: 'ERR_PARSE_ARGS_UNKNOWN_OPTION' 360 | }); 361 | }); 362 | 363 | test('unknown option -r in short option group -bar', () => { 364 | const args = ['-bar']; 365 | const options = { b: { type: 'boolean' }, a: { type: 'boolean' } }; 366 | assert.throws(() => { parseArgs({ args, options }); }, { 367 | code: 'ERR_PARSE_ARGS_UNKNOWN_OPTION' 368 | }); 369 | }); 370 | 371 | test('unknown option with explicit value', () => { 372 | const args = ['--foo', '--bar=baz']; 373 | const options = { foo: { type: 'boolean' } }; 374 | assert.throws(() => { parseArgs({ args, options }); }, { 375 | code: 'ERR_PARSE_ARGS_UNKNOWN_OPTION' 376 | }); 377 | }); 378 | 379 | test('unexpected positional', () => { 380 | const args = ['foo']; 381 | const options = { foo: { type: 'boolean' } }; 382 | assert.throws(() => { parseArgs({ args, options }); }, { 383 | code: 'ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL' 384 | }); 385 | }); 386 | 387 | test('unexpected positional after --', () => { 388 | const args = ['--', 'foo']; 389 | const options = { foo: { type: 'boolean' } }; 390 | assert.throws(() => { parseArgs({ args, options }); }, { 391 | code: 'ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL' 392 | }); 393 | }); 394 | 395 | test('-- by itself is not a positional', () => { 396 | const args = ['--foo', '--']; 397 | const options = { foo: { type: 'boolean' } }; 398 | const result = parseArgs({ args, options }); 399 | const expected = { values: { __proto__: null, foo: true }, 400 | positionals: [] }; 401 | assert.deepStrictEqual(result, expected); 402 | }); 403 | 404 | test('string option used as boolean', () => { 405 | const args = ['--foo']; 406 | const options = { foo: { type: 'string' } }; 407 | assert.throws(() => { parseArgs({ args, options }); }, { 408 | code: 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE' 409 | }); 410 | }); 411 | 412 | test('boolean option used with value', () => { 413 | const args = ['--foo=bar']; 414 | const options = { foo: { type: 'boolean' } }; 415 | assert.throws(() => { parseArgs({ args, options }); }, { 416 | code: 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE' 417 | }); 418 | }); 419 | 420 | test('invalid short option length', () => { 421 | const args = []; 422 | const options = { foo: { short: 'fo', type: 'boolean' } }; 423 | assert.throws(() => { parseArgs({ args, options }); }, { 424 | code: 'ERR_INVALID_ARG_VALUE' 425 | }); 426 | }); 427 | 428 | test('null prototype: when no options then values.toString is undefined', () => { 429 | const result = parseArgs({ args: [] }); 430 | assert.strictEqual(result.values.toString, undefined); 431 | }); 432 | 433 | test('null prototype: when --toString then values.toString is true', () => { 434 | const args = ['--toString']; 435 | const options = { toString: { type: 'boolean' } }; 436 | const expectedResult = { values: { __proto__: null, toString: true }, positionals: [] }; 437 | 438 | const result = parseArgs({ args, options }); 439 | assert.deepStrictEqual(result, expectedResult); 440 | }); 441 | 442 | const candidateGreedyOptions = [ 443 | '', 444 | '-', 445 | '--', 446 | 'abc', 447 | '123', 448 | '-s', 449 | '--foo', 450 | ]; 451 | 452 | candidateGreedyOptions.forEach((value) => { 453 | test(`greedy: when short option with value '${value}' then eaten`, () => { 454 | const args = ['-w', value]; 455 | const options = { with: { type: 'string', short: 'w' } }; 456 | const expectedResult = { values: { __proto__: null, with: value }, positionals: [] }; 457 | 458 | const result = parseArgs({ args, options, strict: false }); 459 | assert.deepStrictEqual(result, expectedResult); 460 | }); 461 | 462 | test(`greedy: when long option with value '${value}' then eaten`, () => { 463 | const args = ['--with', value]; 464 | const options = { with: { type: 'string', short: 'w' } }; 465 | const expectedResult = { values: { __proto__: null, with: value }, positionals: [] }; 466 | 467 | const result = parseArgs({ args, options, strict: false }); 468 | assert.deepStrictEqual(result, expectedResult); 469 | }); 470 | }); 471 | 472 | test('strict: when candidate option value is plain text then does not throw', () => { 473 | const args = ['--with', 'abc']; 474 | const options = { with: { type: 'string' } }; 475 | const expectedResult = { values: { __proto__: null, with: 'abc' }, positionals: [] }; 476 | 477 | const result = parseArgs({ args, options, strict: true }); 478 | assert.deepStrictEqual(result, expectedResult); 479 | }); 480 | 481 | test("strict: when candidate option value is '-' then does not throw", () => { 482 | const args = ['--with', '-']; 483 | const options = { with: { type: 'string' } }; 484 | const expectedResult = { values: { __proto__: null, with: '-' }, positionals: [] }; 485 | 486 | const result = parseArgs({ args, options, strict: true }); 487 | assert.deepStrictEqual(result, expectedResult); 488 | }); 489 | 490 | test("strict: when candidate option value is '--' then throws", () => { 491 | const args = ['--with', '--']; 492 | const options = { with: { type: 'string' } }; 493 | 494 | assert.throws(() => { 495 | parseArgs({ args, options }); 496 | }, { 497 | code: 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE' 498 | }); 499 | }); 500 | 501 | test('strict: when candidate option value is short option then throws', () => { 502 | const args = ['--with', '-a']; 503 | const options = { with: { type: 'string' } }; 504 | 505 | assert.throws(() => { 506 | parseArgs({ args, options }); 507 | }, { 508 | code: 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE' 509 | }); 510 | }); 511 | 512 | test('strict: when candidate option value is short option digit then throws', () => { 513 | const args = ['--with', '-1']; 514 | const options = { with: { type: 'string' } }; 515 | 516 | assert.throws(() => { 517 | parseArgs({ args, options }); 518 | }, { 519 | code: 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE' 520 | }); 521 | }); 522 | 523 | test('strict: when candidate option value is long option then throws', () => { 524 | const args = ['--with', '--foo']; 525 | const options = { with: { type: 'string' } }; 526 | 527 | assert.throws(() => { 528 | parseArgs({ args, options }); 529 | }, { 530 | code: 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE' 531 | }); 532 | }); 533 | 534 | test('strict: when short option and suspect value then throws with short option in error message', () => { 535 | const args = ['-w', '--foo']; 536 | const options = { with: { type: 'string', short: 'w' } }; 537 | 538 | assert.throws(() => { 539 | parseArgs({ args, options }); 540 | }, /for '-w'/ 541 | ); 542 | }); 543 | 544 | test('strict: when long option and suspect value then throws with long option in error message', () => { 545 | const args = ['--with', '--foo']; 546 | const options = { with: { type: 'string' } }; 547 | 548 | assert.throws(() => { 549 | parseArgs({ args, options }); 550 | }, /for '--with'/ 551 | ); 552 | }); 553 | 554 | test('strict: when short option and suspect value then throws with whole expected message', () => { 555 | const args = ['-w', '--foo']; 556 | const options = { with: { type: 'string', short: 'w' } }; 557 | 558 | assert.throws(() => { 559 | parseArgs({ args, options }); 560 | }, /To specify an option argument starting with a dash use '--with=-XYZ' or '-w-XYZ'\./ 561 | ); 562 | }); 563 | 564 | test('strict: when long option and suspect value then throws with whole expected message', () => { 565 | const args = ['--with', '--foo']; 566 | const options = { with: { type: 'string', short: 'w' } }; 567 | 568 | assert.throws(() => { 569 | parseArgs({ args, options }); 570 | }, /To specify an option argument starting with a dash use '--with=-XYZ'/ 571 | ); 572 | }); 573 | 574 | test('tokens: positional', () => { 575 | const args = ['one']; 576 | const expectedTokens = [ 577 | { kind: 'positional', index: 0, value: 'one' }, 578 | ]; 579 | const { tokens } = parseArgs({ strict: false, args, tokens: true }); 580 | assert.deepStrictEqual(tokens, expectedTokens); 581 | }); 582 | 583 | test('tokens: -- followed by option-like', () => { 584 | const args = ['--', '--foo']; 585 | const expectedTokens = [ 586 | { kind: 'option-terminator', index: 0 }, 587 | { kind: 'positional', index: 1, value: '--foo' }, 588 | ]; 589 | const { tokens } = parseArgs({ strict: false, args, tokens: true }); 590 | assert.deepStrictEqual(tokens, expectedTokens); 591 | }); 592 | 593 | test('tokens: strict:true boolean short', () => { 594 | const args = ['-f']; 595 | const options = { 596 | file: { short: 'f', type: 'boolean' } 597 | }; 598 | const expectedTokens = [ 599 | { kind: 'option', name: 'file', rawName: '-f', 600 | index: 0, value: undefined, inlineValue: undefined }, 601 | ]; 602 | const { tokens } = parseArgs({ strict: true, args, options, tokens: true }); 603 | assert.deepStrictEqual(tokens, expectedTokens); 604 | }); 605 | 606 | test('tokens: strict:true boolean long', () => { 607 | const args = ['--file']; 608 | const options = { 609 | file: { short: 'f', type: 'boolean' } 610 | }; 611 | const expectedTokens = [ 612 | { kind: 'option', name: 'file', rawName: '--file', 613 | index: 0, value: undefined, inlineValue: undefined }, 614 | ]; 615 | const { tokens } = parseArgs({ strict: true, args, options, tokens: true }); 616 | assert.deepStrictEqual(tokens, expectedTokens); 617 | }); 618 | 619 | test('tokens: strict:false boolean short', () => { 620 | const args = ['-f']; 621 | const expectedTokens = [ 622 | { kind: 'option', name: 'f', rawName: '-f', 623 | index: 0, value: undefined, inlineValue: undefined }, 624 | ]; 625 | const { tokens } = parseArgs({ strict: false, args, tokens: true }); 626 | assert.deepStrictEqual(tokens, expectedTokens); 627 | }); 628 | 629 | test('tokens: strict:false boolean long', () => { 630 | const args = ['--file']; 631 | const expectedTokens = [ 632 | { kind: 'option', name: 'file', rawName: '--file', 633 | index: 0, value: undefined, inlineValue: undefined }, 634 | ]; 635 | const { tokens } = parseArgs({ strict: false, args, tokens: true }); 636 | assert.deepStrictEqual(tokens, expectedTokens); 637 | }); 638 | 639 | test('tokens: strict:false boolean option group', () => { 640 | const args = ['-ab']; 641 | const expectedTokens = [ 642 | { kind: 'option', name: 'a', rawName: '-a', 643 | index: 0, value: undefined, inlineValue: undefined }, 644 | { kind: 'option', name: 'b', rawName: '-b', 645 | index: 0, value: undefined, inlineValue: undefined }, 646 | ]; 647 | const { tokens } = parseArgs({ strict: false, args, tokens: true }); 648 | assert.deepStrictEqual(tokens, expectedTokens); 649 | }); 650 | 651 | test('tokens: strict:false boolean option group with repeated option', () => { 652 | // Also positional to check index correct after grouop 653 | const args = ['-aa', 'pos']; 654 | const expectedTokens = [ 655 | { kind: 'option', name: 'a', rawName: '-a', 656 | index: 0, value: undefined, inlineValue: undefined }, 657 | { kind: 'option', name: 'a', rawName: '-a', 658 | index: 0, value: undefined, inlineValue: undefined }, 659 | { kind: 'positional', index: 1, value: 'pos' }, 660 | ]; 661 | const { tokens } = parseArgs({ strict: false, allowPositionals: true, args, tokens: true }); 662 | assert.deepStrictEqual(tokens, expectedTokens); 663 | }); 664 | 665 | test('tokens: strict:true string short with value after space', () => { 666 | // Also positional to check index correct after out-of-line. 667 | const args = ['-f', 'bar', 'ppp']; 668 | const options = { 669 | file: { short: 'f', type: 'string' } 670 | }; 671 | const expectedTokens = [ 672 | { kind: 'option', name: 'file', rawName: '-f', 673 | index: 0, value: 'bar', inlineValue: false }, 674 | { kind: 'positional', index: 2, value: 'ppp' }, 675 | ]; 676 | const { tokens } = parseArgs({ strict: true, allowPositionals: true, args, options, tokens: true }); 677 | assert.deepStrictEqual(tokens, expectedTokens); 678 | }); 679 | 680 | test('tokens: strict:true string short with value inline', () => { 681 | const args = ['-fBAR']; 682 | const options = { 683 | file: { short: 'f', type: 'string' } 684 | }; 685 | const expectedTokens = [ 686 | { kind: 'option', name: 'file', rawName: '-f', 687 | index: 0, value: 'BAR', inlineValue: true }, 688 | ]; 689 | const { tokens } = parseArgs({ strict: true, args, options, tokens: true }); 690 | assert.deepStrictEqual(tokens, expectedTokens); 691 | }); 692 | 693 | test('tokens: strict:false string short missing value', () => { 694 | const args = ['-f']; 695 | const options = { 696 | file: { short: 'f', type: 'string' } 697 | }; 698 | const expectedTokens = [ 699 | { kind: 'option', name: 'file', rawName: '-f', 700 | index: 0, value: undefined, inlineValue: undefined }, 701 | ]; 702 | const { tokens } = parseArgs({ strict: false, args, options, tokens: true }); 703 | assert.deepStrictEqual(tokens, expectedTokens); 704 | }); 705 | 706 | test('tokens: strict:true string long with value after space', () => { 707 | // Also positional to check index correct after out-of-line. 708 | const args = ['--file', 'bar', 'ppp']; 709 | const options = { 710 | file: { short: 'f', type: 'string' } 711 | }; 712 | const expectedTokens = [ 713 | { kind: 'option', name: 'file', rawName: '--file', 714 | index: 0, value: 'bar', inlineValue: false }, 715 | { kind: 'positional', index: 2, value: 'ppp' }, 716 | ]; 717 | const { tokens } = parseArgs({ strict: true, allowPositionals: true, args, options, tokens: true }); 718 | assert.deepStrictEqual(tokens, expectedTokens); 719 | }); 720 | 721 | test('tokens: strict:true string long with value inline', () => { 722 | // Also positional to check index correct after out-of-line. 723 | const args = ['--file=bar', 'pos']; 724 | const options = { 725 | file: { short: 'f', type: 'string' } 726 | }; 727 | const expectedTokens = [ 728 | { kind: 'option', name: 'file', rawName: '--file', 729 | index: 0, value: 'bar', inlineValue: true }, 730 | { kind: 'positional', index: 1, value: 'pos' }, 731 | ]; 732 | const { tokens } = parseArgs({ strict: true, allowPositionals: true, args, options, tokens: true }); 733 | assert.deepStrictEqual(tokens, expectedTokens); 734 | }); 735 | 736 | test('tokens: strict:false string long with value inline', () => { 737 | const args = ['--file=bar']; 738 | const expectedTokens = [ 739 | { kind: 'option', name: 'file', rawName: '--file', 740 | index: 0, value: 'bar', inlineValue: true }, 741 | ]; 742 | const { tokens } = parseArgs({ strict: false, args, tokens: true }); 743 | assert.deepStrictEqual(tokens, expectedTokens); 744 | }); 745 | 746 | test('tokens: strict:false string long missing value', () => { 747 | const args = ['--file']; 748 | const options = { 749 | file: { short: 'f', type: 'string' } 750 | }; 751 | const expectedTokens = [ 752 | { kind: 'option', name: 'file', rawName: '--file', 753 | index: 0, value: undefined, inlineValue: undefined }, 754 | ]; 755 | const { tokens } = parseArgs({ strict: false, args, options, tokens: true }); 756 | assert.deepStrictEqual(tokens, expectedTokens); 757 | }); 758 | 759 | test('tokens: strict:true complex option group with value after space', () => { 760 | // Also positional to check index correct afterwards. 761 | const args = ['-ab', 'c', 'pos']; 762 | const options = { 763 | alpha: { short: 'a', type: 'boolean' }, 764 | beta: { short: 'b', type: 'string' }, 765 | }; 766 | const expectedTokens = [ 767 | { kind: 'option', name: 'alpha', rawName: '-a', 768 | index: 0, value: undefined, inlineValue: undefined }, 769 | { kind: 'option', name: 'beta', rawName: '-b', 770 | index: 0, value: 'c', inlineValue: false }, 771 | { kind: 'positional', index: 2, value: 'pos' }, 772 | ]; 773 | const { tokens } = parseArgs({ strict: true, allowPositionals: true, args, options, tokens: true }); 774 | assert.deepStrictEqual(tokens, expectedTokens); 775 | }); 776 | 777 | test('tokens: strict:true complex option group with inline value', () => { 778 | // Also positional to check index correct afterwards. 779 | const args = ['-abc', 'pos']; 780 | const options = { 781 | alpha: { short: 'a', type: 'boolean' }, 782 | beta: { short: 'b', type: 'string' }, 783 | }; 784 | const expectedTokens = [ 785 | { kind: 'option', name: 'alpha', rawName: '-a', 786 | index: 0, value: undefined, inlineValue: undefined }, 787 | { kind: 'option', name: 'beta', rawName: '-b', 788 | index: 0, value: 'c', inlineValue: true }, 789 | { kind: 'positional', index: 1, value: 'pos' }, 790 | ]; 791 | const { tokens } = parseArgs({ strict: true, allowPositionals: true, args, options, tokens: true }); 792 | assert.deepStrictEqual(tokens, expectedTokens); 793 | }); 794 | 795 | test('tokens: strict:false with single dashes', () => { 796 | const args = ['--file', '-', '-']; 797 | const options = { 798 | file: { short: 'f', type: 'string' }, 799 | }; 800 | const expectedTokens = [ 801 | { kind: 'option', name: 'file', rawName: '--file', 802 | index: 0, value: '-', inlineValue: false }, 803 | { kind: 'positional', index: 2, value: '-' }, 804 | ]; 805 | const { tokens } = parseArgs({ strict: false, args, options, tokens: true }); 806 | assert.deepStrictEqual(tokens, expectedTokens); 807 | }); 808 | 809 | test('tokens: strict:false with -- --', () => { 810 | const args = ['--', '--']; 811 | const expectedTokens = [ 812 | { kind: 'option-terminator', index: 0 }, 813 | { kind: 'positional', index: 1, value: '--' }, 814 | ]; 815 | const { tokens } = parseArgs({ strict: false, args, tokens: true }); 816 | assert.deepStrictEqual(tokens, expectedTokens); 817 | }); 818 | --------------------------------------------------------------------------------