From 9eefc9d96d6960b1792d3262563ade0b592b3d86 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Mon, 3 Aug 2020 22:58:55 -0700 Subject: [PATCH] feat!: adds support for ESM and Deno (#295) --- .editorconfig | 12 - .github/workflows/ci.yaml | 45 +- .github/workflows/release-please.yml | 22 +- .gitignore | 2 +- .mocharc.json | 6 - .nycrc | 6 +- CHANGELOG.md | 504 -------- README.md | 50 +- browser.js | 26 + deno.ts | 35 + docs/CHANGELOG-full.md | 503 ++++++++ example.js | 3 - index.ts | 1124 ------------------ lib/index.ts | 49 + lib/string-utils.ts | 40 + lib/yargs-parser-types.ts | 11 +- lib/yargs-parser.ts | 1103 +++++++++++++++++ package.json | 46 +- renovate.json | 3 +- rollup.config.js | 17 + scripts/replace-legacy-export.cjs | 11 + test/browser/yargs-test.cjs | 55 + test/browser/yargs-test.html | 21 + test/deno/yargs-test.ts | 46 + test/fixtures/{settings.js => settings.cjs} | 0 test/{ => typescript}/tokenize-arg-string.ts | 22 +- test/{ => typescript}/types.ts | 2 +- test/{yargs-parser.js => yargs-parser.cjs} | 5 +- tsconfig.json | 10 +- tsconfig.test.json | 6 +- 30 files changed, 2083 insertions(+), 1702 deletions(-) delete mode 100644 .editorconfig delete mode 100644 .mocharc.json create mode 100644 browser.js create mode 100644 deno.ts create mode 100644 docs/CHANGELOG-full.md delete mode 100755 example.js delete mode 100644 index.ts create mode 100644 lib/index.ts create mode 100644 lib/string-utils.ts create mode 100644 lib/yargs-parser.ts create mode 100644 rollup.config.js create mode 100755 scripts/replace-legacy-export.cjs create mode 100644 test/browser/yargs-test.cjs create mode 100644 test/browser/yargs-test.html create mode 100644 test/deno/yargs-test.ts rename test/fixtures/{settings.js => settings.cjs} (100%) rename test/{ => typescript}/tokenize-arg-string.ts (92%) rename test/{ => typescript}/types.ts (85%) rename test/{yargs-parser.js => yargs-parser.cjs} (99%) diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 4039ff11..00000000 --- a/.editorconfig +++ /dev/null @@ -1,12 +0,0 @@ -root = true - -[*] -charset = utf-8 -end_of_line = lf -indent_size = 2 -indent_style = space -insert_final_newline = true -trim_trailing_whitespace = true - -[*.md] -trim_trailing_whitespace = false diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0213d80f..67e7de55 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -17,6 +17,8 @@ jobs: node-version: ${{ matrix.node }} - run: node --version - run: npm install + env: + PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true - run: npm test windows: runs-on: windows-latest @@ -26,14 +28,53 @@ jobs: with: node-version: 12 - run: npm install + env: + PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true - run: npm test coverage: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: - node-version: 13 + node-version: 14 - run: npm install + env: + PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true - run: npm test - run: npm run coverage + deno: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 14 + - run: npm install + env: + PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true + - run: npm run compile + - uses: denolib/setup-deno@v2 + with: + deno-version: v1.x + - run: | + deno --version + deno test --allow-read test/deno/yargs-test.ts + browser: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 14 + - run: npm install + - run: npm run test:browser + typescript: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 14 + - run: npm install + - run: npm run test:typescript diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 70660547..86f5a9db 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -7,8 +7,28 @@ jobs: release-please: runs-on: ubuntu-latest steps: - - uses: bcoe/release-please-action@v1.2.1 + - uses: bcoe/release-please-action@v1.6.3 with: token: ${{ secrets.GITHUB_TOKEN }} release-type: node package-name: yargs-parser + release-deno: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 14 + - run: npm install + env: + PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true + - run: npm run compile + - name: push Deno release + run: | + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + git remote add gh-token "https://${{ secrets.GITHUB_TOKEN}}@github.com/yargs/yargs-parser.git" + git checkout -b deno + git add -f build + git commit -a -m 'build: deploy latest Deno build' + git push origin +deno diff --git a/.gitignore b/.gitignore index 791cb484..3059a185 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ .idea -build/ .nyc_output node_modules .DS_Store package-lock.json ./test/fixtures/package.json coverage +build diff --git a/.mocharc.json b/.mocharc.json deleted file mode 100644 index ef1c3645..00000000 --- a/.mocharc.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "spec": [ - "build/test", - "test" - ] -} diff --git a/.nycrc b/.nycrc index 89f6c3a0..3b315160 100644 --- a/.nycrc +++ b/.nycrc @@ -7,7 +7,7 @@ "html", "text" ], - "lines": 100, - "branches": "97", - "statements": "100" + "lines": 99.5, + "branches": "98", + "statements": "99.5" } diff --git a/CHANGELOG.md b/CHANGELOG.md index d91dc516..fe6e61d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -95,507 +95,3 @@ leaks externally. ### Reverts * revert 16.0.0 CHANGELOG entry ([920320a](https://www.github.com/yargs/yargs-parser/commit/920320ad9861bbfd58eda39221ae211540fc1daf)) - -## [15.0.0](https://github.com/yargs/yargs-parser/compare/v14.0.0...v15.0.0) (2019-10-07) - - -### Features - -* rework `collect-unknown-options` into `unknown-options-as-args`, providing more comprehensive functionality ([ef771ca](https://github.com/yargs/yargs-parser/commit/ef771ca)) - - -### BREAKING CHANGES - -* rework `collect-unknown-options` into `unknown-options-as-args`, providing more comprehensive functionality - - - -## [14.0.0](https://github.com/yargs/yargs-parser/compare/v13.1.1...v14.0.0) (2019-09-06) - - -### Bug Fixes - -* boolean arrays with default values ([#185](https://github.com/yargs/yargs-parser/issues/185)) ([7d42572](https://github.com/yargs/yargs-parser/commit/7d42572)) -* boolean now behaves the same as other array types ([#184](https://github.com/yargs/yargs-parser/issues/184)) ([17ca3bd](https://github.com/yargs/yargs-parser/commit/17ca3bd)) -* eatNargs() for 'opt.narg === 0' and boolean typed options ([#188](https://github.com/yargs/yargs-parser/issues/188)) ([c5a1db0](https://github.com/yargs/yargs-parser/commit/c5a1db0)) -* maybeCoerceNumber now takes precedence over coerce return value ([#182](https://github.com/yargs/yargs-parser/issues/182)) ([2f26436](https://github.com/yargs/yargs-parser/commit/2f26436)) -* take into account aliases when appending arrays from config object ([#199](https://github.com/yargs/yargs-parser/issues/199)) ([f8a2d3f](https://github.com/yargs/yargs-parser/commit/f8a2d3f)) - - -### Features - -* add configuration option to "collect-unknown-options" ([#181](https://github.com/yargs/yargs-parser/issues/181)) ([7909cc4](https://github.com/yargs/yargs-parser/commit/7909cc4)) -* maybeCoerceNumber() now takes into account arrays ([#187](https://github.com/yargs/yargs-parser/issues/187)) ([31c204b](https://github.com/yargs/yargs-parser/commit/31c204b)) - - -### BREAKING CHANGES - -* unless "parse-numbers" is set to "false", arrays of numeric strings are now parsed as numbers, rather than strings. -* we have dropped the broken "defaulted" functionality; we would like to revisit adding this in the future. -* maybeCoerceNumber now takes precedence over coerce return value (#182) - - - -### [13.1.1](https://www.github.com/yargs/yargs-parser/compare/v13.1.0...v13.1.1) (2019-06-10) - - -### Bug Fixes - -* convert values to strings when tokenizing ([#167](https://www.github.com/yargs/yargs-parser/issues/167)) ([57b7883](https://www.github.com/yargs/yargs-parser/commit/57b7883)) -* nargs should allow duplicates when duplicate-arguments-array=false ([#164](https://www.github.com/yargs/yargs-parser/issues/164)) ([47ccb0b](https://www.github.com/yargs/yargs-parser/commit/47ccb0b)) -* should populate "_" when given config with "short-option-groups" false ([#179](https://www.github.com/yargs/yargs-parser/issues/179)) ([6055974](https://www.github.com/yargs/yargs-parser/commit/6055974)) - -## [13.1.0](https://github.com/yargs/yargs-parser/compare/v13.0.0...v13.1.0) (2019-05-05) - - -### Features - -* add `strip-aliased` and `strip-dashed` configuration options. ([#172](https://github.com/yargs/yargs-parser/issues/172)) ([a3936aa](https://github.com/yargs/yargs-parser/commit/a3936aa)) -* support boolean which do not consume next argument. ([#171](https://github.com/yargs/yargs-parser/issues/171)) ([0ae7fcb](https://github.com/yargs/yargs-parser/commit/0ae7fcb)) - - - - -# [13.0.0](https://github.com/yargs/yargs-parser/compare/v12.0.0...v13.0.0) (2019-02-02) - - -### Features - -* don't coerce number from string with leading '0' or '+' ([#158](https://github.com/yargs/yargs-parser/issues/158)) ([18d0fd5](https://github.com/yargs/yargs-parser/commit/18d0fd5)) - - -### BREAKING CHANGES - -* options with leading '+' or '0' now parse as strings - - - - -# [12.0.0](https://github.com/yargs/yargs-parser/compare/v11.1.1...v12.0.0) (2019-01-29) - - -### Bug Fixes - -* better handling of quoted strings ([#153](https://github.com/yargs/yargs-parser/issues/153)) ([2fb71b2](https://github.com/yargs/yargs-parser/commit/2fb71b2)) - - -### Features - -* default value is now used if no right-hand value provided for numbers/strings ([#156](https://github.com/yargs/yargs-parser/issues/156)) ([5a7c46a](https://github.com/yargs/yargs-parser/commit/5a7c46a)) - - -### BREAKING CHANGES - -* a flag with no right-hand value no longer populates defaulted options with `undefined`. -* quotes at beginning and endings of strings are not removed during parsing. - - - - -## [11.1.1](https://github.com/yargs/yargs-parser/compare/v11.1.0...v11.1.1) (2018-11-19) - - -### Bug Fixes - -* ensure empty string is added into argv._ ([#140](https://github.com/yargs/yargs-parser/issues/140)) ([79cda98](https://github.com/yargs/yargs-parser/commit/79cda98)) - - -### Reverts - -* make requiresArg work in conjunction with arrays ([#136](https://github.com/yargs/yargs-parser/issues/136)) ([f4a3063](https://github.com/yargs/yargs-parser/commit/f4a3063)) - - - - -# [11.1.0](https://github.com/yargs/yargs-parser/compare/v11.0.0...v11.1.0) (2018-11-10) - - -### Bug Fixes - -* handling of one char alias ([#139](https://github.com/yargs/yargs-parser/issues/139)) ([ee56e31](https://github.com/yargs/yargs-parser/commit/ee56e31)) - - -### Features - -* add halt-at-non-option configuration option ([#130](https://github.com/yargs/yargs-parser/issues/130)) ([a849fce](https://github.com/yargs/yargs-parser/commit/a849fce)) - - - - -# [11.0.0](https://github.com/yargs/yargs-parser/compare/v10.1.0...v11.0.0) (2018-10-06) - - -### Bug Fixes - -* flatten-duplicate-arrays:false for more than 2 arrays ([#128](https://github.com/yargs/yargs-parser/issues/128)) ([2bc395f](https://github.com/yargs/yargs-parser/commit/2bc395f)) -* hyphenated flags combined with dot notation broke parsing ([#131](https://github.com/yargs/yargs-parser/issues/131)) ([dc788da](https://github.com/yargs/yargs-parser/commit/dc788da)) -* make requiresArg work in conjunction with arrays ([#136](https://github.com/yargs/yargs-parser/issues/136)) ([77ae1d4](https://github.com/yargs/yargs-parser/commit/77ae1d4)) - - -### Chores - -* update dependencies ([6dc42a1](https://github.com/yargs/yargs-parser/commit/6dc42a1)) - - -### Features - -* also add camelCase array options ([#125](https://github.com/yargs/yargs-parser/issues/125)) ([08c0117](https://github.com/yargs/yargs-parser/commit/08c0117)) -* array.type can now be provided, supporting coercion ([#132](https://github.com/yargs/yargs-parser/issues/132)) ([4b8cfce](https://github.com/yargs/yargs-parser/commit/4b8cfce)) - - -### BREAKING CHANGES - -* drops Node 4 support -* the argv object is now populated differently (correctly) when hyphens and dot notation are used in conjunction. - - - - -# [10.1.0](https://github.com/yargs/yargs-parser/compare/v10.0.0...v10.1.0) (2018-06-29) - - -### Features - -* add `set-placeholder-key` configuration ([#123](https://github.com/yargs/yargs-parser/issues/123)) ([19386ee](https://github.com/yargs/yargs-parser/commit/19386ee)) - - - - -# [10.0.0](https://github.com/yargs/yargs-parser/compare/v9.0.2...v10.0.0) (2018-04-04) - - -### Bug Fixes - -* do not set boolean flags if not defined in `argv` ([#119](https://github.com/yargs/yargs-parser/issues/119)) ([f6e6599](https://github.com/yargs/yargs-parser/commit/f6e6599)) - - -### BREAKING CHANGES - -* `boolean` flags defined without a `default` value will now behave like other option type and won't be set in the parsed results when the user doesn't set the corresponding CLI arg. - -Previous behavior: -```js -var parse = require('yargs-parser'); - -parse('--flag', {boolean: ['flag']}); -// => { _: [], flag: true } - -parse('--no-flag', {boolean: ['flag']}); -// => { _: [], flag: false } - -parse('', {boolean: ['flag']}); -// => { _: [], flag: false } -``` - -New behavior: -```js -var parse = require('yargs-parser'); - -parse('--flag', {boolean: ['flag']}); -// => { _: [], flag: true } - -parse('--no-flag', {boolean: ['flag']}); -// => { _: [], flag: false } - -parse('', {boolean: ['flag']}); -// => { _: [] } => flag not set similarly to other option type -``` - - - - -## [9.0.2](https://github.com/yargs/yargs-parser/compare/v9.0.1...v9.0.2) (2018-01-20) - - -### Bug Fixes - -* nargs was still aggressively consuming too many arguments ([9b28aad](https://github.com/yargs/yargs-parser/commit/9b28aad)) - - - - -## [9.0.1](https://github.com/yargs/yargs-parser/compare/v9.0.0...v9.0.1) (2018-01-20) - - -### Bug Fixes - -* nargs was consuming too many arguments ([4fef206](https://github.com/yargs/yargs-parser/commit/4fef206)) - - - - -# [9.0.0](https://github.com/yargs/yargs-parser/compare/v8.1.0...v9.0.0) (2018-01-20) - - -### Features - -* narg arguments no longer consume flag arguments ([#114](https://github.com/yargs/yargs-parser/issues/114)) ([60bb9b3](https://github.com/yargs/yargs-parser/commit/60bb9b3)) - - -### BREAKING CHANGES - -* arguments of form --foo, -abc, will no longer be consumed by nargs - - - - -# [8.1.0](https://github.com/yargs/yargs-parser/compare/v8.0.0...v8.1.0) (2017-12-20) - - -### Bug Fixes - -* allow null config values ([#108](https://github.com/yargs/yargs-parser/issues/108)) ([d8b14f9](https://github.com/yargs/yargs-parser/commit/d8b14f9)) -* ensure consistent parsing of dot-notation arguments ([#102](https://github.com/yargs/yargs-parser/issues/102)) ([c9bd79c](https://github.com/yargs/yargs-parser/commit/c9bd79c)) -* implement [@antoniom](https://github.com/antoniom)'s fix for camel-case expansion ([3087e1d](https://github.com/yargs/yargs-parser/commit/3087e1d)) -* only run coercion functions once, despite aliases. ([#76](https://github.com/yargs/yargs-parser/issues/76)) ([#103](https://github.com/yargs/yargs-parser/issues/103)) ([507aaef](https://github.com/yargs/yargs-parser/commit/507aaef)) -* scientific notation circumvented bounds check ([#110](https://github.com/yargs/yargs-parser/issues/110)) ([3571f57](https://github.com/yargs/yargs-parser/commit/3571f57)) -* tokenizer should ignore spaces at the beginning of the argString ([#106](https://github.com/yargs/yargs-parser/issues/106)) ([f34ead9](https://github.com/yargs/yargs-parser/commit/f34ead9)) - - -### Features - -* make combining arrays a configurable option ([#111](https://github.com/yargs/yargs-parser/issues/111)) ([c8bf536](https://github.com/yargs/yargs-parser/commit/c8bf536)) -* merge array from arguments with array from config ([#83](https://github.com/yargs/yargs-parser/issues/83)) ([806ddd6](https://github.com/yargs/yargs-parser/commit/806ddd6)) - - - - -# [8.0.0](https://github.com/yargs/yargs-parser/compare/v7.0.0...v8.0.0) (2017-10-05) - - -### Bug Fixes - -* Ignore multiple spaces between arguments. ([#100](https://github.com/yargs/yargs-parser/issues/100)) ([d137227](https://github.com/yargs/yargs-parser/commit/d137227)) - - -### Features - -* allow configuration of prefix for boolean negation ([#94](https://github.com/yargs/yargs-parser/issues/94)) ([00bde7d](https://github.com/yargs/yargs-parser/commit/00bde7d)) -* reworking how numbers are parsed ([#104](https://github.com/yargs/yargs-parser/issues/104)) ([fba00eb](https://github.com/yargs/yargs-parser/commit/fba00eb)) - - -### BREAKING CHANGES - -* strings that fail `Number.isSafeInteger()` are no longer coerced into numbers. - - - - -# [7.0.0](https://github.com/yargs/yargs-parser/compare/v6.0.1...v7.0.0) (2017-05-02) - - -### Chores - -* revert populate-- logic ([#91](https://github.com/yargs/yargs-parser/issues/91)) ([6003e6d](https://github.com/yargs/yargs-parser/commit/6003e6d)) - - -### BREAKING CHANGES - -* populate-- now defaults to false. - - - - -## [6.0.1](https://github.com/yargs/yargs-parser/compare/v6.0.0...v6.0.1) (2017-05-01) - - -### Bug Fixes - -* default '--' to undefined when not provided; this is closer to the array API ([#90](https://github.com/yargs/yargs-parser/issues/90)) ([4e739cc](https://github.com/yargs/yargs-parser/commit/4e739cc)) - - - - -# [6.0.0](https://github.com/yargs/yargs-parser/compare/v4.2.1...v6.0.0) (2017-05-01) - - -### Bug Fixes - -* environment variables should take precedence over config file ([#81](https://github.com/yargs/yargs-parser/issues/81)) ([76cee1f](https://github.com/yargs/yargs-parser/commit/76cee1f)) -* parsing hints should apply for dot notation keys ([#86](https://github.com/yargs/yargs-parser/issues/86)) ([3e47d62](https://github.com/yargs/yargs-parser/commit/3e47d62)) - - -### Chores - -* upgrade to newest version of camelcase ([#87](https://github.com/yargs/yargs-parser/issues/87)) ([f1903aa](https://github.com/yargs/yargs-parser/commit/f1903aa)) - - -### Features - -* add -- option which allows arguments after the -- flag to be returned separated from positional arguments ([#84](https://github.com/yargs/yargs-parser/issues/84)) ([2572ca8](https://github.com/yargs/yargs-parser/commit/2572ca8)) -* when parsing stops, we now populate "--" by default ([#88](https://github.com/yargs/yargs-parser/issues/88)) ([cd666db](https://github.com/yargs/yargs-parser/commit/cd666db)) - - -### BREAKING CHANGES - -* rather than placing arguments in "_", when parsing is stopped via "--"; we now populate an array called "--" by default. -* camelcase now requires Node 4+. -* environment variables will now override config files (args, env, config-file, config-object) - - - - -# [5.0.0](https://github.com/yargs/yargs-parser/compare/v4.2.1...v5.0.0) (2017-02-18) - - -### Bug Fixes - -* environment variables should take precedence over config file ([#81](https://github.com/yargs/yargs-parser/issues/81)) ([76cee1f](https://github.com/yargs/yargs-parser/commit/76cee1f)) - - -### BREAKING CHANGES - -* environment variables will now override config files (args, env, config-file, config-object) - - - - -## [4.2.1](https://github.com/yargs/yargs-parser/compare/v4.2.0...v4.2.1) (2017-01-02) - - -### Bug Fixes - -* flatten/duplicate regression ([#75](https://github.com/yargs/yargs-parser/issues/75)) ([68d68a0](https://github.com/yargs/yargs-parser/commit/68d68a0)) - - - - -# [4.2.0](https://github.com/yargs/yargs-parser/compare/v4.1.0...v4.2.0) (2016-12-01) - - -### Bug Fixes - -* inner objects in configs had their keys appended to top-level key when dot-notation was disabled ([#72](https://github.com/yargs/yargs-parser/issues/72)) ([0b1b5f9](https://github.com/yargs/yargs-parser/commit/0b1b5f9)) - - -### Features - -* allow multiple arrays to be provided, rather than always combining ([#71](https://github.com/yargs/yargs-parser/issues/71)) ([0f0fb2d](https://github.com/yargs/yargs-parser/commit/0f0fb2d)) - - - - -# [4.1.0](https://github.com/yargs/yargs-parser/compare/v4.0.2...v4.1.0) (2016-11-07) - - -### Features - -* apply coercions to default options ([#65](https://github.com/yargs/yargs-parser/issues/65)) ([c79052b](https://github.com/yargs/yargs-parser/commit/c79052b)) -* handle dot notation boolean options ([#63](https://github.com/yargs/yargs-parser/issues/63)) ([02c3545](https://github.com/yargs/yargs-parser/commit/02c3545)) - - - - -## [4.0.2](https://github.com/yargs/yargs-parser/compare/v4.0.1...v4.0.2) (2016-09-30) - - -### Bug Fixes - -* whoops, let's make the assign not change the Object key order ([29d069a](https://github.com/yargs/yargs-parser/commit/29d069a)) - - - - -## [4.0.1](https://github.com/yargs/yargs-parser/compare/v4.0.0...v4.0.1) (2016-09-30) - - -### Bug Fixes - -* lodash.assign was deprecated ([#59](https://github.com/yargs/yargs-parser/issues/59)) ([5e7eb11](https://github.com/yargs/yargs-parser/commit/5e7eb11)) - - - - -# [4.0.0](https://github.com/yargs/yargs-parser/compare/v3.2.0...v4.0.0) (2016-09-26) - - -### Bug Fixes - -* coerce should be applied to the final objects and arrays created ([#57](https://github.com/yargs/yargs-parser/issues/57)) ([4ca69da](https://github.com/yargs/yargs-parser/commit/4ca69da)) - - -### BREAKING CHANGES - -* coerce is no longer applied to individual arguments in an implicit array. - - - - -# [3.2.0](https://github.com/yargs/yargs-parser/compare/v3.1.0...v3.2.0) (2016-08-13) - - -### Features - -* coerce full array instead of each element ([#51](https://github.com/yargs/yargs-parser/issues/51)) ([cc4dc56](https://github.com/yargs/yargs-parser/commit/cc4dc56)) - - - - -# [3.1.0](https://github.com/yargs/yargs-parser/compare/v3.0.0...v3.1.0) (2016-08-09) - - -### Bug Fixes - -* address pkgConf parsing bug outlined in [#37](https://github.com/yargs/yargs-parser/issues/37) ([#45](https://github.com/yargs/yargs-parser/issues/45)) ([be76ee6](https://github.com/yargs/yargs-parser/commit/be76ee6)) -* better parsing of negative values ([#44](https://github.com/yargs/yargs-parser/issues/44)) ([2e43692](https://github.com/yargs/yargs-parser/commit/2e43692)) -* check aliases when guessing defaults for arguments fixes [#41](https://github.com/yargs/yargs-parser/issues/41) ([#43](https://github.com/yargs/yargs-parser/issues/43)) ([f3e4616](https://github.com/yargs/yargs-parser/commit/f3e4616)) - - -### Features - -* added coerce option, for providing specialized argument parsing ([#42](https://github.com/yargs/yargs-parser/issues/42)) ([7b49cd2](https://github.com/yargs/yargs-parser/commit/7b49cd2)) - - - - -# [3.0.0](https://github.com/yargs/yargs-parser/compare/v2.4.1...v3.0.0) (2016-08-07) - - -### Bug Fixes - -* parsing issue with numeric character in group of options ([#19](https://github.com/yargs/yargs-parser/issues/19)) ([f743236](https://github.com/yargs/yargs-parser/commit/f743236)) -* upgraded lodash.assign ([5d7fdf4](https://github.com/yargs/yargs-parser/commit/5d7fdf4)) - -### BREAKING CHANGES - -* subtle change to how values are parsed in a group of single-character arguments. -* _first released in 3.1.0, better handling of negative values should be considered a breaking change._ - - - - -## [2.4.1](https://github.com/yargs/yargs-parser/compare/v2.4.0...v2.4.1) (2016-07-16) - - -### Bug Fixes - -* **count:** do not increment a default value ([#39](https://github.com/yargs/yargs-parser/issues/39)) ([b04a189](https://github.com/yargs/yargs-parser/commit/b04a189)) - - - - -# [2.4.0](https://github.com/yargs/yargs-parser/compare/v2.3.0...v2.4.0) (2016-04-11) - - -### Features - -* **environment:** Support nested options in environment variables ([#26](https://github.com/yargs/yargs-parser/issues/26)) thanks [@elas7](https://github.com/elas7) \o/ ([020778b](https://github.com/yargs/yargs-parser/commit/020778b)) - - - - -# [2.3.0](https://github.com/yargs/yargs-parser/compare/v2.2.0...v2.3.0) (2016-04-09) - - -### Bug Fixes - -* **boolean:** fix for boolean options with non boolean defaults (#20) ([2dbe86b](https://github.com/yargs/yargs-parser/commit/2dbe86b)), closes [(#20](https://github.com/(/issues/20) -* **package:** remove tests from tarball ([0353c0d](https://github.com/yargs/yargs-parser/commit/0353c0d)) -* **parsing:** handle calling short option with an empty string as the next value. ([a867165](https://github.com/yargs/yargs-parser/commit/a867165)) -* boolean flag when next value contains the strings 'true' or 'false'. ([69941a6](https://github.com/yargs/yargs-parser/commit/69941a6)) -* update dependencies; add standard-version bin for next release (#24) ([822d9d5](https://github.com/yargs/yargs-parser/commit/822d9d5)) - -### Features - -* **configuration:** Allow to pass configuration objects to yargs-parser ([0780900](https://github.com/yargs/yargs-parser/commit/0780900)) -* **normalize:** allow normalize to work with arrays ([e0eaa1a](https://github.com/yargs/yargs-parser/commit/e0eaa1a)) diff --git a/README.md b/README.md index eff195c3..c3c346ee 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ npm i yargs-parser --save ``` ```js -var argv = require('yargs-parser')(process.argv.slice(2)) +const argv = require('yargs-parser')(process.argv.slice(2)) console.log(argv) ``` @@ -30,7 +30,7 @@ node example.js --foo=33 --bar hello _or parse a string!_ ```js -var argv = require('yargs-parser')('--foo=99 --bar=33') +const argv = require('yargs-parser')('--foo=99 --bar=33') console.log(argv) ``` @@ -41,11 +41,55 @@ console.log(argv) Convert an array of mixed types before passing to `yargs-parser`: ```js -var parse = require('yargs-parser') +const parse = require('yargs-parser') parse(['-f', 11, '--zoom', 55].join(' ')) // <-- array to string parse(['-f', 11, '--zoom', 55].map(String)) // <-- array of strings ``` +## Deno Example + +As of `v19` `yargs-parser` supports [Deno](https://github.com/denoland/deno): + +```typescript +import parser from "https://github.com/yargs/yargs-parser/raw/deno/deno.ts"; + +const argv = parser('--foo=99 --bar=9987930', { + string: ['bar'] +}) +console.log(argv) +``` + +## ESM Example + +As of `v19` `yargs-parser` supports ESM (_both in Node.js and in the browser_): + +**Node.js:** + +```js +import parser from 'yargs-parser' + +const argv = parser('--foo=99 --bar=9987930', { + string: ['bar'] +}) +console.log(argv) +``` + +**Browsers:** + +```html + + + + +``` + ## API ### require('yargs-parser')(args, opts={}) diff --git a/browser.js b/browser.js new file mode 100644 index 00000000..934edee1 --- /dev/null +++ b/browser.js @@ -0,0 +1,26 @@ +// Main entrypoint for ESM web browser environments. Avoids using Node.js +// specific libraries, such as "path". +// +// TODO: figure out reasonable web equivalents for "resolve", "normalize", etc. +import { YargsParser } from './build/lib/yargs-parser.js' +const parser = new YargsParser({ + cwd: () => { return '' }, + format: (str, arg) => { return str.replace('%s', arg) }, + normalize: (str) => { return str }, + resolve: (str) => { return str }, + require: () => { + throw Error('loading config from files not currently supported in browser') + }, + env: () => {} +}) + +const yargsParser = function Parser (args, opts) { + const result = parser.parse(args.slice(), opts) + return result.argv +} + +yargsParser.detailed = function (args, opts) { + return parser.parse(args.slice(), opts) +} + +export default yargsParser diff --git a/deno.ts b/deno.ts new file mode 100644 index 00000000..f60b7f7c --- /dev/null +++ b/deno.ts @@ -0,0 +1,35 @@ +/* global Deno */ +// Main entrypoint for Deno. +// +// TODO: find reasonable replacement for require logic. +import * as path from 'https://deno.land/std/path/mod.ts' +import { YargsParser } from './build/lib/yargs-parser.js' +import { Arguments, ArgsInput, Parser, Options, DetailedArguments } from './build/lib/yargs-parser-types.d.ts' + +const parser = new YargsParser({ + cwd: Deno.cwd, + env: () => { + Deno.env.toObject() + }, + format: (str: string, arg: string) => { return str.replace('%s', arg) }, + normalize: path.posix.normalize, + resolve: path.posix.resolve, + require: (path: string) => { + if (!path.match(/\.json$/)) { + throw Error('only .json config files are supported in Deno') + } else { + return JSON.parse(Deno.readTextFileSync(path)) + } + } +}) + +const yargsParser: Parser = function Parser (args: ArgsInput, opts?: Partial): Arguments { + const result = parser.parse(args.slice(), opts) + return result.argv +} + +yargsParser.detailed = function (args: ArgsInput, opts?: Partial): DetailedArguments { + return parser.parse(args.slice(), opts) +} + +export default yargsParser diff --git a/docs/CHANGELOG-full.md b/docs/CHANGELOG-full.md new file mode 100644 index 00000000..330089ed --- /dev/null +++ b/docs/CHANGELOG-full.md @@ -0,0 +1,503 @@ +## [15.0.0](https://github.com/yargs/yargs-parser/compare/v14.0.0...v15.0.0) (2019-10-07) + + +### Features + +* rework `collect-unknown-options` into `unknown-options-as-args`, providing more comprehensive functionality ([ef771ca](https://github.com/yargs/yargs-parser/commit/ef771ca)) + + +### BREAKING CHANGES + +* rework `collect-unknown-options` into `unknown-options-as-args`, providing more comprehensive functionality + + + +## [14.0.0](https://github.com/yargs/yargs-parser/compare/v13.1.1...v14.0.0) (2019-09-06) + + +### Bug Fixes + +* boolean arrays with default values ([#185](https://github.com/yargs/yargs-parser/issues/185)) ([7d42572](https://github.com/yargs/yargs-parser/commit/7d42572)) +* boolean now behaves the same as other array types ([#184](https://github.com/yargs/yargs-parser/issues/184)) ([17ca3bd](https://github.com/yargs/yargs-parser/commit/17ca3bd)) +* eatNargs() for 'opt.narg === 0' and boolean typed options ([#188](https://github.com/yargs/yargs-parser/issues/188)) ([c5a1db0](https://github.com/yargs/yargs-parser/commit/c5a1db0)) +* maybeCoerceNumber now takes precedence over coerce return value ([#182](https://github.com/yargs/yargs-parser/issues/182)) ([2f26436](https://github.com/yargs/yargs-parser/commit/2f26436)) +* take into account aliases when appending arrays from config object ([#199](https://github.com/yargs/yargs-parser/issues/199)) ([f8a2d3f](https://github.com/yargs/yargs-parser/commit/f8a2d3f)) + + +### Features + +* add configuration option to "collect-unknown-options" ([#181](https://github.com/yargs/yargs-parser/issues/181)) ([7909cc4](https://github.com/yargs/yargs-parser/commit/7909cc4)) +* maybeCoerceNumber() now takes into account arrays ([#187](https://github.com/yargs/yargs-parser/issues/187)) ([31c204b](https://github.com/yargs/yargs-parser/commit/31c204b)) + + +### BREAKING CHANGES + +* unless "parse-numbers" is set to "false", arrays of numeric strings are now parsed as numbers, rather than strings. +* we have dropped the broken "defaulted" functionality; we would like to revisit adding this in the future. +* maybeCoerceNumber now takes precedence over coerce return value (#182) + + + +### [13.1.1](https://www.github.com/yargs/yargs-parser/compare/v13.1.0...v13.1.1) (2019-06-10) + + +### Bug Fixes + +* convert values to strings when tokenizing ([#167](https://www.github.com/yargs/yargs-parser/issues/167)) ([57b7883](https://www.github.com/yargs/yargs-parser/commit/57b7883)) +* nargs should allow duplicates when duplicate-arguments-array=false ([#164](https://www.github.com/yargs/yargs-parser/issues/164)) ([47ccb0b](https://www.github.com/yargs/yargs-parser/commit/47ccb0b)) +* should populate "_" when given config with "short-option-groups" false ([#179](https://www.github.com/yargs/yargs-parser/issues/179)) ([6055974](https://www.github.com/yargs/yargs-parser/commit/6055974)) + +## [13.1.0](https://github.com/yargs/yargs-parser/compare/v13.0.0...v13.1.0) (2019-05-05) + + +### Features + +* add `strip-aliased` and `strip-dashed` configuration options. ([#172](https://github.com/yargs/yargs-parser/issues/172)) ([a3936aa](https://github.com/yargs/yargs-parser/commit/a3936aa)) +* support boolean which do not consume next argument. ([#171](https://github.com/yargs/yargs-parser/issues/171)) ([0ae7fcb](https://github.com/yargs/yargs-parser/commit/0ae7fcb)) + + + + +# [13.0.0](https://github.com/yargs/yargs-parser/compare/v12.0.0...v13.0.0) (2019-02-02) + + +### Features + +* don't coerce number from string with leading '0' or '+' ([#158](https://github.com/yargs/yargs-parser/issues/158)) ([18d0fd5](https://github.com/yargs/yargs-parser/commit/18d0fd5)) + + +### BREAKING CHANGES + +* options with leading '+' or '0' now parse as strings + + + + +# [12.0.0](https://github.com/yargs/yargs-parser/compare/v11.1.1...v12.0.0) (2019-01-29) + + +### Bug Fixes + +* better handling of quoted strings ([#153](https://github.com/yargs/yargs-parser/issues/153)) ([2fb71b2](https://github.com/yargs/yargs-parser/commit/2fb71b2)) + + +### Features + +* default value is now used if no right-hand value provided for numbers/strings ([#156](https://github.com/yargs/yargs-parser/issues/156)) ([5a7c46a](https://github.com/yargs/yargs-parser/commit/5a7c46a)) + + +### BREAKING CHANGES + +* a flag with no right-hand value no longer populates defaulted options with `undefined`. +* quotes at beginning and endings of strings are not removed during parsing. + + + + +## [11.1.1](https://github.com/yargs/yargs-parser/compare/v11.1.0...v11.1.1) (2018-11-19) + + +### Bug Fixes + +* ensure empty string is added into argv._ ([#140](https://github.com/yargs/yargs-parser/issues/140)) ([79cda98](https://github.com/yargs/yargs-parser/commit/79cda98)) + + +### Reverts + +* make requiresArg work in conjunction with arrays ([#136](https://github.com/yargs/yargs-parser/issues/136)) ([f4a3063](https://github.com/yargs/yargs-parser/commit/f4a3063)) + + + + +# [11.1.0](https://github.com/yargs/yargs-parser/compare/v11.0.0...v11.1.0) (2018-11-10) + + +### Bug Fixes + +* handling of one char alias ([#139](https://github.com/yargs/yargs-parser/issues/139)) ([ee56e31](https://github.com/yargs/yargs-parser/commit/ee56e31)) + + +### Features + +* add halt-at-non-option configuration option ([#130](https://github.com/yargs/yargs-parser/issues/130)) ([a849fce](https://github.com/yargs/yargs-parser/commit/a849fce)) + + + + +# [11.0.0](https://github.com/yargs/yargs-parser/compare/v10.1.0...v11.0.0) (2018-10-06) + + +### Bug Fixes + +* flatten-duplicate-arrays:false for more than 2 arrays ([#128](https://github.com/yargs/yargs-parser/issues/128)) ([2bc395f](https://github.com/yargs/yargs-parser/commit/2bc395f)) +* hyphenated flags combined with dot notation broke parsing ([#131](https://github.com/yargs/yargs-parser/issues/131)) ([dc788da](https://github.com/yargs/yargs-parser/commit/dc788da)) +* make requiresArg work in conjunction with arrays ([#136](https://github.com/yargs/yargs-parser/issues/136)) ([77ae1d4](https://github.com/yargs/yargs-parser/commit/77ae1d4)) + + +### Chores + +* update dependencies ([6dc42a1](https://github.com/yargs/yargs-parser/commit/6dc42a1)) + + +### Features + +* also add camelCase array options ([#125](https://github.com/yargs/yargs-parser/issues/125)) ([08c0117](https://github.com/yargs/yargs-parser/commit/08c0117)) +* array.type can now be provided, supporting coercion ([#132](https://github.com/yargs/yargs-parser/issues/132)) ([4b8cfce](https://github.com/yargs/yargs-parser/commit/4b8cfce)) + + +### BREAKING CHANGES + +* drops Node 4 support +* the argv object is now populated differently (correctly) when hyphens and dot notation are used in conjunction. + + + + +# [10.1.0](https://github.com/yargs/yargs-parser/compare/v10.0.0...v10.1.0) (2018-06-29) + + +### Features + +* add `set-placeholder-key` configuration ([#123](https://github.com/yargs/yargs-parser/issues/123)) ([19386ee](https://github.com/yargs/yargs-parser/commit/19386ee)) + + + + +# [10.0.0](https://github.com/yargs/yargs-parser/compare/v9.0.2...v10.0.0) (2018-04-04) + + +### Bug Fixes + +* do not set boolean flags if not defined in `argv` ([#119](https://github.com/yargs/yargs-parser/issues/119)) ([f6e6599](https://github.com/yargs/yargs-parser/commit/f6e6599)) + + +### BREAKING CHANGES + +* `boolean` flags defined without a `default` value will now behave like other option type and won't be set in the parsed results when the user doesn't set the corresponding CLI arg. + +Previous behavior: +```js +var parse = require('yargs-parser'); + +parse('--flag', {boolean: ['flag']}); +// => { _: [], flag: true } + +parse('--no-flag', {boolean: ['flag']}); +// => { _: [], flag: false } + +parse('', {boolean: ['flag']}); +// => { _: [], flag: false } +``` + +New behavior: +```js +var parse = require('yargs-parser'); + +parse('--flag', {boolean: ['flag']}); +// => { _: [], flag: true } + +parse('--no-flag', {boolean: ['flag']}); +// => { _: [], flag: false } + +parse('', {boolean: ['flag']}); +// => { _: [] } => flag not set similarly to other option type +``` + + + + +## [9.0.2](https://github.com/yargs/yargs-parser/compare/v9.0.1...v9.0.2) (2018-01-20) + + +### Bug Fixes + +* nargs was still aggressively consuming too many arguments ([9b28aad](https://github.com/yargs/yargs-parser/commit/9b28aad)) + + + + +## [9.0.1](https://github.com/yargs/yargs-parser/compare/v9.0.0...v9.0.1) (2018-01-20) + + +### Bug Fixes + +* nargs was consuming too many arguments ([4fef206](https://github.com/yargs/yargs-parser/commit/4fef206)) + + + + +# [9.0.0](https://github.com/yargs/yargs-parser/compare/v8.1.0...v9.0.0) (2018-01-20) + + +### Features + +* narg arguments no longer consume flag arguments ([#114](https://github.com/yargs/yargs-parser/issues/114)) ([60bb9b3](https://github.com/yargs/yargs-parser/commit/60bb9b3)) + + +### BREAKING CHANGES + +* arguments of form --foo, -abc, will no longer be consumed by nargs + + + + +# [8.1.0](https://github.com/yargs/yargs-parser/compare/v8.0.0...v8.1.0) (2017-12-20) + + +### Bug Fixes + +* allow null config values ([#108](https://github.com/yargs/yargs-parser/issues/108)) ([d8b14f9](https://github.com/yargs/yargs-parser/commit/d8b14f9)) +* ensure consistent parsing of dot-notation arguments ([#102](https://github.com/yargs/yargs-parser/issues/102)) ([c9bd79c](https://github.com/yargs/yargs-parser/commit/c9bd79c)) +* implement [@antoniom](https://github.com/antoniom)'s fix for camel-case expansion ([3087e1d](https://github.com/yargs/yargs-parser/commit/3087e1d)) +* only run coercion functions once, despite aliases. ([#76](https://github.com/yargs/yargs-parser/issues/76)) ([#103](https://github.com/yargs/yargs-parser/issues/103)) ([507aaef](https://github.com/yargs/yargs-parser/commit/507aaef)) +* scientific notation circumvented bounds check ([#110](https://github.com/yargs/yargs-parser/issues/110)) ([3571f57](https://github.com/yargs/yargs-parser/commit/3571f57)) +* tokenizer should ignore spaces at the beginning of the argString ([#106](https://github.com/yargs/yargs-parser/issues/106)) ([f34ead9](https://github.com/yargs/yargs-parser/commit/f34ead9)) + + +### Features + +* make combining arrays a configurable option ([#111](https://github.com/yargs/yargs-parser/issues/111)) ([c8bf536](https://github.com/yargs/yargs-parser/commit/c8bf536)) +* merge array from arguments with array from config ([#83](https://github.com/yargs/yargs-parser/issues/83)) ([806ddd6](https://github.com/yargs/yargs-parser/commit/806ddd6)) + + + + +# [8.0.0](https://github.com/yargs/yargs-parser/compare/v7.0.0...v8.0.0) (2017-10-05) + + +### Bug Fixes + +* Ignore multiple spaces between arguments. ([#100](https://github.com/yargs/yargs-parser/issues/100)) ([d137227](https://github.com/yargs/yargs-parser/commit/d137227)) + + +### Features + +* allow configuration of prefix for boolean negation ([#94](https://github.com/yargs/yargs-parser/issues/94)) ([00bde7d](https://github.com/yargs/yargs-parser/commit/00bde7d)) +* reworking how numbers are parsed ([#104](https://github.com/yargs/yargs-parser/issues/104)) ([fba00eb](https://github.com/yargs/yargs-parser/commit/fba00eb)) + + +### BREAKING CHANGES + +* strings that fail `Number.isSafeInteger()` are no longer coerced into numbers. + + + + +# [7.0.0](https://github.com/yargs/yargs-parser/compare/v6.0.1...v7.0.0) (2017-05-02) + + +### Chores + +* revert populate-- logic ([#91](https://github.com/yargs/yargs-parser/issues/91)) ([6003e6d](https://github.com/yargs/yargs-parser/commit/6003e6d)) + + +### BREAKING CHANGES + +* populate-- now defaults to false. + + + + +## [6.0.1](https://github.com/yargs/yargs-parser/compare/v6.0.0...v6.0.1) (2017-05-01) + + +### Bug Fixes + +* default '--' to undefined when not provided; this is closer to the array API ([#90](https://github.com/yargs/yargs-parser/issues/90)) ([4e739cc](https://github.com/yargs/yargs-parser/commit/4e739cc)) + + + + +# [6.0.0](https://github.com/yargs/yargs-parser/compare/v4.2.1...v6.0.0) (2017-05-01) + + +### Bug Fixes + +* environment variables should take precedence over config file ([#81](https://github.com/yargs/yargs-parser/issues/81)) ([76cee1f](https://github.com/yargs/yargs-parser/commit/76cee1f)) +* parsing hints should apply for dot notation keys ([#86](https://github.com/yargs/yargs-parser/issues/86)) ([3e47d62](https://github.com/yargs/yargs-parser/commit/3e47d62)) + + +### Chores + +* upgrade to newest version of camelcase ([#87](https://github.com/yargs/yargs-parser/issues/87)) ([f1903aa](https://github.com/yargs/yargs-parser/commit/f1903aa)) + + +### Features + +* add -- option which allows arguments after the -- flag to be returned separated from positional arguments ([#84](https://github.com/yargs/yargs-parser/issues/84)) ([2572ca8](https://github.com/yargs/yargs-parser/commit/2572ca8)) +* when parsing stops, we now populate "--" by default ([#88](https://github.com/yargs/yargs-parser/issues/88)) ([cd666db](https://github.com/yargs/yargs-parser/commit/cd666db)) + + +### BREAKING CHANGES + +* rather than placing arguments in "_", when parsing is stopped via "--"; we now populate an array called "--" by default. +* camelcase now requires Node 4+. +* environment variables will now override config files (args, env, config-file, config-object) + + + + +# [5.0.0](https://github.com/yargs/yargs-parser/compare/v4.2.1...v5.0.0) (2017-02-18) + + +### Bug Fixes + +* environment variables should take precedence over config file ([#81](https://github.com/yargs/yargs-parser/issues/81)) ([76cee1f](https://github.com/yargs/yargs-parser/commit/76cee1f)) + + +### BREAKING CHANGES + +* environment variables will now override config files (args, env, config-file, config-object) + + + + +## [4.2.1](https://github.com/yargs/yargs-parser/compare/v4.2.0...v4.2.1) (2017-01-02) + + +### Bug Fixes + +* flatten/duplicate regression ([#75](https://github.com/yargs/yargs-parser/issues/75)) ([68d68a0](https://github.com/yargs/yargs-parser/commit/68d68a0)) + + + + +# [4.2.0](https://github.com/yargs/yargs-parser/compare/v4.1.0...v4.2.0) (2016-12-01) + + +### Bug Fixes + +* inner objects in configs had their keys appended to top-level key when dot-notation was disabled ([#72](https://github.com/yargs/yargs-parser/issues/72)) ([0b1b5f9](https://github.com/yargs/yargs-parser/commit/0b1b5f9)) + + +### Features + +* allow multiple arrays to be provided, rather than always combining ([#71](https://github.com/yargs/yargs-parser/issues/71)) ([0f0fb2d](https://github.com/yargs/yargs-parser/commit/0f0fb2d)) + + + + +# [4.1.0](https://github.com/yargs/yargs-parser/compare/v4.0.2...v4.1.0) (2016-11-07) + + +### Features + +* apply coercions to default options ([#65](https://github.com/yargs/yargs-parser/issues/65)) ([c79052b](https://github.com/yargs/yargs-parser/commit/c79052b)) +* handle dot notation boolean options ([#63](https://github.com/yargs/yargs-parser/issues/63)) ([02c3545](https://github.com/yargs/yargs-parser/commit/02c3545)) + + + + +## [4.0.2](https://github.com/yargs/yargs-parser/compare/v4.0.1...v4.0.2) (2016-09-30) + + +### Bug Fixes + +* whoops, let's make the assign not change the Object key order ([29d069a](https://github.com/yargs/yargs-parser/commit/29d069a)) + + + + +## [4.0.1](https://github.com/yargs/yargs-parser/compare/v4.0.0...v4.0.1) (2016-09-30) + + +### Bug Fixes + +* lodash.assign was deprecated ([#59](https://github.com/yargs/yargs-parser/issues/59)) ([5e7eb11](https://github.com/yargs/yargs-parser/commit/5e7eb11)) + + + + +# [4.0.0](https://github.com/yargs/yargs-parser/compare/v3.2.0...v4.0.0) (2016-09-26) + + +### Bug Fixes + +* coerce should be applied to the final objects and arrays created ([#57](https://github.com/yargs/yargs-parser/issues/57)) ([4ca69da](https://github.com/yargs/yargs-parser/commit/4ca69da)) + + +### BREAKING CHANGES + +* coerce is no longer applied to individual arguments in an implicit array. + + + + +# [3.2.0](https://github.com/yargs/yargs-parser/compare/v3.1.0...v3.2.0) (2016-08-13) + + +### Features + +* coerce full array instead of each element ([#51](https://github.com/yargs/yargs-parser/issues/51)) ([cc4dc56](https://github.com/yargs/yargs-parser/commit/cc4dc56)) + + + + +# [3.1.0](https://github.com/yargs/yargs-parser/compare/v3.0.0...v3.1.0) (2016-08-09) + + +### Bug Fixes + +* address pkgConf parsing bug outlined in [#37](https://github.com/yargs/yargs-parser/issues/37) ([#45](https://github.com/yargs/yargs-parser/issues/45)) ([be76ee6](https://github.com/yargs/yargs-parser/commit/be76ee6)) +* better parsing of negative values ([#44](https://github.com/yargs/yargs-parser/issues/44)) ([2e43692](https://github.com/yargs/yargs-parser/commit/2e43692)) +* check aliases when guessing defaults for arguments fixes [#41](https://github.com/yargs/yargs-parser/issues/41) ([#43](https://github.com/yargs/yargs-parser/issues/43)) ([f3e4616](https://github.com/yargs/yargs-parser/commit/f3e4616)) + + +### Features + +* added coerce option, for providing specialized argument parsing ([#42](https://github.com/yargs/yargs-parser/issues/42)) ([7b49cd2](https://github.com/yargs/yargs-parser/commit/7b49cd2)) + + + + +# [3.0.0](https://github.com/yargs/yargs-parser/compare/v2.4.1...v3.0.0) (2016-08-07) + + +### Bug Fixes + +* parsing issue with numeric character in group of options ([#19](https://github.com/yargs/yargs-parser/issues/19)) ([f743236](https://github.com/yargs/yargs-parser/commit/f743236)) +* upgraded lodash.assign ([5d7fdf4](https://github.com/yargs/yargs-parser/commit/5d7fdf4)) + +### BREAKING CHANGES + +* subtle change to how values are parsed in a group of single-character arguments. +* _first released in 3.1.0, better handling of negative values should be considered a breaking change._ + + + + +## [2.4.1](https://github.com/yargs/yargs-parser/compare/v2.4.0...v2.4.1) (2016-07-16) + + +### Bug Fixes + +* **count:** do not increment a default value ([#39](https://github.com/yargs/yargs-parser/issues/39)) ([b04a189](https://github.com/yargs/yargs-parser/commit/b04a189)) + + + + +# [2.4.0](https://github.com/yargs/yargs-parser/compare/v2.3.0...v2.4.0) (2016-04-11) + + +### Features + +* **environment:** Support nested options in environment variables ([#26](https://github.com/yargs/yargs-parser/issues/26)) thanks [@elas7](https://github.com/elas7) \o/ ([020778b](https://github.com/yargs/yargs-parser/commit/020778b)) + + + + +# [2.3.0](https://github.com/yargs/yargs-parser/compare/v2.2.0...v2.3.0) (2016-04-09) + + +### Bug Fixes + +* **boolean:** fix for boolean options with non boolean defaults (#20) ([2dbe86b](https://github.com/yargs/yargs-parser/commit/2dbe86b)), closes [(#20](https://github.com/(/issues/20) +* **package:** remove tests from tarball ([0353c0d](https://github.com/yargs/yargs-parser/commit/0353c0d)) +* **parsing:** handle calling short option with an empty string as the next value. ([a867165](https://github.com/yargs/yargs-parser/commit/a867165)) +* boolean flag when next value contains the strings 'true' or 'false'. ([69941a6](https://github.com/yargs/yargs-parser/commit/69941a6)) +* update dependencies; add standard-version bin for next release (#24) ([822d9d5](https://github.com/yargs/yargs-parser/commit/822d9d5)) + +### Features + +* **configuration:** Allow to pass configuration objects to yargs-parser ([0780900](https://github.com/yargs/yargs-parser/commit/0780900)) +* **normalize:** allow normalize to work with arrays ([e0eaa1a](https://github.com/yargs/yargs-parser/commit/e0eaa1a)) diff --git a/example.js b/example.js deleted file mode 100755 index cbfe167d..00000000 --- a/example.js +++ /dev/null @@ -1,3 +0,0 @@ -var parser = require('./') -var parse = parser('--foo "-bar"') -console.log(parse) diff --git a/index.ts b/index.ts deleted file mode 100644 index dab5030b..00000000 --- a/index.ts +++ /dev/null @@ -1,1124 +0,0 @@ -import * as path from 'path' -import * as util from 'util' -import { tokenizeArgString } from './lib/tokenize-arg-string' -import type { - ArgsInput, - Arguments, - ArrayFlagsKey, - ArrayOption, - CoerceCallback, - Configuration, - DefaultValuesForType, - DefaultValuesForTypeKey, - DetailedArguments, - Flag, - Flags, - FlagsKey, - StringFlag, - BooleanFlag, - NumberFlag, - ConfigsFlag, - CoercionsFlag, - Options, - OptionsDefault, - Parser -} from './lib/yargs-parser-types' -import type { Dictionary, ValueOf } from './lib/common-types' - -// See https://github.com/yargs/yargs-parser#supported-nodejs-versions for our -// version support policy. The YARGS_MIN_NODE_VERSION is used for testing only. -const minNodeVersion = (process && process.env && process.env.YARGS_MIN_NODE_VERSION) - ? Number(process.env.YARGS_MIN_NODE_VERSION) : 10 -if (process && process.version) { - const major = Number(process.version.match(/v([^.]+)/)![1]) - if (major < minNodeVersion) { - throw Error(`yargs parser supports a minimum Node.js version of ${minNodeVersion}. Read our version support policy: https://github.com/yargs/yargs-parser#supported-nodejs-versions`) - } -} -import camelCase = require('camelcase') -import decamelize = require('decamelize') - -function parse (argsInput: ArgsInput, options?: Partial): DetailedArguments { - const opts: Partial = Object.assign({ - alias: undefined, - array: undefined, - boolean: undefined, - config: undefined, - configObjects: undefined, - configuration: undefined, - coerce: undefined, - count: undefined, - default: undefined, - envPrefix: undefined, - narg: undefined, - normalize: undefined, - string: undefined, - number: undefined, - __: undefined, - key: undefined - }, options) - // allow a string argument to be passed in rather - // than an argv array. - const args = tokenizeArgString(argsInput) - - // aliases might have transitive relationships, normalize this. - const aliases = combineAliases(Object.assign(Object.create(null), opts.alias)) - const configuration: Configuration = Object.assign({ - 'boolean-negation': true, - 'camel-case-expansion': true, - 'combine-arrays': false, - 'dot-notation': true, - 'duplicate-arguments-array': true, - 'flatten-duplicate-arrays': true, - 'greedy-arrays': true, - 'halt-at-non-option': false, - 'nargs-eats-options': false, - 'negation-prefix': 'no-', - 'parse-numbers': true, - 'populate--': false, - 'set-placeholder-key': false, - 'short-option-groups': true, - 'strip-aliased': false, - 'strip-dashed': false, - 'unknown-options-as-args': false - }, opts.configuration) - const defaults: OptionsDefault = Object.assign(Object.create(null), opts.default) - const configObjects = opts.configObjects || [] - const envPrefix = opts.envPrefix - const notFlagsOption = configuration['populate--'] - const notFlagsArgv: string = notFlagsOption ? '--' : '_' - const newAliases: Dictionary = Object.create(null) - const defaulted: Dictionary = Object.create(null) - // allow a i18n handler to be passed in, default to a fake one (util.format). - const __ = opts.__ || util.format - const flags: Flags = { - aliases: Object.create(null), - arrays: Object.create(null), - bools: Object.create(null), - strings: Object.create(null), - numbers: Object.create(null), - counts: Object.create(null), - normalize: Object.create(null), - configs: Object.create(null), - nargs: Object.create(null), - coercions: Object.create(null), - keys: [] - } - const negative = /^-([0-9]+(\.[0-9]+)?|\.[0-9]+)$/ - const negatedBoolean = new RegExp('^--' + configuration['negation-prefix'] + '(.+)') - - ;([] as ArrayOption[]).concat(opts.array || []).filter(Boolean).forEach(function (opt) { - const key = typeof opt === 'object' ? opt.key : opt - - // assign to flags[bools|strings|numbers] - const assignment: ArrayFlagsKey | undefined = Object.keys(opt).map(function (key) { - const arrayFlagKeys: Record = { - boolean: 'bools', - string: 'strings', - number: 'numbers' - } - return arrayFlagKeys[key] - }).filter(Boolean).pop() - - // assign key to be coerced - if (assignment) { - flags[assignment][key] = true - } - - flags.arrays[key] = true - flags.keys.push(key) - }) - - ;([] as string[]).concat(opts.boolean || []).filter(Boolean).forEach(function (key) { - flags.bools[key] = true - flags.keys.push(key) - }) - - ;([] as string[]).concat(opts.string || []).filter(Boolean).forEach(function (key) { - flags.strings[key] = true - flags.keys.push(key) - }) - - ;([] as string[]).concat(opts.number || []).filter(Boolean).forEach(function (key) { - flags.numbers[key] = true - flags.keys.push(key) - }) - - ;([] as string[]).concat(opts.count || []).filter(Boolean).forEach(function (key) { - flags.counts[key] = true - flags.keys.push(key) - }) - - ;([] as string[]).concat(opts.normalize || []).filter(Boolean).forEach(function (key) { - flags.normalize[key] = true - flags.keys.push(key) - }) - - if (typeof opts.narg === 'object') { - Object.entries(opts.narg).forEach(([key, value]) => { - if (typeof value === 'number') { - flags.nargs[key] = value - flags.keys.push(key) - } - }) - } - - if (typeof opts.coerce === 'object') { - Object.entries(opts.coerce).forEach(([key, value]) => { - if (typeof value === 'function') { - flags.coercions[key] = value - flags.keys.push(key) - } - }) - } - - if (typeof opts.config !== 'undefined') { - if (Array.isArray(opts.config) || typeof opts.config === 'string') { - ;([] as string[]).concat(opts.config).filter(Boolean).forEach(function (key) { - flags.configs[key] = true - }) - } else if (typeof opts.config === 'object') { - Object.entries(opts.config).forEach(([key, value]) => { - if (typeof value === 'boolean' || typeof value === 'function') { - flags.configs[key] = value - } - }) - } - } - - // create a lookup table that takes into account all - // combinations of aliases: {f: ['foo'], foo: ['f']} - extendAliases(opts.key, aliases, opts.default, flags.arrays) - - // apply default values to all aliases. - Object.keys(defaults).forEach(function (key) { - (flags.aliases[key] || []).forEach(function (alias) { - defaults[alias] = defaults[key] - }) - }) - - let error: Error | null = null - checkConfiguration() - - let notFlags: string[] = [] - - const argv: Arguments = Object.assign(Object.create(null), { _: [] }) - // TODO(bcoe): for the first pass at removing object prototype we didn't - // remove all prototypes from objects returned by this API, we might want - // to gradually move towards doing so. - const argvReturn: { [argName: string]: any } = {} - - for (let i = 0; i < args.length; i++) { - const arg = args[i] - let broken: boolean - let key: string | undefined - let letters: string[] - let m: RegExpMatchArray | null - let next: string - let value: string - - // any unknown option (except for end-of-options, "--") - if (arg !== '--' && isUnknownOptionAsArg(arg)) { - argv._.push(arg) - // -- separated by = - } else if (arg.match(/^--.+=/) || ( - !configuration['short-option-groups'] && arg.match(/^-.+=/) - )) { - // Using [\s\S] instead of . because js doesn't support the - // 'dotall' regex modifier. See: - // http://stackoverflow.com/a/1068308/13216 - m = arg.match(/^--?([^=]+)=([\s\S]*)$/) - - // arrays format = '--f=a b c' - if (m !== null && Array.isArray(m) && m.length >= 3) { - if (checkAllAliases(m[1], flags.arrays)) { - i = eatArray(i, m[1], args, m[2]) - } else if (checkAllAliases(m[1], flags.nargs) !== false) { - // nargs format = '--f=monkey washing cat' - i = eatNargs(i, m[1], args, m[2]) - } else { - setArg(m[1], m[2]) - } - } - } else if (arg.match(negatedBoolean) && configuration['boolean-negation']) { - m = arg.match(negatedBoolean) - if (m !== null && Array.isArray(m) && m.length >= 2) { - key = m[1] - setArg(key, checkAllAliases(key, flags.arrays) ? [false] : false) - } - - // -- separated by space. - } else if (arg.match(/^--.+/) || ( - !configuration['short-option-groups'] && arg.match(/^-[^-]+/) - )) { - m = arg.match(/^--?(.+)/) - if (m !== null && Array.isArray(m) && m.length >= 2) { - key = m[1] - if (checkAllAliases(key, flags.arrays)) { - // array format = '--foo a b c' - i = eatArray(i, key, args) - } else if (checkAllAliases(key, flags.nargs) !== false) { - // nargs format = '--foo a b c' - // should be truthy even if: flags.nargs[key] === 0 - i = eatNargs(i, key, args) - } else { - next = args[i + 1] - - if (next !== undefined && (!next.match(/^-/) || - next.match(negative)) && - !checkAllAliases(key, flags.bools) && - !checkAllAliases(key, flags.counts)) { - setArg(key, next) - i++ - } else if (/^(true|false)$/.test(next)) { - setArg(key, next) - i++ - } else { - setArg(key, defaultValue(key)) - } - } - } - - // dot-notation flag separated by '='. - } else if (arg.match(/^-.\..+=/)) { - m = arg.match(/^-([^=]+)=([\s\S]*)$/) - if (m !== null && Array.isArray(m) && m.length >= 3) { - setArg(m[1], m[2]) - } - - // dot-notation flag separated by space. - } else if (arg.match(/^-.\..+/) && !arg.match(negative)) { - next = args[i + 1] - m = arg.match(/^-(.\..+)/) - if (m !== null && Array.isArray(m) && m.length >= 2) { - key = m[1] - if (next !== undefined && !next.match(/^-/) && - !checkAllAliases(key, flags.bools) && - !checkAllAliases(key, flags.counts)) { - setArg(key, next) - i++ - } else { - setArg(key, defaultValue(key)) - } - } - } else if (arg.match(/^-[^-]+/) && !arg.match(negative)) { - letters = arg.slice(1, -1).split('') - broken = false - - for (let j = 0; j < letters.length; j++) { - next = arg.slice(j + 2) - - if (letters[j + 1] && letters[j + 1] === '=') { - value = arg.slice(j + 3) - key = letters[j] - - if (checkAllAliases(key, flags.arrays)) { - // array format = '-f=a b c' - i = eatArray(i, key, args, value) - } else if (checkAllAliases(key, flags.nargs) !== false) { - // nargs format = '-f=monkey washing cat' - i = eatNargs(i, key, args, value) - } else { - setArg(key, value) - } - - broken = true - break - } - - if (next === '-') { - setArg(letters[j], next) - continue - } - - // current letter is an alphabetic character and next value is a number - if (/[A-Za-z]/.test(letters[j]) && - /^-?\d+(\.\d*)?(e-?\d+)?$/.test(next) && - checkAllAliases(next, flags.bools) === false) { - setArg(letters[j], next) - broken = true - break - } - - if (letters[j + 1] && letters[j + 1].match(/\W/)) { - setArg(letters[j], next) - broken = true - break - } else { - setArg(letters[j], defaultValue(letters[j])) - } - } - - key = arg.slice(-1)[0] - - if (!broken && key !== '-') { - if (checkAllAliases(key, flags.arrays)) { - // array format = '-f a b c' - i = eatArray(i, key, args) - } else if (checkAllAliases(key, flags.nargs) !== false) { - // nargs format = '-f a b c' - // should be truthy even if: flags.nargs[key] === 0 - i = eatNargs(i, key, args) - } else { - next = args[i + 1] - - if (next !== undefined && (!/^(-|--)[^-]/.test(next) || - next.match(negative)) && - !checkAllAliases(key, flags.bools) && - !checkAllAliases(key, flags.counts)) { - setArg(key, next) - i++ - } else if (/^(true|false)$/.test(next)) { - setArg(key, next) - i++ - } else { - setArg(key, defaultValue(key)) - } - } - } - } else if (arg.match(/^-[0-9]$/) && - arg.match(negative) && - checkAllAliases(arg.slice(1), flags.bools)) { - // single-digit boolean alias, e.g: xargs -0 - key = arg.slice(1) - setArg(key, defaultValue(key)) - } else if (arg === '--') { - notFlags = args.slice(i + 1) - break - } else if (configuration['halt-at-non-option']) { - notFlags = args.slice(i) - break - } else { - const maybeCoercedNumber = maybeCoerceNumber('_', arg) - if (typeof maybeCoercedNumber === 'string' || typeof maybeCoercedNumber === 'number') { - argv._.push(maybeCoercedNumber) - } - } - } - - // order of precedence: - // 1. command line arg - // 2. value from env var - // 3. value from config file - // 4. value from config objects - // 5. configured default value - applyEnvVars(argv, true) // special case: check env vars that point to config file - applyEnvVars(argv, false) - setConfig(argv) - setConfigObjects() - applyDefaultsAndAliases(argv, flags.aliases, defaults, true) - applyCoercions(argv) - if (configuration['set-placeholder-key']) setPlaceholderKeys(argv) - - // for any counts either not in args or without an explicit default, set to 0 - Object.keys(flags.counts).forEach(function (key) { - if (!hasKey(argv, key.split('.'))) setArg(key, 0) - }) - - // '--' defaults to undefined. - if (notFlagsOption && notFlags.length) argv[notFlagsArgv] = [] - notFlags.forEach(function (key) { - argv[notFlagsArgv].push(key) - }) - - if (configuration['camel-case-expansion'] && configuration['strip-dashed']) { - Object.keys(argv).filter(key => key !== '--' && key.includes('-')).forEach(key => { - delete argv[key] - }) - } - - if (configuration['strip-aliased']) { - ;([] as string[]).concat(...Object.keys(aliases).map(k => aliases[k])).forEach(alias => { - if (configuration['camel-case-expansion']) { - delete argv[alias.split('.').map(prop => camelCase(prop)).join('.')] - } - - delete argv[alias] - }) - } - - // how many arguments should we consume, based - // on the nargs option? - function eatNargs (i: number, key: string, args: string[], argAfterEqualSign?: string): number { - let ii - let toEat = checkAllAliases(key, flags.nargs) - // NaN has a special meaning for the array type, indicating that one or - // more values are expected. - toEat = typeof toEat !== 'number' || isNaN(toEat) ? 1 : toEat - - if (toEat === 0) { - if (!isUndefined(argAfterEqualSign)) { - error = Error(__('Argument unexpected for: %s', key)) - } - setArg(key, defaultValue(key)) - return i - } - - let available = isUndefined(argAfterEqualSign) ? 0 : 1 - if (configuration['nargs-eats-options']) { - // classic behavior, yargs eats positional and dash arguments. - if (args.length - (i + 1) + available < toEat) { - error = Error(__('Not enough arguments following: %s', key)) - } - available = toEat - } else { - // nargs will not consume flag arguments, e.g., -abc, --foo, - // and terminates when one is observed. - for (ii = i + 1; ii < args.length; ii++) { - if (!args[ii].match(/^-[^0-9]/) || args[ii].match(negative) || isUnknownOptionAsArg(args[ii])) available++ - else break - } - if (available < toEat) error = Error(__('Not enough arguments following: %s', key)) - } - - let consumed = Math.min(available, toEat) - if (!isUndefined(argAfterEqualSign) && consumed > 0) { - setArg(key, argAfterEqualSign) - consumed-- - } - for (ii = i + 1; ii < (consumed + i + 1); ii++) { - setArg(key, args[ii]) - } - - return (i + consumed) - } - - // if an option is an array, eat all non-hyphenated arguments - // following it... YUM! - // e.g., --foo apple banana cat becomes ["apple", "banana", "cat"] - function eatArray (i: number, key: string, args: string[], argAfterEqualSign?: string): number { - let argsToSet = [] - let next = argAfterEqualSign || args[i + 1] - // If both array and nargs are configured, enforce the nargs count: - const nargsCount = checkAllAliases(key, flags.nargs) - - if (checkAllAliases(key, flags.bools) && !(/^(true|false)$/.test(next))) { - argsToSet.push(true) - } else if (isUndefined(next) || - (isUndefined(argAfterEqualSign) && /^-/.test(next) && !negative.test(next) && !isUnknownOptionAsArg(next))) { - // for keys without value ==> argsToSet remains an empty [] - // set user default value, if available - if (defaults[key] !== undefined) { - const defVal = defaults[key] - argsToSet = Array.isArray(defVal) ? defVal : [defVal] - } - } else { - // value in --option=value is eaten as is - if (!isUndefined(argAfterEqualSign)) { - argsToSet.push(processValue(key, argAfterEqualSign)) - } - for (let ii = i + 1; ii < args.length; ii++) { - if ((!configuration['greedy-arrays'] && argsToSet.length > 0) || - (nargsCount && typeof nargsCount === 'number' && argsToSet.length >= nargsCount)) break - next = args[ii] - if (/^-/.test(next) && !negative.test(next) && !isUnknownOptionAsArg(next)) break - i = ii - argsToSet.push(processValue(key, next)) - } - } - - // If both array and nargs are configured, create an error if less than - // nargs positionals were found. NaN has special meaning, indicating - // that at least one value is required (more are okay). - if (typeof nargsCount === 'number' && ((nargsCount && argsToSet.length < nargsCount) || - (isNaN(nargsCount) && argsToSet.length === 0))) { - error = Error(__('Not enough arguments following: %s', key)) - } - - setArg(key, argsToSet) - return i - } - - function setArg (key: string, val: any): void { - if (/-/.test(key) && configuration['camel-case-expansion']) { - const alias = key.split('.').map(function (prop) { - return camelCase(prop) - }).join('.') - addNewAlias(key, alias) - } - - const value = processValue(key, val) - const splitKey = key.split('.') - setKey(argv, splitKey, value) - - // handle populating aliases of the full key - if (flags.aliases[key]) { - flags.aliases[key].forEach(function (x) { - const keyProperties = x.split('.') - setKey(argv, keyProperties, value) - }) - } - - // handle populating aliases of the first element of the dot-notation key - if (splitKey.length > 1 && configuration['dot-notation']) { - ;(flags.aliases[splitKey[0]] || []).forEach(function (x) { - let keyProperties = x.split('.') - - // expand alias with nested objects in key - const a = ([] as string[]).concat(splitKey) - a.shift() // nuke the old key. - keyProperties = keyProperties.concat(a) - - // populate alias only if is not already an alias of the full key - // (already populated above) - if (!(flags.aliases[key] || []).includes(keyProperties.join('.'))) { - setKey(argv, keyProperties, value) - } - }) - } - - // Set normalize getter and setter when key is in 'normalize' but isn't an array - if (checkAllAliases(key, flags.normalize) && !checkAllAliases(key, flags.arrays)) { - const keys = [key].concat(flags.aliases[key] || []) - keys.forEach(function (key) { - Object.defineProperty(argvReturn, key, { - enumerable: true, - get () { - return val - }, - set (value) { - val = typeof value === 'string' ? path.normalize(value) : value - } - }) - }) - } - } - - function addNewAlias (key: string, alias: string): void { - if (!(flags.aliases[key] && flags.aliases[key].length)) { - flags.aliases[key] = [alias] - newAliases[alias] = true - } - if (!(flags.aliases[alias] && flags.aliases[alias].length)) { - addNewAlias(alias, key) - } - } - - function processValue (key: string, val: any) { - // strings may be quoted, clean this up as we assign values. - if (typeof val === 'string' && - (val[0] === "'" || val[0] === '"') && - val[val.length - 1] === val[0] - ) { - val = val.substring(1, val.length - 1) - } - - // handle parsing boolean arguments --foo=true --bar false. - if (checkAllAliases(key, flags.bools) || checkAllAliases(key, flags.counts)) { - if (typeof val === 'string') val = val === 'true' - } - - let value = Array.isArray(val) - ? val.map(function (v) { return maybeCoerceNumber(key, v) }) - : maybeCoerceNumber(key, val) - - // increment a count given as arg (either no value or value parsed as boolean) - if (checkAllAliases(key, flags.counts) && (isUndefined(value) || typeof value === 'boolean')) { - value = increment() - } - - // Set normalized value when key is in 'normalize' and in 'arrays' - if (checkAllAliases(key, flags.normalize) && checkAllAliases(key, flags.arrays)) { - if (Array.isArray(val)) value = val.map(path.normalize) - else value = path.normalize(val) - } - return value - } - - function maybeCoerceNumber (key: string, value: string | number | null | undefined) { - if (!checkAllAliases(key, flags.strings) && !checkAllAliases(key, flags.bools) && !Array.isArray(value)) { - const shouldCoerceNumber = isNumber(value) && configuration['parse-numbers'] && ( - Number.isSafeInteger(Math.floor(parseFloat(`${value}`))) - ) - if (shouldCoerceNumber || (!isUndefined(value) && checkAllAliases(key, flags.numbers))) value = Number(value) - } - return value - } - - // set args from config.json file, this should be - // applied last so that defaults can be applied. - function setConfig (argv: Arguments): void { - const configLookup = Object.create(null) - - // expand defaults/aliases, in-case any happen to reference - // the config.json file. - applyDefaultsAndAliases(configLookup, flags.aliases, defaults) - - Object.keys(flags.configs).forEach(function (configKey) { - const configPath = argv[configKey] || configLookup[configKey] - if (configPath) { - try { - let config = null - const resolvedConfigPath = path.resolve(process.cwd(), configPath) - const resolveConfig = flags.configs[configKey] - - if (typeof resolveConfig === 'function') { - try { - config = resolveConfig(resolvedConfigPath) - } catch (e) { - config = e - } - if (config instanceof Error) { - error = config - return - } - } else { - config = require(resolvedConfigPath) - } - - setConfigObject(config) - } catch (ex) { - if (argv[configKey]) error = Error(__('Invalid JSON config file: %s', configPath)) - } - } - }) - } - - // set args from config object. - // it recursively checks nested objects. - function setConfigObject (config: { [key: string]: any }, prev?: string): void { - Object.keys(config).forEach(function (key) { - const value = config[key] - const fullKey = prev ? prev + '.' + key : key - - // if the value is an inner object and we have dot-notation - // enabled, treat inner objects in config the same as - // heavily nested dot notations (foo.bar.apple). - if (typeof value === 'object' && value !== null && !Array.isArray(value) && configuration['dot-notation']) { - // if the value is an object but not an array, check nested object - setConfigObject(value, fullKey) - } else { - // setting arguments via CLI takes precedence over - // values within the config file. - if (!hasKey(argv, fullKey.split('.')) || (checkAllAliases(fullKey, flags.arrays) && configuration['combine-arrays'])) { - setArg(fullKey, value) - } - } - }) - } - - // set all config objects passed in opts - function setConfigObjects (): void { - if (typeof configObjects !== 'undefined') { - configObjects.forEach(function (configObject) { - setConfigObject(configObject) - }) - } - } - - function applyEnvVars (argv: Arguments, configOnly: boolean): void { - if (process) { - if (typeof envPrefix === 'undefined') return - - const prefix = typeof envPrefix === 'string' ? envPrefix : '' - Object.keys(process.env).forEach(function (envVar) { - if (prefix === '' || envVar.lastIndexOf(prefix, 0) === 0) { - // get array of nested keys and convert them to camel case - const keys = envVar.split('__').map(function (key, i) { - if (i === 0) { - key = key.substring(prefix.length) - } - return camelCase(key) - }) - - if (((configOnly && flags.configs[keys.join('.')]) || !configOnly) && !hasKey(argv, keys)) { - setArg(keys.join('.'), process.env[envVar]) - } - } - }) - } - } - - function applyCoercions (argv: Arguments): void { - let coerce: false | CoerceCallback - const applied: Set = new Set() - Object.keys(argv).forEach(function (key) { - if (!applied.has(key)) { // If we haven't already coerced this option via one of its aliases - coerce = checkAllAliases(key, flags.coercions) - if (typeof coerce === 'function') { - try { - const value = maybeCoerceNumber(key, coerce(argv[key])) - ;(([] as string[]).concat(flags.aliases[key] || [], key)).forEach(ali => { - applied.add(ali) - argv[ali] = value - }) - } catch (err) { - error = err - } - } - } - }) - } - - function setPlaceholderKeys (argv: Arguments): Arguments { - flags.keys.forEach((key) => { - // don't set placeholder keys for dot notation options 'foo.bar'. - if (~key.indexOf('.')) return - if (typeof argv[key] === 'undefined') argv[key] = undefined - }) - return argv - } - - function applyDefaultsAndAliases (obj: { [key: string]: any }, aliases: { [key: string]: string[] }, defaults: { [key: string]: any }, canLog: boolean = false): void { - Object.keys(defaults).forEach(function (key) { - if (!hasKey(obj, key.split('.'))) { - setKey(obj, key.split('.'), defaults[key]) - if (canLog) defaulted[key] = true - - ;(aliases[key] || []).forEach(function (x) { - if (hasKey(obj, x.split('.'))) return - setKey(obj, x.split('.'), defaults[key]) - }) - } - }) - } - - function hasKey (obj: { [key: string]: any }, keys: string[]): boolean { - let o = obj - - if (!configuration['dot-notation']) keys = [keys.join('.')] - - keys.slice(0, -1).forEach(function (key) { - o = (o[key] || {}) - }) - - const key = keys[keys.length - 1] - - if (typeof o !== 'object') return false - else return key in o - } - - function setKey (obj: { [key: string]: any }, keys: string[], value: any): void { - let o = obj - - if (!configuration['dot-notation']) keys = [keys.join('.')] - - keys.slice(0, -1).forEach(function (key) { - // TODO(bcoe): in the next major version of yargs, switch to - // Object.create(null) for dot notation: - key = sanitizeKey(key) - - if (typeof o === 'object' && o[key] === undefined) { - o[key] = {} - } - - if (typeof o[key] !== 'object' || Array.isArray(o[key])) { - // ensure that o[key] is an array, and that the last item is an empty object. - if (Array.isArray(o[key])) { - o[key].push({}) - } else { - o[key] = [o[key], {}] - } - - // we want to update the empty object at the end of the o[key] array, so set o to that object - o = o[key][o[key].length - 1] - } else { - o = o[key] - } - }) - - // TODO(bcoe): in the next major version of yargs, switch to - // Object.create(null) for dot notation: - const key = sanitizeKey(keys[keys.length - 1]) - - const isTypeArray = checkAllAliases(keys.join('.'), flags.arrays) - const isValueArray = Array.isArray(value) - let duplicate = configuration['duplicate-arguments-array'] - - // nargs has higher priority than duplicate - if (!duplicate && checkAllAliases(key, flags.nargs)) { - duplicate = true - if ((!isUndefined(o[key]) && flags.nargs[key] === 1) || (Array.isArray(o[key]) && o[key].length === flags.nargs[key])) { - o[key] = undefined - } - } - - if (value === increment()) { - o[key] = increment(o[key]) - } else if (Array.isArray(o[key])) { - if (duplicate && isTypeArray && isValueArray) { - o[key] = configuration['flatten-duplicate-arrays'] ? o[key].concat(value) : (Array.isArray(o[key][0]) ? o[key] : [o[key]]).concat([value]) - } else if (!duplicate && Boolean(isTypeArray) === Boolean(isValueArray)) { - o[key] = value - } else { - o[key] = o[key].concat([value]) - } - } else if (o[key] === undefined && isTypeArray) { - o[key] = isValueArray ? value : [value] - } else if (duplicate && !( - o[key] === undefined || - checkAllAliases(key, flags.counts) || - checkAllAliases(key, flags.bools) - )) { - o[key] = [o[key], value] - } else { - o[key] = value - } - } - - // extend the aliases list with inferred aliases. - function extendAliases (...args: Array<{ [key: string]: any } | undefined>) { - args.forEach(function (obj) { - Object.keys(obj || {}).forEach(function (key) { - // short-circuit if we've already added a key - // to the aliases array, for example it might - // exist in both 'opts.default' and 'opts.key'. - if (flags.aliases[key]) return - - flags.aliases[key] = ([] as string[]).concat(aliases[key] || []) - // For "--option-name", also set argv.optionName - flags.aliases[key].concat(key).forEach(function (x) { - if (/-/.test(x) && configuration['camel-case-expansion']) { - const c = camelCase(x) - if (c !== key && flags.aliases[key].indexOf(c) === -1) { - flags.aliases[key].push(c) - newAliases[c] = true - } - } - }) - // For "--optionName", also set argv['option-name'] - flags.aliases[key].concat(key).forEach(function (x) { - if (x.length > 1 && /[A-Z]/.test(x) && configuration['camel-case-expansion']) { - const c = decamelize(x, '-') - if (c !== key && flags.aliases[key].indexOf(c) === -1) { - flags.aliases[key].push(c) - newAliases[c] = true - } - } - }) - flags.aliases[key].forEach(function (x) { - flags.aliases[x] = [key].concat(flags.aliases[key].filter(function (y) { - return x !== y - })) - }) - }) - }) - } - - // return the 1st set flag for any of a key's aliases (or false if no flag set) - function checkAllAliases (key: string, flag: StringFlag): ValueOf | false - function checkAllAliases (key: string, flag: BooleanFlag): ValueOf | false - function checkAllAliases (key: string, flag: NumberFlag): ValueOf | false - function checkAllAliases (key: string, flag: ConfigsFlag): ValueOf | false - function checkAllAliases (key: string, flag: CoercionsFlag): ValueOf | false - function checkAllAliases (key: string, flag: Flag): ValueOf | false { - const toCheck = ([] as string[]).concat(flags.aliases[key] || [], key) - const keys = Object.keys(flag) - const setAlias = toCheck.find(key => keys.includes(key)) - return setAlias ? flag[setAlias] : false - } - - function hasAnyFlag (key: string): boolean { - const flagsKeys = Object.keys(flags) as FlagsKey[] - const toCheck = ([] as Array<{ [key: string]: any } | string[]>).concat(flagsKeys.map(k => flags[k])) - return toCheck.some(function (flag) { - return Array.isArray(flag) ? flag.includes(key) : flag[key] - }) - } - - function hasFlagsMatching (arg: string, ...patterns: RegExp[]): boolean { - const toCheck = ([] as RegExp[]).concat(...patterns) - return toCheck.some(function (pattern) { - const match = arg.match(pattern) - return match && hasAnyFlag(match[1]) - }) - } - - // based on a simplified version of the short flag group parsing logic - function hasAllShortFlags (arg: string): boolean { - // if this is a negative number, or doesn't start with a single hyphen, it's not a short flag group - if (arg.match(negative) || !arg.match(/^-[^-]+/)) { return false } - let hasAllFlags = true - let next: string - const letters = arg.slice(1).split('') - for (let j = 0; j < letters.length; j++) { - next = arg.slice(j + 2) - - if (!hasAnyFlag(letters[j])) { - hasAllFlags = false - break - } - - if ((letters[j + 1] && letters[j + 1] === '=') || - next === '-' || - (/[A-Za-z]/.test(letters[j]) && /^-?\d+(\.\d*)?(e-?\d+)?$/.test(next)) || - (letters[j + 1] && letters[j + 1].match(/\W/))) { - break - } - } - return hasAllFlags - } - - function isUnknownOptionAsArg (arg: string): boolean { - return configuration['unknown-options-as-args'] && isUnknownOption(arg) - } - - function isUnknownOption (arg: string): boolean { - // ignore negative numbers - if (arg.match(negative)) { return false } - // if this is a short option group and all of them are configured, it isn't unknown - if (hasAllShortFlags(arg)) { return false } - // e.g. '--count=2' - const flagWithEquals = /^-+([^=]+?)=[\s\S]*$/ - // e.g. '-a' or '--arg' - const normalFlag = /^-+([^=]+?)$/ - // e.g. '-a-' - const flagEndingInHyphen = /^-+([^=]+?)-$/ - // e.g. '-abc123' - const flagEndingInDigits = /^-+([^=]+?\d+)$/ - // e.g. '-a/usr/local' - const flagEndingInNonWordCharacters = /^-+([^=]+?)\W+.*$/ - // check the different types of flag styles, including negatedBoolean, a pattern defined near the start of the parse method - return !hasFlagsMatching(arg, flagWithEquals, negatedBoolean, normalFlag, flagEndingInHyphen, flagEndingInDigits, flagEndingInNonWordCharacters) - } - - // make a best effort to pick a default value - // for an option based on name and type. - function defaultValue (key: string) { - if (!checkAllAliases(key, flags.bools) && - !checkAllAliases(key, flags.counts) && - `${key}` in defaults) { - return defaults[key] - } else { - return defaultForType(guessType(key)) - } - } - - // return a default value, given the type of a flag., - function defaultForType (type: K): DefaultValuesForType[K] { - const def: DefaultValuesForType = { - boolean: true, - string: '', - number: undefined, - array: [] - } - - return def[type] - } - - // given a flag, enforce a default type. - function guessType (key: string): DefaultValuesForTypeKey { - let type: DefaultValuesForTypeKey = 'boolean' - if (checkAllAliases(key, flags.strings)) type = 'string' - else if (checkAllAliases(key, flags.numbers)) type = 'number' - else if (checkAllAliases(key, flags.bools)) type = 'boolean' - else if (checkAllAliases(key, flags.arrays)) type = 'array' - return type - } - - function isNumber (x: null | undefined | number | string): boolean { - if (x === null || x === undefined) return false - // if loaded from config, may already be a number. - if (typeof x === 'number') return true - // hexadecimal. - if (/^0x[0-9a-f]+$/i.test(x)) return true - // don't treat 0123 as a number; as it drops the leading '0'. - if (x.length > 1 && x[0] === '0') return false - return /^[-]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(x) - } - - function isUndefined (num: any): num is undefined { - return num === undefined - } - - // check user configuration settings for inconsistencies - function checkConfiguration (): void { - // count keys should not be set as array/narg - Object.keys(flags.counts).find(key => { - if (checkAllAliases(key, flags.arrays)) { - error = Error(__('Invalid configuration: %s, opts.count excludes opts.array.', key)) - return true - } else if (checkAllAliases(key, flags.nargs)) { - error = Error(__('Invalid configuration: %s, opts.count excludes opts.narg.', key)) - return true - } - return false - }) - } - - return { - argv: Object.assign(argvReturn, argv), - error: error, - aliases: Object.assign({}, flags.aliases), - newAliases: Object.assign({}, newAliases), - defaulted: Object.assign({}, defaulted), - configuration: configuration - } -} - -// if any aliases reference each other, we should -// merge them together. -function combineAliases (aliases: Dictionary): Dictionary { - const aliasArrays: Array = [] - const combined: Dictionary = Object.create(null) - let change = true - - // turn alias lookup hash {key: ['alias1', 'alias2']} into - // a simple array ['key', 'alias1', 'alias2'] - Object.keys(aliases).forEach(function (key) { - aliasArrays.push( - ([] as string[]).concat(aliases[key], key) - ) - }) - - // combine arrays until zero changes are - // made in an iteration. - while (change) { - change = false - for (let i = 0; i < aliasArrays.length; i++) { - for (let ii = i + 1; ii < aliasArrays.length; ii++) { - const intersect = aliasArrays[i].filter(function (v) { - return aliasArrays[ii].indexOf(v) !== -1 - }) - - if (intersect.length) { - aliasArrays[i] = aliasArrays[i].concat(aliasArrays[ii]) - aliasArrays.splice(ii, 1) - change = true - break - } - } - } - } - - // map arrays back to the hash-lookup (de-dupe while - // we're at it). - aliasArrays.forEach(function (aliasArray) { - aliasArray = aliasArray.filter(function (v, i, self) { - return self.indexOf(v) === i - }) - const lastAlias = aliasArray.pop() - if (lastAlias !== undefined && typeof lastAlias === 'string') { - combined[lastAlias] = aliasArray - } - }) - - return combined -} - -// this function should only be called when a count is given as an arg -// it is NOT called to set a default value -// thus we can start the count at 1 instead of 0 -function increment (orig?: number | undefined): number { - return orig !== undefined ? orig + 1 : 1 -} - -// TODO(bcoe): in the next major version of yargs, switch to -// Object.create(null) for dot notation: -function sanitizeKey (key: string): string { - if (key === '__proto__') return '___proto___' - return key -} - -const yargsParser: Parser = function Parser (args: ArgsInput, opts?: Partial): Arguments { - const result = parse(args.slice(), opts) - return result.argv -} - -// parse arguments and return detailed -// meta information, aliases, etc. -yargsParser.detailed = function (args: ArgsInput, opts?: Partial): DetailedArguments { - return parse(args.slice(), opts) -} - -export = yargsParser diff --git a/lib/index.ts b/lib/index.ts new file mode 100644 index 00000000..d903020d --- /dev/null +++ b/lib/index.ts @@ -0,0 +1,49 @@ +// Main entrypoint for libraries using yargs-parser in Node.js +// CJS and ESM environments: +import { format } from 'util' +import { readFileSync } from 'fs' +import { normalize, resolve } from 'path' +import { ArgsInput, Arguments, Parser, Options, DetailedArguments } from './yargs-parser-types.js' +import { YargsParser } from './yargs-parser.js' + +// See https://github.com/yargs/yargs-parser#supported-nodejs-versions for our +// version support policy. The YARGS_MIN_NODE_VERSION is used for testing only. +const minNodeVersion = (process && process.env && process.env.YARGS_MIN_NODE_VERSION) + ? Number(process.env.YARGS_MIN_NODE_VERSION) : 10 +if (process && process.version) { + const major = Number(process.version.match(/v([^.]+)/)![1]) + if (major < minNodeVersion) { + throw Error(`yargs parser supports a minimum Node.js version of ${minNodeVersion}. Read our version support policy: https://github.com/yargs/yargs-parser#supported-nodejs-versions`) + } +} + +// Creates a yargs-parser instance using Node.js standard libraries: +const env = process ? process.env as { [key: string]: string } : {} +const parser = new YargsParser({ + cwd: process.cwd, + env: () => { + return env + }, + format, + normalize, + resolve, + // TODO: figure out a way to combine ESM and CJS coverage, such that + // we can exercise all the lines below: + require: (path: string) => { + if (typeof require !== 'undefined') { + return require(path) + } else if (path.match(/\.json$/)) { + return readFileSync(path, 'utf8') + } else { + throw Error('only .json config files are supported in ESM') + } + } +}) +const yargsParser: Parser = function Parser (args: ArgsInput, opts?: Partial): Arguments { + const result = parser.parse(args.slice(), opts) + return result.argv +} +yargsParser.detailed = function (args: ArgsInput, opts?: Partial): DetailedArguments { + return parser.parse(args.slice(), opts) +} +export default yargsParser diff --git a/lib/string-utils.ts b/lib/string-utils.ts new file mode 100644 index 00000000..214dd82c --- /dev/null +++ b/lib/string-utils.ts @@ -0,0 +1,40 @@ +export function camelCase (str: string): string { + str = str.toLocaleLowerCase() + if (str.indexOf('-') === -1 && str.indexOf('_') === -1) { + return str + } else { + let camelcase = '' + let nextChrUpper = false + const leadingHyphens = str.match(/^-+/) + for (let i = leadingHyphens ? leadingHyphens[0].length : 0; i < str.length; i++) { + let chr = str.charAt(i) + if (nextChrUpper) { + nextChrUpper = false + chr = chr.toLocaleUpperCase() + } + if (i !== 0 && (chr === '-' || chr === '_')) { + nextChrUpper = true + continue + } else if (chr !== '-' && chr !== '_') { + camelcase += chr + } + } + return camelcase + } +} + +export function decamelize (str: string, joinString?: string): string { + const lowercase = str.toLocaleLowerCase() + joinString = joinString || '-' + let notCamelcase = '' + for (let i = 0; i < str.length; i++) { + const chrLower = lowercase.charAt(i) + const chrString = str.charAt(i) + if (chrLower !== chrString) { + notCamelcase += `${joinString}${lowercase.charAt(i)}` + } else { + notCamelcase += chrString + } + } + return notCamelcase +} diff --git a/lib/yargs-parser-types.ts b/lib/yargs-parser-types.ts index 9d4b38b8..35136277 100644 --- a/lib/yargs-parser-types.ts +++ b/lib/yargs-parser-types.ts @@ -1,4 +1,4 @@ -import type { Dictionary, KeyOf, ValueOf } from './common-types' +import type { Dictionary, KeyOf, ValueOf } from './common-types.js' export type ArgsInput = string | any[]; @@ -113,6 +113,15 @@ export interface Options { key: Dictionary; } +export interface YargsParserMixin { + cwd: Function; + format: Function; + normalize: Function; + require: Function; + resolve: Function; + env: Function; +} + export type OptionsDefault = ValueOf, 'default'>>; export interface Parser { diff --git a/lib/yargs-parser.ts b/lib/yargs-parser.ts new file mode 100644 index 00000000..90078178 --- /dev/null +++ b/lib/yargs-parser.ts @@ -0,0 +1,1103 @@ +import { tokenizeArgString } from './tokenize-arg-string.js' +import type { + ArgsInput, + Arguments, + ArrayFlagsKey, + ArrayOption, + CoerceCallback, + Configuration, + DefaultValuesForType, + DefaultValuesForTypeKey, + DetailedArguments, + Flag, + Flags, + FlagsKey, + StringFlag, + BooleanFlag, + NumberFlag, + ConfigsFlag, + CoercionsFlag, + Options, + OptionsDefault, + YargsParserMixin +} from './yargs-parser-types.js' +import type { Dictionary, ValueOf } from './common-types.js' +import { camelCase, decamelize } from './string-utils.js' + +let mixin: YargsParserMixin +export class YargsParser { + constructor (_mixin: YargsParserMixin) { + mixin = _mixin + } + + parse (argsInput: ArgsInput, options?: Partial): DetailedArguments { + const opts: Partial = Object.assign({ + alias: undefined, + array: undefined, + boolean: undefined, + config: undefined, + configObjects: undefined, + configuration: undefined, + coerce: undefined, + count: undefined, + default: undefined, + envPrefix: undefined, + narg: undefined, + normalize: undefined, + string: undefined, + number: undefined, + __: undefined, + key: undefined + }, options) + // allow a string argument to be passed in rather + // than an argv array. + const args = tokenizeArgString(argsInput) + + // aliases might have transitive relationships, normalize this. + const aliases = combineAliases(Object.assign(Object.create(null), opts.alias)) + const configuration: Configuration = Object.assign({ + 'boolean-negation': true, + 'camel-case-expansion': true, + 'combine-arrays': false, + 'dot-notation': true, + 'duplicate-arguments-array': true, + 'flatten-duplicate-arrays': true, + 'greedy-arrays': true, + 'halt-at-non-option': false, + 'nargs-eats-options': false, + 'negation-prefix': 'no-', + 'parse-numbers': true, + 'populate--': false, + 'set-placeholder-key': false, + 'short-option-groups': true, + 'strip-aliased': false, + 'strip-dashed': false, + 'unknown-options-as-args': false + }, opts.configuration) + const defaults: OptionsDefault = Object.assign(Object.create(null), opts.default) + const configObjects = opts.configObjects || [] + const envPrefix = opts.envPrefix + const notFlagsOption = configuration['populate--'] + const notFlagsArgv: string = notFlagsOption ? '--' : '_' + const newAliases: Dictionary = Object.create(null) + const defaulted: Dictionary = Object.create(null) + // allow a i18n handler to be passed in, default to a fake one (util.format). + const __ = opts.__ || mixin.format + const flags: Flags = { + aliases: Object.create(null), + arrays: Object.create(null), + bools: Object.create(null), + strings: Object.create(null), + numbers: Object.create(null), + counts: Object.create(null), + normalize: Object.create(null), + configs: Object.create(null), + nargs: Object.create(null), + coercions: Object.create(null), + keys: [] + } + const negative = /^-([0-9]+(\.[0-9]+)?|\.[0-9]+)$/ + const negatedBoolean = new RegExp('^--' + configuration['negation-prefix'] + '(.+)') + + ;([] as ArrayOption[]).concat(opts.array || []).filter(Boolean).forEach(function (opt) { + const key = typeof opt === 'object' ? opt.key : opt + + // assign to flags[bools|strings|numbers] + const assignment: ArrayFlagsKey | undefined = Object.keys(opt).map(function (key) { + const arrayFlagKeys: Record = { + boolean: 'bools', + string: 'strings', + number: 'numbers' + } + return arrayFlagKeys[key] + }).filter(Boolean).pop() + + // assign key to be coerced + if (assignment) { + flags[assignment][key] = true + } + + flags.arrays[key] = true + flags.keys.push(key) + }) + + ;([] as string[]).concat(opts.boolean || []).filter(Boolean).forEach(function (key) { + flags.bools[key] = true + flags.keys.push(key) + }) + + ;([] as string[]).concat(opts.string || []).filter(Boolean).forEach(function (key) { + flags.strings[key] = true + flags.keys.push(key) + }) + + ;([] as string[]).concat(opts.number || []).filter(Boolean).forEach(function (key) { + flags.numbers[key] = true + flags.keys.push(key) + }) + + ;([] as string[]).concat(opts.count || []).filter(Boolean).forEach(function (key) { + flags.counts[key] = true + flags.keys.push(key) + }) + + ;([] as string[]).concat(opts.normalize || []).filter(Boolean).forEach(function (key) { + flags.normalize[key] = true + flags.keys.push(key) + }) + + if (typeof opts.narg === 'object') { + Object.entries(opts.narg).forEach(([key, value]) => { + if (typeof value === 'number') { + flags.nargs[key] = value + flags.keys.push(key) + } + }) + } + + if (typeof opts.coerce === 'object') { + Object.entries(opts.coerce).forEach(([key, value]) => { + if (typeof value === 'function') { + flags.coercions[key] = value + flags.keys.push(key) + } + }) + } + + if (typeof opts.config !== 'undefined') { + if (Array.isArray(opts.config) || typeof opts.config === 'string') { + ;([] as string[]).concat(opts.config).filter(Boolean).forEach(function (key) { + flags.configs[key] = true + }) + } else if (typeof opts.config === 'object') { + Object.entries(opts.config).forEach(([key, value]) => { + if (typeof value === 'boolean' || typeof value === 'function') { + flags.configs[key] = value + } + }) + } + } + + // create a lookup table that takes into account all + // combinations of aliases: {f: ['foo'], foo: ['f']} + extendAliases(opts.key, aliases, opts.default, flags.arrays) + + // apply default values to all aliases. + Object.keys(defaults).forEach(function (key) { + (flags.aliases[key] || []).forEach(function (alias) { + defaults[alias] = defaults[key] + }) + }) + + let error: Error | null = null + checkConfiguration() + + let notFlags: string[] = [] + + const argv: Arguments = Object.assign(Object.create(null), { _: [] }) + // TODO(bcoe): for the first pass at removing object prototype we didn't + // remove all prototypes from objects returned by this API, we might want + // to gradually move towards doing so. + const argvReturn: { [argName: string]: any } = {} + + for (let i = 0; i < args.length; i++) { + const arg = args[i] + let broken: boolean + let key: string | undefined + let letters: string[] + let m: RegExpMatchArray | null + let next: string + let value: string + + // any unknown option (except for end-of-options, "--") + if (arg !== '--' && isUnknownOptionAsArg(arg)) { + argv._.push(arg) + // -- separated by = + } else if (arg.match(/^--.+=/) || ( + !configuration['short-option-groups'] && arg.match(/^-.+=/) + )) { + // Using [\s\S] instead of . because js doesn't support the + // 'dotall' regex modifier. See: + // http://stackoverflow.com/a/1068308/13216 + m = arg.match(/^--?([^=]+)=([\s\S]*)$/) + + // arrays format = '--f=a b c' + if (m !== null && Array.isArray(m) && m.length >= 3) { + if (checkAllAliases(m[1], flags.arrays)) { + i = eatArray(i, m[1], args, m[2]) + } else if (checkAllAliases(m[1], flags.nargs) !== false) { + // nargs format = '--f=monkey washing cat' + i = eatNargs(i, m[1], args, m[2]) + } else { + setArg(m[1], m[2]) + } + } + } else if (arg.match(negatedBoolean) && configuration['boolean-negation']) { + m = arg.match(negatedBoolean) + if (m !== null && Array.isArray(m) && m.length >= 2) { + key = m[1] + setArg(key, checkAllAliases(key, flags.arrays) ? [false] : false) + } + + // -- separated by space. + } else if (arg.match(/^--.+/) || ( + !configuration['short-option-groups'] && arg.match(/^-[^-]+/) + )) { + m = arg.match(/^--?(.+)/) + if (m !== null && Array.isArray(m) && m.length >= 2) { + key = m[1] + if (checkAllAliases(key, flags.arrays)) { + // array format = '--foo a b c' + i = eatArray(i, key, args) + } else if (checkAllAliases(key, flags.nargs) !== false) { + // nargs format = '--foo a b c' + // should be truthy even if: flags.nargs[key] === 0 + i = eatNargs(i, key, args) + } else { + next = args[i + 1] + + if (next !== undefined && (!next.match(/^-/) || + next.match(negative)) && + !checkAllAliases(key, flags.bools) && + !checkAllAliases(key, flags.counts)) { + setArg(key, next) + i++ + } else if (/^(true|false)$/.test(next)) { + setArg(key, next) + i++ + } else { + setArg(key, defaultValue(key)) + } + } + } + + // dot-notation flag separated by '='. + } else if (arg.match(/^-.\..+=/)) { + m = arg.match(/^-([^=]+)=([\s\S]*)$/) + if (m !== null && Array.isArray(m) && m.length >= 3) { + setArg(m[1], m[2]) + } + + // dot-notation flag separated by space. + } else if (arg.match(/^-.\..+/) && !arg.match(negative)) { + next = args[i + 1] + m = arg.match(/^-(.\..+)/) + if (m !== null && Array.isArray(m) && m.length >= 2) { + key = m[1] + if (next !== undefined && !next.match(/^-/) && + !checkAllAliases(key, flags.bools) && + !checkAllAliases(key, flags.counts)) { + setArg(key, next) + i++ + } else { + setArg(key, defaultValue(key)) + } + } + } else if (arg.match(/^-[^-]+/) && !arg.match(negative)) { + letters = arg.slice(1, -1).split('') + broken = false + + for (let j = 0; j < letters.length; j++) { + next = arg.slice(j + 2) + + if (letters[j + 1] && letters[j + 1] === '=') { + value = arg.slice(j + 3) + key = letters[j] + + if (checkAllAliases(key, flags.arrays)) { + // array format = '-f=a b c' + i = eatArray(i, key, args, value) + } else if (checkAllAliases(key, flags.nargs) !== false) { + // nargs format = '-f=monkey washing cat' + i = eatNargs(i, key, args, value) + } else { + setArg(key, value) + } + + broken = true + break + } + + if (next === '-') { + setArg(letters[j], next) + continue + } + + // current letter is an alphabetic character and next value is a number + if (/[A-Za-z]/.test(letters[j]) && + /^-?\d+(\.\d*)?(e-?\d+)?$/.test(next) && + checkAllAliases(next, flags.bools) === false) { + setArg(letters[j], next) + broken = true + break + } + + if (letters[j + 1] && letters[j + 1].match(/\W/)) { + setArg(letters[j], next) + broken = true + break + } else { + setArg(letters[j], defaultValue(letters[j])) + } + } + + key = arg.slice(-1)[0] + + if (!broken && key !== '-') { + if (checkAllAliases(key, flags.arrays)) { + // array format = '-f a b c' + i = eatArray(i, key, args) + } else if (checkAllAliases(key, flags.nargs) !== false) { + // nargs format = '-f a b c' + // should be truthy even if: flags.nargs[key] === 0 + i = eatNargs(i, key, args) + } else { + next = args[i + 1] + + if (next !== undefined && (!/^(-|--)[^-]/.test(next) || + next.match(negative)) && + !checkAllAliases(key, flags.bools) && + !checkAllAliases(key, flags.counts)) { + setArg(key, next) + i++ + } else if (/^(true|false)$/.test(next)) { + setArg(key, next) + i++ + } else { + setArg(key, defaultValue(key)) + } + } + } + } else if (arg.match(/^-[0-9]$/) && + arg.match(negative) && + checkAllAliases(arg.slice(1), flags.bools)) { + // single-digit boolean alias, e.g: xargs -0 + key = arg.slice(1) + setArg(key, defaultValue(key)) + } else if (arg === '--') { + notFlags = args.slice(i + 1) + break + } else if (configuration['halt-at-non-option']) { + notFlags = args.slice(i) + break + } else { + const maybeCoercedNumber = maybeCoerceNumber('_', arg) + if (typeof maybeCoercedNumber === 'string' || typeof maybeCoercedNumber === 'number') { + argv._.push(maybeCoercedNumber) + } + } + } + + // order of precedence: + // 1. command line arg + // 2. value from env var + // 3. value from config file + // 4. value from config objects + // 5. configured default value + applyEnvVars(argv, true) // special case: check env vars that point to config file + applyEnvVars(argv, false) + setConfig(argv) + setConfigObjects() + applyDefaultsAndAliases(argv, flags.aliases, defaults, true) + applyCoercions(argv) + if (configuration['set-placeholder-key']) setPlaceholderKeys(argv) + + // for any counts either not in args or without an explicit default, set to 0 + Object.keys(flags.counts).forEach(function (key) { + if (!hasKey(argv, key.split('.'))) setArg(key, 0) + }) + + // '--' defaults to undefined. + if (notFlagsOption && notFlags.length) argv[notFlagsArgv] = [] + notFlags.forEach(function (key) { + argv[notFlagsArgv].push(key) + }) + + if (configuration['camel-case-expansion'] && configuration['strip-dashed']) { + Object.keys(argv).filter(key => key !== '--' && key.includes('-')).forEach(key => { + delete argv[key] + }) + } + + if (configuration['strip-aliased']) { + ;([] as string[]).concat(...Object.keys(aliases).map(k => aliases[k])).forEach(alias => { + if (configuration['camel-case-expansion']) { + delete argv[alias.split('.').map(prop => camelCase(prop)).join('.')] + } + + delete argv[alias] + }) + } + + // how many arguments should we consume, based + // on the nargs option? + function eatNargs (i: number, key: string, args: string[], argAfterEqualSign?: string): number { + let ii + let toEat = checkAllAliases(key, flags.nargs) + // NaN has a special meaning for the array type, indicating that one or + // more values are expected. + toEat = typeof toEat !== 'number' || isNaN(toEat) ? 1 : toEat + + if (toEat === 0) { + if (!isUndefined(argAfterEqualSign)) { + error = Error(__('Argument unexpected for: %s', key)) + } + setArg(key, defaultValue(key)) + return i + } + + let available = isUndefined(argAfterEqualSign) ? 0 : 1 + if (configuration['nargs-eats-options']) { + // classic behavior, yargs eats positional and dash arguments. + if (args.length - (i + 1) + available < toEat) { + error = Error(__('Not enough arguments following: %s', key)) + } + available = toEat + } else { + // nargs will not consume flag arguments, e.g., -abc, --foo, + // and terminates when one is observed. + for (ii = i + 1; ii < args.length; ii++) { + if (!args[ii].match(/^-[^0-9]/) || args[ii].match(negative) || isUnknownOptionAsArg(args[ii])) available++ + else break + } + if (available < toEat) error = Error(__('Not enough arguments following: %s', key)) + } + + let consumed = Math.min(available, toEat) + if (!isUndefined(argAfterEqualSign) && consumed > 0) { + setArg(key, argAfterEqualSign) + consumed-- + } + for (ii = i + 1; ii < (consumed + i + 1); ii++) { + setArg(key, args[ii]) + } + + return (i + consumed) + } + + // if an option is an array, eat all non-hyphenated arguments + // following it... YUM! + // e.g., --foo apple banana cat becomes ["apple", "banana", "cat"] + function eatArray (i: number, key: string, args: string[], argAfterEqualSign?: string): number { + let argsToSet = [] + let next = argAfterEqualSign || args[i + 1] + // If both array and nargs are configured, enforce the nargs count: + const nargsCount = checkAllAliases(key, flags.nargs) + + if (checkAllAliases(key, flags.bools) && !(/^(true|false)$/.test(next))) { + argsToSet.push(true) + } else if (isUndefined(next) || + (isUndefined(argAfterEqualSign) && /^-/.test(next) && !negative.test(next) && !isUnknownOptionAsArg(next))) { + // for keys without value ==> argsToSet remains an empty [] + // set user default value, if available + if (defaults[key] !== undefined) { + const defVal = defaults[key] + argsToSet = Array.isArray(defVal) ? defVal : [defVal] + } + } else { + // value in --option=value is eaten as is + if (!isUndefined(argAfterEqualSign)) { + argsToSet.push(processValue(key, argAfterEqualSign)) + } + for (let ii = i + 1; ii < args.length; ii++) { + if ((!configuration['greedy-arrays'] && argsToSet.length > 0) || + (nargsCount && typeof nargsCount === 'number' && argsToSet.length >= nargsCount)) break + next = args[ii] + if (/^-/.test(next) && !negative.test(next) && !isUnknownOptionAsArg(next)) break + i = ii + argsToSet.push(processValue(key, next)) + } + } + + // If both array and nargs are configured, create an error if less than + // nargs positionals were found. NaN has special meaning, indicating + // that at least one value is required (more are okay). + if (typeof nargsCount === 'number' && ((nargsCount && argsToSet.length < nargsCount) || + (isNaN(nargsCount) && argsToSet.length === 0))) { + error = Error(__('Not enough arguments following: %s', key)) + } + + setArg(key, argsToSet) + return i + } + + function setArg (key: string, val: any): void { + if (/-/.test(key) && configuration['camel-case-expansion']) { + const alias = key.split('.').map(function (prop) { + return camelCase(prop) + }).join('.') + addNewAlias(key, alias) + } + + const value = processValue(key, val) + const splitKey = key.split('.') + setKey(argv, splitKey, value) + + // handle populating aliases of the full key + if (flags.aliases[key]) { + flags.aliases[key].forEach(function (x) { + const keyProperties = x.split('.') + setKey(argv, keyProperties, value) + }) + } + + // handle populating aliases of the first element of the dot-notation key + if (splitKey.length > 1 && configuration['dot-notation']) { + ;(flags.aliases[splitKey[0]] || []).forEach(function (x) { + let keyProperties = x.split('.') + + // expand alias with nested objects in key + const a = ([] as string[]).concat(splitKey) + a.shift() // nuke the old key. + keyProperties = keyProperties.concat(a) + + // populate alias only if is not already an alias of the full key + // (already populated above) + if (!(flags.aliases[key] || []).includes(keyProperties.join('.'))) { + setKey(argv, keyProperties, value) + } + }) + } + + // Set normalize getter and setter when key is in 'normalize' but isn't an array + if (checkAllAliases(key, flags.normalize) && !checkAllAliases(key, flags.arrays)) { + const keys = [key].concat(flags.aliases[key] || []) + keys.forEach(function (key) { + Object.defineProperty(argvReturn, key, { + enumerable: true, + get () { + return val + }, + set (value) { + val = typeof value === 'string' ? mixin.normalize(value) : value + } + }) + }) + } + } + + function addNewAlias (key: string, alias: string): void { + if (!(flags.aliases[key] && flags.aliases[key].length)) { + flags.aliases[key] = [alias] + newAliases[alias] = true + } + if (!(flags.aliases[alias] && flags.aliases[alias].length)) { + addNewAlias(alias, key) + } + } + + function processValue (key: string, val: any) { + // strings may be quoted, clean this up as we assign values. + if (typeof val === 'string' && + (val[0] === "'" || val[0] === '"') && + val[val.length - 1] === val[0] + ) { + val = val.substring(1, val.length - 1) + } + + // handle parsing boolean arguments --foo=true --bar false. + if (checkAllAliases(key, flags.bools) || checkAllAliases(key, flags.counts)) { + if (typeof val === 'string') val = val === 'true' + } + + let value = Array.isArray(val) + ? val.map(function (v) { return maybeCoerceNumber(key, v) }) + : maybeCoerceNumber(key, val) + + // increment a count given as arg (either no value or value parsed as boolean) + if (checkAllAliases(key, flags.counts) && (isUndefined(value) || typeof value === 'boolean')) { + value = increment() + } + + // Set normalized value when key is in 'normalize' and in 'arrays' + if (checkAllAliases(key, flags.normalize) && checkAllAliases(key, flags.arrays)) { + if (Array.isArray(val)) value = val.map((val) => { return mixin.normalize(val) }) + else value = mixin.normalize(val) + } + return value + } + + function maybeCoerceNumber (key: string, value: string | number | null | undefined) { + if (!checkAllAliases(key, flags.strings) && !checkAllAliases(key, flags.bools) && !Array.isArray(value)) { + const shouldCoerceNumber = isNumber(value) && configuration['parse-numbers'] && ( + Number.isSafeInteger(Math.floor(parseFloat(`${value}`))) + ) + if (shouldCoerceNumber || (!isUndefined(value) && checkAllAliases(key, flags.numbers))) value = Number(value) + } + return value + } + + // set args from config.json file, this should be + // applied last so that defaults can be applied. + function setConfig (argv: Arguments): void { + const configLookup = Object.create(null) + + // expand defaults/aliases, in-case any happen to reference + // the config.json file. + applyDefaultsAndAliases(configLookup, flags.aliases, defaults) + + Object.keys(flags.configs).forEach(function (configKey) { + const configPath = argv[configKey] || configLookup[configKey] + if (configPath) { + try { + let config = null + const resolvedConfigPath = mixin.resolve(mixin.cwd(), configPath) + const resolveConfig = flags.configs[configKey] + + if (typeof resolveConfig === 'function') { + try { + config = resolveConfig(resolvedConfigPath) + } catch (e) { + config = e + } + if (config instanceof Error) { + error = config + return + } + } else { + config = mixin.require(resolvedConfigPath) + } + + setConfigObject(config) + } catch (ex) { + if (argv[configKey]) error = Error(__('Invalid JSON config file: %s', configPath)) + } + } + }) + } + + // set args from config object. + // it recursively checks nested objects. + function setConfigObject (config: { [key: string]: any }, prev?: string): void { + Object.keys(config).forEach(function (key) { + const value = config[key] + const fullKey = prev ? prev + '.' + key : key + + // if the value is an inner object and we have dot-notation + // enabled, treat inner objects in config the same as + // heavily nested dot notations (foo.bar.apple). + if (typeof value === 'object' && value !== null && !Array.isArray(value) && configuration['dot-notation']) { + // if the value is an object but not an array, check nested object + setConfigObject(value, fullKey) + } else { + // setting arguments via CLI takes precedence over + // values within the config file. + if (!hasKey(argv, fullKey.split('.')) || (checkAllAliases(fullKey, flags.arrays) && configuration['combine-arrays'])) { + setArg(fullKey, value) + } + } + }) + } + + // set all config objects passed in opts + function setConfigObjects (): void { + if (typeof configObjects !== 'undefined') { + configObjects.forEach(function (configObject) { + setConfigObject(configObject) + }) + } + } + + function applyEnvVars (argv: Arguments, configOnly: boolean): void { + if (typeof envPrefix === 'undefined') return + + const prefix = typeof envPrefix === 'string' ? envPrefix : '' + const env = mixin.env() + Object.keys(env).forEach(function (envVar) { + if (prefix === '' || envVar.lastIndexOf(prefix, 0) === 0) { + // get array of nested keys and convert them to camel case + const keys = envVar.split('__').map(function (key, i) { + if (i === 0) { + key = key.substring(prefix.length) + } + return camelCase(key) + }) + + if (((configOnly && flags.configs[keys.join('.')]) || !configOnly) && !hasKey(argv, keys)) { + setArg(keys.join('.'), env[envVar]) + } + } + }) + } + + function applyCoercions (argv: Arguments): void { + let coerce: false | CoerceCallback + const applied: Set = new Set() + Object.keys(argv).forEach(function (key) { + if (!applied.has(key)) { // If we haven't already coerced this option via one of its aliases + coerce = checkAllAliases(key, flags.coercions) + if (typeof coerce === 'function') { + try { + const value = maybeCoerceNumber(key, coerce(argv[key])) + ;(([] as string[]).concat(flags.aliases[key] || [], key)).forEach(ali => { + applied.add(ali) + argv[ali] = value + }) + } catch (err) { + error = err + } + } + } + }) + } + + function setPlaceholderKeys (argv: Arguments): Arguments { + flags.keys.forEach((key) => { + // don't set placeholder keys for dot notation options 'foo.bar'. + if (~key.indexOf('.')) return + if (typeof argv[key] === 'undefined') argv[key] = undefined + }) + return argv + } + + function applyDefaultsAndAliases (obj: { [key: string]: any }, aliases: { [key: string]: string[] }, defaults: { [key: string]: any }, canLog: boolean = false): void { + Object.keys(defaults).forEach(function (key) { + if (!hasKey(obj, key.split('.'))) { + setKey(obj, key.split('.'), defaults[key]) + if (canLog) defaulted[key] = true + + ;(aliases[key] || []).forEach(function (x) { + if (hasKey(obj, x.split('.'))) return + setKey(obj, x.split('.'), defaults[key]) + }) + } + }) + } + + function hasKey (obj: { [key: string]: any }, keys: string[]): boolean { + let o = obj + + if (!configuration['dot-notation']) keys = [keys.join('.')] + + keys.slice(0, -1).forEach(function (key) { + o = (o[key] || {}) + }) + + const key = keys[keys.length - 1] + + if (typeof o !== 'object') return false + else return key in o + } + + function setKey (obj: { [key: string]: any }, keys: string[], value: any): void { + let o = obj + + if (!configuration['dot-notation']) keys = [keys.join('.')] + + keys.slice(0, -1).forEach(function (key) { + // TODO(bcoe): in the next major version of yargs, switch to + // Object.create(null) for dot notation: + key = sanitizeKey(key) + + if (typeof o === 'object' && o[key] === undefined) { + o[key] = {} + } + + if (typeof o[key] !== 'object' || Array.isArray(o[key])) { + // ensure that o[key] is an array, and that the last item is an empty object. + if (Array.isArray(o[key])) { + o[key].push({}) + } else { + o[key] = [o[key], {}] + } + + // we want to update the empty object at the end of the o[key] array, so set o to that object + o = o[key][o[key].length - 1] + } else { + o = o[key] + } + }) + + // TODO(bcoe): in the next major version of yargs, switch to + // Object.create(null) for dot notation: + const key = sanitizeKey(keys[keys.length - 1]) + + const isTypeArray = checkAllAliases(keys.join('.'), flags.arrays) + const isValueArray = Array.isArray(value) + let duplicate = configuration['duplicate-arguments-array'] + + // nargs has higher priority than duplicate + if (!duplicate && checkAllAliases(key, flags.nargs)) { + duplicate = true + if ((!isUndefined(o[key]) && flags.nargs[key] === 1) || (Array.isArray(o[key]) && o[key].length === flags.nargs[key])) { + o[key] = undefined + } + } + + if (value === increment()) { + o[key] = increment(o[key]) + } else if (Array.isArray(o[key])) { + if (duplicate && isTypeArray && isValueArray) { + o[key] = configuration['flatten-duplicate-arrays'] ? o[key].concat(value) : (Array.isArray(o[key][0]) ? o[key] : [o[key]]).concat([value]) + } else if (!duplicate && Boolean(isTypeArray) === Boolean(isValueArray)) { + o[key] = value + } else { + o[key] = o[key].concat([value]) + } + } else if (o[key] === undefined && isTypeArray) { + o[key] = isValueArray ? value : [value] + } else if (duplicate && !( + o[key] === undefined || + checkAllAliases(key, flags.counts) || + checkAllAliases(key, flags.bools) + )) { + o[key] = [o[key], value] + } else { + o[key] = value + } + } + + // extend the aliases list with inferred aliases. + function extendAliases (...args: Array<{ [key: string]: any } | undefined>) { + args.forEach(function (obj) { + Object.keys(obj || {}).forEach(function (key) { + // short-circuit if we've already added a key + // to the aliases array, for example it might + // exist in both 'opts.default' and 'opts.key'. + if (flags.aliases[key]) return + + flags.aliases[key] = ([] as string[]).concat(aliases[key] || []) + // For "--option-name", also set argv.optionName + flags.aliases[key].concat(key).forEach(function (x) { + if (/-/.test(x) && configuration['camel-case-expansion']) { + const c = camelCase(x) + if (c !== key && flags.aliases[key].indexOf(c) === -1) { + flags.aliases[key].push(c) + newAliases[c] = true + } + } + }) + // For "--optionName", also set argv['option-name'] + flags.aliases[key].concat(key).forEach(function (x) { + if (x.length > 1 && /[A-Z]/.test(x) && configuration['camel-case-expansion']) { + const c = decamelize(x, '-') + if (c !== key && flags.aliases[key].indexOf(c) === -1) { + flags.aliases[key].push(c) + newAliases[c] = true + } + } + }) + flags.aliases[key].forEach(function (x) { + flags.aliases[x] = [key].concat(flags.aliases[key].filter(function (y) { + return x !== y + })) + }) + }) + }) + } + + // return the 1st set flag for any of a key's aliases (or false if no flag set) + function checkAllAliases (key: string, flag: StringFlag): ValueOf | false + function checkAllAliases (key: string, flag: BooleanFlag): ValueOf | false + function checkAllAliases (key: string, flag: NumberFlag): ValueOf | false + function checkAllAliases (key: string, flag: ConfigsFlag): ValueOf | false + function checkAllAliases (key: string, flag: CoercionsFlag): ValueOf | false + function checkAllAliases (key: string, flag: Flag): ValueOf | false { + const toCheck = ([] as string[]).concat(flags.aliases[key] || [], key) + const keys = Object.keys(flag) + const setAlias = toCheck.find(key => keys.includes(key)) + return setAlias ? flag[setAlias] : false + } + + function hasAnyFlag (key: string): boolean { + const flagsKeys = Object.keys(flags) as FlagsKey[] + const toCheck = ([] as Array<{ [key: string]: any } | string[]>).concat(flagsKeys.map(k => flags[k])) + return toCheck.some(function (flag) { + return Array.isArray(flag) ? flag.includes(key) : flag[key] + }) + } + + function hasFlagsMatching (arg: string, ...patterns: RegExp[]): boolean { + const toCheck = ([] as RegExp[]).concat(...patterns) + return toCheck.some(function (pattern) { + const match = arg.match(pattern) + return match && hasAnyFlag(match[1]) + }) + } + + // based on a simplified version of the short flag group parsing logic + function hasAllShortFlags (arg: string): boolean { + // if this is a negative number, or doesn't start with a single hyphen, it's not a short flag group + if (arg.match(negative) || !arg.match(/^-[^-]+/)) { return false } + let hasAllFlags = true + let next: string + const letters = arg.slice(1).split('') + for (let j = 0; j < letters.length; j++) { + next = arg.slice(j + 2) + + if (!hasAnyFlag(letters[j])) { + hasAllFlags = false + break + } + + if ((letters[j + 1] && letters[j + 1] === '=') || + next === '-' || + (/[A-Za-z]/.test(letters[j]) && /^-?\d+(\.\d*)?(e-?\d+)?$/.test(next)) || + (letters[j + 1] && letters[j + 1].match(/\W/))) { + break + } + } + return hasAllFlags + } + + function isUnknownOptionAsArg (arg: string): boolean { + return configuration['unknown-options-as-args'] && isUnknownOption(arg) + } + + function isUnknownOption (arg: string): boolean { + // ignore negative numbers + if (arg.match(negative)) { return false } + // if this is a short option group and all of them are configured, it isn't unknown + if (hasAllShortFlags(arg)) { return false } + // e.g. '--count=2' + const flagWithEquals = /^-+([^=]+?)=[\s\S]*$/ + // e.g. '-a' or '--arg' + const normalFlag = /^-+([^=]+?)$/ + // e.g. '-a-' + const flagEndingInHyphen = /^-+([^=]+?)-$/ + // e.g. '-abc123' + const flagEndingInDigits = /^-+([^=]+?\d+)$/ + // e.g. '-a/usr/local' + const flagEndingInNonWordCharacters = /^-+([^=]+?)\W+.*$/ + // check the different types of flag styles, including negatedBoolean, a pattern defined near the start of the parse method + return !hasFlagsMatching(arg, flagWithEquals, negatedBoolean, normalFlag, flagEndingInHyphen, flagEndingInDigits, flagEndingInNonWordCharacters) + } + + // make a best effort to pick a default value + // for an option based on name and type. + function defaultValue (key: string) { + if (!checkAllAliases(key, flags.bools) && + !checkAllAliases(key, flags.counts) && + `${key}` in defaults) { + return defaults[key] + } else { + return defaultForType(guessType(key)) + } + } + + // return a default value, given the type of a flag., + function defaultForType (type: K): DefaultValuesForType[K] { + const def: DefaultValuesForType = { + boolean: true, + string: '', + number: undefined, + array: [] + } + + return def[type] + } + + // given a flag, enforce a default type. + function guessType (key: string): DefaultValuesForTypeKey { + let type: DefaultValuesForTypeKey = 'boolean' + if (checkAllAliases(key, flags.strings)) type = 'string' + else if (checkAllAliases(key, flags.numbers)) type = 'number' + else if (checkAllAliases(key, flags.bools)) type = 'boolean' + else if (checkAllAliases(key, flags.arrays)) type = 'array' + return type + } + + function isNumber (x: null | undefined | number | string): boolean { + if (x === null || x === undefined) return false + // if loaded from config, may already be a number. + if (typeof x === 'number') return true + // hexadecimal. + if (/^0x[0-9a-f]+$/i.test(x)) return true + // don't treat 0123 as a number; as it drops the leading '0'. + if (x.length > 1 && x[0] === '0') return false + return /^[-]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(x) + } + + function isUndefined (num: any): num is undefined { + return num === undefined + } + + // check user configuration settings for inconsistencies + function checkConfiguration (): void { + // count keys should not be set as array/narg + Object.keys(flags.counts).find(key => { + if (checkAllAliases(key, flags.arrays)) { + error = Error(__('Invalid configuration: %s, opts.count excludes opts.array.', key)) + return true + } else if (checkAllAliases(key, flags.nargs)) { + error = Error(__('Invalid configuration: %s, opts.count excludes opts.narg.', key)) + return true + } + return false + }) + } + + return { + argv: Object.assign(argvReturn, argv), + error: error, + aliases: Object.assign({}, flags.aliases), + newAliases: Object.assign({}, newAliases), + defaulted: Object.assign({}, defaulted), + configuration: configuration + } + } +} + +// if any aliases reference each other, we should +// merge them together. +function combineAliases (aliases: Dictionary): Dictionary { + const aliasArrays: Array = [] + const combined: Dictionary = Object.create(null) + let change = true + + // turn alias lookup hash {key: ['alias1', 'alias2']} into + // a simple array ['key', 'alias1', 'alias2'] + Object.keys(aliases).forEach(function (key) { + aliasArrays.push( + ([] as string[]).concat(aliases[key], key) + ) + }) + + // combine arrays until zero changes are + // made in an iteration. + while (change) { + change = false + for (let i = 0; i < aliasArrays.length; i++) { + for (let ii = i + 1; ii < aliasArrays.length; ii++) { + const intersect = aliasArrays[i].filter(function (v) { + return aliasArrays[ii].indexOf(v) !== -1 + }) + + if (intersect.length) { + aliasArrays[i] = aliasArrays[i].concat(aliasArrays[ii]) + aliasArrays.splice(ii, 1) + change = true + break + } + } + } + } + + // map arrays back to the hash-lookup (de-dupe while + // we're at it). + aliasArrays.forEach(function (aliasArray) { + aliasArray = aliasArray.filter(function (v, i, self) { + return self.indexOf(v) === i + }) + const lastAlias = aliasArray.pop() + if (lastAlias !== undefined && typeof lastAlias === 'string') { + combined[lastAlias] = aliasArray + } + }) + + return combined +} + +// this function should only be called when a count is given as an arg +// it is NOT called to set a default value +// thus we can start the count at 1 instead of 0 +function increment (orig?: number | undefined): number { + return orig !== undefined ? orig + 1 : 1 +} + +// TODO(bcoe): in the next major version of yargs, switch to +// Object.create(null) for dot notation: +function sanitizeKey (key: string): string { + if (key === '__proto__') return '___proto___' + return key +} diff --git a/package.json b/package.json index 7dd03ccc..96a25a60 100644 --- a/package.json +++ b/package.json @@ -2,15 +2,29 @@ "name": "yargs-parser", "version": "18.1.3", "description": "the mighty option parser used by yargs", - "main": "build/index.js", + "main": "build/index.cjs", + "exports": { + "import": "./build/lib/index.js", + "require": "./build/index.cjs" + }, + "type": "module", + "module": "./build/lib/index.js", + "types": "./build/index.cjs.d.ts", "scripts": { - "fix": "standardx --fix ./*.ts && standardx --fix **/*.ts", - "pretest": "npm run compile -- -p tsconfig.test.json", - "test": "c8 --reporter=text --reporter=html mocha test/*.js", + "check": "standardx '**/*.ts' && standardx '**/*.js' && standardx '**/*.cjs'", + "fix": "standardx --fix '**/*.ts' && standardx --fix '**/*.js' && standardx --fix '**/*.cjs'", + "pretest": "rimraf build && tsc -p tsconfig.test.json && cross-env NODE_ENV=test npm run build:cjs", + "test": "c8 --reporter=text --reporter=html mocha test/*.cjs", + "test:browser": "start-server-and-test 'serve ./ -p 8080' http://127.0.0.1:8080/package.json 'node ./test/browser/yargs-test.cjs'", + "pretest:typescript": "npm run pretest", + "test:typescript": "c8 mocha ./build/test/typescript/*.js", "posttest": "npm run check", "coverage": "c8 report --check-coverage", - "check": "standardx ./*.ts && standardx **/*.ts", - "compile": "rimraf build && tsc", + "precompile": "rimraf build", + "compile": "tsc", + "postcompile": "npm run build:cjs", + "build:cjs": "rollup -c", + "postbuild:cjs": "node scripts/replace-legacy-export.cjs", "prepare": "npm run compile" }, "repository": { @@ -36,33 +50,33 @@ "@types/node": "^10.0.3", "@typescript-eslint/eslint-plugin": "^3.0.0", "@typescript-eslint/parser": "^3.0.0", - "c8": "^7.1.2", + "@wessberg/rollup-plugin-ts": "^1.2.28", + "c8": "^7.3.0", "chai": "^4.2.0", + "cross-env": "^7.0.2", "eslint": "^7.0.0", "eslint-plugin-import": "^2.20.1", "eslint-plugin-node": "^11.0.0", "gts": "^2.0.0-alpha.4", "mocha": "^8.0.0", + "puppeteer": "^5.2.1", "rimraf": "^3.0.2", + "rollup": "^2.22.1", + "serve": "^11.3.2", "standardx": "^5.0.0", + "start-server-and-test": "^1.11.2", "typescript": "^3.7.0" }, - "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0" - }, "files": [ - "index.js", - "build", - "lib/**/*.js" + "browser.js", + "build" ], "engines": { "node": ">=10" }, "standardx": { "ignore": [ - "build", - "example.js" + "build" ] } } diff --git a/renovate.json b/renovate.json index 5ec138e4..f6dd45e1 100644 --- a/renovate.json +++ b/renovate.json @@ -4,6 +4,5 @@ ], "pinVersions": false, "rebaseStalePrs": true, - "gitAuthor": null, - "ignoreDeps": ["decamelize"] + "gitAuthor": null } diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 00000000..935ebda1 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,17 @@ +import ts from '@wessberg/rollup-plugin-ts' + +const output = { + format: 'cjs', + file: './build/index.cjs', + exports: 'default' +} + +if (process.env.NODE_ENV === 'test') output.sourcemap = true + +export default { + input: './lib/index.ts', + output, + plugins: [ + ts({ /* options */ }) + ] +} diff --git a/scripts/replace-legacy-export.cjs b/scripts/replace-legacy-export.cjs new file mode 100755 index 00000000..c4af84bd --- /dev/null +++ b/scripts/replace-legacy-export.cjs @@ -0,0 +1,11 @@ +#!/usr/bin/env node +'use strict' + +const { readFileSync, writeFileSync } = require('fs') + +// Cleanup the export statement in CJS typings file generated by rollup: +const legacyTypings = require('path').resolve(__dirname, '../build/index.cjs.d.ts') +const contents = readFileSync(legacyTypings, 'utf8').replace( + 'export { yargsParser as default };', 'export = yargsParser;' +) +writeFileSync(legacyTypings, contents, 'utf8') diff --git a/test/browser/yargs-test.cjs b/test/browser/yargs-test.cjs new file mode 100644 index 00000000..8be641d8 --- /dev/null +++ b/test/browser/yargs-test.cjs @@ -0,0 +1,55 @@ +const { deepStrictEqual } = require('assert') +const puppeteer = require('puppeteer') + +// Runs a browser window with a given argv string and options: +let browser +async function parse (argv, opts) { + if (!browser) { + browser = await puppeteer.launch() + } + const page = await browser.newPage() + opts = encodeURIComponent(JSON.stringify(opts)) + await page.goto(`http://127.0.0.1:8080/test/browser/yargs-test?argv=${encodeURIComponent(argv)}&opts=${opts}`) + const element = await page.$('#output') + return JSON.parse(await page.evaluate(element => element.textContent, element)) +} + +// The actual tests: +async function tests () { + { + const output = await parse('--hello world --x 102') + deepStrictEqual(output, { + _: [], + hello: 'world', + x: 102 + }) + console.info('✅ parse simple string') + } + + { + const output = await parse('--hello world --x 102', { + alias: { + hello: ['goodbye'], + x: ['example'] + } + }) + deepStrictEqual(output, { + _: [], + hello: 'world', + x: 102, + example: 102, + goodbye: 'world' + }) + console.info('✅ parse with aliases') + } +} + +tests().then(() => { + console.info('👌all tests finished') + browser.close() +}).catch((err) => { + console.error(err.stack) + console.error('❌some tests failed') + process.exitCode = 1 + browser.close() +}) diff --git a/test/browser/yargs-test.html b/test/browser/yargs-test.html new file mode 100644 index 00000000..71a7ff98 --- /dev/null +++ b/test/browser/yargs-test.html @@ -0,0 +1,21 @@ + + +
+ + diff --git a/test/deno/yargs-test.ts b/test/deno/yargs-test.ts new file mode 100644 index 00000000..b766aa52 --- /dev/null +++ b/test/deno/yargs-test.ts @@ -0,0 +1,46 @@ +/* global Deno */ + +import { + assertEquals +} from 'https://deno.land/std/testing/asserts.ts' +import parser from '../../deno.ts' + +Deno.test('parse string', () => { + const parsed = parser('--foo --bar 99') + assertEquals(parsed.foo, true) + assertEquals(parsed.bar, 99) +}) + +Deno.test('parse array', () => { + const parsed = parser(['--foo', '--bar', '99']) + assertEquals(parsed.foo, true) + assertEquals(parsed.bar, 99) +}) + +Deno.test('aliases', () => { + const parsed = parser(['--bar', '99'], { + alias: { + bar: ['foo'], + foo: ['f'] + } + }) + assertEquals(parsed.bar, 99) + assertEquals(parsed.foo, 99) + assertEquals(parsed.f, 99) +}) + +const jsonPath = './test/fixtures/config.json' +Deno.test('should load options and values from default config if specified', () => { + var argv = parser(['--foo', 'bar'], { + alias: { + z: 'zoom' + }, + default: { + settings: jsonPath + }, + config: 'settings' + }) + assertEquals(argv.herp, 'derp') + assertEquals(argv.zoom, 55) + assertEquals(argv.zoom, 55) +}) diff --git a/test/fixtures/settings.js b/test/fixtures/settings.cjs similarity index 100% rename from test/fixtures/settings.js rename to test/fixtures/settings.cjs diff --git a/test/tokenize-arg-string.ts b/test/typescript/tokenize-arg-string.ts similarity index 92% rename from test/tokenize-arg-string.ts rename to test/typescript/tokenize-arg-string.ts index 7621d45c..052aa939 100644 --- a/test/tokenize-arg-string.ts +++ b/test/typescript/tokenize-arg-string.ts @@ -1,28 +1,26 @@ /* global describe, it */ -import { expect, should } from 'chai' -import { tokenizeArgString } from '../lib/tokenize-arg-string' - -should() +import { strictEqual } from 'assert' +import { tokenizeArgString } from '../../lib/tokenize-arg-string.js' describe('TokenizeArgString', function () { it('handles unquoted string', function () { const args = tokenizeArgString('--foo 99') - args[0].should.equal('--foo') - args[1].should.equal('99') + strictEqual(args[0], '--foo') + strictEqual(args[1], '99') }) it('handles unquoted numbers', function () { const args = tokenizeArgString(['--foo', 9]) - args[0].should.equal('--foo') - args[1].should.equal('9') + strictEqual(args[0], '--foo') + strictEqual(args[1], '9') }) it('handles quoted string with no spaces', function () { const args = tokenizeArgString("--foo 'hello'") - args[0].should.equal('--foo') - args[1].should.equal("'hello'") + strictEqual(args[0], '--foo') + strictEqual(args[1], "'hello'") }) - +/* it('handles single quoted string with spaces', function () { const args = tokenizeArgString("--foo 'hello world' --bar='foo bar'") args[0].should.equal('--foo') @@ -129,5 +127,5 @@ describe('TokenizeArgString', function () { const args = tokenizeArgString(['--foo', '-bar']) expect(args[0]).to.equal('--foo') expect(args[1]).to.equal('-bar') - }) + }) */ }) diff --git a/test/types.ts b/test/typescript/types.ts similarity index 85% rename from test/types.ts rename to test/typescript/types.ts index e29ff542..39631057 100644 --- a/test/types.ts +++ b/test/typescript/types.ts @@ -1,6 +1,6 @@ /* global describe, it */ -import * as yargsParser from '../' +import yargsParser from '../../lib/index.js' import * as assert from 'assert' describe('types', function () { diff --git a/test/yargs-parser.js b/test/yargs-parser.cjs similarity index 99% rename from test/yargs-parser.js rename to test/yargs-parser.cjs index 8d552444..159e1dc7 100644 --- a/test/yargs-parser.js +++ b/test/yargs-parser.cjs @@ -4,7 +4,7 @@ require('chai').should() const { expect } = require('chai') const fs = require('fs') -const parser = require('../') +const parser = require('../build/index.cjs') const path = require('path') describe('yargs-parser', function () { @@ -598,7 +598,7 @@ describe('yargs-parser', function () { }) it('should load options and values from a JS file when config has .js extention', function () { - var jsPath = path.resolve(__dirname, './fixtures/settings.js') + var jsPath = path.resolve(__dirname, './fixtures/settings.cjs') var argv = parser(['--settings', jsPath, '--foo', 'bar'], { config: ['settings'] }) @@ -2116,7 +2116,6 @@ describe('yargs-parser', function () { var result = parser([], { envPrefix: '' }) - result.oneFish.should.equal('twofish') result.redFish.should.equal('bluefish') }) diff --git a/tsconfig.json b/tsconfig.json index 8e20a300..9cf3eed3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,10 +3,12 @@ "compilerOptions": { "outDir": "build", "rootDir": ".", - "sourceMap": false - }, + "sourceMap": false, + "target": "es2017", + "moduleResolution": "node", + "module": "es2015" + }, "include": [ - "./*.ts", "lib/**/*.ts" ] -} +} \ No newline at end of file diff --git a/tsconfig.test.json b/tsconfig.test.json index 62fec111..0a884940 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -4,8 +4,6 @@ "sourceMap": true }, "include": [ - "./*.ts", - "lib/**/*.ts", - "test/**/*.ts" + "test/typescript/*.ts" ] -} +} \ No newline at end of file