diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..ac6ca880 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,21 @@ +{ + "overrides": [ + { + "files": "*.ts", + "parser": "@typescript-eslint/parser", + "rules": { + "no-unused-vars": "off", + "no-useless-constructor": "off", + "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/no-useless-constructor": "error" + } + } + ], + "parserOptions": { + "ecmaVersion": 2018, + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint/eslint-plugin" + ] +} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000..9af2578d --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,91 @@ +on: + push: + branches: + - main + pull_request: + types: [ assigned, opened, synchronize, reopened, labeled ] +name: ci +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + node: [10, 12, 14] + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node }} + - run: node --version + - run: npm install + env: + PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true + - run: npm test + - run: npm run check + windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + 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@v2 + - uses: actions/setup-node@v1 + with: + 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 + optimize: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 14 + - run: cd test/tscc && npm install && npx @tscc/tscc + - run: cd test/tscc && node out.js diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 00000000..7ab9a096 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,46 @@ +on: + push: + branches: + - main +name: release-please +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: google-github-actions/release-please-action@v2 + id: release + with: + token: ${{ secrets.GITHUB_TOKEN }} + command: manifest + default-branch: main + - 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 'chore: ${{ steps.release.outputs.tag_name }} release' + git push origin +deno + git tag -a ${{ steps.release.outputs.tag_name }}-deno -m 'chore: ${{ steps.release.outputs.tag_name }} release' + git push origin ${{ steps.release.outputs.tag_name }}-deno + if: ${{ steps.release.outputs.releases_created }} + - uses: actions/setup-node@v1 + with: + node-version: 14 + registry-url: 'https://external-dot-oss-automation.appspot.com/' + if: ${{ steps.release.outputs.releases_created }} + - run: npm install + if: ${{ steps.release.outputs.releases_created }} + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + if: ${{ steps.release.outputs.releases_created }} diff --git a/.gitignore b/.gitignore index 24546300..245f7bd3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,9 @@ +.idea .nyc_output node_modules .DS_Store +package-lock.json ./test/fixtures/package.json +coverage +build +example.* diff --git a/.nycrc b/.nycrc new file mode 100644 index 00000000..3b315160 --- /dev/null +++ b/.nycrc @@ -0,0 +1,13 @@ +{ + "exclude": [ + "build/test/**", + "test/**" + ], + "reporter": [ + "html", + "text" + ], + "lines": 99.5, + "branches": "98", + "statements": "99.5" +} diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 00000000..6f365a22 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "20.2.9" +} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f3760a19..00000000 --- a/.travis.yml +++ /dev/null @@ -1,15 +0,0 @@ -language: node_js -os: - - linux -node_js: - - "0.10" - - "0.12" - - "4.1" - - "node" -after_script: npm run coverage -deploy: - provider: npm - email: ben@npmjs.com - api_key: $NPM_TOKEN - on: - tags: true diff --git a/CHANGELOG.md b/CHANGELOG.md index cd060f3c..fa838940 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,169 +1,264 @@ -# Change Log +# Changelog All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. - -# [5.0.0](https://github.com/yargs/yargs-parser/compare/v4.2.1...v5.0.0) (2017-02-18) +### [20.2.9](https://www.github.com/yargs/yargs-parser/compare/yargs-parser-v20.2.8...yargs-parser-v20.2.9) (2021-06-20) ### 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)) +* **build:** fixed automated release pipeline ([1fe9135](https://www.github.com/yargs/yargs-parser/commit/1fe9135884790a083615419b2861683e2597dac3)) +### [20.2.8](https://www.github.com/yargs/yargs-parser/compare/yargs-parser-v20.2.7...yargs-parser-v20.2.8) (2021-06-20) -### BREAKING CHANGES -* environment variables will now override config files (args, env, config-file, config-object) +### Bug Fixes + +* **deno:** force relese for Deno ([6687c97](https://www.github.com/yargs/yargs-parser/commit/6687c972d0f3ca7865a97908dde3080b05f8b026)) +* **locale:** Turkish camelize and decamelize issues with toLocaleLowerCase/toLocaleUpperCase ([2617303](https://www.github.com/yargs/yargs-parser/commit/261730383e02448562f737b94bbd1f164aed5143)) +* **perf:** address slow parse when using unknown-options-as-args ([#394](https://www.github.com/yargs/yargs-parser/issues/394)) ([441f059](https://www.github.com/yargs/yargs-parser/commit/441f059d585d446551068ad213db79ac91daf83a)) +* **string-utils:** detect [0,1] ranged values as numbers ([#388](https://www.github.com/yargs/yargs-parser/issues/388)) ([efcc32c](https://www.github.com/yargs/yargs-parser/commit/efcc32c2d6b09aba31abfa2db9bd947befe5586b)) + +### [20.2.7](https://www.github.com/yargs/yargs-parser/compare/v20.2.6...v20.2.7) (2021-03-10) + + +### Bug Fixes + +* **deno:** force relese for Deno ([6687c97](https://www.github.com/yargs/yargs-parser/commit/6687c972d0f3ca7865a97908dde3080b05f8b026)) + +### [20.2.6](https://www.github.com/yargs/yargs-parser/compare/v20.2.5...v20.2.6) (2021-02-22) + + +### Bug Fixes + +* **populate--:** -- should always be array ([#354](https://www.github.com/yargs/yargs-parser/issues/354)) ([585ae8f](https://www.github.com/yargs/yargs-parser/commit/585ae8ffad74cc02974f92d788e750137fd65146)) + +### [20.2.5](https://www.github.com/yargs/yargs-parser/compare/v20.2.4...v20.2.5) (2021-02-13) + + +### Bug Fixes +* do not lowercase camel cased string ([#348](https://www.github.com/yargs/yargs-parser/issues/348)) ([5f4da1f](https://www.github.com/yargs/yargs-parser/commit/5f4da1f17d9d50542d2aaa206c9806ce3e320335)) +### [20.2.4](https://www.github.com/yargs/yargs-parser/compare/v20.2.3...v20.2.4) (2020-11-09) - -## [4.2.1](https://github.com/yargs/yargs-parser/compare/v4.2.0...v4.2.1) (2017-01-02) + +### Bug Fixes + +* **deno:** address import issues in Deno ([#339](https://www.github.com/yargs/yargs-parser/issues/339)) ([3b54e5e](https://www.github.com/yargs/yargs-parser/commit/3b54e5eef6e9a7b7c6eec7c12bab3ba3b8ba8306)) + +### [20.2.3](https://www.github.com/yargs/yargs-parser/compare/v20.2.2...v20.2.3) (2020-10-16) ### Bug Fixes -* flatten/duplicate regression ([#75](https://github.com/yargs/yargs-parser/issues/75)) ([68d68a0](https://github.com/yargs/yargs-parser/commit/68d68a0)) +* **exports:** node 13.0 and 13.1 require the dotted object form _with_ a string fallback ([#336](https://www.github.com/yargs/yargs-parser/issues/336)) ([3ae7242](https://www.github.com/yargs/yargs-parser/commit/3ae7242040ff876d28dabded60ac226e00150c88)) +### [20.2.2](https://www.github.com/yargs/yargs-parser/compare/v20.2.1...v20.2.2) (2020-10-14) - -# [4.2.0](https://github.com/yargs/yargs-parser/compare/v4.1.0...v4.2.0) (2016-12-01) +### Bug Fixes + +* **exports:** node 13.0-13.6 require a string fallback ([#333](https://www.github.com/yargs/yargs-parser/issues/333)) ([291aeda](https://www.github.com/yargs/yargs-parser/commit/291aeda06b685b7a015d83bdf2558e180b37388d)) + +### [20.2.1](https://www.github.com/yargs/yargs-parser/compare/v20.2.0...v20.2.1) (2020-10-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)) +* **deno:** update types for deno ^1.4.0 ([#330](https://www.github.com/yargs/yargs-parser/issues/330)) ([0ab92e5](https://www.github.com/yargs/yargs-parser/commit/0ab92e50b090f11196334c048c9c92cecaddaf56)) + +## [20.2.0](https://www.github.com/yargs/yargs-parser/compare/v20.1.0...v20.2.0) (2020-09-21) ### 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)) +* **string-utils:** export looksLikeNumber helper ([#324](https://www.github.com/yargs/yargs-parser/issues/324)) ([c8580a2](https://www.github.com/yargs/yargs-parser/commit/c8580a2327b55f6342acecb6e72b62963d506750)) +### Bug Fixes + +* **unknown-options-as-args:** convert positionals that look like numbers ([#326](https://www.github.com/yargs/yargs-parser/issues/326)) ([f85ebb4](https://www.github.com/yargs/yargs-parser/commit/f85ebb4face9d4b0f56147659404cbe0002f3dad)) - -# [4.1.0](https://github.com/yargs/yargs-parser/compare/v4.0.2...v4.1.0) (2016-11-07) +## [20.1.0](https://www.github.com/yargs/yargs-parser/compare/v20.0.0...v20.1.0) (2020-09-20) ### 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)) +* adds parse-positional-numbers configuration ([#321](https://www.github.com/yargs/yargs-parser/issues/321)) ([9cec00a](https://www.github.com/yargs/yargs-parser/commit/9cec00a622251292ffb7dce6f78f5353afaa0d4c)) + + +### Bug Fixes + +* **build:** update release-please; make labels kick off builds ([#323](https://www.github.com/yargs/yargs-parser/issues/323)) ([09f448b](https://www.github.com/yargs/yargs-parser/commit/09f448b4cd66e25d2872544718df46dab8af062a)) +## [20.0.0](https://www.github.com/yargs/yargs-parser/compare/v19.0.4...v20.0.0) (2020-09-09) - -## [4.0.2](https://github.com/yargs/yargs-parser/compare/v4.0.1...v4.0.2) (2016-09-30) +### ⚠ BREAKING CHANGES +* do not ship type definitions (#318) ### Bug Fixes -* whoops, let's make the assign not change the Object key order ([29d069a](https://github.com/yargs/yargs-parser/commit/29d069a)) +* only strip camel case if hyphenated ([#316](https://www.github.com/yargs/yargs-parser/issues/316)) ([95a9e78](https://www.github.com/yargs/yargs-parser/commit/95a9e785127b9bbf2d1db1f1f808ca1fb100e82a)), closes [#315](https://www.github.com/yargs/yargs-parser/issues/315) +### Code Refactoring - -## [4.0.1](https://github.com/yargs/yargs-parser/compare/v4.0.0...v4.0.1) (2016-09-30) +* do not ship type definitions ([#318](https://www.github.com/yargs/yargs-parser/issues/318)) ([8fbd56f](https://www.github.com/yargs/yargs-parser/commit/8fbd56f1d0b6c44c30fca62708812151ca0ce330)) + +### [19.0.4](https://www.github.com/yargs/yargs-parser/compare/v19.0.3...v19.0.4) (2020-08-27) ### Bug Fixes -* lodash.assign was deprecated ([#59](https://github.com/yargs/yargs-parser/issues/59)) ([5e7eb11](https://github.com/yargs/yargs-parser/commit/5e7eb11)) +* **build:** fixing publication ([#310](https://www.github.com/yargs/yargs-parser/issues/310)) ([5d3c6c2](https://www.github.com/yargs/yargs-parser/commit/5d3c6c29a9126248ba601920d9cf87c78e161ff5)) + +### [19.0.3](https://www.github.com/yargs/yargs-parser/compare/v19.0.2...v19.0.3) (2020-08-27) +### Bug Fixes + +* **build:** switch to action for publish ([#308](https://www.github.com/yargs/yargs-parser/issues/308)) ([5c2f305](https://www.github.com/yargs/yargs-parser/commit/5c2f30585342bcd8aaf926407c863099d256d174)) - -# [4.0.0](https://github.com/yargs/yargs-parser/compare/v3.2.0...v4.0.0) (2016-09-26) +### [19.0.2](https://www.github.com/yargs/yargs-parser/compare/v19.0.1...v19.0.2) (2020-08-27) ### 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)) +* **types:** envPrefix should be optional ([#305](https://www.github.com/yargs/yargs-parser/issues/305)) ([ae3f180](https://www.github.com/yargs/yargs-parser/commit/ae3f180e14df2de2fd962145f4518f9aa0e76523)) +### [19.0.1](https://www.github.com/yargs/yargs-parser/compare/v19.0.0...v19.0.1) (2020-08-09) -### BREAKING CHANGES -* coerce is no longer applied to individual arguments in an implicit array. +### Bug Fixes + +* **build:** push tag created for deno ([2186a14](https://www.github.com/yargs/yargs-parser/commit/2186a14989749887d56189867602e39e6679f8b0)) +## [19.0.0](https://www.github.com/yargs/yargs-parser/compare/v18.1.3...v19.0.0) (2020-08-09) - -# [3.2.0](https://github.com/yargs/yargs-parser/compare/v3.1.0...v3.2.0) (2016-08-13) +### ⚠ BREAKING CHANGES +* adds support for ESM and Deno (#295) +* **ts:** projects using `@types/yargs-parser` may see variations in type definitions. +* drops Node 6. begin following Node.js LTS schedule (#278) ### 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)) +* adds support for ESM and Deno ([#295](https://www.github.com/yargs/yargs-parser/issues/295)) ([195bc4a](https://www.github.com/yargs/yargs-parser/commit/195bc4a7f20c2a8f8e33fbb6ba96ef6e9a0120a1)) +* expose camelCase and decamelize helpers ([#296](https://www.github.com/yargs/yargs-parser/issues/296)) ([39154ce](https://www.github.com/yargs/yargs-parser/commit/39154ceb5bdcf76b5f59a9219b34cedb79b67f26)) +* **deps:** update to latest camelcase/decamelize ([#281](https://www.github.com/yargs/yargs-parser/issues/281)) ([8931ab0](https://www.github.com/yargs/yargs-parser/commit/8931ab08f686cc55286f33a95a83537da2be5516)) + + +### Bug Fixes + +* boolean numeric short option ([#294](https://www.github.com/yargs/yargs-parser/issues/294)) ([f600082](https://www.github.com/yargs/yargs-parser/commit/f600082c959e092076caf420bbbc9d7a231e2418)) +* raise permission error for Deno if config load fails ([#298](https://www.github.com/yargs/yargs-parser/issues/298)) ([1174e2b](https://www.github.com/yargs/yargs-parser/commit/1174e2b3f0c845a1cd64e14ffc3703e730567a84)) +* **deps:** update dependency decamelize to v3 ([#274](https://www.github.com/yargs/yargs-parser/issues/274)) ([4d98698](https://www.github.com/yargs/yargs-parser/commit/4d98698bc6767e84ec54a0842908191739be73b7)) +* **types:** switch back to using Partial types ([#293](https://www.github.com/yargs/yargs-parser/issues/293)) ([bdc80ba](https://www.github.com/yargs/yargs-parser/commit/bdc80ba59fa13bc3025ce0a85e8bad9f9da24ea7)) + + +### Build System + +* drops Node 6. begin following Node.js LTS schedule ([#278](https://www.github.com/yargs/yargs-parser/issues/278)) ([9014ed7](https://www.github.com/yargs/yargs-parser/commit/9014ed722a32768b96b829e65a31705db5c1458a)) + + +### Code Refactoring + +* **ts:** move index.js to TypeScript ([#292](https://www.github.com/yargs/yargs-parser/issues/292)) ([f78d2b9](https://www.github.com/yargs/yargs-parser/commit/f78d2b97567ac4828624406e420b4047c710b789)) + +### [18.1.3](https://www.github.com/yargs/yargs-parser/compare/v18.1.2...v18.1.3) (2020-04-16) + + +### Bug Fixes + +* **setArg:** options using camel-case and dot-notation populated twice ([#268](https://www.github.com/yargs/yargs-parser/issues/268)) ([f7e15b9](https://www.github.com/yargs/yargs-parser/commit/f7e15b9800900b9856acac1a830a5f35847be73e)) + +### [18.1.2](https://www.github.com/yargs/yargs-parser/compare/v18.1.1...v18.1.2) (2020-03-26) +### Bug Fixes + +* **array, nargs:** support -o=--value and --option=--value format ([#262](https://www.github.com/yargs/yargs-parser/issues/262)) ([41d3f81](https://www.github.com/yargs/yargs-parser/commit/41d3f8139e116706b28de9b0de3433feb08d2f13)) - -# [3.1.0](https://github.com/yargs/yargs-parser/compare/v3.0.0...v3.1.0) (2016-08-09) +### [18.1.1](https://www.github.com/yargs/yargs-parser/compare/v18.1.0...v18.1.1) (2020-03-16) ### 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)) +* \_\_proto\_\_ will now be replaced with \_\_\_proto\_\_\_ in parse ([#258](https://www.github.com/yargs/yargs-parser/issues/258)), patching a potential +prototype pollution vulnerability. This was reported by the Snyk Security Research Team.([63810ca](https://www.github.com/yargs/yargs-parser/commit/63810ca1ae1a24b08293a4d971e70e058c7a41e2)) + +## [18.1.0](https://www.github.com/yargs/yargs-parser/compare/v18.0.0...v18.1.0) (2020-03-07) ### 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)) +* introduce single-digit boolean aliases ([#255](https://www.github.com/yargs/yargs-parser/issues/255)) ([9c60265](https://www.github.com/yargs/yargs-parser/commit/9c60265fd7a03cb98e6df3e32c8c5e7508d9f56f)) +## [18.0.0](https://www.github.com/yargs/yargs-parser/compare/v17.1.0...v18.0.0) (2020-03-02) - -# [3.0.0](https://github.com/yargs/yargs-parser/compare/v2.4.1...v3.0.0) (2016-08-07) +### ⚠ BREAKING CHANGES +* the narg count is now enforced when parsing arrays. -### Bug Fixes +### Features -* 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)) +* NaN can now be provided as a value for nargs, indicating "at least" one value is expected for array ([#251](https://www.github.com/yargs/yargs-parser/issues/251)) ([9db4be8](https://www.github.com/yargs/yargs-parser/commit/9db4be81417a2c7097128db34d86fe70ef4af70c)) -### BREAKING CHANGES +## [17.1.0](https://www.github.com/yargs/yargs-parser/compare/v17.0.1...v17.1.0) (2020-03-01) -* 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._ +### Features +* introduce greedy-arrays config, for specifying whether arrays consume multiple positionals ([#249](https://www.github.com/yargs/yargs-parser/issues/249)) ([60e880a](https://www.github.com/yargs/yargs-parser/commit/60e880a837046314d89fa4725f923837fd33a9eb)) - -## [2.4.1](https://github.com/yargs/yargs-parser/compare/v2.4.0...v2.4.1) (2016-07-16) +### [17.0.1](https://www.github.com/yargs/yargs-parser/compare/v17.0.0...v17.0.1) (2020-02-29) ### 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)) +* normalized keys were not enumerable ([#247](https://www.github.com/yargs/yargs-parser/issues/247)) ([57119f9](https://www.github.com/yargs/yargs-parser/commit/57119f9f17cf27499bd95e61c2f72d18314f11ba)) +## [17.0.0](https://www.github.com/yargs/yargs-parser/compare/v16.1.0...v17.0.0) (2020-02-10) - -# [2.4.0](https://github.com/yargs/yargs-parser/compare/v2.3.0...v2.4.0) (2016-04-11) +### ⚠ BREAKING CHANGES +* this reverts parsing behavior of booleans to that of yargs@14 +* objects used during parsing are now created with a null +prototype. There may be some scenarios where this change in behavior +leaks externally. ### 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)) +* boolean arguments will not be collected into an implicit array ([#236](https://www.github.com/yargs/yargs-parser/issues/236)) ([34c4e19](https://www.github.com/yargs/yargs-parser/commit/34c4e19bae4e7af63e3cb6fa654a97ed476e5eb5)) +* introduce nargs-eats-options config option ([#246](https://www.github.com/yargs/yargs-parser/issues/246)) ([d50822a](https://www.github.com/yargs/yargs-parser/commit/d50822ac10e1b05f2e9643671ca131ac251b6732)) +### Bug Fixes - -# [2.3.0](https://github.com/yargs/yargs-parser/compare/v2.2.0...v2.3.0) (2016-04-09) +* address bugs with "uknown-options-as-args" ([bc023e3](https://www.github.com/yargs/yargs-parser/commit/bc023e3b13e20a118353f9507d1c999bf388a346)) +* array should take precedence over nargs, but enforce nargs ([#243](https://www.github.com/yargs/yargs-parser/issues/243)) ([4cbc188](https://www.github.com/yargs/yargs-parser/commit/4cbc188b7abb2249529a19c090338debdad2fe6c)) +* support keys that collide with object prototypes ([#234](https://www.github.com/yargs/yargs-parser/issues/234)) ([1587b6d](https://www.github.com/yargs/yargs-parser/commit/1587b6d91db853a9109f1be6b209077993fee4de)) +* unknown options terminated with digits now handled by unknown-options-as-args ([#238](https://www.github.com/yargs/yargs-parser/issues/238)) ([d36cdfa](https://www.github.com/yargs/yargs-parser/commit/d36cdfa854254d7c7e0fe1d583818332ac46c2a5)) +## [16.1.0](https://www.github.com/yargs/yargs-parser/compare/v16.0.0...v16.1.0) (2019-11-01) -### 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)) +### ⚠ BREAKING CHANGES + +* populate error if incompatible narg/count or array/count options are used (#191) ### 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)) +* options that have had their default value used are now tracked ([#211](https://www.github.com/yargs/yargs-parser/issues/211)) ([a525234](https://www.github.com/yargs/yargs-parser/commit/a525234558c847deedd73f8792e0a3b77b26e2c0)) +* populate error if incompatible narg/count or array/count options are used ([#191](https://www.github.com/yargs/yargs-parser/issues/191)) ([84a401f](https://www.github.com/yargs/yargs-parser/commit/84a401f0fa3095e0a19661670d1570d0c3b9d3c9)) + + +### Reverts + +* revert 16.0.0 CHANGELOG entry ([920320a](https://www.github.com/yargs/yargs-parser/commit/920320ad9861bbfd58eda39221ae211540fc1daf)) diff --git a/README.md b/README.md index 6d5916c3..26148407 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,15 @@ # yargs-parser -[![Build Status](https://travis-ci.org/yargs/yargs-parser.png)](https://travis-ci.org/yargs/yargs-parser) -[![Coverage Status](https://coveralls.io/repos/yargs/yargs-parser/badge.svg?branch=)](https://coveralls.io/r/yargs/yargs-parser?branch=master) +![ci](https://github.com/yargs/yargs-parser/workflows/ci/badge.svg) [![NPM version](https://img.shields.io/npm/v/yargs-parser.svg)](https://www.npmjs.com/package/yargs-parser) -[![Windows Tests](https://img.shields.io/appveyor/ci/bcoe/yargs-parser/master.svg?label=Windows%20Tests)](https://ci.appveyor.com/project/bcoe/yargs-parser) -[![Standard Version](https://img.shields.io/badge/release-standard%20version-brightgreen.svg)](https://github.com/conventional-changelog/standard-version) - +[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org) +![nycrc config on GitHub](https://img.shields.io/nycrc/yargs/yargs-parser) The mighty option parser used by [yargs](https://github.com/yargs/yargs). visit the [yargs website](http://yargs.js.org/) for more examples, and thorough usage instructions. - + ## Example @@ -20,37 +18,81 @@ 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) ``` -```sh -node example.js --foo=33 --bar hello +```console +$ node example.js --foo=33 --bar hello { _: [], foo: 33, bar: 'hello' } ``` _or parse a string!_ ```js -var argv = require('./')('--foo=99 --bar=33') +const argv = require('yargs-parser')('--foo=99 --bar=33') console.log(argv) ``` -```sh +```console { _: [], foo: 99, bar: 33 } ``` 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://deno.land/x/yargs_parser/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={}) +### parser(args, opts={}) Parses command line arguments returning a simple mapping of keys and values. @@ -59,25 +101,31 @@ Parses command line arguments returning a simple mapping of keys and values. * `args`: a string or array of strings representing the options to parse. * `opts`: provide a set of hints indicating how `args` should be parsed: * `opts.alias`: an object representing the set of aliases for a key: `{alias: {foo: ['f']}}`. - * `opts.array`: indicate that keys should be parsed as an array: `{array: ['foo', 'bar']}`. + * `opts.array`: indicate that keys should be parsed as an array: `{array: ['foo', 'bar']}`.
+ Indicate that keys should be parsed as an array and coerced to booleans / numbers:
+ `{array: [{ key: 'foo', boolean: true }, {key: 'bar', number: true}]}`. * `opts.boolean`: arguments should be parsed as booleans: `{boolean: ['x', 'y']}`. - * `opts.config`: indicate a key that represents a path to a configuration file (this file will be loaded and parsed). * `opts.coerce`: provide a custom synchronous function that returns a coerced value from the argument provided - (or throws an error), e.g. `{coerce: {foo: function (arg) {return modifiedArg}}}`. + (or throws an error). For arrays the function is called only once for the entire array:
+ `{coerce: {foo: function (arg) {return modifiedArg}}}`. + * `opts.config`: indicate a key that represents a path to a configuration file (this file will be loaded and parsed). + * `opts.configObjects`: configuration objects to parse, their properties will be set as arguments:
+ `{configObjects: [{'x': 5, 'y': 33}, {'z': 44}]}`. + * `opts.configuration`: provide configuration options to the yargs-parser (see: [configuration](#configuration)). * `opts.count`: indicate a key that should be used as a counter, e.g., `-vvv` = `{v: 3}`. * `opts.default`: provide default values for keys: `{default: {x: 33, y: 'hello world!'}}`. * `opts.envPrefix`: environment variables (`process.env`) with the prefix provided should be parsed. * `opts.narg`: specify that a key requires `n` arguments: `{narg: {x: 2}}`. * `opts.normalize`: `path.normalize()` will be applied to values set to this key. - * `opts.string`: keys should be treated as strings (even if they resemble a number `-x 33`). - * `opts.configuration`: provide configuration options to the yargs-parser (see: [configuration](#configuration)). * `opts.number`: keys should be treated as numbers. + * `opts.string`: keys should be treated as strings (even if they resemble a number `-x 33`). **returns:** * `obj`: an object representing the parsed value of `args` * `key/value`: key value pairs for each argument and their aliases. * `_`: an array representing the positional arguments. + * [optional] `--`: an array with arguments after the end-of-options flag `--`. ### require('yargs-parser').detailed(args, opts={}) @@ -94,12 +142,17 @@ yargs engine. * `argv`: an object representing the parsed value of `args` * `key/value`: key value pairs for each argument and their aliases. * `_`: an array representing the positional arguments. + * [optional] `--`: an array with arguments after the end-of-options flag `--`. * `error`: populated with an error object if an exception occurred during parsing. * `aliases`: the inferred list of aliases built by combining lists in `opts.alias`. -* `newAliases`: any new aliases added via camel-case expansion. -* `configuration`: the configuration loaded from the `yargs` stanza in package.json. +* `newAliases`: any new aliases added via camel-case expansion: + * `boolean`: `{ fooBar: true }` +* `defaulted`: any new argument created by `opts.default`, no aliases included. + * `boolean`: `{ foo: true }` +* `configuration`: given by default settings and `opts.configuration`. + ### Configuration The yargs-parser applies several automated transformations on the keys provided @@ -121,15 +174,15 @@ var parsed = parser(['--no-dice'], { Should a group of short-options be treated as boolean flags? -```sh -node example.js -abc +```console +$ node example.js -abc { _: [], a: true, b: true, c: true } ``` _if disabled:_ -```sh -node example.js -abc +```console +$ node example.js -abc { _: [], abc: true } ``` @@ -140,15 +193,15 @@ node example.js -abc Should hyphenated arguments be expanded into camel-case aliases? -```sh -node example.js --foo-bar +```console +$ node example.js --foo-bar { _: [], 'foo-bar': true, fooBar: true } ``` _if disabled:_ -```sh -node example.js --foo-bar +```console +$ node example.js --foo-bar { _: [], 'foo-bar': true } ``` @@ -159,15 +212,15 @@ node example.js --foo-bar Should keys that contain `.` be treated as objects? -```sh -node example.js --foo.bar +```console +$ node example.js --foo.bar { _: [], foo: { bar: true } } ``` _if disabled:_ -```sh -node example.js --foo.bar +```console +$ node example.js --foo.bar { _: [], "foo.bar": true } ``` @@ -178,18 +231,37 @@ node example.js --foo.bar Should keys that look like numbers be treated as such? -```sh -node example.js --foo=99.3 +```console +$ node example.js --foo=99.3 { _: [], foo: 99.3 } ``` _if disabled:_ -```sh -node example.js --foo=99.3 +```console +$ node example.js --foo=99.3 { _: [], foo: "99.3" } ``` +### parse positional numbers + +* default: `true` +* key: `parse-positional-numbers` + +Should positional keys that look like numbers be treated as such. + +```console +$ node example.js 99.3 +{ _: [99.3] } +``` + +_if disabled:_ + +```console +$ node example.js 99.3 +{ _: ['99.3'] } +``` + ### boolean negation * default: `true` @@ -197,18 +269,26 @@ node example.js --foo=99.3 Should variables prefixed with `--no` be treated as negations? -```sh -node example.js --no-foo +```console +$ node example.js --no-foo { _: [], foo: false } ``` _if disabled:_ -```sh -node example.js --no-foo +```console +$ node example.js --no-foo { _: [], "no-foo": true } ``` +### combine arrays + +* default: `false` +* key: `combine-arrays` + +Should arrays be combined when provided by both command line arguments and +a configuration file. + ### duplicate arguments array * default: `true` @@ -216,15 +296,15 @@ node example.js --no-foo Should arguments be coerced into an array when duplicated: -```sh -node example.js -x 1 -x 2 +```console +$ node example.js -x 1 -x 2 { _: [], x: [1, 2] } ``` _if disabled:_ -```sh -node example.js -x 1 -x 2 +```console +$ node example.js -x 1 -x 2 { _: [], x: 2 } ``` @@ -235,18 +315,199 @@ node example.js -x 1 -x 2 Should array arguments be coerced into a single array when duplicated: -```sh -node example.js -x 1 2 -x 3 4 +```console +$ node example.js -x 1 2 -x 3 4 { _: [], x: [1, 2, 3, 4] } ``` _if disabled:_ -```sh -node example.js -x 1 2 -x 3 4 +```console +$ node example.js -x 1 2 -x 3 4 { _: [], x: [[1, 2], [3, 4]] } ``` +### greedy arrays + +* default: `true` +* key: `greedy-arrays` + +Should arrays consume more than one positional argument following their flag. + +```console +$ node example --arr 1 2 +{ _: [], arr: [1, 2] } +``` + +_if disabled:_ + +```console +$ node example --arr 1 2 +{ _: [2], arr: [1] } +``` + +**Note: in `v18.0.0` we are considering defaulting greedy arrays to `false`.** + +### nargs eats options + +* default: `false` +* key: `nargs-eats-options` + +Should nargs consume dash options as well as positional arguments. + +### negation prefix + +* default: `no-` +* key: `negation-prefix` + +The prefix to use for negated boolean variables. + +```console +$ node example.js --no-foo +{ _: [], foo: false } +``` + +_if set to `quux`:_ + +```console +$ node example.js --quuxfoo +{ _: [], foo: false } +``` + +### populate -- + +* default: `false`. +* key: `populate--` + +Should unparsed flags be stored in `--` or `_`. + +_If disabled:_ + +```console +$ node example.js a -b -- x y +{ _: [ 'a', 'x', 'y' ], b: true } +``` + +_If enabled:_ + +```console +$ node example.js a -b -- x y +{ _: [ 'a' ], '--': [ 'x', 'y' ], b: true } +``` + +### set placeholder key + +* default: `false`. +* key: `set-placeholder-key`. + +Should a placeholder be added for keys not set via the corresponding CLI argument? + +_If disabled:_ + +```console +$ node example.js -a 1 -c 2 +{ _: [], a: 1, c: 2 } +``` + +_If enabled:_ + +```console +$ node example.js -a 1 -c 2 +{ _: [], a: 1, b: undefined, c: 2 } +``` + +### halt at non-option + +* default: `false`. +* key: `halt-at-non-option`. + +Should parsing stop at the first positional argument? This is similar to how e.g. `ssh` parses its command line. + +_If disabled:_ + +```console +$ node example.js -a run b -x y +{ _: [ 'b' ], a: 'run', x: 'y' } +``` + +_If enabled:_ + +```console +$ node example.js -a run b -x y +{ _: [ 'b', '-x', 'y' ], a: 'run' } +``` + +### strip aliased + +* default: `false` +* key: `strip-aliased` + +Should aliases be removed before returning results? + +_If disabled:_ + +```console +$ node example.js --test-field 1 +{ _: [], 'test-field': 1, testField: 1, 'test-alias': 1, testAlias: 1 } +``` + +_If enabled:_ + +```console +$ node example.js --test-field 1 +{ _: [], 'test-field': 1, testField: 1 } +``` + +### strip dashed + +* default: `false` +* key: `strip-dashed` + +Should dashed keys be removed before returning results? This option has no effect if +`camel-case-expansion` is disabled. + +_If disabled:_ + +```console +$ node example.js --test-field 1 +{ _: [], 'test-field': 1, testField: 1 } +``` + +_If enabled:_ + +```console +$ node example.js --test-field 1 +{ _: [], testField: 1 } +``` + +### unknown options as args + +* default: `false` +* key: `unknown-options-as-args` + +Should unknown options be treated like regular arguments? An unknown option is one that is not +configured in `opts`. + +_If disabled_ + +```console +$ node example.js --unknown-option --known-option 2 --string-option --unknown-option2 +{ _: [], unknownOption: true, knownOption: 2, stringOption: '', unknownOption2: true } +``` + +_If enabled_ + +```console +$ node example.js --unknown-option --known-option 2 --string-option --unknown-option2 +{ _: ['--unknown-option'], knownOption: 2, stringOption: '--unknown-option2' } +``` + +## Supported Node.js Versions + +Libraries in this ecosystem make a best effort to track +[Node.js' release schedule](https://nodejs.org/en/about/releases/). Here's [a +post on why we think this is important](https://medium.com/the-node-js-collection/maintainers-should-consider-following-node-js-release-schedule-ab08ed4de71a). + ## Special Thanks The yargs project evolves from optimist and minimist. It owes its diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 2f85f7fe..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,21 +0,0 @@ -environment: - matrix: - - nodejs_version: '5' - - nodejs_version: '4' - - nodejs_version: '0.12' -install: - - ps: Install-Product node $env:nodejs_version - - set CI=true - - npm -g install npm@latest - - set PATH=%APPDATA%\npm;%PATH% - - npm install -matrix: - fast_finish: true -build: off -version: '{build}' -shallow_clone: true -clone_depth: 1 -test_script: - - node --version - - npm --version - - npm test diff --git a/browser.js b/browser.js new file mode 100644 index 00000000..241202c7 --- /dev/null +++ b/browser.js @@ -0,0 +1,29 @@ +// 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 { camelCase, decamelize, looksLikeNumber } from './build/lib/string-utils.js' +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) +} +yargsParser.camelCase = camelCase +yargsParser.decamelize = decamelize +yargsParser.looksLikeNumber = looksLikeNumber + +export default yargsParser diff --git a/deno.ts b/deno.ts new file mode 100644 index 00000000..1074dc64 --- /dev/null +++ b/deno.ts @@ -0,0 +1,38 @@ +/* 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 { camelCase, decamelize, looksLikeNumber } from './build/lib/string-utils.js' +import { YargsParser } from './build/lib/yargs-parser.js' +import type { 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) +} +yargsParser.camelCase = camelCase +yargsParser.decamelize = decamelize +yargsParser.looksLikeNumber = looksLikeNumber + +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 1c1ea630..00000000 --- a/example.js +++ /dev/null @@ -1,3 +0,0 @@ -var parser = require('./') -var parse = parser(['-cats', 'meow']) -console.log(parse) diff --git a/index.js b/index.js deleted file mode 100644 index b71faf58..00000000 --- a/index.js +++ /dev/null @@ -1,759 +0,0 @@ -var camelCase = require('camelcase') -var path = require('path') -var tokenizeArgString = require('./lib/tokenize-arg-string') -var util = require('util') - -function parse (args, opts) { - if (!opts) opts = {} - // allow a string argument to be passed in rather - // than an argv array. - args = tokenizeArgString(args) - // aliases might have transitive relationships, normalize this. - var aliases = combineAliases(opts.alias || {}) - var configuration = assign({ - 'short-option-groups': true, - 'camel-case-expansion': true, - 'dot-notation': true, - 'parse-numbers': true, - 'boolean-negation': true, - 'duplicate-arguments-array': true, - 'flatten-duplicate-arrays': true - }, opts.configuration) - var defaults = opts.default || {} - var configObjects = opts.configObjects || [] - var envPrefix = opts.envPrefix - var newAliases = {} - // allow a i18n handler to be passed in, default to a fake one (util.format). - var __ = opts.__ || function (str) { - return util.format.apply(util, Array.prototype.slice.call(arguments)) - } - var error = null - var flags = { - aliases: {}, - arrays: {}, - bools: {}, - strings: {}, - numbers: {}, - counts: {}, - normalize: {}, - configs: {}, - defaulted: {}, - nargs: {}, - coercions: {} - } - var negative = /^-[0-9]+(\.[0-9]+)?/ - - ;[].concat(opts.array).filter(Boolean).forEach(function (key) { - flags.arrays[key] = true - }) - - ;[].concat(opts.boolean).filter(Boolean).forEach(function (key) { - flags.bools[key] = true - }) - - ;[].concat(opts.string).filter(Boolean).forEach(function (key) { - flags.strings[key] = true - }) - - ;[].concat(opts.number).filter(Boolean).forEach(function (key) { - flags.numbers[key] = true - }) - - ;[].concat(opts.count).filter(Boolean).forEach(function (key) { - flags.counts[key] = true - }) - - ;[].concat(opts.normalize).filter(Boolean).forEach(function (key) { - flags.normalize[key] = true - }) - - Object.keys(opts.narg || {}).forEach(function (k) { - flags.nargs[k] = opts.narg[k] - }) - - Object.keys(opts.coerce || {}).forEach(function (k) { - flags.coercions[k] = opts.coerce[k] - }) - - if (Array.isArray(opts.config) || typeof opts.config === 'string') { - ;[].concat(opts.config).filter(Boolean).forEach(function (key) { - flags.configs[key] = true - }) - } else { - Object.keys(opts.config || {}).forEach(function (k) { - flags.configs[k] = opts.config[k] - }) - } - - // 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] - }) - }) - - var argv = { _: [] } - - Object.keys(flags.bools).forEach(function (key) { - setArg(key, !(key in defaults) ? false : defaults[key]) - setDefaulted(key) - }) - - var notFlags = [] - if (args.indexOf('--') !== -1) { - notFlags = args.slice(args.indexOf('--') + 1) - args = args.slice(0, args.indexOf('--')) - } - - for (var i = 0; i < args.length; i++) { - var arg = args[i] - var broken - var key - var letters - var m - var next - var value - - // -- seperated by = - 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]*)$/) - - // nargs format = '--f=monkey washing cat' - if (checkAllAliases(m[1], flags.nargs)) { - args.splice(i + 1, 0, m[2]) - i = eatNargs(i, m[1], args) - // arrays format = '--f=a b c' - } else if (checkAllAliases(m[1], flags.arrays) && args.length > i + 1) { - args.splice(i + 1, 0, m[2]) - i = eatArray(i, m[1], args) - } else { - setArg(m[1], m[2]) - } - } else if (arg.match(/^--no-.+/) && configuration['boolean-negation']) { - key = arg.match(/^--no-(.+)/)[1] - setArg(key, false) - - // -- seperated by space. - } else if (arg.match(/^--.+/) || ( - !configuration['short-option-groups'] && arg.match(/^-.+/) - )) { - key = arg.match(/^--?(.+)/)[1] - - // nargs format = '--foo a b c' - if (checkAllAliases(key, flags.nargs)) { - i = eatNargs(i, key, args) - // array format = '--foo a b c' - } else if (checkAllAliases(key, flags.arrays) && args.length > i + 1) { - i = eatArray(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, defaultForType(guessType(key, flags))) - } - } - - // dot-notation flag seperated by '='. - } else if (arg.match(/^-.\..+=/)) { - m = arg.match(/^-([^=]+)=([\s\S]*)$/) - setArg(m[1], m[2]) - - // dot-notation flag seperated by space. - } else if (arg.match(/^-.\..+/)) { - next = args[i + 1] - key = arg.match(/^-(.\..+)/)[1] - - if (next !== undefined && !next.match(/^-/) && - !checkAllAliases(key, flags.bools) && - !checkAllAliases(key, flags.counts)) { - setArg(key, next) - i++ - } else { - setArg(key, defaultForType(guessType(key, flags))) - } - } else if (arg.match(/^-[^-]+/) && !arg.match(negative)) { - letters = arg.slice(1, -1).split('') - broken = false - - for (var 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] - - // nargs format = '-f=monkey washing cat' - if (checkAllAliases(key, flags.nargs)) { - args.splice(i + 1, 0, value) - i = eatNargs(i, key, args) - // array format = '-f=a b c' - } else if (checkAllAliases(key, flags.arrays) && args.length > i + 1) { - args.splice(i + 1, 0, value) - i = eatArray(i, key, args) - } 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)) { - 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], defaultForType(guessType(letters[j], flags))) - } - } - - key = arg.slice(-1)[0] - - if (!broken && key !== '-') { - // nargs format = '-f a b c' - if (checkAllAliases(key, flags.nargs)) { - i = eatNargs(i, key, args) - // array format = '-f a b c' - } else if (checkAllAliases(key, flags.arrays) && args.length > i + 1) { - i = eatArray(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, defaultForType(guessType(key, flags))) - } - } - } - } else { - argv._.push( - flags.strings['_'] || !isNumber(arg) ? arg : Number(arg) - ) - } - } - - // 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) - applyCoercions(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) - }) - - notFlags.forEach(function (key) { - argv._.push(key) - }) - - // how many arguments should we consume, based - // on the nargs option? - function eatNargs (i, key, args) { - var toEat = checkAllAliases(key, flags.nargs) - - if (args.length - (i + 1) < toEat) error = Error(__('Not enough arguments following: %s', key)) - - for (var ii = i + 1; ii < (toEat + i + 1); ii++) { - setArg(key, args[ii]) - } - - return (i + toEat) - } - - // 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, key, args) { - var start = i + 1 - var argsToSet = [] - var multipleArrayFlag = i > 0 - for (var ii = i + 1; ii < args.length; ii++) { - if (/^-/.test(args[ii]) && !negative.test(args[ii])) { - if (ii === start) { - setArg(key, defaultForType('array')) - } - multipleArrayFlag = true - break - } - i = ii - argsToSet.push(args[ii]) - } - if (multipleArrayFlag) { - setArg(key, argsToSet.map(function (arg) { - return processValue(key, arg) - })) - } else { - argsToSet.forEach(function (arg) { - setArg(key, arg) - }) - } - - return i - } - - function setArg (key, val) { - unsetDefaulted(key) - - if (/-/.test(key) && !(flags.aliases[key] && flags.aliases[key].length) && configuration['camel-case-expansion']) { - var c = camelCase(key) - flags.aliases[key] = [c] - newAliases[c] = true - } - - var value = processValue(key, val) - - var splitKey = key.split('.') - setKey(argv, splitKey, value) - - // handle populating aliases of the full key - if (flags.aliases[key]) { - flags.aliases[key].forEach(function (x) { - x = x.split('.') - setKey(argv, x, 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) { - x = x.split('.') - - // expand alias with nested objects in key - var a = [].concat(splitKey) - a.shift() // nuke the old key. - x = x.concat(a) - - setKey(argv, x, 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)) { - var keys = [key].concat(flags.aliases[key] || []) - keys.forEach(function (key) { - argv.__defineSetter__(key, function (v) { - val = path.normalize(v) - }) - - argv.__defineGetter__(key, function () { - return typeof val === 'string' ? path.normalize(val) : val - }) - }) - } - } - - function processValue (key, val) { - // handle parsing boolean arguments --foo=true --bar false. - if (checkAllAliases(key, flags.bools) || checkAllAliases(key, flags.counts)) { - if (typeof val === 'string') val = val === 'true' - } - - var value = val - if (!checkAllAliases(key, flags.strings) && !checkAllAliases(key, flags.coercions)) { - if (isNumber(val)) value = Number(val) - if (!isUndefined(val) && !isNumber(val) && checkAllAliases(key, flags.numbers)) value = NaN - } - - // 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 - } - - // set args from config.json file, this should be - // applied last so that defaults can be applied. - function setConfig (argv) { - var configLookup = {} - - // 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) { - var configPath = argv[configKey] || configLookup[configKey] - if (configPath) { - try { - var config = null - var resolvedConfigPath = path.resolve(process.cwd(), configPath) - - if (typeof flags.configs[configKey] === 'function') { - try { - config = flags.configs[configKey](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, prev) { - Object.keys(config).forEach(function (key) { - var value = config[key] - var 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' && !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('.')) || (flags.defaulted[fullKey])) { - setArg(fullKey, value) - } - } - }) - } - - // set all config objects passed in opts - function setConfigObjects () { - if (typeof configObjects === 'undefined') return - configObjects.forEach(function (configObject) { - setConfigObject(configObject) - }) - } - - function applyEnvVars (argv, configOnly) { - if (typeof envPrefix === 'undefined') return - - var 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 - var 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) || flags.defaulted[keys.join('.')])) { - setArg(keys.join('.'), process.env[envVar]) - } - } - }) - } - - function applyCoercions (argv) { - var coerce - Object.keys(argv).forEach(function (key) { - coerce = checkAllAliases(key, flags.coercions) - if (typeof coerce === 'function') { - try { - argv[key] = coerce(argv[key]) - } catch (err) { - error = err - } - } - }) - } - - function applyDefaultsAndAliases (obj, aliases, defaults) { - Object.keys(defaults).forEach(function (key) { - if (!hasKey(obj, key.split('.'))) { - setKey(obj, key.split('.'), defaults[key]) - - ;(aliases[key] || []).forEach(function (x) { - if (hasKey(obj, x.split('.'))) return - setKey(obj, x.split('.'), defaults[key]) - }) - } - }) - } - - function hasKey (obj, keys) { - var o = obj - - if (!configuration['dot-notation']) keys = [keys.join('.')] - - keys.slice(0, -1).forEach(function (key) { - o = (o[key] || {}) - }) - - var key = keys[keys.length - 1] - - if (typeof o !== 'object') return false - else return key in o - } - - function setKey (obj, keys, value) { - var o = obj - - if (!configuration['dot-notation']) keys = [keys.join('.')] - - keys.slice(0, -1).forEach(function (key) { - if (o[key] === undefined) o[key] = {} - o = o[key] - }) - - var key = keys[keys.length - 1] - - var isTypeArray = checkAllAliases(key, flags.arrays) - var isValueArray = Array.isArray(value) - var duplicate = configuration['duplicate-arguments-array'] - - 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) : [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.bools) || checkAllAliases(keys.join('.'), flags.bools) || checkAllAliases(key, flags.counts))) { - o[key] = [ o[key], value ] - } else { - o[key] = value - } - } - - // extend the aliases list with inferred aliases. - function extendAliases () { - Array.prototype.slice.call(arguments).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] = [].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']) { - var c = camelCase(x) - 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 - })) - }) - }) - }) - } - - // check if a flag is set for any of a key's aliases. - function checkAllAliases (key, flag) { - var isSet = false - var toCheck = [].concat(flags.aliases[key] || [], key) - - toCheck.forEach(function (key) { - if (flag[key]) isSet = flag[key] - }) - - return isSet - } - - function setDefaulted (key) { - [].concat(flags.aliases[key] || [], key).forEach(function (k) { - flags.defaulted[k] = true - }) - } - - function unsetDefaulted (key) { - [].concat(flags.aliases[key] || [], key).forEach(function (k) { - delete flags.defaulted[k] - }) - } - - // return a default value, given the type of a flag., - // e.g., key of type 'string' will default to '', rather than 'true'. - function defaultForType (type) { - var def = { - boolean: true, - string: '', - number: undefined, - array: [] - } - - return def[type] - } - - // given a flag, enforce a default type. - function guessType (key, flags) { - var type = 'boolean' - - if (checkAllAliases(key, flags.strings)) type = 'string' - else if (checkAllAliases(key, flags.numbers)) type = 'number' - else if (checkAllAliases(key, flags.arrays)) type = 'array' - - return type - } - - function isNumber (x) { - if (!configuration['parse-numbers']) return false - if (typeof x === 'number') return true - if (/^0x[0-9a-f]+$/i.test(x)) return true - return /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(x) - } - - function isUndefined (num) { - return num === undefined - } - - return { - argv: argv, - error: error, - aliases: flags.aliases, - newAliases: newAliases, - configuration: configuration - } -} - -// if any aliases reference each other, we should -// merge them together. -function combineAliases (aliases) { - var aliasArrays = [] - var change = true - var combined = {} - - // turn alias lookup hash {key: ['alias1', 'alias2']} into - // a simple array ['key', 'alias1', 'alias2'] - Object.keys(aliases).forEach(function (key) { - aliasArrays.push( - [].concat(aliases[key], key) - ) - }) - - // combine arrays until zero changes are - // made in an iteration. - while (change) { - change = false - for (var i = 0; i < aliasArrays.length; i++) { - for (var ii = i + 1; ii < aliasArrays.length; ii++) { - var 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 - }) - combined[aliasArray.pop()] = aliasArray - }) - - return combined -} - -function assign (defaults, configuration) { - var o = {} - configuration = configuration || {} - - Object.keys(defaults).forEach(function (k) { - o[k] = defaults[k] - }) - Object.keys(configuration).forEach(function (k) { - o[k] = configuration[k] - }) - - return o -} - -// 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) { - return orig !== undefined ? orig + 1 : 1 -} - -function Parser (args, opts) { - var result = parse(args.slice(), opts) - - return result.argv -} - -// parse arguments and return detailed -// meta information, aliases, etc. -Parser.detailed = function (args, opts) { - return parse(args.slice(), opts) -} - -module.exports = Parser diff --git a/lib/index.ts b/lib/index.ts new file mode 100644 index 00000000..c0bfac81 --- /dev/null +++ b/lib/index.ts @@ -0,0 +1,61 @@ +/** + * @fileoverview Main entrypoint for libraries using yargs-parser in Node.js + * CJS and ESM environments. + * + * @license + * Copyright (c) 2016, Contributors + * SPDX-License-Identifier: ISC + */ + +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 { camelCase, decamelize, looksLikeNumber } from './string-utils.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) +} +yargsParser.camelCase = camelCase +yargsParser.decamelize = decamelize +yargsParser.looksLikeNumber = looksLikeNumber +export default yargsParser diff --git a/lib/string-utils.ts b/lib/string-utils.ts new file mode 100644 index 00000000..5932a4c7 --- /dev/null +++ b/lib/string-utils.ts @@ -0,0 +1,63 @@ +/** + * @license + * Copyright (c) 2016, Contributors + * SPDX-License-Identifier: ISC + */ + +export function camelCase (str: string): string { + // Handle the case where an argument is provided as camel case, e.g., fooBar. + // by ensuring that the string isn't already mixed case: + const isCamelCase = str !== str.toLowerCase() && str !== str.toUpperCase() + + if (!isCamelCase) { + str = str.toLowerCase() + } + + 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.toUpperCase() + } + if (i !== 0 && (chr === '-' || chr === '_')) { + nextChrUpper = true + } else if (chr !== '-' && chr !== '_') { + camelcase += chr + } + } + return camelcase + } +} + +export function decamelize (str: string, joinString?: string): string { + const lowercase = str.toLowerCase() + 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 && i > 0) { + notCamelcase += `${joinString}${lowercase.charAt(i)}` + } else { + notCamelcase += chrString + } + } + return notCamelcase +} + +export function looksLikeNumber (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 (/^0[^.]/.test(x)) return false + return /^[-]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(x) +} diff --git a/lib/tokenize-arg-string.js b/lib/tokenize-arg-string.js deleted file mode 100644 index 23d39e1f..00000000 --- a/lib/tokenize-arg-string.js +++ /dev/null @@ -1,34 +0,0 @@ -// take an un-split argv string and tokenize it. -module.exports = function (argString) { - if (Array.isArray(argString)) return argString - - var i = 0 - var c = null - var opening = null - var args = [] - - for (var ii = 0; ii < argString.length; ii++) { - c = argString.charAt(ii) - - // split on spaces unless we're in quotes. - if (c === ' ' && !opening) { - i++ - continue - } - - // don't split the string if we're in matching - // opening or closing single and double quotes. - if (c === opening) { - opening = null - continue - } else if ((c === "'" || c === '"') && !opening) { - opening = c - continue - } - - if (!args[i]) args[i] = '' - args[i] += c - } - - return args -} diff --git a/lib/tokenize-arg-string.ts b/lib/tokenize-arg-string.ts new file mode 100644 index 00000000..3d63e875 --- /dev/null +++ b/lib/tokenize-arg-string.ts @@ -0,0 +1,46 @@ +/** + * @license + * Copyright (c) 2016, Contributors + * SPDX-License-Identifier: ISC + */ + +// take an un-split argv string and tokenize it. +export function tokenizeArgString (argString: string | any[]): string[] { + if (Array.isArray(argString)) { + return argString.map(e => typeof e !== 'string' ? e + '' : e) + } + + argString = argString.trim() + + let i = 0 + let prevC: string | null = null + let c: string | null = null + let opening: string | null = null + const args: string[] = [] + + for (let ii = 0; ii < argString.length; ii++) { + prevC = c + c = argString.charAt(ii) + + // split on spaces unless we're in quotes. + if (c === ' ' && !opening) { + if (!(prevC === ' ')) { + i++ + } + continue + } + + // don't split the string if we're in matching + // opening or closing single and double quotes. + if (c === opening) { + opening = null + } else if ((c === "'" || c === '"') && !opening) { + opening = c + } + + if (!args[i]) args[i] = '' + args[i] += c + } + + return args +} diff --git a/lib/yargs-parser-types.ts b/lib/yargs-parser-types.ts new file mode 100644 index 00000000..9e03ff2c --- /dev/null +++ b/lib/yargs-parser-types.ts @@ -0,0 +1,201 @@ +/** + * @license + * Copyright (c) 2016, Contributors + * SPDX-License-Identifier: ISC + */ + +/** + * An object whose all properties have the same type, where each key is a string. + */ +export interface Dictionary { + [key: string]: T; +} + +/** + * Returns the keys of T. + */ +export type KeyOf = { + [K in keyof T]: string extends K ? never : number extends K ? never : K +} extends { [_ in keyof T]: infer U } ? U : never; + +/** + * Returns the type of a Dictionary or array values. + */ +export type ValueOf = T extends (infer U)[] ? U : T[keyof T]; + +export type ArgsInput = string | any[]; + +export type ArgsOutput = (string | number)[]; + +export interface Arguments { + /** Non-option arguments */ + _: ArgsOutput; + /** Arguments after the end-of-options flag `--` */ + '--'?: ArgsOutput; + /** All remaining options */ + [argName: string]: any; +} + +export interface DetailedArguments { + /** An object representing the parsed value of `args` */ + argv: Arguments; + /** Populated with an error object if an exception occurred during parsing. */ + error: Error | null; + /** The inferred list of aliases built by combining lists in opts.alias. */ + aliases: Dictionary; + /** Any new aliases added via camel-case expansion. */ + newAliases: Dictionary; + /** Any new argument created by opts.default, no aliases included. */ + defaulted: Dictionary; + /** The configuration loaded from the yargs stanza in package.json. */ + configuration: Configuration; +} + +export interface Configuration { + /** Should variables prefixed with --no be treated as negations? Default is `true` */ + 'boolean-negation': boolean; + /** Should hyphenated arguments be expanded into camel-case aliases? Default is `true` */ + 'camel-case-expansion': boolean; + /** Should arrays be combined when provided by both command line arguments and a configuration file? Default is `false` */ + 'combine-arrays': boolean; + /** Should keys that contain `.` be treated as objects? Default is `true` */ + 'dot-notation': boolean; + /** Should arguments be coerced into an array when duplicated? Default is `true` */ + 'duplicate-arguments-array': boolean; + /** Should array arguments be coerced into a single array when duplicated? Default is `true` */ + 'flatten-duplicate-arrays': boolean; + /** Should arrays consume more than one positional argument following their flag? Default is `true` */ + 'greedy-arrays': boolean; + /** Should parsing stop at the first text argument? This is similar to how e.g. ssh parses its command line. Default is `false` */ + 'halt-at-non-option': boolean; + /** Should nargs consume dash options as well as positional arguments? Default is `false` */ + 'nargs-eats-options': boolean; + /** The prefix to use for negated boolean variables. Default is `'no-'` */ + 'negation-prefix': string; + /** Should positional values that look like numbers be parsed? Default is `true` */ + 'parse-positional-numbers': boolean; + /** Should keys that look like numbers be treated as such? Default is `true` */ + 'parse-numbers': boolean; + /** Should unparsed flags be stored in -- or _? Default is `false` */ + 'populate--': boolean; + /** Should a placeholder be added for keys not set via the corresponding CLI argument? Default is `false` */ + 'set-placeholder-key': boolean; + /** Should a group of short-options be treated as boolean flags? Default is `true` */ + 'short-option-groups': boolean; + /** Should aliases be removed before returning results? Default is `false` */ + 'strip-aliased': boolean; + /** Should dashed keys be removed before returning results? This option has no effect if camel-case-expansion is disabled. Default is `false` */ + 'strip-dashed': boolean; + /** Should unknown options be treated like regular arguments? An unknown option is one that is not configured in opts. Default is `false` */ + 'unknown-options-as-args': boolean; +} + +export type ArrayOption = string | { key: string; boolean?: boolean, string?: boolean, number?: boolean, integer?: boolean }; + +export type CoerceCallback = (arg: any) => any; + +export type ConfigCallback = (configPath: string) => { [key: string]: any } | Error; + +export interface Options { + /** An object representing the set of aliases for a key: `{ alias: { foo: ['f']} }`. */ + alias: Dictionary; + /** + * Indicate that keys should be parsed as an array: `{ array: ['foo', 'bar'] }`. + * Indicate that keys should be parsed as an array and coerced to booleans / numbers: + * { array: [ { key: 'foo', boolean: true }, {key: 'bar', number: true} ] }`. + */ + array: ArrayOption | ArrayOption[]; + /** Arguments should be parsed as booleans: `{ boolean: ['x', 'y'] }`. */ + boolean: string | string[]; + /** Indicate a key that represents a path to a configuration file (this file will be loaded and parsed). */ + config: string | string[] | Dictionary; + /** configuration objects to parse, their properties will be set as arguments */ + configObjects: Dictionary[]; + /** Provide configuration options to the yargs-parser. */ + configuration: Partial; + /** + * Provide a custom synchronous function that returns a coerced value from the argument provided (or throws an error), e.g. + * `{ coerce: { foo: function (arg) { return modifiedArg } } }`. + */ + coerce: Dictionary; + /** Indicate a key that should be used as a counter, e.g., `-vvv = {v: 3}`. */ + count: string | string[]; + /** Provide default values for keys: `{ default: { x: 33, y: 'hello world!' } }`. */ + default: Dictionary; + /** Environment variables (`process.env`) with the prefix provided should be parsed. */ + envPrefix?: string; + /** Specify that a key requires n arguments: `{ narg: {x: 2} }`. */ + narg: Dictionary; + /** `path.normalize()` will be applied to values set to this key. */ + normalize: string | string[]; + /** Keys should be treated as strings (even if they resemble a number `-x 33`). */ + string: string | string[]; + /** Keys should be treated as numbers. */ + number: string | string[]; + /** i18n handler, defaults to util.format */ + __: (format: any, ...param: any[]) => string; + /** alias lookup table defaults */ + 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 { + (args: ArgsInput, opts?: Partial): Arguments; + detailed(args: ArgsInput, opts?: Partial): DetailedArguments; + camelCase(str: string): string; + decamelize(str: string, joinString?: string): string; + looksLikeNumber(x: null | undefined | number | string): boolean; +} + +export type StringFlag = Dictionary; +export type BooleanFlag = Dictionary; +export type NumberFlag = Dictionary; +export type ConfigsFlag = Dictionary; +export type CoercionsFlag = Dictionary; +export type KeysFlag = string[]; + +export interface Flags { + aliases: StringFlag; + arrays: BooleanFlag; + bools: BooleanFlag; + strings: BooleanFlag; + numbers: BooleanFlag; + counts: BooleanFlag; + normalize: BooleanFlag; + configs: ConfigsFlag; + nargs: NumberFlag; + coercions: CoercionsFlag; + keys: KeysFlag; +} + +export type Flag = ValueOf>; + +export type FlagValue = ValueOf; + +export type FlagsKey = KeyOf>; + +export type ArrayFlagsKey = Extract; + +export enum DefaultValuesForTypeKey { + BOOLEAN = 'boolean', + STRING = 'string', + NUMBER = 'number', + ARRAY = 'array', +} + +export interface DefaultValuesForType { + [DefaultValuesForTypeKey.BOOLEAN]: boolean; + [DefaultValuesForTypeKey.STRING]: string; + [DefaultValuesForTypeKey.NUMBER]: undefined; + [DefaultValuesForTypeKey.ARRAY]: any[]; +} diff --git a/lib/yargs-parser.ts b/lib/yargs-parser.ts new file mode 100644 index 00000000..1499fc60 --- /dev/null +++ b/lib/yargs-parser.ts @@ -0,0 +1,1118 @@ +/** + * @license + * Copyright (c) 2016, Contributors + * SPDX-License-Identifier: ISC + */ + +import { tokenizeArgString } from './tokenize-arg-string.js' +import type { + ArgsInput, + Arguments, + ArrayFlagsKey, + ArrayOption, + CoerceCallback, + Configuration, + DefaultValuesForType, + DetailedArguments, + Dictionary, + Flag, + Flags, + FlagsKey, + StringFlag, + BooleanFlag, + NumberFlag, + ConfigsFlag, + CoercionsFlag, + Options, + OptionsDefault, + ValueOf, + YargsParserMixin +} from './yargs-parser-types.js' +import { DefaultValuesForTypeKey } from './yargs-parser-types.js' +import { camelCase, decamelize, looksLikeNumber } 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, + 'parse-positional-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] + const truncatedArg = arg.replace(/^-{3,}/, '---') + 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)) { + pushPositional(arg) + // ---, ---=, ----, etc, + } else if (truncatedArg.match(/---+(=|$)/)) { + // options without key name are invalid. + pushPositional(arg) + continue + // -- 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 { + pushPositional(arg) + } + } + + // 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'] && alias.includes('-')) { + delete argv[alias.split('.').map(prop => camelCase(prop)).join('.')] + } + + delete argv[alias] + }) + } + + // Push argument into positional array, applying numeric coercion: + function pushPositional (arg: string) { + const maybeCoercedNumber = maybeCoerceNumber('_', arg) + if (typeof maybeCoercedNumber === 'string' || typeof maybeCoercedNumber === 'number') { + argv._.push(maybeCoercedNumber) + } + } + + // 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 (!configuration['parse-positional-numbers'] && key === '_') return value + if (!checkAllAliases(key, flags.strings) && !checkAllAliases(key, flags.bools) && !Array.isArray(value)) { + const shouldCoerceNumber = looksLikeNumber(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) { + // Deno will receive a PermissionDenied error if an attempt is + // made to load config without the --allow-read flag: + if (ex.name === 'PermissionDenied') error = ex + else 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 { + arg = arg.replace(/^-{3,}/, '--') + // 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 = { + [DefaultValuesForTypeKey.BOOLEAN]: true, + [DefaultValuesForTypeKey.STRING]: '', + [DefaultValuesForTypeKey.NUMBER]: undefined, + [DefaultValuesForTypeKey.ARRAY]: [] + } + + return def[type] + } + + // given a flag, enforce a default type. + function guessType (key: string): DefaultValuesForTypeKey { + let type: DefaultValuesForTypeKey = DefaultValuesForTypeKey.BOOLEAN + if (checkAllAliases(key, flags.strings)) type = DefaultValuesForTypeKey.STRING + else if (checkAllAliases(key, flags.numbers)) type = DefaultValuesForTypeKey.NUMBER + else if (checkAllAliases(key, flags.bools)) type = DefaultValuesForTypeKey.BOOLEAN + else if (checkAllAliases(key, flags.arrays)) type = DefaultValuesForTypeKey.ARRAY + return type + } + + 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 { + aliases: Object.assign({}, flags.aliases), + argv: Object.assign(argvReturn, argv), + configuration: configuration, + defaulted: Object.assign({}, defaulted), + error: error, + newAliases: Object.assign({}, newAliases) + } + } +} + +// 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 d025a7ef..f97aa9e5 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,37 @@ { "name": "yargs-parser", - "version": "5.0.0", + "version": "20.2.9", "description": "the mighty option parser used by yargs", - "main": "index.js", + "main": "build/index.cjs", + "exports": { + ".": [ + { + "import": "./build/lib/index.js", + "require": "./build/index.cjs" + }, + "./build/index.cjs" + ] + }, + "type": "module", + "module": "./build/lib/index.js", "scripts": { - "pretest": "standard", - "test": "nyc mocha test/*.js", - "coverage": "nyc report --reporter=text-lcov | coveralls", - "release": "standard-version" + "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", + "coverage": "c8 report --check-coverage", + "precompile": "rimraf build", + "compile": "tsc", + "postcompile": "npm run build:cjs", + "build:cjs": "rollup -c", + "prepare": "npm run compile" }, "repository": { - "url": "git@github.com:yargs/yargs-parser.git" + "type": "git", + "url": "https://github.com/yargs/yargs-parser.git" }, "keywords": [ "argument", @@ -26,18 +47,41 @@ "author": "Ben Coe ", "license": "ISC", "devDependencies": { - "chai": "^3.5.0", - "coveralls": "^2.11.12", - "mocha": "^3.0.1", - "nyc": "^10.0.0", - "standard": "^8.0.0", - "standard-version": "^4.0.0" - }, - "dependencies": { - "camelcase": "^3.0.0" + "@types/chai": "^4.2.11", + "@types/mocha": "^8.0.0", + "@types/node": "^14.0.0", + "@typescript-eslint/eslint-plugin": "^3.10.1", + "@typescript-eslint/parser": "^3.10.1", + "@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": "^3.0.0", + "mocha": "^9.0.0", + "puppeteer": "^10.0.0", + "rimraf": "^3.0.2", + "rollup": "^2.22.1", + "rollup-plugin-cleanup": "^3.1.1", + "serve": "^12.0.0", + "standardx": "^7.0.0", + "start-server-and-test": "^1.11.2", + "ts-transform-default-export": "^1.0.2", + "typescript": "^4.0.0" }, "files": [ - "lib", - "index.js" - ] + "browser.js", + "build", + "!*.d.ts" + ], + "engines": { + "node": ">=10" + }, + "standardx": { + "ignore": [ + "build" + ] + } } diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 00000000..fe643cbb --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,6 @@ +{ + "bootstrap-sha": "686ec15fb7e4a996763c60cf6661b2698f28dac4", + "packages": { + ".": {} + } +} diff --git a/renovate.json b/renovate.json new file mode 100644 index 00000000..f6dd45e1 --- /dev/null +++ b/renovate.json @@ -0,0 +1,8 @@ +{ + "extends": [ + "config:base" + ], + "pinVersions": false, + "rebaseStalePrs": true, + "gitAuthor": null +} diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 00000000..ff47101d --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,27 @@ +import cleanup from 'rollup-plugin-cleanup' +import ts from '@wessberg/rollup-plugin-ts' +import transformDefaultExport from 'ts-transform-default-export' + +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({ + transformers: ({ program }) => ({ + afterDeclarations: transformDefaultExport(program) + }) + }), + cleanup({ + comments: 'none', + extensions: ['*'] + }) + ] +} 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..f025ac62 --- /dev/null +++ b/test/deno/yargs-test.ts @@ -0,0 +1,65 @@ +/* global Deno */ + +import { + assertEquals +} from 'https://deno.land/std/testing/asserts.ts' +import parser from '../../deno.ts' + +// Parser: +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', () => { + const argv = parser(['--foo', 'bar'], { + alias: { + z: 'zoom' + }, + default: { + settings: jsonPath + }, + config: 'settings' + }) + assertEquals(argv.herp, 'derp') + assertEquals(argv.zoom, 55) + assertEquals(argv.zoom, 55) +}) + +// String utilities: +Deno.test('convert hyphenated string to camelcase', () => { + assertEquals(parser.camelCase('hello-world'), 'helloWorld') +}) + +Deno.test('convert camelcase string to hyphenated', () => { + assertEquals(parser.decamelize('helloWorld'), 'hello-world') +}) + +Deno.test('it detects strings that could be parsed as numbers', () => { + assertEquals(parser.looksLikeNumber('3293'), true) + assertEquals(parser.looksLikeNumber('0x10'), true) + assertEquals(parser.looksLikeNumber('0x10'), true) + + assertEquals(parser.looksLikeNumber('0100'), false) + assertEquals(parser.looksLikeNumber('apple'), false) +}) diff --git a/test/fixtures/config.json b/test/fixtures/config.json index 43376463..7065ae61 100644 --- a/test/fixtures/config.json +++ b/test/fixtures/config.json @@ -3,5 +3,14 @@ "z": 55, "foo": "baz", "version": "1.0.2", - "truthy": true + "truthy": true, + "toString": "method name", + "__proto__": { + "aaa": 99 + }, + "bar": { + "__proto__": { + "bbb": 100 + } + } } 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/string-utils.cjs b/test/string-utils.cjs new file mode 100644 index 00000000..081dd084 --- /dev/null +++ b/test/string-utils.cjs @@ -0,0 +1,39 @@ +/* global describe, it */ + +const { strictEqual } = require('assert') +const { camelCase, decamelize, looksLikeNumber } = require('../build/index.cjs') + +describe('string-utils', function () { + describe('camelCase', () => { + it('converts string with hypen in middle to camel case', () => { + strictEqual(camelCase('hello-world'), 'helloWorld') + }) + it('removes leading hyphens', () => { + strictEqual(camelCase('-goodnight-moon'), 'goodnightMoon') + }) + it('camelCase string stays as is', () => { + strictEqual(camelCase('iAmCamelCase'), 'iAmCamelCase') + }) + it('uppercase string with underscore to camel case', () => { + strictEqual(camelCase('NODE_VERSION'), 'nodeVersion') + }) + }) + describe('decamelize', () => { + it('adds hyphens back to camelcase string', () => { + strictEqual(decamelize('helloWorld'), 'hello-world') + }) + }) + describe('looksLikeNumber', () => { + it('it detects strings that could be parsed as numbers', () => { + strictEqual(looksLikeNumber('3293'), true) + strictEqual(looksLikeNumber('0x10'), true) + strictEqual(looksLikeNumber('.1'), true) + strictEqual(looksLikeNumber('0.1'), true) + strictEqual(looksLikeNumber('0.10'), true) + + strictEqual(looksLikeNumber('00.1'), false) + strictEqual(looksLikeNumber('0100'), false) + strictEqual(looksLikeNumber('apple'), false) + }) + }) +}) diff --git a/test/tokenize-arg-string.js b/test/tokenize-arg-string.js deleted file mode 100644 index 9b9f9a06..00000000 --- a/test/tokenize-arg-string.js +++ /dev/null @@ -1,40 +0,0 @@ -/* global describe, it */ - -var tokenizeArgString = require('../lib/tokenize-arg-string') - -require('chai').should() - -describe('TokenizeArgString', function () { - it('handles unquoted string', function () { - var args = tokenizeArgString('--foo 99') - args[0].should.equal('--foo') - args[1].should.equal('99') - }) - - it('handles quoted string with no spaces', function () { - var args = tokenizeArgString("--foo 'hello'") - args[0].should.equal('--foo') - args[1].should.equal('hello') - }) - - it('handles single quoted string with spaces', function () { - var args = tokenizeArgString("--foo 'hello world' --bar='foo bar'") - args[0].should.equal('--foo') - args[1].should.equal('hello world') - args[2].should.equal('--bar=foo bar') - }) - - it('handles double quoted string with spaces', function () { - var args = tokenizeArgString('--foo "hello world" --bar="foo bar"') - args[0].should.equal('--foo') - args[1].should.equal('hello world') - args[2].should.equal('--bar=foo bar') - }) - - it('handles quoted string with embeded quotes', function () { - var args = tokenizeArgString('--foo "hello \'world\'" --bar=\'foo "bar"\'') - args[0].should.equal('--foo') - args[1].should.equal('hello \'world\'') - args[2].should.equal('--bar=foo "bar"') - }) -}) diff --git a/test/tscc/.gitignore b/test/tscc/.gitignore new file mode 100644 index 00000000..a6c7c285 --- /dev/null +++ b/test/tscc/.gitignore @@ -0,0 +1 @@ +*.js diff --git a/test/tscc/lib b/test/tscc/lib new file mode 120000 index 00000000..58677ddb --- /dev/null +++ b/test/tscc/lib @@ -0,0 +1 @@ +../../lib \ No newline at end of file diff --git a/test/tscc/optimized.ts b/test/tscc/optimized.ts new file mode 100644 index 00000000..0efd1be7 --- /dev/null +++ b/test/tscc/optimized.ts @@ -0,0 +1,47 @@ +import { YargsParser } from './lib/yargs-parser' + +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: () => {} +}) + +// Workaround the need for proper nodejs typings and externs +// eslint-disable-next-line no-eval +const anyDeepEqual = eval('require("assert").strict.deepEqual') as any +function deepEqual (actual: T, expected: T, message?: string): void { + anyDeepEqual(actual, expected, message) +} + +// Quoted properties aren't allowed in these lint settings. +const FLAG_X = 'x' + +function runTests () { + deepEqual(parser.parse([]).argv, { _: [] }, 'empty argv') + deepEqual( + parser.parse([`--${FLAG_X}=42`]).argv, + { _: [], [FLAG_X]: 42 }, + 'guessed numeric value') + deepEqual( + parser.parse([`--${FLAG_X}=str`]).argv, + { _: [], [FLAG_X]: 'str' }, + 'guessed string value') + deepEqual( + parser.parse([`--no-${FLAG_X}`]).argv, + { _: [], [FLAG_X]: false }, + 'guessed boolean negation') + + // Default values for types: + deepEqual( + parser.parse([`--${FLAG_X}`]).argv, + { _: [], [FLAG_X]: true }, + 'guessed boolean default true') + + console.log('ok') +} +runTests() diff --git a/test/tscc/package.json b/test/tscc/package.json new file mode 100644 index 00000000..2ac7a8ca --- /dev/null +++ b/test/tscc/package.json @@ -0,0 +1,8 @@ +{ + "name": "optimized-test", + "version": "0.0.0", + "dependencies": { + "@tscc/tscc": "^0.6.4", + "@types/node": "^10.0.3" + } +} diff --git a/test/tscc/tscc.spec.json b/test/tscc/tscc.spec.json new file mode 100644 index 00000000..ea86963a --- /dev/null +++ b/test/tscc/tscc.spec.json @@ -0,0 +1,5 @@ +{ + "modules": { + "out": "optimized.ts" + } +} diff --git a/test/tscc/tsconfig.json b/test/tscc/tsconfig.json new file mode 100644 index 00000000..854aec18 --- /dev/null +++ b/test/tscc/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "es2017", + "moduleResolution": "node" + }, + "include": [ + "./*.ts", + "./lib/**/*.ts" + ] +} diff --git a/test/typescript/tokenize-arg-string.ts b/test/typescript/tokenize-arg-string.ts new file mode 100644 index 00000000..efaae775 --- /dev/null +++ b/test/typescript/tokenize-arg-string.ts @@ -0,0 +1,131 @@ +/* global describe, it */ +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') + strictEqual(args[0], '--foo') + strictEqual(args[1], '99') + }) + + it('handles unquoted numbers', function () { + const args = tokenizeArgString(['--foo', 9]) + strictEqual(args[0], '--foo') + strictEqual(args[1], '9') + }) + + it('handles quoted string with no spaces', function () { + const args = tokenizeArgString("--foo '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'") + strictEqual(args[0], '--foo') + strictEqual(args[1], "'hello world'") + strictEqual(args[2], "--bar='foo bar'") + }) + + it('handles double quoted string with spaces', function () { + const args = tokenizeArgString('--foo "hello world" --bar="foo bar"') + strictEqual(args[0], '--foo') + strictEqual(args[1], '"hello world"') + strictEqual(args[2], '--bar="foo bar"') + }) + + it('handles single quoted empty string', function () { + const args = tokenizeArgString('--foo \'\' --bar=\'\'') + strictEqual(args[0], '--foo') + strictEqual(args[1], "''") + strictEqual(args[2], "--bar=''") + }) + + it('handles double quoted empty string', function () { + const args = tokenizeArgString('--foo "" --bar=""') + strictEqual(args[0], '--foo') + strictEqual(args[1], '""') + strictEqual(args[2], '--bar=""') + }) + + it('handles quoted string with embedded quotes', function () { + const args = tokenizeArgString('--foo "hello \'world\'" --bar=\'foo "bar"\'') + strictEqual(args[0], '--foo') + strictEqual(args[1], '"hello \'world\'"') + strictEqual(args[2], '--bar=\'foo "bar"\'') + }) + + // https://github.com/yargs/yargs-parser/pull/100 + // https://github.com/yargs/yargs-parser/pull/106 + it('ignores unneeded spaces', function () { + const args = tokenizeArgString(' foo bar "foo bar" ') + strictEqual(args[0], 'foo') + strictEqual(args[1], 'bar') + strictEqual(args[2], '"foo bar"') + }) + + it('handles boolean options', function () { + const args = tokenizeArgString('--foo -bar') + strictEqual(args[0], '--foo') + strictEqual(args[1], '-bar') + }) + + it('handles empty string', function () { + const args = tokenizeArgString('') + strictEqual(args.length, 0) + }) + + it('handles array with unquoted string', function () { + const args = tokenizeArgString(['--foo', '99']) + strictEqual(args[0], '--foo') + strictEqual(args[1], '99') + }) + + it('handles array with quoted string with no spaces', function () { + const args = tokenizeArgString(['--foo', "'hello'"]) + strictEqual(args[0], '--foo') + strictEqual(args[1], "'hello'") + }) + + it('handles array with single quoted string with spaces', function () { + const args = tokenizeArgString(['--foo', "'hello world'", "--bar='foo bar'"]) + strictEqual(args[0], '--foo') + strictEqual(args[1], "'hello world'") + strictEqual(args[2], "--bar='foo bar'") + }) + + it('handles array with double quoted string with spaces', function () { + const args = tokenizeArgString(['--foo', '"hello world"', '--bar="foo bar"']) + strictEqual(args[0], '--foo') + strictEqual(args[1], '"hello world"') + strictEqual(args[2], '--bar="foo bar"') + }) + + it('handles array with single quoted empty string', function () { + const args = tokenizeArgString(['--foo', "''", "--bar=''"]) + strictEqual(args[0], '--foo') + strictEqual(args[1], "''") + strictEqual(args[2], "--bar=''") + }) + + it('handles array with double quoted empty string', function () { + const args = tokenizeArgString(['--foo', '""', '--bar=""']) + strictEqual(args[0], '--foo') + strictEqual(args[1], '""') + strictEqual(args[2], '--bar=""') + }) + + it('handles array with quoted string with embedded quotes', function () { + const args = tokenizeArgString(['--foo', '"hello \'world\'"', '--bar=\'foo "bar"\'']) + strictEqual(args[0], '--foo') + strictEqual(args[1], '"hello \'world\'"') + strictEqual(args[2], '--bar=\'foo "bar"\'') + }) + + it('handles array with boolean options', function () { + const args = tokenizeArgString(['--foo', '-bar']) + strictEqual(args[0], '--foo') + strictEqual(args[1], '-bar') + }) +}) diff --git a/test/typescript/types.ts b/test/typescript/types.ts new file mode 100644 index 00000000..39631057 --- /dev/null +++ b/test/typescript/types.ts @@ -0,0 +1,13 @@ +/* global describe, it */ + +import yargsParser from '../../lib/index.js' +import * as assert from 'assert' + +describe('types', function () { + it('allows a partial options object to be provided', () => { + const argv = yargsParser('--foo 99', { + string: 'foo' + }) + assert.strictEqual(argv.foo, '99') + }) +}) diff --git a/test/yargs-parser.cjs b/test/yargs-parser.cjs new file mode 100644 index 00000000..95bee24a --- /dev/null +++ b/test/yargs-parser.cjs @@ -0,0 +1,3999 @@ +/* global beforeEach, describe, it */ + +require('chai').should() + +const { expect } = require('chai') +const fs = require('fs') +const parser = require('../build/index.cjs') +const path = require('path') + +describe('yargs-parser', function () { + it('should parse a "short boolean"', function () { + const parse = parser(['-b']) + parse.should.not.have.property('--') + parse.should.have.property('b').to.be.ok.and.be.a('boolean') + parse.should.have.property('_').with.length(0) + }) + + it('should parse a "long boolean"', function () { + const parse = parser('--bool') + parse.should.not.have.property('--') + parse.should.have.property('bool', true) + parse.should.have.property('_').with.length(0) + }) + + it('should place bare options in the _ array', function () { + const parse = parser('foo bar baz') + parse.should.have.property('_').and.deep.equal(['foo', 'bar', 'baz']) + }) + + it('should set the value of the final option in a group to the next supplied value', function () { + const parse = parser(['-cats', 'meow']) + parse.should.have.property('c', true) + parse.should.have.property('a', true) + parse.should.have.property('t', true) + parse.should.have.property('s', 'meow') + parse.should.have.property('_').with.length(0) + }) + + it('should set the value of a single long option to the next supplied value', function () { + const parse = parser(['--pow', 'xixxle']) + parse.should.have.property('pow', 'xixxle') + parse.should.have.property('_').with.length(0) + }) + + it('should set the value of a single long option to the next supplied value, even if the value is empty', function () { + const parse = parser(['--pow', '']) + parse.should.have.property('pow', '') + parse.should.have.property('_').with.length(0) + }) + + it('should set the value of a single long option if an = was used', function () { + const parse = parser(['--pow=xixxle']) + parse.should.have.property('pow', 'xixxle') + parse.should.have.property('_').with.length(0) + }) + + it('should set the value of multiple long options to the next supplied values relative to each', function () { + const parse = parser(['--host', 'localhost', '--port', '555']) + parse.should.have.property('host', 'localhost') + parse.should.have.property('port', 555) + parse.should.have.property('_').with.length(0) + }) + + it('should set the value of multiple long options if = signs were used', function () { + const parse = parser(['--host=localhost', '--port=555']) + parse.should.have.property('host', 'localhost') + parse.should.have.property('port', 555) + parse.should.have.property('_').with.length(0) + }) + + it('should still set values appropriately if a mix of short, long, and grouped short options are specified', function () { + const parse = parser(['-h', 'localhost', '-fp', '555', 'script.js']) + parse.should.have.property('f', true) + parse.should.have.property('p', 555) + parse.should.have.property('h', 'localhost') + parse.should.have.property('_').and.deep.equal(['script.js']) + }) + + it('should still set values appropriately if a mix of short and long options are specified', function () { + const parse = parser(['-h', 'localhost', '--port', '555']) + parse.should.have.property('h', 'localhost') + parse.should.have.property('port', 555) + parse.should.have.property('_').with.length(0) + }) + + it('should explicitly set a boolean option to false if preceded by "--no-"', function () { + const parse = parser(['--no-moo']) + parse.should.have.property('moo', false) + parse.should.have.property('_').with.length(0) + }) + + it('should still set values appropriately if we supply a comprehensive list of various types of options', function () { + const parse = parser([ + '--name=meowmers', 'bare', '-cats', 'woo', + '-h', 'awesome', '--multi=quux', + '--key', 'value', + '-b', '--bool', '--no-meep', '--multi=baz', + '--', '--not-a-flag', '-', '-h', '-multi', '--', 'eek' + ], { + configuration: { + 'populate--': false + } + }) + parse.should.have.property('c', true) + parse.should.have.property('a', true) + parse.should.have.property('t', true) + parse.should.have.property('s', 'woo') + parse.should.have.property('h', 'awesome') + parse.should.have.property('b', true) + parse.should.have.property('bool', true) + parse.should.have.property('key', 'value') + parse.should.have.property('multi').and.deep.equal(['quux', 'baz']) + parse.should.have.property('meep', false) + parse.should.have.property('name', 'meowmers') + parse.should.have.property('_').and.deep.equal(['bare', '--not-a-flag', '-', '-h', '-multi', '--', 'eek']) + }) + + it('should parse numbers appropriately', function () { + const argv = parser([ + '-x', '1234', + '-y', '5.67', + '-z', '1e7', + '-w', '10f', + '--hex', '0xdeadbeef', + '789' + ]) + argv.should.have.property('x', 1234).and.be.a('number') + argv.should.have.property('y', 5.67).and.be.a('number') + argv.should.have.property('z', 1e7).and.be.a('number') + argv.should.have.property('w', '10f').and.be.a('string') + argv.should.have.property('hex', 0xdeadbeef).and.be.a('number') + argv.should.have.property('_').and.deep.equal([789]) + argv._[0].should.be.a('number') + }) + + // addresses: https://github.com/yargs/yargs-parser/issues/33 + it('should handle parsing negative #s', function () { + const argv = parser([ + '-33', '-177', '33', + '--n1', '-33', + '-n', '-44', + '--n2=-55', + '--foo.bar', '-33', + '-o=-55', + '--bounds', '-180', '99', '-180', '90', + '--other', '-99', '-220' + ], { + array: 'bounds', + narg: { other: 2 } + }) + + argv._.should.deep.equal([-33, -177, 33]) + argv.n1.should.equal(-33) + argv.n.should.equal(-44) + argv.n2.should.equal(-55) + argv.foo.bar.should.equal(-33) + argv.o.should.equal(-55) + argv.bounds.should.deep.equal([-180, 99, -180, 90]) + argv.other.should.deep.equal([-99, -220]) + }) + + it('should handle negative (and positive) numbers with decimal places, with or without a leading 0', function () { + const argv = parser([ + '-0.1', '-1.1', '-.5', '-.1', '.1', '.5', + '--bounds', '-5.1', '-.1', '.1', + '--other', '.9', '-.5' + ], { + array: 'bounds', + narg: { other: 2 } + }) + + argv._.should.deep.equal([-0.1, -1.1, -0.5, -0.1, 0.1, 0.5]) + argv.bounds.should.deep.equal([-5.1, -0.1, 0.1]) + argv.other.should.deep.equal([0.9, -0.5]) + }) + + it('should set the value of a single short option to the next supplied value, even if the value is empty', function () { + const parse = parser(['-p', '']) + parse.should.have.property('p', '') + parse.should.have.property('_').with.length(0) + }) + + it('should not set the next value as the value of a short option if that option is explicitly defined as a boolean', function () { + const parse = parser(['-t', 'moo'], { + boolean: 't' + }) + parse.should.have.property('t', true).and.be.a('boolean') + parse.should.have.property('_').and.deep.equal(['moo']) + }) + + it('should set boolean options values if the next value is "true" or "false"', function () { + const parse = parser(['--verbose', 'false', 'moo', '-t', 'true'], { + boolean: ['t', 'verbose'], + default: { + verbose: true + } + }) + parse.should.have.property('verbose', false).and.be.a('boolean') + parse.should.have.property('t', true).and.be.a('boolean') + parse.should.have.property('_').and.deep.equal(['moo']) + }) + + it('should not set boolean options values if the next value only contains the words "true" or "false"', function () { + const parse = parser(['--verbose', 'aaatrueaaa', 'moo', '-t', 'aaafalseaaa'], { + boolean: ['t', 'verbose'] + }) + parse.should.have.property('verbose', true).and.be.a('boolean') + parse.should.have.property('t', true).and.be.a('boolean') + parse.should.have.property('_').and.deep.equal(['aaatrueaaa', 'moo', 'aaafalseaaa']) + }) + + it('should not use next value for boolean/number/string configured with zero narg', function () { + const parse = parser(['--bool', 'false', '--nr', '7', '--str', 'foo'], { + boolean: ['bool'], + number: ['nr'], + string: ['str'], + narg: { bool: 0, nr: 0, str: 0 } + }) + parse.should.have.property('bool', true).and.be.a('boolean') + parse.should.have.property('nr', undefined).and.be.a('undefined') + parse.should.have.property('str', '').and.be.a('string') + parse.should.have.property('_').and.deep.equal(['false', 7, 'foo']) + }) + + it('should allow defining options as boolean in groups', function () { + const parse = parser(['-x', '-z', 'one', 'two', 'three'], { + boolean: ['x', 'y', 'z'] + }) + parse.should.have.property('x', true).and.be.a('boolean') + parse.should.not.have.property('y') + parse.should.have.property('z', true).and.be.a('boolean') + parse.should.have.property('_').and.deep.equal(['one', 'two', 'three']) + }) + + it('should correctly parse dot-notation boolean flags', function () { + const parse = parser(['--nested', '--n.v', '--n.y', 'foo'], { + boolean: ['nested', 'n.v'] + }) + + parse.should.have.property('nested', true).and.be.a('boolean') + parse.should.have.property('n').and.deep.equal({ + v: true, + y: 'foo' + }) + }) + + it('should preserve newlines in option values', function () { + let args = parser(['-s', 'X\nX']) + args.should.have.property('_').with.length(0) + args.should.have.property('s', 'X\nX') + // reproduce in bash: + // VALUE="new + // line" + // node program.js --s="$VALUE" + args = parser(['--s=X\nX']) + args.should.have.property('_').with.length(0) + args.should.have.property('s', 'X\nX') + }) + + it('should not convert numbers to type number if explicitly defined as strings', function () { + const s = parser(['-s', '0001234'], { + string: 's' + }).s + s.should.be.a('string').and.equal('0001234') + const x = parser(['-x', '56'], { + string: ['x'] + }).x + x.should.be.a('string').and.equal('56') + }) + + it('should default numbers to undefined', function () { + const n = parser(['-n'], { + number: ['n'] + }).n + expect(n).to.equal(undefined) + }) + + it('should default number to NaN if value is not a valid number', function () { + const n = parser(['-n', 'string'], { + number: ['n'] + }).n + expect(n).to.deep.equal(NaN) + }) + + // Fixes: https://github.com/bcoe/yargs/issues/68 + it('should parse flag arguments with no right-hand value as strings, if defined as strings', function () { + let s = parser(['-s'], { + string: ['s'] + }).s + s.should.be.a('string').and.equal('') + + s = parser(['-sf'], { + string: ['s'] + }).s + s.should.be.a('string').and.equal('') + + s = parser(['--string'], { + string: ['string'] + }).string + s.should.be.a('string').and.equal('') + }) + + it('should leave all non-hyphenated values as strings if _ is defined as a string', function () { + const s = parser([' ', ' '], { + string: ['_'] + })._ + s.should.have.length(2) + s[0].should.be.a('string').and.equal(' ') + s[1].should.be.a('string').and.equal(' ') + }) + + describe('normalize', function () { + it('should normalize redundant paths', function () { + const a = parser(['-s', ['', 'tmp', '..', ''].join(path.sep)], { + alias: { + s: ['save'] + }, + normalize: 's' + }) + a.should.have.property('s', path.sep) + a.should.have.property('save', path.sep) + }) + + it('should normalize redundant paths when a value is later assigned', function () { + const a = parser(['-s'], { + normalize: ['s'] + }) + a.should.have.property('s', true) + a.s = ['', 'path', 'to', 'new', 'dir', '..', '..', ''].join(path.sep) + a.s.should.equal(['', 'path', 'to', ''].join(path.sep)) + }) + + it('should normalize when key is also an array', function () { + const a = parser(['-s', ['', 'tmp', '..', ''].join(path.sep), ['', 'path', 'to', 'new', 'dir', '..', '..', ''].join(path.sep)], { + alias: { + s: ['save'] + }, + normalize: 's', + array: 's' + }) + const expected = [path.sep, ['', 'path', 'to', ''].join(path.sep)] + a.should.have.property('s').and.deep.equal(expected) + a.should.have.property('save').and.deep.equal(expected) + }) + + it('should allow normalized keys to be enumerated', () => { + const a = parser(['-s', ['', 'tmp', '..', ''].join(path.sep)], { + alias: { + s: ['save'] + }, + normalize: 's' + }) + Object.keys(a).should.include('s') + }) + }) + + describe('alias', function () { + it('should set alias value to the same value as the full option', function () { + const argv = parser(['-f', '11', '--zoom', '55'], { + alias: { + z: ['zoom'] + } + }) + argv.should.have.property('zoom', 55) + argv.should.have.property('z', 55) + argv.should.have.property('f', 11) + }) + + it('should allow multiple aliases to be specified', function () { + const argv = parser(['-f', '11', '--zoom', '55'], { + alias: { + z: ['zm', 'zoom'] + } + }) + + argv.should.have.property('zoom', 55) + argv.should.have.property('z', 55) + argv.should.have.property('zm', 55) + argv.should.have.property('f', 11) + }) + + // regression, see https://github.com/chevex/yargs/issues/63 + it('should not add the same key to argv multiple times, when creating camel-case aliases', function () { + const argv = parser(['--health-check=banana', '--second-key', 'apple', '-t=blarg'], { + alias: { + h: ['health-check'], + 'second-key': ['s'], + 'third-key': ['t'] + }, + default: { + h: 'apple', + 'second-key': 'banana', + 'third-key': 'third' + } + }) + + // before this fix, yargs failed parsing + // one but not all forms of an arg. + argv.secondKey.should.eql('apple') + argv.s.should.eql('apple') + argv['second-key'].should.eql('apple') + + argv.healthCheck.should.eql('banana') + argv.h.should.eql('banana') + argv['health-check'].should.eql('banana') + + argv.thirdKey.should.eql('blarg') + argv.t.should.eql('blarg') + argv['third-key'].should.eql('blarg') + }) + + it('should allow transitive aliases to be specified', function () { + const argv = parser(['-f', '11', '--zoom', '55'], { + alias: { + z: 'zm', + zm: 'zoom' + } + }) + + argv.should.have.property('zoom', 55) + argv.should.have.property('z', 55) + argv.should.have.property('zm', 55) + argv.should.have.property('f', 11) + }) + + it('should merge two lists of aliases if they collide', function () { + const argv = parser(['-f', '11', '--zoom', '55'], { + alias: { + z: 'zm', + zoom: 'zoop', + zm: 'zoom' + } + }) + + argv.should.have.property('zoom', 55) + argv.should.have.property('zoop', 55) + argv.should.have.property('z', 55) + argv.should.have.property('zm', 55) + argv.should.have.property('f', 11) + }) + + it('should set single-digit boolean alias', function () { + const argv = parser(['-f', '11', '-0'], { + alias: { + 0: 'print0' + }, + boolean: [ + 'print0' + ] + }) + argv.should.have.property('print0', true) + argv.should.have.property('f', 11) + }) + + it('should not set single-digit alias if no alias defined', function () { + const argv = parser(['-f', '11', '-0', '-1']) + argv.should.have.property('f', 11) + argv._.should.deep.equal([-0, -1]) + }) + + it('should not set single-digit boolean alias if no boolean defined', function () { + const argv = parser(['-f', '11', '-9'], { + alias: { + 0: 'print0' + } + }) + argv.should.have.property('f', 11) + argv._.should.deep.equal([-9]) + }) + + it('should be able to negate set single-digit boolean alias', function () { + const argv = parser(['--no-9'], { + alias: { + 9: 'max' + }, + boolean: [ + 'max' + ] + }) + argv.should.have.property('max', false) + }) + }) + + it('should assign data after forward slash to the option before the slash', function () { + let parse = parser(['-I/foo/bar/baz']) + parse.should.have.property('_').with.length(0) + parse.should.have.property('I', '/foo/bar/baz') + parse = parser(['-xyz/foo/bar/baz']) + parse.should.have.property('x', true) + parse.should.have.property('y', true) + parse.should.have.property('z', '/foo/bar/baz') + parse.should.have.property('_').with.length(0) + }) + + const jsonPath = path.resolve(__dirname, './fixtures/config.json') + describe('config', function () { + // See: https://github.com/chevex/yargs/issues/12 + it('should load options and values from default config if specified', function () { + const argv = parser(['--foo', 'bar'], { + alias: { + z: 'zoom' + }, + default: { + settings: jsonPath + }, + config: 'settings' + }) + + argv.should.have.property('herp', 'derp') + argv.should.have.property('zoom', 55) + argv.should.have.property('foo').and.deep.equal('bar') + }) + + it('should use value from config file, if argv value is using default value', function () { + const argv = parser([], { + alias: { + z: 'zoom' + }, + config: ['settings'], + default: { + settings: jsonPath, + foo: 'banana' + } + }) + + argv.should.have.property('herp', 'derp') + argv.should.have.property('zoom', 55) + argv.should.have.property('foo').and.deep.equal('baz') + }) + + it('should combine values from config file and argv, if argv value is an array', function () { + const argv = parser(['--foo', 'bar'], { + config: ['settings'], + array: ['foo'], + default: { + settings: jsonPath + }, + configuration: { + 'combine-arrays': true + } + }) + + argv.should.have.property('foo').and.deep.equal(['bar', 'baz']) + }) + + it('should use value from config file, if argv key is a boolean', function () { + const argv = parser([], { + config: ['settings'], + default: { + settings: jsonPath + }, + boolean: ['truthy'] + }) + + argv.should.have.property('truthy', true) + }) + + it('should use value from cli, if cli overrides boolean argv key', function () { + const argv = parser(['--no-truthy'], { + config: ['settings'], + default: { + settings: jsonPath + }, + boolean: ['truthy'] + }) + + argv.should.have.property('truthy', false) + }) + + it('should use cli value, if cli value is set and both cli and default value match', function () { + const argv = parser(['--foo', 'banana'], { + alias: { + z: 'zoom' + }, + config: ['settings'], + default: { + settings: jsonPath, + foo: 'banana' + } + }) + + argv.should.have.property('herp', 'derp') + argv.should.have.property('zoom', 55) + argv.should.have.property('foo').and.deep.equal('banana') + }) + + it("should allow config to be set as flag in 'option'", function () { + const argv = parser(['--settings', jsonPath, '--foo', 'bar'], { + alias: { + z: 'zoom' + }, + config: ['settings'] + }) + + argv.should.have.property('herp', 'derp') + argv.should.have.property('zoom', 55) + argv.should.have.property('foo').and.deep.equal('bar') + }) + + it('should load options and values from a JS file when config has .js extention', function () { + const jsPath = path.resolve(__dirname, './fixtures/settings.cjs') + const argv = parser(['--settings', jsPath, '--foo', 'bar'], { + config: ['settings'] + }) + + argv.should.have.property('herp', 'derp') + argv.should.have.property('foo', 'bar') + argv.should.have.property('calculate').and.be.a('function') + }) + + it('should raise an appropriate error if JSON file is not found', function () { + const argv = parser.detailed(['--settings', 'fake.json', '--foo', 'bar'], { + alias: { + z: 'zoom' + }, + config: ['settings'] + }) + + argv.error.message.should.equal('Invalid JSON config file: fake.json') + }) + + // see: https://github.com/bcoe/yargs/issues/172 + it('should not raise an exception if config file is set as default argument value', function () { + const argv = parser.detailed([], { + default: { + config: 'foo.json' + }, + config: ['config'] + }) + + expect(argv.error).to.equal(null) + }) + + it('should load nested options from config file', function () { + const jsonPath = path.resolve(__dirname, './fixtures/nested_config.json') + const argv = parser(['--settings', jsonPath, '--nested.foo', 'bar'], { + config: ['settings'] + }) + + argv.should.have.property('a', 'a') + argv.should.have.property('b', 'b') + argv.should.have.property('nested').and.deep.equal({ + foo: 'bar', + bar: 'bar' + }) + }) + + it('should use nested value from config file, if argv value is using default value', function () { + const jsonPath = path.resolve(__dirname, './fixtures/nested_config.json') + const argv = parser(['--settings', jsonPath], { + config: ['settings'], + default: { + 'nested.foo': 'banana' + } + }) + + argv.should.have.property('a', 'a') + argv.should.have.property('b', 'b') + argv.should.have.property('nested').and.deep.equal({ + foo: 'baz', + bar: 'bar' + }) + }) + + it('allows a custom parsing function to be provided', function () { + const jsPath = path.resolve(__dirname, './fixtures/config.txt') + const argv = parser(['--settings', jsPath, '--foo', 'bar'], { + config: { + settings: function (configPath) { + // as an example, parse an environment + // variable style config: + // FOO=99 + // BATMAN=grumpy + const config = {} + const txt = fs.readFileSync(configPath, 'utf-8') + txt.split(/\r?\n/).forEach(function (l) { + const kv = l.split('=') + config[kv[0].toLowerCase()] = kv[1] + }) + return config + } + } + }) + + argv.batman.should.equal('grumpy') + argv.awesome.should.equal('banana') + argv.foo.should.equal('bar') + }) + + it('allows a custom parsing function to be provided as an alias', function () { + const jsPath = path.resolve(__dirname, './fixtures/config.json') + const argv = parser(['--settings', jsPath, '--foo', 'bar'], { + config: { + s: function (configPath) { + return JSON.parse(fs.readFileSync(configPath, 'utf-8')) + } + }, + alias: { + s: ['settings'] + } + }) + + argv.should.have.property('herp', 'derp') + argv.should.have.property('foo', 'bar') + }) + + it('outputs an error returned by the parsing function', function () { + const argv = parser.detailed(['--settings=./package.json'], { + config: { + settings: function (configPath) { + return Error('someone set us up the bomb') + } + } + }) + + argv.error.message.should.equal('someone set us up the bomb') + }) + + it('outputs an error if thrown by the parsing function', function () { + const argv = parser.detailed(['--settings=./package.json'], { + config: { + settings: function (configPath) { + throw Error('someone set us up the bomb') + } + } + }) + + argv.error.message.should.equal('someone set us up the bomb') + }) + + it('should not pollute the prototype', function () { + const argv = parser(['--foo', 'bar'], { + alias: { + z: 'zoom' + }, + default: { + settings: jsonPath + }, + config: 'settings' + }) + + argv.should.have.property('herp', 'derp') + argv.should.have.property('zoom', 55) + argv.should.have.property('foo').and.deep.equal('bar') + + expect({}.bbb).to.equal(undefined) + expect({}.aaa).to.equal(undefined) + }) + }) + + describe('config objects', function () { + it('should load options from config object', function () { + const argv = parser(['--foo', 'bar'], { + configObjects: [{ + apple: 'apple', + banana: 42, + foo: 'baz', + gotcha: null + }] + }) + + argv.should.have.property('apple', 'apple') + argv.should.have.property('banana', 42) + argv.should.have.property('foo', 'bar') + argv.should.have.property('gotcha', null) + }) + + it('should use value from config object, if argv value is using default value', function () { + const argv = parser([], { + configObjects: [{ + apple: 'apple', + banana: 42, + foo: 'baz' + }], + default: { + foo: 'banana' + } + }) + + argv.should.have.property('apple', 'apple') + argv.should.have.property('banana', 42) + argv.should.have.property('foo', 'baz') + }) + + it('should use value from config object to all aliases', function () { + const argv = parser([], { + configObjects: [{ + apple: 'apple', + banana: 42, + foo: 'baz' + }], + alias: { + a: ['apple'], + banana: ['b'] + } + }) + + argv.should.have.property('apple', 'apple') + argv.should.have.property('a', 'apple') + argv.should.have.property('banana', 42) + argv.should.have.property('b', 42) + argv.should.have.property('foo', 'baz') + }) + + it('should load nested options from config object', function () { + const argv = parser(['--nested.foo', 'bar'], { + configObjects: [{ + a: 'a', + nested: { + foo: 'baz', + bar: 'bar' + }, + b: 'b' + }] + }) + + argv.should.have.property('a', 'a') + argv.should.have.property('b', 'b') + argv.should.have.property('nested').and.deep.equal({ + foo: 'bar', + bar: 'bar' + }) + }) + + it('should use nested value from config object, if argv value is using default value', function () { + const argv = parser([], { + configObjects: [{ + a: 'a', + nested: { + foo: 'baz', + bar: 'bar' + }, + b: 'b' + }], + default: { + 'nested.foo': 'banana' + } + }) + + argv.should.have.property('a', 'a') + argv.should.have.property('b', 'b') + argv.should.have.property('nested').and.deep.equal({ + foo: 'baz', + bar: 'bar' + }) + }) + + it('should load objects with first object having greatest priority', function () { + const argv = parser(['--foo', 'bar'], { + configObjects: [{ + bar: 'baz' + }, { + bar: 'quux', + foo: 'spam' + }] + }) + + argv.should.have.property('foo', 'bar') + argv.should.have.property('bar', 'baz') + }) + + it('should combine array typed options with alias and camel-case', function () { + const argv = parser(['--camEl', 'foo', '--camEl', 'bar', '-a', 'red'], { + array: ['cam-el', 'apple'], + alias: { apple: 'a' }, + configObjects: [{ camEl: 'baz' }, { a: 'sweet' }], + configuration: { + 'combine-arrays': true, + 'camel-case-expansion': true + } + }) + + argv['cam-el'].should.deep.equal(['foo', 'bar', 'baz']) + argv.apple.should.deep.equal(['red', 'sweet']) + }) + }) + + describe('dot notation', function () { + it('should allow object graph traversal via dot notation', function () { + const argv = parser([ + '--foo.bar', '3', '--foo.baz', '4', + '--foo.quux.quibble', '5', '--foo.quux.o_O', + '--beep.boop' + ]) + argv.should.have.property('foo').and.deep.equal({ + bar: 3, + baz: 4, + quux: { + quibble: 5, + o_O: true + } + }) + argv.should.have.property('beep').and.deep.equal({ boop: true }) + }) + + it('should apply defaults to dot notation arguments', function () { + const argv = parser([], { + default: { + 'foo.bar': 99 + } + }) + + argv.foo.bar.should.eql(99) + }) + + // see #279 + it('should allow default to be overridden when an alias is provided', function () { + const argv = parser(['--foo.bar', '200'], { + default: { + 'foo.bar': 99 + } + }) + + argv.foo.bar.should.eql(200) + }) + + // see #279 + it('should also override alias', function () { + const argv = parser(['--foo.bar', '200'], { + alias: { + 'foo.bar': ['f'] + }, + default: { + 'foo.bar': 99 + } + }) + + argv.f.should.eql(200) + }) + + // see #279 + it('should not set an undefined dot notation key', function () { + const argv = parser(['--foo.bar', '200'], { + default: { + 'foo.bar': 99 + }, + alias: { + 'foo.bar': ['f'] + } + }) + + ; ('foo.bar' in argv).should.equal(false) + }) + + it('should respect .string() for dot notation arguments', function () { + const argv = parser(['--foo.bar', '99', '--bar.foo=99'], { + string: ['foo.bar'] + }) + + argv.foo.bar.should.eql('99') + argv.bar.foo.should.eql(99) + }) + + it('should populate aliases when dot notation is used', function () { + const argv = parser(['--foo.bar', '99'], { + alias: { + foo: ['f'] + } + }) + + argv.f.bar.should.eql(99) + argv.foo.bar.should.eql(99) + }) + + // see #267 + it('should populate aliases when dot notation is used on camel-cased option', function () { + const argv = parser(['--foo-baz.bar', '99'], { + alias: { + 'foo-baz': ['f'] + } + }) + + argv.f.bar.should.eql(99) + argv['foo-baz'].bar.should.eql(99) + argv.fooBaz.bar.should.eql(99) + }) + + it('should populate aliases when nested dot notation is used', function () { + const argv = parser(['--foo.bar.snuh', '99', '--foo.apple', '33', '--foo.bar.cool', '11'], { + alias: { + foo: ['f'] + } + }) + + argv.f.bar.snuh.should.eql(99) + argv.foo.bar.snuh.should.eql(99) + + argv.f.apple.should.eql(33) + argv.foo.apple.should.eql(33) + + argv.f.bar.cool.should.eql(11) + argv.foo.bar.cool.should.eql(11) + }) + + it("should allow flags to use dot notation, when separated by '='", function () { + const argv = parser(['-f.foo=99']) + argv.f.foo.should.eql(99) + }) + + it("should allow flags to use dot notation, when separated by ' '", function () { + const argv = parser(['-f.foo', '99']) + argv.f.foo.should.eql(99) + }) + + it('should allow flags to use dot notation when no right-hand-side is given', function () { + const argv = parser(['-f.foo', '99', '-f.bar']) + argv.f.foo.should.eql(99) + argv.f.bar.should.eql(true) + }) + + it('should not pollute the prototype', function () { + parser(['-f.__proto__.foo', '99', '-x.y.__proto__.bar', '100', '--__proto__', '200']) + Object.keys({}.__proto__).length.should.equal(0) // eslint-disable-line + expect({}.foo).to.equal(undefined) + expect({}.bar).to.equal(undefined) + }) + }) + + it('should set boolean and alias using explicit true', function () { + const aliased = ['-h', 'true'] + const aliasedArgv = parser(aliased, { + boolean: ['h'], + alias: { + h: ['herp'] + } + }) + + aliasedArgv.should.have.property('herp', true) + aliasedArgv.should.have.property('h', true) + aliasedArgv.should.have.property('_').with.length(0) + }) + + // regression, see https://github.com/substack/node-optimist/issues/71 + it('should set boolean and --x=true', function () { + let parsed = parser(['--boool', '--other=true'], { + boolean: ['boool'] + }) + parsed.should.have.property('boool', true) + parsed.should.have.property('other', 'true') + parsed = parser(['--boool', '--other=false'], { + boolean: ['boool'] + }) + parsed.should.have.property('boool', true) + parsed.should.have.property('other', 'false') + }) + + // regression, see https://github.com/chevex/yargs/issues/66 + it('should set boolean options values if next value is "true" or "false" with = as separator', function () { + const argv = parser(['--bool=false'], { + boolean: ['b'], + alias: { + b: ['bool'] + }, + default: { + b: true + } + }) + + argv.bool.should.eql(false) + }) + + describe('short options', function () { + it('should set the value of multiple single short options to the next supplied values relative to each', function () { + const parse = parser(['-h', 'localhost', '-p', '555']) + parse.should.have.property('h', 'localhost') + parse.should.have.property('p', 555) + parse.should.have.property('_').with.length(0) + }) + + it('should set the value of a single short option to the next supplied value', function () { + const parse = parser(['-h', 'localhost']) + parse.should.have.property('h', 'localhost') + parse.should.have.property('_').with.length(0) + }) + + it('should expand grouped short options to a hash with a key for each', function () { + const parse = parser(['-cats']) + parse.should.have.property('c', true) + parse.should.have.property('a', true) + parse.should.have.property('t', true) + parse.should.have.property('s', true) + parse.should.have.property('_').with.length(0) + }) + + it('should set n to the numeric value 123', function () { + const argv = parser(['-n123']) + argv.should.have.property('n', 123) + }) + + it('should set n to the numeric value 123, with n at the end of a group', function () { + const argv = parser(['-ab5n123']) + argv.should.have.property('a', true) + argv.should.have.property('b', true) + argv.should.have.property('5', true) + argv.should.have.property('n', 123) + argv.should.have.property('_').with.length(0) + }) + + it('should set n to the numeric value 123, with = as separator', function () { + const argv = parser(['-n=123']) + argv.should.have.property('n', 123) + }) + + it('should set n to the numeric value 123, with n at the end of a group and = as separator', function () { + const argv = parser(['-ab5n=123']) + argv.should.have.property('a', true) + argv.should.have.property('b', true) + argv.should.have.property('5', true) + argv.should.have.property('n', 123) + argv.should.have.property('_').with.length(0) + }) + }) + + describe('whitespace', function () { + it('should be whitespace', function () { + const argv = parser(['-x', '\t']) + argv.should.have.property('x', '\t') + }) + }) + + describe('boolean modifier function', function () { + it('should prevent yargs from sucking in the next option as the value of the first option', function () { + // Arrange & Act + const result = parser(['-b', '123'], { + boolean: ['b'] + }) + // Assert + result.should.have.property('b').that.is.a('boolean').and.is.true // eslint-disable-line + result.should.have.property('_').and.deep.equal([123]) + }) + + // Fixes: https://github.com/yargs/yargs-parser/issues/283 + it('should set boolean numeric option, with numeric option at the end of a group', function () { + const result = parser(['-x1'], { boolean: ['x', '1'] }) + expect(result).to.have.property('x', true) + expect(result).to.have.property('1', true) + }) + + it('should set boolean numeric option, with numeric option at the start of a group', function () { + const result = parser(['-1x'], { boolean: ['x', '1'] }) + expect(result).to.have.property('x', true) + expect(result).to.have.property('1', true) + }) + + it('should set boolean numeric option, with numeric option as part of a group', function () { + const result = parser(['-x1b'], { boolean: ['x', '1', 'b'] }) + expect(result).to.have.property('x', true) + expect(result).to.have.property('1', true) + expect(result).to.have.property('b', true) + }) + }) + + describe('defaults', function () { + function checkNoArgs (opts, hasAlias) { + it('should set defaults if no args', function () { + const result = parser([], opts) + result.should.have.property('flag', true) + if (hasAlias) { + result.should.have.property('f', true) + } + }) + } + + function checkExtraArg (opts, hasAlias) { + it('should set defaults if one extra arg', function () { + const result = parser(['extra'], opts) + result.should.have.property('flag', true) + result.should.have.property('_').and.deep.equal(['extra']) + if (hasAlias) { + result.should.have.property('f', true) + } + }) + } + + function checkStringArg (opts, hasAlias) { + it('should set defaults even if arg looks like a string', function () { + const result = parser(['--flag', 'extra'], opts) + result.should.have.property('flag', true) + result.should.have.property('_').and.deep.equal(['extra']) + if (hasAlias) { + result.should.have.property('f', true) + } + }) + } + + describe('for options with aliases', function () { + const opts = { + alias: { + flag: ['f'] + }, + default: { + flag: true + } + } + + checkNoArgs(opts, true) + checkExtraArg(opts, true) + }) + + describe('for typed options without aliases', function () { + const opts = { + boolean: ['flag'], + default: { + flag: true + } + } + + checkNoArgs(opts) + checkExtraArg(opts) + checkStringArg(opts) + }) + + describe('for typed options with aliases', function () { + const opts = { + alias: { + flag: ['f'] + }, + boolean: ['flag'], + default: { + flag: true + } + } + + checkNoArgs(opts, true) + checkExtraArg(opts, true) + checkStringArg(opts, true) + }) + + describe('for boolean options', function () { + [true, false, undefined, null].forEach(function (def) { + describe('with explicit ' + def + ' default', function () { + const opts = { + default: { + flag: def + }, + boolean: ['flag'] + } + + it('should set true if --flag in arg', function () { + parser(['--flag'], opts).flag.should.be.true // eslint-disable-line + }) + + it('should set false if --no-flag in arg', function () { + parser(['--no-flag'], opts).flag.should.be.false // eslint-disable-line + }) + + it('should set ' + def + ' if no flag in arg', function () { + expect(parser([], opts).flag).to.equal(def) + }) + }) + }) + + describe('without any default value', function () { + let opts = null + + beforeEach(function () { + opts = { + boolean: ['flag'] + } + }) + + it('should set true if --flag in arg', function () { + parser(['--flag'], opts).flag.should.be.true // eslint-disable-line + }) + + it('should set false if --no-flag in arg', function () { + parser(['--no-flag'], opts).flag.should.be.false // eslint-disable-line + }) + + it('should not add property if no flag in arg', function () { + parser([''], opts).should.not.have.property('flag') + }) + }) + + // Fixes: https://github.com/bcoe/yargs/issues/341 + it('should apply defaults to camel-case form of argument', function () { + const argv = parser([], { + default: { + 'foo-bar': 99 + } + }) + + argv.fooBar.should.equal(99) + }) + + // Fixes: https://github.com/yargs/yargs-parser/issues/77 + it('should combine dot-notation and camel-case expansion', function () { + const argv = parser(['--dot-notation.foo.bar']) + + argv.should.satisfy(function (args) { + return args.dotNotation.foo.bar + }) + }) + }) + + it('should define option as boolean and set default to true', function () { + const argv = parser([], { + boolean: ['sometrue'], + default: { + sometrue: true + } + }) + argv.should.have.property('sometrue', true) + }) + + it('should define option as boolean and set default to false', function () { + const argv = parser([], { + default: { + somefalse: false + }, + boolean: ['somefalse'] + }) + argv.should.have.property('somefalse', false) + }) + + it('should set boolean options to false by default', function () { + const parse = parser(['moo'], { + boolean: ['t', 'verbose'], + default: { + verbose: false, + t: false + } + }) + parse.should.have.property('verbose', false).and.be.a('boolean') + parse.should.have.property('t', false).and.be.a('boolean') + parse.should.have.property('_').and.deep.equal(['moo']) + }) + + describe('track defaulted', function () { + it('should log defaulted options - not specified by user', function () { + const parsed = parser.detailed('', { + default: { foo: 'abc', 'bar.prop': 33, baz: 'x' }, + configObjects: [{ baz: 'xyz' }] + }) + expect(parsed.argv).to.eql({ _: [], baz: 'xyz', foo: 'abc', bar: { prop: 33 } }) + parsed.defaulted.should.deep.equal({ foo: true, 'bar.prop': true }) + }) + + it('should not log defaulted options - specified without value', function () { + const parsed = parser.detailed('--foo --bar.prop', { + default: { foo: 'abc', 'bar.prop': 33 } + }) + parsed.argv.should.deep.equal({ _: [], foo: 'abc', bar: { prop: 33 } }) + parsed.defaulted.should.deep.equal({}) + }) + + it('should log defaulted options - no aliases included', function () { + const parsed = parser.detailed('', { + default: { kaa: 'abc' }, + alias: { foo: 'kaa' } + }) + parsed.argv.should.deep.equal({ _: [], kaa: 'abc', foo: 'abc' }) + parsed.defaulted.should.deep.equal({ kaa: true }) + }) + + it('setting an alias excludes associated key from defaulted', function () { + const parsed = parser.detailed('--foo abc', { + default: { kaa: 'abc' }, + alias: { foo: 'kaa' } + }) + parsed.argv.should.deep.equal({ _: [], kaa: 'abc', foo: 'abc' }) + parsed.defaulted.should.deep.equal({}) + }) + }) + }) + + describe('camelCase', function () { + function runTests (strict) { + if (!strict) { + // Skip this test in strict mode because this option is not specified + it('should provide options with dashes as camelCase properties', function () { + const result = parser(['--some-option']) + + result.should.have.property('some-option').that.is.a('boolean').and.is.true // eslint-disable-line + result.should.have.property('someOption').that.is.a('boolean').and.is.true // eslint-disable-line + }) + } + + it('should provide count options with dashes as camelCase properties', function () { + const result = parser(['--some-option', '--some-option', '--some-option'], { + count: ['some-option'] + }) + + result.should.have.property('some-option', 3) + result.should.have.property('someOption', 3) + }) + + it('should provide options with dashes and aliases as camelCase properties', function () { + const result = parser(['--some-option'], { + alias: { + 'some-horse': 'o' + } + }) + + result.should.have.property('some-option').that.is.a('boolean').and.is.true // eslint-disable-line + result.should.have.property('someOption').that.is.a('boolean').and.is.true // eslint-disable-line + }) + + it('should provide defaults of options with dashes as camelCase properties', function () { + const result = parser([], { + default: { + 'some-option': 'asdf' + } + }) + + result.should.have.property('some-option', 'asdf') + result.should.have.property('someOption', 'asdf') + }) + + it('should provide aliases of options with dashes as camelCase properties', function () { + const result = parser([], { + default: { + 'some-option': 'asdf' + }, + alias: { + 'some-option': ['o'] + } + }) + + result.should.have.property('o', 'asdf') + result.should.have.property('some-option', 'asdf') + result.should.have.property('someOption', 'asdf') + }) + + it('should not apply camel-case logic to 1-character options', function () { + const result = parser(['-p', 'hello'], { + alias: { + p: 'parallel', + P: 'parallel-series' + } + }) + + result.should.not.have.property('P', 'hello') + result.should.not.have.property('parallel-series', 'hello') + result.should.not.have.property('parallelSeries', 'hello') + result.should.have.property('parallel', 'hello') + result.should.have.property('p', 'hello') + }) + + it('should provide aliases of options with dashes as camelCase properties', function () { + const result = parser([], { + alias: { + o: ['some-option'] + }, + default: { + o: 'asdf' + } + }) + + result.should.have.property('o', 'asdf') + result.should.have.property('some-option', 'asdf') + result.should.have.property('someOption', 'asdf') + }) + + it('should provide aliases with dashes as camelCase properties', function () { + const result = parser(['--some-option', 'val'], { + alias: { + o: 'some-option' + } + }) + + result.should.have.property('o').that.is.a('string').and.equals('val') + result.should.have.property('some-option').that.is.a('string').and.equals('val') + result.should.have.property('someOption').that.is.a('string').and.equals('val') + }) + + // https://github.com/yargs/yargs-parser/issues/95 + it('should not duplicate option values when equivalent dashed aliases are provided', function () { + const result = parser(['--someOption', 'val'], { + alias: { + someOption: 'some-option' + } + }) + + result.should.have.property('some-option').that.is.a('string').and.equals('val') + result.should.have.property('someOption').that.is.a('string').and.equals('val') + }) + } + + describe('dashes and camelCase', function () { + runTests() + }) + + describe('dashes and camelCase (strict)', function () { + runTests(true) + }) + }) + + describe('-', function () { + it('should set - as value of n', function () { + const argv = parser(['-n', '-']) + argv.should.have.property('n', '-') + argv.should.have.property('_').with.length(0) + }) + + it('should set - as a non-hyphenated value', function () { + const argv = parser(['-']) + argv.should.have.property('_').and.deep.equal(['-']) + }) + + it('should set - as a value of f', function () { + const argv = parser(['-f-']) + argv.should.have.property('f', '-') + argv.should.have.property('_').with.length(0) + }) + + it('should set b to true and set - as a non-hyphenated value when b is set as a boolean', function () { + const argv = parser(['-b', '-'], { + boolean: ['b'] + }) + + argv.should.have.property('b', true) + argv.should.have.property('_').and.deep.equal(['-']) + }) + + it('should set - as the value of s when s is set as a string', function () { + const argv = parser(['-s', '-'], { + string: ['s'] + }) + + argv.should.have.property('s', '-') + argv.should.have.property('_').with.length(0) + }) + }) + + describe('count', function () { + it('should count the number of times a boolean is present', function () { + let parsed + + parsed = parser(['-x'], { + count: ['verbose'] + }) + parsed.verbose.should.equal(0) + + parsed = parser(['--verbose'], { + count: ['verbose'] + }) + parsed.verbose.should.equal(1) + + parsed = parser(['--verbose', '--verbose'], { + count: ['verbose'] + }) + parsed.verbose.should.equal(2) + + parsed = parser(['-vvv'], { + alias: { + v: ['verbose'] + }, + count: ['verbose'] + }) + parsed.verbose.should.equal(3) + + parsed = parser(['--verbose', '--verbose', '-v', '--verbose'], { + count: ['verbose'], + alias: { + v: ['verbose'] + } + }) + parsed.verbose.should.equal(4) + + parsed = parser(['--verbose', '--verbose', '-v', '-vv'], { + count: ['verbose'], + alias: { + v: ['verbose'] + } + }) + parsed.verbose.should.equal(5) + }) + + it('should not consume the next argument', function () { + let parsed = parser(['-v', 'moo'], { + count: 'v' + }) + parsed.v.should.equal(1) + parsed.should.have.property('_').and.deep.equal(['moo']) + + parsed = parser(['--verbose', 'moomoo', '--verbose'], { + count: 'verbose' + }) + parsed.verbose.should.equal(2) + parsed.should.have.property('_').and.deep.equal(['moomoo']) + }) + + it('should use a default value as is when no arg given', function () { + let parsed = parser([], { + count: 'v', + default: { v: 3 } + }) + parsed.v.should.equal(3) + + parsed = parser([], { + count: 'v', + default: { v: undefined } + }) + expect(parsed.v).to.be.undefined // eslint-disable-line + + parsed = parser([], { + count: 'v', + default: { v: null } + }) + expect(parsed.v).to.be.null // eslint-disable-line + + parsed = parser([], { + count: 'v', + default: { v: false } + }) + parsed.v.should.equal(false) + + parsed = parser([], { + count: 'v', + default: { v: 'hello' } + }) + parsed.v.should.equal('hello') + }) + + it('should ignore a default value when arg given', function () { + const parsed = parser(['-vv', '-v', '-v'], { + count: 'v', + default: { v: 1 } + }) + parsed.v.should.equal(4) + }) + + it('should increment regardless of arg value', function () { + const parsed = parser([ + '-v', + '-v=true', + '-v', 'true', + '-v=false', + '-v', 'false', + '--no-v', + '-v=999', + '-v=foobar' + ], { count: 'v' }) + parsed.v.should.equal(8) + }) + + it('should add an error if counter is also set as array', function () { + const argv = parser.detailed(['--counter', '--counter', '--counter'], { + count: ['counter'], + array: ['counter'] + }) + + argv.error.message.should.equal('Invalid configuration: counter, opts.count excludes opts.array.') + }) + + it('should add an error if counter is also set as narg', function () { + const argv = parser.detailed(['--counter', 'foo', 'bar'], { + count: ['counter'], + narg: { counter: 2 } + }) + + argv.error.message.should.equal('Invalid configuration: counter, opts.count excludes opts.narg.') + }) + }) + + describe('array', function () { + it('should group values into an array if the same option is specified multiple times (duplicate-arguments-array=true)', function () { + const parse = parser(['-v', 'a', '-v', 'b', '-v', 'c'], { configuration: { 'duplicate-arguments-array': true } }) + parse.should.have.property('v').and.deep.equal(['a', 'b', 'c']) + parse.should.have.property('_').with.length(0) + }) + it('should keep only the last value if the same option is specified multiple times (duplicate-arguments-false)', function () { + const parse = parser(['-v', 'a', '-v', 'b', '-v', 'c'], { configuration: { 'duplicate-arguments-array': false } }) + parse.should.have.property('v').and.equal('c') + parse.should.have.property('_').with.length(0) + }) + + it('should default an array to an empty array if passed as first option followed by another', function () { + const result = parser(['-a', '-b'], { + array: 'a' + }) + result.should.have.property('a').and.deep.equal([]) + }) + + it('should not attempt to default array if an element has already been populated', function () { + const result = parser(['-a', 'foo', 'bar', '-b'], { + array: 'a' + }) + result.should.have.property('a').and.deep.equal(['foo', 'bar']) + }) + + it('should default argument to empty array if no value given', function () { + const result = parser(['-b', '--tag'], { + array: ['b', 'tag'], + default: { tag: [] } + }) + result.b.should.deep.equal([]) + result.tag.should.deep.equal([]) + }) + + it('should place default of argument in array, when default provided', function () { + const result = parser(['-b', '--tag'], { + array: ['b', 'tag'], + default: { b: 33, tag: ['foo'] } + }) + result.b.should.deep.equal([33]) + result.tag.should.deep.equal(['foo']) + }) + + it('should place value of argument in array, when one argument provided', function () { + const result = parser(['-b', '33'], { + array: ['b'] + }) + Array.isArray(result.b).should.equal(true) + result.b[0].should.equal(33) + }) + + it('should add multiple argument values to the array', function () { + const result = parser(['-b', '33', '-b', 'hello'], { + array: 'b' + }) + Array.isArray(result.b).should.equal(true) + result.b.should.include(33) + result.b.should.include('hello') + }) + + it('should allow array: true, to be set inside an option block', function () { + const result = parser(['-b', '33'], { + array: 'b' + }) + Array.isArray(result.b).should.equal(true) + result.b.should.include(33) + }) + + // issue #103 + it('should default camel-case alias to array type', function () { + const result = parser(['--ca-path', 'http://www.example.com'], { + array: ['ca-path'] + }) + + Array.isArray(result['ca-path']).should.equal(true) + Array.isArray(result.caPath).should.equal(true) + }) + + it('should default alias to array type', function () { + const result = parser(['--ca-path', 'http://www.example.com'], { + array: 'ca-path', + alias: { + 'ca-path': 'c' + } + }) + + Array.isArray(result['ca-path']).should.equal(true) + Array.isArray(result.caPath).should.equal(true) + Array.isArray(result.c).should.equal(true) + }) + + // see: https://github.com/bcoe/yargs/issues/162 + it('should eat non-hyphenated arguments until hyphenated option is hit', function () { + const result = parser(['-a=hello', 'world', '-b', + '33', '22', '--foo', 'red', 'green', + '--bar=cat', 'dog'], { + array: ['a', 'b', 'foo', 'bar'] + }) + + Array.isArray(result.a).should.equal(true) + result.a.should.include('hello') + result.a.should.include('world') + + Array.isArray(result.b).should.equal(true) + result.b.should.include(33) + result.b.should.include(22) + + Array.isArray(result.foo).should.equal(true) + result.foo.should.include('red') + result.foo.should.include('green') + + Array.isArray(result.bar).should.equal(true) + result.bar.should.include('cat') + result.bar.should.include('dog') + }) + + // see: https://github.com/yargs/yargs-parser/pull/13 + it('should support array for --foo= format when the key is a number', function () { + const result = parser(['--1=a', 'b'], { + array: ['1'] + }) + + Array.isArray(result['1']).should.equal(true) + result['1'][0].should.equal('a') + result['1'][1].should.equal('b') + }) + + it('should support array for -f= and --bar= format when the value is dashed', function () { + const result = parser(['-f=--dog', 'cat', '--bar=--red', 'green'], { + array: ['f', 'bar'] + }) + + Array.isArray(result.f).should.equal(true) + result.f[0].should.equal('--dog') + result.f[1].should.equal('cat') + + Array.isArray(result.bar).should.equal(true) + result.bar[0].should.equal('--red') + result.bar[1].should.equal('green') + }) + + it('should create an array when passing an argument twice with same value', function () { + const result = parser(['-x', 'val1', '-x', 'val1']) + result.should.have.property('x').that.is.an('array').and.to.deep.equal(['val1', 'val1']) + }) + + it('should eat camelCase switch with camelCase array option', function () { + const result = parser(['--someOption', '1', '2'], { + array: ['someOption'] + }) + Array.isArray(result.someOption).should.equal(true) + result.someOption.should.deep.equal([1, 2]) + }) + it('should eat hyphenated switch with hyphenated array option', function () { + const result = parser(['--some-option', '1', '2'], { + array: ['some-option'] + }) + Array.isArray(result['some-option']).should.equal(true) + result['some-option'].should.deep.equal([1, 2]) + }) + it('should eat camelCase switch with hyphenated array option', function () { + const result = parser(['--someOption', '1', '2'], { + array: ['some-option'] + }) + Array.isArray(result['some-option']).should.equal(true) + result['some-option'].should.deep.equal([1, 2]) + }) + it('should eat hyphenated switch with camelCase array option', function () { + const result = parser(['--some-option', '1', '2'], { + array: ['someOption'] + }) + Array.isArray(result.someOption).should.equal(true) + result.someOption.should.deep.equal([1, 2]) + }) + + // see https://github.com/yargs/yargs-parser/issues/6 + it('should respect the type `boolean` option for arrays', function () { + const result = parser(['-x=true', 'false'], { + array: [{ key: 'x', boolean: true }] + }) + result.should.have.property('x').that.is.an('array').and.to.deep.equal([true, false]) + }) + + it('should respect type `boolean` without value for arrays', function () { + const result = parser(['-x', '-x'], { + array: [{ key: 'x', boolean: true }], + configuration: { 'flatten-duplicate-arrays': false } + }) + result.x.should.deep.equal([[true], [true]]) + }) + + it('should respect `boolean negation` for arrays', function () { + const result = parser(['--no-bool', '--no-bool'], { + array: [{ key: 'bool', boolean: true }], + configuration: { 'flatten-duplicate-arrays': false } + }) + result.bool.should.deep.equal([[false], [false]]) + }) + + it('should respect the type `number` option for arrays', function () { + const result = parser(['-x=5', '2'], { + array: [{ key: 'x', number: true }] + }) + result.should.have.property('x').that.is.an('array').and.to.deep.equal([5, 2]) + }) + + it('should respect the type `string` option for arrays', function () { + const result = parser(['-x=5', '2'], { + configuration: { + 'parse-numbers': true + }, + array: [{ key: 'x', string: true }] + }) + result.should.have.property('x').that.is.an('array').and.to.deep.equal(['5', '2']) + }) + + it('should eat non-hyphenated arguments until hyphenated option is hit - combined with coercion', function () { + const result = parser([ + '-a=hello', 'world', + '-b', '33', '22', + '--foo', 'true', 'false', + '--bar=cat', 'dog' + ], { + array: [ + 'a', + { key: 'b', integer: true }, + { key: 'foo', boolean: true }, + 'bar' + ] + }) + + Array.isArray(result.a).should.equal(true) + result.a.should.include('hello') + result.a.should.include('world') + + Array.isArray(result.b).should.equal(true) + result.b.should.include(33) + result.b.should.include(22) + + Array.isArray(result.foo).should.equal(true) + result.foo.should.include(true) + result.foo.should.include(false) + + Array.isArray(result.bar).should.equal(true) + result.bar.should.include('cat') + result.bar.should.include('dog') + }) + }) + + describe('nargs', function () { + it('should allow the number of arguments following a key to be specified', function () { + const result = parser(['--foo', 'apple', 'bar'], { + narg: { + foo: 2 + } + }) + + Array.isArray(result.foo).should.equal(true) + result.foo[0].should.equal('apple') + result.foo[1].should.equal('bar') + }) + + it('should raise an exception if -f== format is used for a key with no expected argument', function () { + const argv = parser.detailed('-f=apple', { + narg: { + f: 0 + } + }) + argv.error.message.should.equal('Argument unexpected for: f') + }) + + it('should raise an exception if --bar== format is used for a key with no expected argument', function () { + const argv = parser.detailed('--bar=apple', { + narg: { + bar: 0 + } + }) + argv.error.message.should.equal('Argument unexpected for: bar') + }) + + it('should raise an exception if there are not enough arguments following key', function () { + const argv = parser.detailed('--foo apple', { + narg: { + foo: 2 + } + }) + argv.error.message.should.equal('Not enough arguments following: foo') + }) + + it('nargs is applied to aliases', function () { + const result = parser(['--bar', 'apple', 'bar'], { + narg: { + foo: 2 + }, + alias: { + foo: 'bar' + } + }) + Array.isArray(result.foo).should.equal(true) + result.foo[0].should.equal('apple') + result.foo[1].should.equal('bar') + }) + + it('should apply nargs to flag arguments', function () { + const result = parser(['-f', 'apple', 'bar', 'blerg'], { + narg: { + f: 2 + } + }) + + result.f[0].should.equal('apple') + result.f[1].should.equal('bar') + result._[0].should.equal('blerg') + }) + + it('should support nargs for -f= and --bar= format arguments', function () { + const result = parser(['-f=apple', 'bar', 'blerg', '--bar=monkey', 'washing', 'cat'], { + narg: { + f: 2, + bar: 2 + } + }) + + result.f[0].should.equal('apple') + result.f[1].should.equal('bar') + result._[0].should.equal('blerg') + + result.bar[0].should.equal('monkey') + result.bar[1].should.equal('washing') + result._[1].should.equal('cat') + }) + + it('should support nargs for -f= and --bar= format arguments with dashed values', function () { + const result = parser(['-f=--apple', 'bar', 'blerg', '--bar=-monkey', 'washing', 'cat'], { + narg: { + f: 2, + bar: 2 + } + }) + + result.f[0].should.equal('--apple') + result.f[1].should.equal('bar') + result._[0].should.equal('blerg') + + result.bar[0].should.equal('-monkey') + result.bar[1].should.equal('washing') + result._[1].should.equal('cat') + }) + + it('should not modify the input args if an = was used', function () { + const expected = ['-f=apple', 'bar', 'blerg', '--bar=monkey', 'washing', 'cat'] + const args = expected.slice() + parser(args, { + narg: { + f: 2, + bar: 2 + } + }) + args.should.deep.equal(expected) + + parser.detailed(args, { + narg: { + f: 2, + bar: 2 + } + }) + args.should.deep.equal(expected) + }) + + it('allows multiple nargs to be set at the same time', function () { + const result = parser(['--foo', 'apple', 'bar', '--bar', 'banana', '-f'], { + narg: { + foo: 2, + bar: 1 + } + }) + + Array.isArray(result.foo).should.equal(true) + result.foo[0].should.equal('apple') + result.foo[1].should.equal('bar') + result.bar.should.equal('banana') + result.f.should.equal(true) + }) + + // see: https://github.com/yargs/yargs-parser/pull/13 + it('should support nargs for --foo= format when the key is a number', function () { + const result = parser(['--1=a', 'b'], { + narg: { + 1: 2 + } + }) + + Array.isArray(result['1']).should.equal(true) + result['1'][0].should.equal('a') + result['1'][1].should.equal('b') + }) + + it('should not treat flag arguments as satisfying narg requirements', function () { + const result = parser.detailed(['--foo', '--bar', '99'], { + narg: { + foo: 1 + } + }) + + result.argv.bar.should.equal(99) + result.error.message.should.equal('Not enough arguments following: foo') + }) + + // See: https://github.com/yargs/yargs-parser/issues/232 + it('should treat flag arguments as satisfying narg requirements, if nargs-eats-options=true', function () { + const result = parser.detailed(['--foo', '--bar', '99', '--batman', 'robin'], { + narg: { + foo: 2 + }, + configuration: { + 'nargs-eats-options': true + } + }) + + result.argv.foo.should.eql(['--bar', 99]) + result.argv.batman.should.eql('robin') + }) + + it('should not consume more than configured nargs', function () { + const result = parser(['--foo', 'a', 'b'], { + narg: { + foo: 1 + } + }) + + result.foo.should.eql('a') + }) + + it('should ignore undefined configured nargs', function () { + const result = parser(['--foo', 'a', 'b'], { + narg: { + foo: undefined + } + }) + + result.foo.should.eql('a') + }) + + it('should default to 1 if configured narg is NaN', function () { + const result = parser(['--foo', 'a', 'b'], { + narg: { + foo: NaN + } + }) + + result.foo.should.eql('a') + }) + }) + + describe('env vars', function () { + it('should apply all env vars if prefix is empty', function () { + process.env.ONE_FISH = 'twofish' + process.env.RED_FISH = 'bluefish' + const result = parser([], { + envPrefix: '' + }) + result.oneFish.should.equal('twofish') + result.redFish.should.equal('bluefish') + }) + + // envPrefix falls back to empty string if not a string + it('should apply all env vars if prefix is not a string', function () { + process.env.ONE_FISH = 'twofish' + process.env.RED_FISH = 'bluefish' + const result = parser([], { + envPrefix: null + }) + + result.oneFish.should.equal('twofish') + result.redFish.should.equal('bluefish') + }) + + it('should not apply all env vars if prefix is undefined', function () { + process.env.ONE_FISH = 'twofish' + process.env.RED_FISH = 'bluefish' + const result = parser([], { + envPrefix: undefined + }) + + expect(result).to.not.have.property('oneFish') + expect(result).to.not.have.property('redFish') + }) + + it('should apply only env vars matching prefix if prefix is valid string', function () { + process.env.ONE_FISH = 'twofish' + process.env.RED_FISH = 'bluefish' + process.env.GREEN_EGGS = 'sam' + process.env.GREEN_HAM = 'iam' + const result = parser([], { + envPrefix: 'GREEN' + }) + + result.eggs.should.equal('sam') + result.ham.should.equal('iam') + expect(result.oneFish).to.be.undefined // eslint-disable-line + expect(result.redFish).to.be.undefined // eslint-disable-line + }) + + it('should set aliases for options defined by env var', function () { + process.env.AIRFORCE_ONE = 'two' + const result = parser([], { + envPrefix: 'AIRFORCE', + alias: { + 1: ['one', 'uno'] + } + }) + + result['1'].should.equal('two') + result.one.should.equal('two') + result.uno.should.equal('two') + }) + + it('should prefer command line value over env var', function () { + process.env.FOO_BAR = 'ignore' + const result = parser(['--foo-bar', 'baz'], { + envPrefix: '' + }) + + result.fooBar.should.equal('baz') + }) + + it('should respect type for args defined by env var', function () { + process.env.MY_TEST_STRING = '1' + process.env.MY_TEST_NUMBER = '2' + const result = parser([], { + string: 'string', + envPrefix: 'MY_TEST_' + }) + + result.string.should.equal('1') + result.number.should.equal(2) + }) + + it('should set option from aliased env var', function () { + process.env.SPACE_X = 'awesome' + const result = parser([], { + alias: { + xactly: 'x' + }, + envPrefix: 'SPACE' + }) + + result.xactly.should.equal('awesome') + }) + + it('should prefer env var value over configured default', function () { + process.env.FOO_BALL = 'wut' + process.env.FOO_BOOL = 'true' + const result = parser([], { + envPrefix: 'FOO', + default: { + ball: 'baz', + bool: false + }, + boolean: 'bool', + string: 'ball' + }) + + result.ball.should.equal('wut') + result.bool.should.equal(true) + }) + + const jsonPath = path.resolve(__dirname, './fixtures/config.json') + it('should prefer environment variables over config file', function () { + process.env.CFG_HERP = 'zerp' + const result = parser(['--cfg', jsonPath], { + envPrefix: 'CFG', + config: 'cfg', + string: 'herp', + default: { + herp: 'nerp' + } + }) + + result.herp.should.equal('zerp') + }) + + it('should support an env var value as config file option', function () { + process.env.TUX_CFG = jsonPath + const result = parser([], { + envPrefix: 'TUX', + config: ['cfg'], + default: { + z: 44 + } + }) + + result.should.have.property('herp') + result.should.have.property('foo') + result.should.have.property('version') + result.should.have.property('truthy') + result.z.should.equal(55) + }) + + it('should prefer cli config file option over env var config file option', function () { + process.env.MUX_CFG = path.resolve(__dirname, '../package.json') + const result = parser(['--cfg', jsonPath], { + envPrefix: 'MUX', + config: 'cfg' + }) + + result.should.have.property('herp') + result.should.have.property('foo') + result.should.have.property('version') + result.should.have.property('truthy') + result.z.should.equal(55) + }) + + it('should apply all nested env vars', function () { + process.env.TEST_A = 'a' + process.env.TEST_NESTED_OPTION__FOO = 'baz' + process.env.TEST_NESTED_OPTION__BAR = 'bar' + const result = parser(['--nestedOption.foo', 'bar'], { + envPrefix: 'TEST' + }) + + result.should.have.property('a', 'a') + result.should.have.property('nestedOption').and.deep.equal({ + foo: 'bar', + bar: 'bar' + }) + }) + + it('should apply nested env var if argv value is using default value', function () { + process.env.TEST_A = 'a' + process.env.TEST_NESTED_OPTION__FOO = 'baz' + process.env.TEST_NESTED_OPTION__BAR = 'bar' + const result = parser([], { + envPrefix: 'TEST', + default: { + 'nestedOption.foo': 'banana' + } + }) + + result.should.have.property('a', 'a') + result.should.have.property('nestedOption').and.deep.equal({ + foo: 'baz', + bar: 'bar' + }) + }) + }) + + describe('configuration', function () { + describe('short option groups', function () { + it('allows short-option-groups to be disabled', function () { + let parse = parser(['-cats=meow'], { + configuration: { + 'short-option-groups': false + } + }) + parse.cats.should.equal('meow') + parse = parser(['-cats', 'meow'], { + configuration: { + 'short-option-groups': false + } + }) + parse.cats.should.equal('meow') + }) + }) + + describe('camel-case expansion', function () { + it('does not expand camel-case aliases', function () { + const parsed = parser.detailed([], { + alias: { + 'foo-bar': ['x'] + }, + configuration: { + 'camel-case-expansion': false + } + }) + + expect(parsed.newAliases.fooBar).to.equal(undefined) + expect(parsed.aliases.fooBar).to.equal(undefined) + }) + + it('does not expand camel-case keys', function () { + const parsed = parser.detailed(['--foo-bar=apple'], { + configuration: { + 'camel-case-expansion': false + } + }) + + expect(parsed.argv.fooBar).to.equal(undefined) + expect(parsed.argv['foo-bar']).to.equal('apple') + }) + }) + + describe('dot notation', function () { + it('does not expand dot notation defaults', function () { + const parsed = parser([], { + default: { + 'foo.bar': 'x' + }, + configuration: { + 'dot-notation': false + } + }) + + expect(parsed['foo.bar']).to.equal('x') + }) + + it('does not expand dot notation arguments', function () { + let parsed = parser(['--foo.bar', 'banana'], { + configuration: { + 'dot-notation': false + } + }) + expect(parsed['foo.bar']).to.equal('banana') + parsed = parser(['--foo.bar=banana'], { + configuration: { + 'dot-notation': false + } + }) + expect(parsed['foo.bar']).to.equal('banana') + }) + + it('should use value from cli, if cli overrides dot notation default', function () { + const parsed = parser(['--foo.bar', 'abc'], { + default: { + 'foo.bar': 'default' + }, + configuration: { + 'dot-notation': false + } + }) + + expect(parsed['foo.bar']).to.equal('abc') + }) + + it('should also override dot notation alias', function () { + const parsed = parser(['--foo.bar', 'abc'], { + alias: { + 'foo.bar': ['alias.bar'] + }, + default: { + 'foo.bar': 'default' + }, + configuration: { + 'dot-notation': false + } + }) + + expect(parsed['alias.bar']).to.equal('abc') + }) + + it('does not expand alias of first element of dot notation arguments', function () { + const parsed = parser(['--foo.bar', 'banana'], { + alias: { + foo: ['f'] + }, + configuration: { + 'dot-notation': false + } + }) + expect(parsed['foo.bar']).to.equal('banana') + expect(parsed).not.to.include.keys('f.bar') + }) + + // addresses https://github.com/yargs/yargs/issues/716 + it('does not append nested-object keys from config to top-level key', function () { + const parsed = parser([], { + alias: { + foo: ['f'] + }, + configuration: { + 'dot-notation': false + }, + configObjects: [ + { + 'website.com': { + a: 'b', + b: 'c' + } + } + ] + }) + + parsed['website.com'].should.deep.equal({ + a: 'b', + b: 'c' + }) + }) + }) + + describe('parse-positional-numbers', () => { + it('does not parse positionals into numbers when false', function () { + const parsed = parser(['5.0', '3'], { + configuration: { + 'parse-positional-numbers': false + } + }) + expect(parsed._[0]).to.equal('5.0') + expect(parsed._[1]).to.equal('3') + }) + + it('parses positionals into numbers by default', function () { + const parsed = parser(['5.0', '3']) + expect(parsed._[0]).to.equal(5) + expect(parsed._[1]).to.equal(3) + }) + }) + + describe('parse numbers', function () { + it('does not coerce defaults into numbers', function () { + const parsed = parser([], { + default: { + foo: '5' + }, + configuration: { + 'parse-numbers': false + } + }) + + expect(parsed.foo).to.equal('5') + }) + + it('does not coerce arguments into numbers', function () { + const parsed = parser(['--foo', '5'], { + configuration: { + 'parse-numbers': false + } + }) + expect(parsed.foo).to.equal('5') + }) + + it('does not coerce positional arguments into numbers', function () { + const parsed = parser(['5'], { + configuration: { + 'parse-numbers': false + } + }) + expect(parsed._[0]).to.equal('5') + }) + + it('parses number if option explicitly set to number type', function () { + const parsed = parser(['--foo', '5', '--bar', '6', '--baz', '7'], { + number: ['bar', 'baz'], + coerce: { + baz: val => val + }, + configuration: { + 'parse-numbers': false + } + }) + expect(parsed.foo).to.equal('5') + expect(parsed.bar).to.equal(6) + expect(parsed.baz).to.equal(7) + }) + + it('should coerce elements of number typed arrays to numbers', function () { + const parsed = parser(['--foo', '4', '--foo', '5', '2'], { + array: ['foo'], + configObjects: [{ foo: ['1', '2', '3'] }], + configuration: { + 'combine-arrays': true, + 'flatten-duplicate-arrays': false + } + }) + + expect(parsed.foo).to.deep.equal([[4], [5, 2], [1, 2, 3]]) + }) + }) + + describe('boolean negation', function () { + it('does not negate arguments prefixed with --no-', function () { + const parsed = parser(['--no-dice'], { + configuration: { + 'boolean-negation': false + } + }) + + parsed['no-dice'].should.equal(true) + expect(parsed.dice).to.equal(undefined) + }) + + it('negates boolean arguments with correct prefix', function () { + const parsed = parser(['--foodice'], { + configuration: { + 'negation-prefix': 'foo' + } + }) + + expect(parsed.dice).to.equal(false) + }) + }) + + describe('duplicate arguments array', function () { + it('adds duplicate argument to array', function () { + const parsed = parser('-x a -x b', { + configuration: { + 'duplicate-arguments-array': true + } + }) + + parsed.x.should.deep.equal(['a', 'b']) + }) + it('keeps only last argument', function () { + const parsed = parser('-x a -x b', { + configuration: { + 'duplicate-arguments-array': false + } + }) + + parsed.x.should.equal('b') + }) + it('does not interfere with nargs', function () { + const parsed = parser('-x a b c -x o p q', { + narg: { x: 3 }, + configuration: { + 'duplicate-arguments-array': false + } + }) + + parsed.x.should.deep.equal(['o', 'p', 'q']) + }) + }) + + describe('flatten duplicate arrays', function () { + it('flattens duplicate array type', function () { + const parsed = parser('-x a b -x c d', { + array: ['x'], + configuration: { + 'flatten-duplicate-arrays': true + } + }) + + parsed.x.should.deep.equal(['a', 'b', 'c', 'd']) + }) + it('nests duplicate array types', function () { + const parsed = parser('-x a b -x c d', { + array: ['x'], + configuration: { + 'flatten-duplicate-arrays': false + } + }) + + parsed.x.should.deep.equal([['a', 'b'], ['c', 'd']]) + }) + it('nests duplicate array types of more than 2', function () { + const parsed = parser('-x a b -x c d -x e f -x g h', { + array: ['x'], + configuration: { + 'flatten-duplicate-arrays': false + } + }) + + parsed.x.should.deep.equal([['a', 'b'], ['c', 'd'], ['e', 'f'], ['g', 'h']]) + }) + it('doesn\'t nests single arrays', function () { + const parsed = parser('-x a b', { + array: ['x'], + configuration: { + 'flatten-duplicate-arrays': false + } + }) + + parsed.x.should.deep.equal(['a', 'b']) + }) + it('flattens duplicate array type, when argument uses dot notation', function () { + const parsed = parser('-x.foo a -x.foo b', { + array: ['x.foo'], + configuration: { + 'flatten-duplicate-arrays': true + } + }) + + parsed.x.should.deep.equal({ foo: ['a', 'b'] }) + }) + }) + + describe('duplicate-arguments-array VS flatten-duplicate-arrays', function () { + /* + duplicate=false, flatten=false + type=array + [-x 1 2 3] => [1, 2, 3] + [-x 1 2 3 -x 2 3 4] => [2, 3, 4] + type=string/number/etc + [-x 1 -x 2 -x 3] => 3 + + duplicate=false, flatten=true + type=array + [-x 1 2 3] => [1, 2, 3] + [-x 1 2 3 -x 2 3 4] => [2, 3, 4] + type=string/number/etc + [-x 1 -x 2 -x 3] => 3 + + duplicate=true, flatten=true + type=array + [-x 1 2 3] => [1, 2, 3] + [-x 1 2 3 -x 2 3 4] => [1, 2, 3, 2, 3, 4] + type=string/number/etc + [-x 1 -x 2 -x 3] => [1, 2, 3] + + duplicate=true, flatten=false + type=array + [-x 1 2 3] => [1, 2, 3] + [-x 1 2 3 -x 2 3 4] => [[1, 2, 3], [2, 3, 4]] + type=string/number/etc + [-x 1 -x 2 -x 3] => [1, 2, 3] + */ + describe('duplicate=false, flatten=false,', function () { + describe('type=array', function () { + it('[-x 1 2 3] => [1, 2, 3]', function () { + const parsed = parser('-x 1 2 3', { + array: ['x'], + configuration: { + 'duplicate-arguments-array': false, + 'flatten-duplicate-arrays': false + } + }) + parsed.x.should.deep.equal([1, 2, 3]) + }) + it('[-x 1 2 3 -x 2 3 4] => [2, 3, 4]', function () { + const parsed = parser('-x 1 2 3 -x 2 3 4', { + array: ['x'], + configuration: { + 'duplicate-arguments-array': false, + 'flatten-duplicate-arrays': false + } + }) + parsed.x.should.deep.equal([2, 3, 4]) + }) + }) + describe('type=number', function () { + it('[-x 1 -x 2 -x 3] => 3', function () { + const parsed = parser('-x 1 -x 2 -x 3', { + number: 'x', + configuration: { + 'duplicate-arguments-array': false, + 'flatten-duplicate-arrays': false + } + }) + parsed.x.should.deep.equal(3) + }) + }) + describe('type=boolean', function () { + it('[-x true -x true -x false] => false', function () { + const parsed = parser('-x true -x true -x false', { + boolean: ['x'], + configuration: { + 'duplicate-arguments-array': false, + 'flatten-duplicate-arrays': false + } + }) + parsed.x.should.deep.equal(false) + }) + }) + }) + describe('duplicate=false, flatten=true,', function () { + describe('type=array', function () { + it('[-x 1 2 3] => [1, 2, 3]', function () { + const parsed = parser('-x 1 2 3', { + array: ['x'], + configuration: { + 'duplicate-arguments-array': false, + 'flatten-duplicate-arrays': true + } + }) + parsed.x.should.deep.equal([1, 2, 3]) + }) + it('[-x 1 2 3 -x 2 3 4] => [2, 3, 4]', function () { + const parsed = parser('-x 1 2 3 -x 2 3 4', { + array: ['x'], + configuration: { + 'duplicate-arguments-array': false, + 'flatten-duplicate-arrays': true + } + }) + parsed.x.should.deep.equal([2, 3, 4]) + }) + }) + describe('type=number', function () { + it('[-x 1 -x 2 -x 3] => 3', function () { + const parsed = parser('-x 1 -x 2 -x 3', { + number: 'x', + configuration: { + 'duplicate-arguments-array': false, + 'flatten-duplicate-arrays': true + } + }) + parsed.x.should.deep.equal(3) + }) + }) + describe('type=boolean', function () { + it('[-x true -x true -x false] => false', function () { + const parsed = parser('-x true -x true -x false', { + boolean: ['x'], + configuration: { + 'duplicate-arguments-array': false, + 'flatten-duplicate-arrays': true + } + }) + parsed.x.should.deep.equal(false) + }) + }) + }) + describe('duplicate=true, flatten=true,', function () { + describe('type=array', function () { + it('[-x 1 2 3] => [1, 2, 3]', function () { + const parsed = parser('-x 1 2 3', { + array: ['x'], + configuration: { + 'duplicate-arguments-array': true, + 'flatten-duplicate-arrays': true + } + }) + parsed.x.should.deep.equal([1, 2, 3]) + }) + it('[-x 1 2 3 -x 2 3 4] => [1, 2, 3, 2, 3, 4]', function () { + const parsed = parser('-x 1 2 3 -x 2 3 4', { + array: ['x'], + configuration: { + 'duplicate-arguments-array': true, + 'flatten-duplicate-arrays': true + } + }) + parsed.x.should.deep.equal([1, 2, 3, 2, 3, 4]) + }) + }) + describe('type=number', function () { + it('[-x 1 -x 2 -x 3] => [1, 2, 3]', function () { + const parsed = parser('-x 1 -x 2 -x 3', { + number: 'x', + configuration: { + 'duplicate-arguments-array': true, + 'flatten-duplicate-arrays': true + } + }) + parsed.x.should.deep.equal([1, 2, 3]) + }) + }) + describe('type=boolean', function () { + // in the casse of boolean arguments, only the last argument is used: + it('[-x true -x true -x false] => false', function () { + const parsed = parser('-x true -x true -x false', { + boolean: ['x'], + configuration: { + 'duplicate-arguments-array': true, + 'flatten-duplicate-arrays': true + } + }) + parsed.x.should.deep.equal(false) + }) + }) + }) + describe('duplicate=true, flatten=false,', function () { + describe('type=array', function () { + it('[-x 1 -x 2 -x 3] => [[1], [2], [3]]', function () { + const parsed = parser('-x 1 -x 2 -x 3', { + array: ['x'], + configuration: { + 'duplicate-arguments-array': true, + 'flatten-duplicate-arrays': false + } + }) + parsed.x.should.deep.equal([[1], [2], [3]]) + }) + it('[-x 1 2 3 -x 2 3 4] => [[1, 2, 3], [ 2, 3, 4]]', function () { + const parsed = parser('-x 1 2 3 -x 2 3 4', { + array: ['x'], + configuration: { + 'duplicate-arguments-array': true, + 'flatten-duplicate-arrays': false + } + }) + parsed.x.should.deep.equal([[1, 2, 3], [2, 3, 4]]) + }) + }) + describe('type=number', function () { + it('[-x 1 -x 2 -x 3] => [1, 2, 3]', function () { + const parsed = parser('-x 1 -x 2 -x 3', { + number: 'x', + configuration: { + 'duplicate-arguments-array': true, + 'flatten-duplicate-arrays': false + } + }) + parsed.x.should.deep.equal([1, 2, 3]) + }) + }) + describe('type=boolean', function () { + it('[-x true -x true -x false] => false', function () { + const parsed = parser('-x true -x true -x false', { + boolean: ['x'], + configuration: { + 'duplicate-arguments-array': true, + 'flatten-duplicate-arrays': false + } + }) + parsed.x.should.deep.equal(false) + }) + }) + }) + }) + + describe('populate--', function () { + it('should populate "_" by default', function () { + const result = parser([ + 'bare', + '--', '-h', 'eek', '--' + ]) + result.should.have.property('_').and.deep.equal(['bare', '-h', 'eek', '--']) + result.should.not.have.property('--') + }) + + it('should populate "_" when given config with "short-option-groups" false', function () { + const result = parser.detailed([ + '--', 'foo' + ], { + configuration: { + 'short-option-groups': false + } + }) + result.argv.should.deep.equal({ _: ['foo'] }) + result.argv.should.not.have.property('--') + result.newAliases.should.deep.equal({}) + }) + + it('should populate the "--" if populate-- is "true"', function () { + const result = parser([ + '--name=meowmers', 'bare', '-cats', 'woo', 'moxy', + '-h', 'awesome', '--multi=quux', + '--key', 'value', + '-b', '--bool', '--no-meep', '--multi=baz', + '--', '--not-a-flag', '-', '-h', '-multi', '--', 'eek' + ], { + configuration: { + 'populate--': true + } + }) + result.should.have.property('c', true) + result.should.have.property('a', true) + result.should.have.property('t', true) + result.should.have.property('s', 'woo') + result.should.have.property('h', 'awesome') + result.should.have.property('b', true) + result.should.have.property('bool', true) + result.should.have.property('key', 'value') + result.should.have.property('multi').and.deep.equal(['quux', 'baz']) + result.should.have.property('meep', false) + result.should.have.property('name', 'meowmers') + result.should.have.property('_').and.deep.equal(['bare', 'moxy']) + result.should.have.property('--').and.deep.equal(['--not-a-flag', '-', '-h', '-multi', '--', 'eek']) + }) + }) + + describe('set-placeholder-key', function () { + it('should not set placeholder key by default', function () { + const parsed = parser([], { + string: ['a'] + }) + parsed.should.not.have.property('a') + }) + + it('should set placeholder key to "undefined"', function () { + const parsed = parser([], { + array: ['a'], + boolean: ['b'], + string: ['c'], + number: ['d'], + count: ['e'], + normalize: ['f'], + narg: { g: 2 }, + coerce: { + h: function (arg) { + return arg + } + }, + configuration: { 'set-placeholder-key': true } + }) + parsed.should.have.property('a') + expect(parsed.a).to.be.equal(undefined) + parsed.should.have.property('b') + expect(parsed.b).to.be.equal(undefined) + parsed.should.have.property('c') + expect(parsed.c).to.be.equal(undefined) + parsed.should.have.property('d') + expect(parsed.d).to.be.equal(undefined) + parsed.should.have.property('e') + expect(parsed.f).to.be.equal(undefined) + parsed.should.have.property('g') + expect(parsed.g).to.be.equal(undefined) + parsed.should.have.property('h') + expect(parsed.h).to.be.equal(undefined) + }) + + it('should not set placeholder for key with a default value', function () { + const parsed = parser([], { + string: ['a'], + default: { a: 'hello' }, + configuration: { 'set-placeholder-key': true } + }) + parsed.a.should.equal('hello') + }) + + it('should not set placeholder key with dot notation', function () { + const parsed = parser([], { + string: ['a.b'] + }) + parsed.should.not.have.property('a') + parsed.should.not.have.property('b') + parsed.should.not.have.property('a.b') + }) + + it('should not set placeholder key with dot notation when `set-placeholder-key` is `true`', function () { + const parsed = parser([], { + string: ['a.b'], + configuration: { + 'set-placeholder-key': true + } + }) + parsed.should.not.have.property('a') + parsed.should.not.have.property('b') + parsed.should.not.have.property('a.b') + }) + }) + + describe('halt-at-non-option', function () { + it('gets the entire rest of line', function () { + const parse = parser(['--foo', './file.js', '--foo', '--bar'], { + configuration: { 'halt-at-non-option': true }, + boolean: ['foo', 'bar'] + }) + parse.should.deep.equal({ foo: true, _: ['./file.js', '--foo', '--bar'] }) + }) + + it('is not influenced by --', function () { + const parse = parser( + ['--foo', './file.js', '--foo', '--', 'barbar', '--bar'], + { configuration: { 'halt-at-non-option': true }, boolean: ['foo', 'bar'] } + ) + parse.should.deep.equal({ + foo: true, + _: ['./file.js', '--foo', '--', 'barbar', '--bar'] + }) + }) + + it('is not influenced by unknown options', function () { + const parse = parser( + ['-v', '--long', 'arg', './file.js', '--foo', '--', 'barbar'], + { configuration: { 'halt-at-non-option': true }, boolean: ['foo'] } + ) + parse.should.deep.equal({ + v: true, + long: 'arg', + _: ['./file.js', '--foo', '--', 'barbar'] + }) + }) + }) + + describe('unknown-options-as-args = true', function () { + it('should ignore unknown options in long format separated by =', function () { + const argv = parser('--known-arg=1 --unknown-arg=2', { + number: ['known-arg'], + configuration: { + 'unknown-options-as-args': true + } + }) + argv.should.deep.equal({ + _: ['--unknown-arg=2'], + 'known-arg': 1, + knownArg: 1 + }) + }) + it('should ignore unknown options in boolean negations', function () { + const argv = parser('--no-known-arg --no-unknown-arg', { + boolean: ['known-arg'], + configuration: { + 'unknown-options-as-args': true + } + }) + argv.should.deep.equal({ + _: ['--no-unknown-arg'], + 'known-arg': false, + knownArg: false + }) + }) + it('should ignore unknown options in long format separated by space', function () { + const argv = parser('--known-arg a --unknown-arg b', { + string: ['known-arg'], + configuration: { + 'unknown-options-as-args': true + } + }) + argv.should.deep.equal({ + _: ['--unknown-arg', 'b'], + 'known-arg': 'a', + knownArg: 'a' + }) + }) + it('should ignore unknown options in short dot format separated by equals', function () { + const argv = parser('-k.arg=a -u.arg=b', { + string: ['k.arg'], + configuration: { + 'unknown-options-as-args': true + } + }) + argv.should.deep.equal({ + _: ['-u.arg=b'], + k: { + arg: 'a' + } + }) + }) + it('should ignore unknown options in short dot format separated by space', function () { + const argv = parser('-k.arg 1 -u.arg 2', { + number: ['k.arg'], + configuration: { + 'unknown-options-as-args': true + } + }) + argv.should.deep.equal({ + _: ['-u.arg', 2], + k: { + arg: 1 + } + }) + }) + it('should ignore unknown options in short format separated by equals', function () { + const argv = parser('-k=a -u=b', { + string: ['k'], + configuration: { + 'unknown-options-as-args': true + } + }) + argv.should.deep.equal({ + _: ['-u=b'], + k: 'a' + }) + }) + it('should ignore unknown options in short format followed by hyphen', function () { + const argv = parser('-k- -u-', { + string: ['k'], + configuration: { + 'unknown-options-as-args': true + } + }) + argv.should.deep.equal({ + _: ['-u-'], + k: '-' + }) + }) + it('should ignore unknown options in short format separated by space', function () { + const argv = parser('-k 1 -u 2', { + number: ['k'], + configuration: { + 'unknown-options-as-args': true + } + }) + argv.should.deep.equal({ + _: ['-u', 2], + k: 1 + }) + }) + it('should allow an unknown arg to be used as the value of another flag in short form', function () { + const argv = parser('-k -u', { + string: ['k'], + narg: { k: 1 }, + configuration: { + 'unknown-options-as-args': true + } + }) + argv.should.deep.equal({ + _: [], + k: '-u' + }) + }) + it('should allow an unknown arg to be used as the value of another flag in long form', function () { + const argv = parser('--known-arg --unknown-arg', { + string: ['known-arg'], + narg: { 'known-arg': 1 }, + configuration: { + 'unknown-options-as-args': true + } + }) + argv.should.deep.equal({ + _: [], + knownArg: '--unknown-arg', + 'known-arg': '--unknown-arg' + }) + }) + it('should allow an unknown arg to be used as the value of another flag in array form', function () { + const argv = parser('--known-arg --unknown-arg1 --unknown-arg2', { + array: ['known-arg'], + configuration: { + 'unknown-options-as-args': true + } + }) + argv.should.deep.equal({ + _: [], + knownArg: ['--unknown-arg1', '--unknown-arg2'], + 'known-arg': ['--unknown-arg1', '--unknown-arg2'] + }) + }) + it('should ignore unknown options in short format followed by a number', function () { + const argv = parser('-k1 -u2', { + number: ['k'], + configuration: { + 'unknown-options-as-args': true + } + }) + argv.should.deep.equal({ + _: ['-u2'], + k: 1 + }) + }) + it('should ignore unknown options in short format followed by a non-word character', function () { + const argv = parser('-k/1/ -u/2/', { + string: ['k'], + configuration: { + 'unknown-options-as-args': true + } + }) + argv.should.deep.equal({ + _: ['-u/2/'], + k: '/1/' + }) + }) + it('should ignore unknown options in short format with multiple flags in one argument where an unknown flag is before the end', function () { + const argv = parser('-kuv', { + boolean: ['k', 'v'], + configuration: { + 'unknown-options-as-args': true + } + }) + argv.should.deep.equal({ + _: ['-kuv'] + }) + }) + it('should parse known options in short format with multiple flags in one argument where no unknown flag is in the argument', function () { + const argv = parser('-kv', { + boolean: ['k', 'v'], + configuration: { + 'unknown-options-as-args': true + } + }) + argv.should.deep.equal({ + _: [], + k: true, + v: true + }) + }) + it('should parse negative numbers', function () { + const argv = parser('-k -33', { + boolean: ['k'], + configuration: { + 'unknown-options-as-args': true + } + }) + argv.should.deep.equal({ + _: [-33], + k: true + }) + }) + + it('should not identify "--" as an unknown option', function () { + const argv = parser('-a -k one -1 -- -b -k two -2', { + boolean: ['k'], + configuration: { + 'unknown-options-as-args': true + } + }) + argv.should.deep.equal({ + _: ['-a', 'one', -1, '-b', '-k', 'two', '-2'], + k: true + }) + }) + + it('should not identify "--" as an unknown option when "populate--" is true', function () { + const argv = parser('-a -k one -1 -- -b -k two -2', { + boolean: ['k'], + configuration: { + 'populate--': true, + 'unknown-options-as-args': true + } + }) + argv.should.deep.equal({ + // populate argv._ with everything before the -- + _: ['-a', 'one', -1], + // and argv['--'] with everything after the -- + '--': ['-b', '-k', 'two', '-2'], + k: true + }) + }) + // See: https://github.com/yargs/yargs/issues/1869 + // ----=hello ---=hello ---- hello. + it('should not populate "--" for key values other than "--"', () => { + const argv = parser('----=test', { + configuration: { + 'populate--': true + } + }) + argv._.should.eql(['----=test']) + expect(argv['--']).to.equal(undefined) + }) + // see: https://github.com/yargs/yargs/issues/1489 + it('should identify "hasOwnProperty" as unknown option', () => { + const argv = parser('--known-arg=1 --hasOwnProperty=33', { + number: ['known-arg'], + configuration: { + 'unknown-options-as-args': true + } + }) + argv.should.deep.equal({ + _: ['--hasOwnProperty=33'], + 'known-arg': 1, + knownArg: 1 + }) + }) + + it('should coerce unknown options that look numeric into numbers', () => { + const argv = parser('--known-arg 33', { + boolean: ['known-arg'] + }) + argv.should.deep.equal({ + _: [33], + 'known-arg': true, + knownArg: true + }) + }) + }) + + // See: https://github.com/yargs/yargs-parser/issues/231 + it('should collect unknown options terminated with digit', function () { + const argv = parser('--known-arg=1 --num2', { + alias: { num: ['n'] }, + number: ['known-arg'], + configuration: { + 'unknown-options-as-args': true + } + }) + argv.should.deep.equal({ + _: ['--num2'], + 'known-arg': 1, + knownArg: 1 + }) + }) + }) + + // addresses: https://github.com/yargs/yargs-parser/issues/41 + it('defaults to empty array if array option is provided no values', function () { + let parsed = parser(['-f'], { + alias: { + f: 'files' + }, + array: ['files'] + }) + parsed.f.should.deep.equal([]) + parsed.files.should.deep.equal([]) + + parsed = parser(['--files'], { + alias: { + f: 'files' + }, + array: ['files'] + }) + parsed.f.should.deep.equal([]) + parsed.files.should.deep.equal([]) + + parsed = parser(['-f', '-y'], { + alias: { + f: 'files' + }, + array: ['files'] + }) + parsed.f.should.deep.equal([]) + parsed.files.should.deep.equal([]) + }) + + describe('coerce', function () { + it('applies coercion function to simple arguments', function () { + const parsed = parser(['--foo', '99'], { + coerce: { + foo: function (arg) { + return arg * -1 + } + } + }) + parsed.foo.should.equal(-99) + }) + + it('applies coercion function to aliases', function () { + const parsed = parser(['--foo', '99'], { + coerce: { + f: function (arg) { + return arg * -1 + } + }, + alias: { + f: ['foo'] + } + }) + parsed.foo.should.equal(-99) + parsed.f.should.equal(-99) + }) + + it('applies coercion function to all dot options', function () { + const parsed = parser(['--foo.bar', 'nananana'], { + coerce: { + foo: function (val) { + val.bar += ', batman!' + return val + } + } + }) + parsed.foo.bar.should.equal('nananana, batman!') + }) + + it('applies coercion to defaults', function () { + const parsed = parser([], { + default: { foo: 'bar' }, + coerce: { + foo: function (val) { + return val.toUpperCase() + } + } + }) + parsed.foo.should.equal('BAR') + }) + + it('applies coercion function to an implicit array', function () { + const parsed = parser(['--foo', '99', '-f', '33'], { + coerce: { + f: function (arg) { + return arg.map(function (a) { + return a * -1 + }) + } + }, + alias: { + f: ['foo'] + } + }) + parsed.f.should.deep.equal([-99, -33]) + parsed.foo.should.deep.equal([-99, -33]) + }) + + it('applies coercion function to an explicit array', function () { + const parsed = parser(['--foo', '99', '-f', '33'], { + coerce: { + f: function (arg) { + return arg.map(function (a) { + return a * -1 + }) + } + }, + array: ['foo'], + alias: { + f: ['foo'] + } + }) + parsed.f.should.deep.equal([-99, -33]) + parsed.foo.should.deep.equal([-99, -33]) + }) + + it('applies coercion function to _', function () { + const parsed = parser(['99', '33'], { + coerce: { + _: function (arg) { + return arg.map(function (a) { + return a * -1 + }) + } + } + }) + parsed._.should.deep.equal([-99, -33]) + }) + + // see: https://github.com/yargs/yargs/issues/550 + it('coercion function can be used to parse large #s', function () { + const fancyNumberParser = function (arg) { + if (arg.length > 10) return arg + else return parseInt(arg) + } + const parsed = parser(['--foo', '88888889999990000998989898989898', '--bar', '998'], { + coerce: { + foo: fancyNumberParser, + bar: fancyNumberParser + } + }) + ; (typeof parsed.foo).should.equal('string') + parsed.foo.should.equal('88888889999990000998989898989898') + ; (typeof parsed.bar).should.equal('number') + parsed.bar.should.equal(998) + }) + + it('populates argv.error, if an error is thrown', function () { + const parsed = parser.detailed(['--foo', '99'], { + coerce: { + foo: function (arg) { + throw Error('banana') + } + } + }) + parsed.error.message.should.equal('banana') + }) + + it('populates argv.error, if an error is thrown for an explicit array', function () { + const parsed = parser.detailed(['--foo', '99'], { + array: ['foo'], + coerce: { + foo: function (arg) { + throw Error('foo is array: ' + Array.isArray(arg)) + } + } + }) + parsed.error.message.should.equal('foo is array: true') + }) + + // see: https://github.com/yargs/yargs-parser/issues/76 + it('only runs coercion functions once, even with aliases', function () { + let runcount = 0 + const func = (arg) => { + runcount++ + return undefined + } + parser(['--foo', 'bar'], { + alias: { + foo: ['f', 'foo-bar', 'bar'], + b: ['bar'] + }, + coerce: { + bar: func + } + }) + runcount.should.equal(1) + }) + }) + + // see: https://github.com/yargs/yargs-parser/issues/37 + it('normalizes all paths in array when provided via config object', function () { + const argv = parser(['--foo', 'bar'], { + array: ['a'], + normalize: ['a'], + configObjects: [{ a: ['bin/../a.txt', 'bin/../b.txt'] }] + }) + argv.a.should.deep.equal(['a.txt', 'b.txt']) + }) + + // see: https://github.com/yargs/yargs/issues/963 + it('does not magically convert numeric strings larger than Number.MAX_SAFE_INTEGER', () => { + const argv = parser(['--foo', '93940495950949399948393']) + argv.foo.should.equal('93940495950949399948393') + }) + + it('does not magically convert scientific notation larger than Number.MAX_SAFE_INTEGER', () => { + const argv = parser(['--foo', '33e99999']) + argv.foo.should.equal('33e99999') + }) + + it('converts numeric options larger than Number.MAX_SAFE_INTEGER to number', () => { + const argv = parser(['--foo', '93940495950949399948393'], { + number: ['foo'] + }) + argv.foo.should.equal(9.39404959509494e+22) + }) + + // see: https://github.com/yargs/yargs/issues/1099 + it('does not magically convert options with leading + to number', () => { + const argv = parser(['--foo', '+5550100', '--bar', '+5550100'], { + number: 'bar' + }) + argv.foo.should.equal('+5550100') + argv.bar.should.equal(5550100) + }) + + // see: https://github.com/yargs/yargs/issues/1099 + it('does not magically convert options with leading 0 to number', () => { + const argv = parser(['--foo', '000000', '--bar', '000000'], { + number: 'bar' + }) + argv.foo.should.equal('000000') + argv.bar.should.equal(0) + }) + + // see: https://github.com/yargs/yargs-parser/issues/101 + describe('dot-notation array arguments combined with string arguments', function () { + it('parses correctly when dot-notation argument is first', function () { + const argv = parser(['--foo.bar', 'baz', '--foo', 'bux']) + Array.isArray(argv.foo).should.equal(true) + argv.foo[0].bar.should.equal('baz') + argv.foo[1].should.equal('bux') + }) + + it('parses correctly when dot-notation argument is last', function () { + const argv = parser(['--foo', 'bux', '--foo.bar', 'baz']) + Array.isArray(argv.foo).should.equal(true) + argv.foo[0].should.equal('bux') + argv.foo[1].bar.should.equal('baz') + }) + + it('parses correctly when there are multiple dot-notation arguments', function () { + const argv = parser(['--foo.first', 'firstvalue', '--foo', 'bux', '--foo.bar', 'baz', '--foo.bla', 'banana']) + Array.isArray(argv.foo).should.equal(true) + argv.foo.length.should.equal(4) + argv.foo[0].first.should.equal('firstvalue') + argv.foo[1].should.equal('bux') + argv.foo[2].bar.should.equal('baz') + argv.foo[3].bla.should.equal('banana') + }) + }) + + // see: https://github.com/yargs/yargs-parser/issues/145 + describe('strings with quotes and dashes', () => { + it('handles double quoted strings', function () { + const args = parser('--foo "hello world" --bar="goodnight\'moon"') + args.foo.should.equal('hello world') + args.bar.should.equal('goodnight\'moon') + const args2 = parser(['--foo', '"hello world"', '--bar="goodnight\'moon"']) + args2.foo.should.equal('hello world') + args2.bar.should.equal('goodnight\'moon') + }) + + it('handles single quoted strings', function () { + const args = parser("--foo 'hello world' --bar='goodnight\"moon'") + args.foo.should.equal('hello world') + args.bar.should.equal('goodnight"moon') + const args2 = parser(['--foo', "'hello world'", "--bar='goodnight\"moon'"]) + args2.foo.should.equal('hello world') + args2.bar.should.equal('goodnight"moon') + }) + + it('handles strings with dashes', function () { + const args = parser('--foo "-hello world" --bar="--goodnight moon"') + args.foo.should.equal('-hello world') + args.bar.should.equal('--goodnight moon') + }) + }) + + // see: https://github.com/yargs/yargs-parser/issues/144 + it('number/string types should use default when no right-hand value', () => { + let argv = parser(['--foo'], { + number: ['foo'], + default: { + foo: 99 + } + }) + argv.foo.should.equal(99) + + argv = parser(['-b'], { + alias: { + bar: 'b' + }, + string: ['bar'], + default: { + bar: 'hello' + } + }) + argv.bar.should.equal('hello') + }) + + describe('stripping', function () { + it('strip-dashed removes expected fields from argv', function () { + const argv = parser(['--test-value', '1'], { + number: ['test-value'], + alias: { + 'test-value': ['alt-test'] + }, + configuration: { + 'strip-dashed': true + } + }) + argv.should.deep.equal({ + _: [], + testValue: 1, + altTest: 1 + }) + }) + + it('strip-aliased removes expected fields from argv', function () { + const argv = parser(['--test-value', '1'], { + number: ['test-value'], + alias: { + 'test-value': ['alt-test'] + }, + configuration: { + 'strip-aliased': true + } + }) + argv.should.deep.equal({ + _: [], + 'test-value': 1, + testValue: 1 + }) + }) + + it('strip-aliased and strip-dashed combined removes expected fields from argv', function () { + const argv = parser(['--test-value', '1'], { + number: ['test-value'], + alias: { + 'test-value': ['alt-test'] + }, + configuration: { + 'strip-aliased': true, + 'strip-dashed': true + } + }) + argv.should.deep.equal({ + _: [], + testValue: 1 + }) + }) + + it('ignores strip-dashed if camel-case-expansion is disabled', function () { + const argv = parser(['--test-value', '1'], { + number: ['test-value'], + configuration: { + 'camel-case-expansion': false, + 'strip-dashed': true + } + }) + argv.should.deep.equal({ + _: [], + 'test-value': 1 + }) + }) + + it('only removes camel case expansion if keys have hyphen', function () { + const argv = parser(['--foo', '1', '-a', '2'], { + configuration: { + 'strip-aliased': true + }, + alias: { + aliased1: ['Foo'], + aliased2: ['A'] + } + }) + argv.should.deep.equal({ + _: [], + foo: 1, + a: 2 + }) + }) + }) + + describe('prototype collisions', () => { + it('parses unknown argument colliding with prototype', () => { + const parse = parser(['--toString']) + parse.toString.should.equal(true) + }) + + it('parses unknown argument colliding with prototype, when unknown options as args', () => { + const parse = parser(['--toString'], { + configuration: { + 'unknown-options-as-args': true + } + }) + parse._.should.include('--toString') + }) + + it('handles "alias" colliding with prototype', () => { + const parse = parser(['-t', '99'], { + alias: { + toString: ['t'] + } + }) + parse.toString.should.equal(99) + parse.t.should.equal(99) + parse['to-string'].should.equal(99) + }) + + it('handles multiple args colliding with alias', () => { + const parse = parser(['--toString', '88', '--toString', '99']) + parse.toString.should.eql([88, 99]) + }) + + it('handle dot notation colliding with alias', () => { + const parse = parser(['--toString.cool', 'apple']) + parse.toString.cool.should.equal('apple') + }) + + it('handles "arrays" colliding with prototype', () => { + const parse = parser(['--toString', '99', '100'], { + array: ['toString'] + }) + parse.toString.should.eql([99, 100]) + }) + + it('handles "arrays" colliding with prototype', () => { + const parse = parser(['--toString', '99', '100'], { + array: ['toString'] + }) + parse.toString.should.eql([99, 100]) + }) + + it('handles "strings" colliding with prototype', () => { + const parse = parser(['--toString', '99'], { + string: ['toString'] + }) + parse.toString.should.eql('99') + }) + + it('handles "numbers" colliding with prototype', () => { + const parse = parser(['--toString', '99'], { + number: ['toString'], + configuration: { + 'parse-numbers': false + } + }) + parse.toString.should.eql(99) + }) + + it('handles "counts" colliding with prototype', () => { + const parse = parser(['--toString', '--toString', '--toString'], { + count: ['toString'] + }) + parse.toString.should.eql(3) + }) + + it('handles "normalize" colliding with prototype', () => { + const parse = parser(['--toString', './node_modules/chai'], { + normalize: ['toString'] + }) + parse.toString.should.include('node_modules') + }) + + it('handles "normalize" colliding with prototype', () => { + const parse = parser(['--toString', './node_modules/chai'], { + normalize: ['toString'] + }) + parse.toString.should.include('node_modules') + }) + + it('handles key in configuration file that collides with prototype', function () { + const argv = parser(['--foo', 'bar'], { + alias: { + z: 'zoom' + }, + default: { + settings: jsonPath + }, + config: 'settings' + }) + argv.toString.should.equal('method name') + }) + + it('handles "nargs" colliding with prototype', () => { + const parse = parser(['--toString', 'apple', 'banana', 'batman', 'robin'], { + narg: { + toString: 3 + } + }) + parse.toString.should.eql(['apple', 'banana', 'batman']) + parse._.should.eql(['robin']) + }) + + it('handles "coercions" colliding with prototype', () => { + const parse = parser(['--toString', '33'], { + coerce: { + toString: (val) => { + return val * 2 + } + } + }) + parse.toString.should.equal(66) + }) + }) + + // See: https://github.com/facebook/jest/issues/9517 + it('does not collect arguments configured as booleans into implicit array', () => { + const parse = parser(['--infinite', 'true', '--infinite', 'true', '--no-infinite'], { + boolean: 'infinite' + }) + parse.infinite.should.equal(false) + }) + + // See: https://github.com/yargs/yargs/issues/1098, + // https://github.com/yargs/yargs/issues/1570 + describe('array with nargs', () => { + it('allows array and nargs to be configured in conjunction, enforcing the nargs value', () => { + const parse = parser.detailed(['-a', 'apple', 'banana'], { + array: 'a', + narg: { + a: 1 + } + }) + expect(parse.error).to.be.null // eslint-disable-line + parse.argv.a.should.eql(['apple']) + parse.argv._.should.eql(['banana']) + }) + + // see; https://github.com/yargs/yargs/issues/1098 + it('allows special NaN count to be provided to narg, to indicate one or more array values', () => { + const parse = parser.detailed(['-a', 'apple', 'banana'], { + array: 'a', + narg: { + a: NaN + } + }) + expect(parse.error).to.be.null // eslint-disable-line + parse.argv.a.should.eql(['apple', 'banana']) + }) + + it('throws error if at least one value not provided for NaN', () => { + const parse = parser.detailed(['-a'], { + array: 'a', + narg: { + a: NaN + } + }) + parse.error.message.should.match(/Not enough arguments/) + }) + + it('returns an error if not enough positionals were provided for nargs', () => { + const parse = parser.detailed(['-a', '33'], { + array: 'a', + narg: { + a: 2 + } + }) + parse.argv.a.should.eql([33]) + parse.error.message.should.equal('Not enough arguments following: a') + }) + + it('returns an error if not enough positionals were provided for nargs even with nargs-eats-options', () => { + const parse = parser.detailed(['-a', '33', '--cat'], { + narg: { + a: 3 + }, + configuration: { + 'nargs-eats-options': true + } + }) + parse.error.message.should.equal('Not enough arguments following: a') + }) + + it('does not raise error if no arguments are provided for boolean option', () => { + const parse = parser.detailed(['-a'], { + array: 'a', + boolean: 'a', + narg: { + a: NaN + } + }) + expect(parse.error).to.be.null // eslint-disable-line + parse.argv.a.should.eql([true]) + }) + }) + + describe('greedy-arrays=false', () => { + it('does not consume more than one argument after array option', () => { + const argv = parser(['--arr', 'foo', 'bar'], { + array: 'arr', + configuration: { + 'greedy-arrays': false + } + }) + argv.arr.should.eql(['foo']) + argv._.should.eql(['bar']) + }) + + it('places argument into array when specified multiple times', () => { + const argv = parser(['--arr', 99, 'foo', '--arr', 'hello', 'bar'], { + array: 'arr', + configuration: { + 'greedy-arrays': false + } + }) + argv.arr.should.eql([99, 'hello']) + argv._.should.eql(['foo', 'bar']) + }) + + it('places boolean arguments into array when specified multiple times', () => { + const argv = parser(['--arr', 101, '--arr', 102, '--arr', 'false'], { + array: 'arr', + boolean: 'arr', + configuration: { + 'greedy-arrays': false + } + }) + argv.arr.should.eql([true, true, false]) + argv._.should.eql([101, 102]) + }) + }) + + it('should replace the key __proto__ with the key ___proto___', function () { + const argv = parser(['-f.__proto__.foo', '99', '-x.y.__proto__.bar', '100', '--__proto__', '200']) + argv.should.eql({ + _: [], + ___proto___: 200, + f: { + ___proto___: { + foo: 99 + } + }, + x: { + y: { + ___proto___: { + bar: 100 + } + } + } + }) + }) + + it('throws error for unsupported Node.js versions', () => { + process.env.YARGS_MIN_NODE_VERSION = '55' + delete require.cache[require.resolve('../')] + expect(() => { + require('../') + }).to.throw(/yargs parser supports a minimum Node.js version of 55/) + delete process.env.YARGS_MIN_NODE_VERSION + }) + + // Refs: https://github.com/yargs/yargs-parser/issues/386 + describe('perf', () => { + const i = 100000 + describe('unknown-options-as-args', () => { + it('parses long chain of "-" with reasonable performance', function () { + this.timeout(500) + const s = (new Array(i).fill('-').join('')) + 'a' + const parsed = parser([s], { configuration: { 'unknown-options-as-args': true } }) + parsed._[0].should.equal(s) + }) + it('parses long chain of "-a-a" with reasonable performance', function () { + this.timeout(500) + const s = '-' + (new Array(i).fill('-a').join('')) + '=35' + const parsed = parser([s], { configuration: { 'unknown-options-as-args': true } }) + parsed._[0].should.equal(s) + }) + }) + it('parses long chain of "-" with reasonable performance', function () { + this.timeout(500) + const s = (new Array(i).fill('-').join('')) + 'a' + const arg = (new Array(i - 2).fill('-').join('')) + 'a' + const parsed = parser([s]) + parsed[arg].should.equal(true) + }) + it('parses long chain of "-a-a" with reasonable performance', function () { + this.timeout(500) + const s = '-' + (new Array(i).fill('-a').join('')) + '=35' + const arg = 'a' + (new Array(i - 1).fill('A').join('')) + const parsed = parser([s]) + parsed[arg].should.equal(35) + }) + }) +}) diff --git a/test/yargs-parser.js b/test/yargs-parser.js deleted file mode 100644 index 5b59c615..00000000 --- a/test/yargs-parser.js +++ /dev/null @@ -1,2378 +0,0 @@ -/* global beforeEach, describe, it */ - -require('chai').should() - -var expect = require('chai').expect -var fs = require('fs') -var parser = require('../') -var path = require('path') - -describe('yargs-parser', function () { - it('should parse a "short boolean"', function () { - var parse = parser([ '-b' ]) - parse.should.have.property('b').to.be.ok.and.be.a('boolean') - parse.should.have.property('_').with.length(0) - }) - - it('should parse a "long boolean"', function () { - var parse = parser('--bool') - parse.should.have.property('bool', true) - parse.should.have.property('_').with.length(0) - }) - - it('should place bare options in the _ array', function () { - var parse = parser('foo bar baz') - parse.should.have.property('_').and.deep.equal(['foo', 'bar', 'baz']) - }) - - it('should set the value of the final option in a group to the next supplied value', function () { - var parse = parser(['-cats', 'meow']) - parse.should.have.property('c', true) - parse.should.have.property('a', true) - parse.should.have.property('t', true) - parse.should.have.property('s', 'meow') - parse.should.have.property('_').with.length(0) - }) - - it('should set the value of a single long option to the next supplied value', function () { - var parse = parser(['--pow', 'xixxle']) - parse.should.have.property('pow', 'xixxle') - parse.should.have.property('_').with.length(0) - }) - - it('should set the value of a single long option to the next supplied value, even if the value is empty', function () { - var parse = parser(['--pow', '']) - parse.should.have.property('pow', '') - parse.should.have.property('_').with.length(0) - }) - - it('should set the value of a single long option if an = was used', function () { - var parse = parser(['--pow=xixxle']) - parse.should.have.property('pow', 'xixxle') - parse.should.have.property('_').with.length(0) - }) - - it('should set the value of multiple long options to the next supplied values relative to each', function () { - var parse = parser(['--host', 'localhost', '--port', '555']) - parse.should.have.property('host', 'localhost') - parse.should.have.property('port', 555) - parse.should.have.property('_').with.length(0) - }) - - it('should set the value of multiple long options if = signs were used', function () { - var parse = parser(['--host=localhost', '--port=555']) - parse.should.have.property('host', 'localhost') - parse.should.have.property('port', 555) - parse.should.have.property('_').with.length(0) - }) - - it('should still set values appropriately if a mix of short, long, and grouped short options are specified', function () { - var parse = parser(['-h', 'localhost', '-fp', '555', 'script.js']) - parse.should.have.property('f', true) - parse.should.have.property('p', 555) - parse.should.have.property('h', 'localhost') - parse.should.have.property('_').and.deep.equal(['script.js']) - }) - - it('should still set values appropriately if a mix of short and long options are specified', function () { - var parse = parser(['-h', 'localhost', '--port', '555']) - parse.should.have.property('h', 'localhost') - parse.should.have.property('port', 555) - parse.should.have.property('_').with.length(0) - }) - - it('should explicitly set a boolean option to false if preceeded by "--no-"', function () { - var parse = parser(['--no-moo']) - parse.should.have.property('moo', false) - parse.should.have.property('_').with.length(0) - }) - - it('should still set values appropriately if we supply a comprehensive list of various types of options', function () { - var parse = parser([ - '--name=meowmers', 'bare', '-cats', 'woo', - '-h', 'awesome', '--multi=quux', - '--key', 'value', - '-b', '--bool', '--no-meep', '--multi=baz', - '--', '--not-a-flag', '-', '-h', '-multi', '--', 'eek' - ]) - parse.should.have.property('c', true) - parse.should.have.property('a', true) - parse.should.have.property('t', true) - parse.should.have.property('s', 'woo') - parse.should.have.property('h', 'awesome') - parse.should.have.property('b', true) - parse.should.have.property('bool', true) - parse.should.have.property('key', 'value') - parse.should.have.property('multi').and.deep.equal(['quux', 'baz']) - parse.should.have.property('meep', false) - parse.should.have.property('name', 'meowmers') - parse.should.have.property('_').and.deep.equal(['bare', '--not-a-flag', '-', '-h', '-multi', '--', 'eek']) - }) - - it('should parse numbers appropriately', function () { - var argv = parser([ - '-x', '1234', - '-y', '5.67', - '-z', '1e7', - '-w', '10f', - '--hex', '0xdeadbeef', - '789' - ]) - argv.should.have.property('x', 1234).and.be.a('number') - argv.should.have.property('y', 5.67).and.be.a('number') - argv.should.have.property('z', 1e7).and.be.a('number') - argv.should.have.property('w', '10f').and.be.a('string') - argv.should.have.property('hex', 0xdeadbeef).and.be.a('number') - argv.should.have.property('_').and.deep.equal([789]) - argv._[0].should.be.a('number') - }) - - // addresses: https://github.com/yargs/yargs-parser/issues/33 - it('should handle parsing negative #s', function () { - var argv = parser([ - '-33', '-177', '33', - '--n1', '-33', - '-n', '-44', - '--n2=-55', - '--foo.bar', '-33', - '-o=-55', - '--bounds', '-180', '99', '-180', '90', - '--other', '-99', '-220' - ], { - array: 'bounds', - narg: {'other': 2} - }) - - argv._.should.deep.equal([-33, -177, 33]) - argv.n1.should.equal(-33) - argv.n.should.equal(-44) - argv.n2.should.equal(-55) - argv.foo.bar.should.equal(-33) - argv.o.should.equal(-55) - argv.bounds.should.deep.equal([-180, 99, -180, 90]) - argv.other.should.deep.equal([-99, -220]) - }) - - it('should set the value of a single short option to the next supplied value, even if the value is empty', function () { - var parse = parser(['-p', '']) - parse.should.have.property('p', '') - parse.should.have.property('_').with.length(0) - }) - - it('should not set the next value as the value of a short option if that option is explicitly defined as a boolean', function () { - var parse = parser([ '-t', 'moo' ], { - boolean: 't' - }) - parse.should.have.property('t', true).and.be.a('boolean') - parse.should.have.property('_').and.deep.equal(['moo']) - }) - - it('should set boolean options values if the next value is "true" or "false"', function () { - var parse = parser(['--verbose', 'false', 'moo', '-t', 'true'], { - boolean: ['t', 'verbose'], - default: { - verbose: true - } - }) - parse.should.have.property('verbose', false).and.be.a('boolean') - parse.should.have.property('t', true).and.be.a('boolean') - parse.should.have.property('_').and.deep.equal(['moo']) - }) - - it('should not set boolean options values if the next value only contains the words "true" or "false"', function () { - var parse = parser(['--verbose', 'aaatrueaaa', 'moo', '-t', 'aaafalseaaa'], { - boolean: ['t', 'verbose'] - }) - parse.should.have.property('verbose', true).and.be.a('boolean') - parse.should.have.property('t', true).and.be.a('boolean') - parse.should.have.property('_').and.deep.equal(['aaatrueaaa', 'moo', 'aaafalseaaa']) - }) - - it('should allow defining options as boolean in groups', function () { - var parse = parser([ '-x', '-z', 'one', 'two', 'three' ], { - boolean: ['x', 'y', 'z'] - }) - parse.should.have.property('x', true).and.be.a('boolean') - parse.should.have.property('y', false).and.be.a('boolean') - parse.should.have.property('z', true).and.be.a('boolean') - parse.should.have.property('_').and.deep.equal(['one', 'two', 'three']) - }) - - it('should correctly parse dot-notation boolean flags', function () { - var parse = parser(['--nested', '--n.v', '--n.y', 'foo'], { - boolean: ['nested', 'n.v'] - }) - - parse.should.have.property('nested', true).and.be.a('boolean') - parse.should.have.property('n').and.deep.equal({ - v: true, - y: 'foo' - }) - }) - - it('should preserve newlines in option values', function () { - var args = parser(['-s', 'X\nX']) - args.should.have.property('_').with.length(0) - args.should.have.property('s', 'X\nX') - // reproduce in bash: - // VALUE="new - // line" - // node program.js --s="$VALUE" - args = parser(['--s=X\nX']) - args.should.have.property('_').with.length(0) - args.should.have.property('s', 'X\nX') - }) - - it('should not convert numbers to type number if explicitly defined as strings', function () { - var s = parser([ '-s', '0001234' ], { - string: 's' - }).s - s.should.be.a('string').and.equal('0001234') - var x = parser([ '-x', '56' ], { - string: ['x'] - }).x - x.should.be.a('string').and.equal('56') - }) - - it('should default numbers to undefined', function () { - var n = parser([ '-n' ], { - number: ['n'] - }).n - expect(n).to.equal(undefined) - }) - - it('should default number to NaN if value is not a valid number', function () { - var n = parser([ '-n', 'string' ], { - number: ['n'] - }).n - expect(n).to.deep.equal(NaN) - }) - - // Fixes: https://github.com/bcoe/yargs/issues/68 - it('should parse flag arguments with no right-hand-value as strings, if defined as strings', function () { - var s = parser([ '-s' ], { - string: ['s'] - }).s - s.should.be.a('string').and.equal('') - - s = parser([ '-sf' ], { - string: ['s'] - }).s - s.should.be.a('string').and.equal('') - - s = parser([ '--string' ], { - string: ['string'] - }).string - s.should.be.a('string').and.equal('') - }) - - it('should leave all non-hyphenated values as strings if _ is defined as a string', function () { - var s = parser([ ' ', ' ' ], { - string: ['_'] - })._ - s.should.have.length(2) - s[0].should.be.a('string').and.equal(' ') - s[1].should.be.a('string').and.equal(' ') - }) - - describe('normalize', function () { - it('should normalize redundant paths', function () { - var a = parser([ '-s', ['', 'tmp', '..', ''].join(path.sep) ], { - alias: { - s: ['save'] - }, - normalize: 's' - }) - a.should.have.property('s', path.sep) - a.should.have.property('save', path.sep) - }) - - it('should normalize redundant paths when a value is later assigned', function () { - var a = parser(['-s'], { - normalize: ['s'] - }) - a.should.have.property('s', true) - a.s = ['', 'path', 'to', 'new', 'dir', '..', '..', ''].join(path.sep) - a.s.should.equal(['', 'path', 'to', ''].join(path.sep)) - }) - - it('should normalize when key is also an array', function () { - var a = parser([ '-s', ['', 'tmp', '..', ''].join(path.sep), ['', 'path', 'to', 'new', 'dir', '..', '..', ''].join(path.sep) ], { - alias: { - s: ['save'] - }, - normalize: 's', - array: 's' - }) - var expected = [path.sep, ['', 'path', 'to', ''].join(path.sep)] - a.should.have.property('s').and.deep.equal(expected) - a.should.have.property('save').and.deep.equal(expected) - }) - }) - - describe('alias', function () { - it('should set alias value to the same value as the full option', function () { - var argv = parser([ '-f', '11', '--zoom', '55' ], { - alias: { - z: ['zoom'] - } - }) - argv.should.have.property('zoom', 55) - argv.should.have.property('z', 55) - argv.should.have.property('f', 11) - }) - - it('should allow multiple aliases to be specified', function () { - var argv = parser([ '-f', '11', '--zoom', '55' ], { - alias: { - z: ['zm', 'zoom'] - } - }) - - argv.should.have.property('zoom', 55) - argv.should.have.property('z', 55) - argv.should.have.property('zm', 55) - argv.should.have.property('f', 11) - }) - - // regression, see https://github.com/chevex/yargs/issues/63 - it('should not add the same key to argv multiple times, when creating camel-case aliases', function () { - var argv = parser(['--health-check=banana', '--second-key', 'apple', '-t=blarg'], { - alias: { - h: ['health-check'], - 'second-key': ['s'], - 'third-key': ['t'] - }, - default: { - h: 'apple', - 'second-key': 'banana', - 'third-key': 'third' - } - }) - - // before this fix, yargs failed parsing - // one but not all forms of an arg. - argv.secondKey.should.eql('apple') - argv.s.should.eql('apple') - argv['second-key'].should.eql('apple') - - argv.healthCheck.should.eql('banana') - argv.h.should.eql('banana') - argv['health-check'].should.eql('banana') - - argv.thirdKey.should.eql('blarg') - argv.t.should.eql('blarg') - argv['third-key'].should.eql('blarg') - }) - - it('should allow transitive aliases to be specified', function () { - var argv = parser([ '-f', '11', '--zoom', '55' ], { - alias: { - z: 'zm', - zm: 'zoom' - } - }) - - argv.should.have.property('zoom', 55) - argv.should.have.property('z', 55) - argv.should.have.property('zm', 55) - argv.should.have.property('f', 11) - }) - - it('should merge two lists of aliases if they collide', function () { - var argv = parser(['-f', '11', '--zoom', '55'], { - alias: { - z: 'zm', - zoom: 'zoop', - zm: 'zoom' - } - }) - - argv.should.have.property('zoom', 55) - argv.should.have.property('zoop', 55) - argv.should.have.property('z', 55) - argv.should.have.property('zm', 55) - argv.should.have.property('f', 11) - }) - }) - - it('should assign data after forward slash to the option before the slash', function () { - var parse = parser(['-I/foo/bar/baz']) - parse.should.have.property('_').with.length(0) - parse.should.have.property('I', '/foo/bar/baz') - parse = parser(['-xyz/foo/bar/baz']) - parse.should.have.property('x', true) - parse.should.have.property('y', true) - parse.should.have.property('z', '/foo/bar/baz') - parse.should.have.property('_').with.length(0) - }) - - describe('config', function () { - var jsonPath = path.resolve(__dirname, './fixtures/config.json') - - // See: https://github.com/chevex/yargs/issues/12 - it('should load options and values from default config if specified', function () { - var argv = parser([ '--foo', 'bar' ], { - alias: { - z: 'zoom' - }, - default: { - settings: jsonPath - }, - config: 'settings' - }) - - argv.should.have.property('herp', 'derp') - argv.should.have.property('zoom', 55) - argv.should.have.property('foo').and.deep.equal('bar') - }) - - it('should use value from config file, if argv value is using default value', function () { - var argv = parser([], { - alias: { - z: 'zoom' - }, - config: ['settings'], - default: { - settings: jsonPath, - foo: 'banana' - } - }) - - argv.should.have.property('herp', 'derp') - argv.should.have.property('zoom', 55) - argv.should.have.property('foo').and.deep.equal('baz') - }) - - it('should use value from config file, if argv key is a boolean', function () { - var argv = parser([], { - config: ['settings'], - default: { - settings: jsonPath - }, - boolean: ['truthy'] - }) - - argv.should.have.property('truthy', true) - }) - - it('should use value from cli, if cli overrides boolean argv key', function () { - var argv = parser(['--no-truthy'], { - config: ['settings'], - default: { - settings: jsonPath - }, - boolean: ['truthy'] - }) - - argv.should.have.property('truthy', false) - }) - - it('should use cli value, if cli value is set and both cli and default value match', function () { - var argv = parser(['--foo', 'banana'], { - alias: { - z: 'zoom' - }, - config: ['settings'], - default: { - settings: jsonPath, - foo: 'banana' - } - }) - - argv.should.have.property('herp', 'derp') - argv.should.have.property('zoom', 55) - argv.should.have.property('foo').and.deep.equal('banana') - }) - - it("should allow config to be set as flag in 'option'", function () { - var argv = parser([ '--settings', jsonPath, '--foo', 'bar' ], { - alias: { - z: 'zoom' - }, - config: ['settings'] - }) - - argv.should.have.property('herp', 'derp') - argv.should.have.property('zoom', 55) - argv.should.have.property('foo').and.deep.equal('bar') - }) - - 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 argv = parser([ '--settings', jsPath, '--foo', 'bar' ], { - config: ['settings'] - }) - - argv.should.have.property('herp', 'derp') - argv.should.have.property('foo', 'bar') - argv.should.have.property('calculate').and.be.a('function') - }) - - it('should raise an appropriate error if JSON file is not found', function () { - var argv = parser.detailed(['--settings', 'fake.json', '--foo', 'bar'], { - alias: { - z: 'zoom' - }, - config: ['settings'] - }) - - argv.error.message.should.equal('Invalid JSON config file: fake.json') - }) - - // see: https://github.com/bcoe/yargs/issues/172 - it('should not raise an exception if config file is set as default argument value', function () { - var argv = parser.detailed([], { - default: { - config: 'foo.json' - }, - config: ['config'] - }) - - expect(argv.error).to.equal(null) - }) - - it('should load nested options from config file', function () { - var jsonPath = path.resolve(__dirname, './fixtures/nested_config.json') - var argv = parser(['--settings', jsonPath, '--nested.foo', 'bar'], { - config: ['settings'] - }) - - argv.should.have.property('a', 'a') - argv.should.have.property('b', 'b') - argv.should.have.property('nested').and.deep.equal({ - foo: 'bar', - bar: 'bar' - }) - }) - - it('should use nested value from config file, if argv value is using default value', function () { - var jsonPath = path.resolve(__dirname, './fixtures/nested_config.json') - var argv = parser(['--settings', jsonPath], { - config: ['settings'], - default: { - 'nested.foo': 'banana' - } - }) - - argv.should.have.property('a', 'a') - argv.should.have.property('b', 'b') - argv.should.have.property('nested').and.deep.equal({ - foo: 'baz', - bar: 'bar' - }) - }) - - it('allows a custom parsing function to be provided', function () { - var jsPath = path.resolve(__dirname, './fixtures/config.txt') - var argv = parser([ '--settings', jsPath, '--foo', 'bar' ], { - config: { - settings: function (configPath) { - // as an example, parse an environment - // variable style config: - // FOO=99 - // BATMAN=grumpy - var config = {} - var txt = fs.readFileSync(configPath, 'utf-8') - txt.split(/\r?\n/).forEach(function (l) { - var kv = l.split('=') - config[kv[0].toLowerCase()] = kv[1] - }) - return config - } - } - }) - - argv.batman.should.equal('grumpy') - argv.awesome.should.equal('banana') - argv.foo.should.equal('bar') - }) - - it('allows a custom parsing function to be provided as an alias', function () { - var jsPath = path.resolve(__dirname, './fixtures/config.json') - var argv = parser([ '--settings', jsPath, '--foo', 'bar' ], { - config: { - s: function (configPath) { - return JSON.parse(fs.readFileSync(configPath, 'utf-8')) - } - }, - alias: { - s: ['settings'] - } - }) - - argv.should.have.property('herp', 'derp') - argv.should.have.property('foo', 'bar') - }) - - it('outputs an error returned by the parsing function', function () { - var argv = parser.detailed(['--settings=./package.json'], { - config: { - settings: function (configPath) { - return Error('someone set us up the bomb') - } - } - }) - - argv.error.message.should.equal('someone set us up the bomb') - }) - - it('outputs an error if thrown by the parsing function', function () { - var argv = parser.detailed(['--settings=./package.json'], { - config: { - settings: function (configPath) { - throw Error('someone set us up the bomb') - } - } - }) - - argv.error.message.should.equal('someone set us up the bomb') - }) - }) - - describe('config objects', function () { - it('should load options from config object', function () { - var argv = parser([ '--foo', 'bar' ], { - configObjects: [{ - apple: 'apple', - banana: 42, - foo: 'baz' - }] - }) - - argv.should.have.property('apple', 'apple') - argv.should.have.property('banana', 42) - argv.should.have.property('foo', 'bar') - }) - - it('should use value from config object, if argv value is using default value', function () { - var argv = parser([], { - configObjects: [{ - apple: 'apple', - banana: 42, - foo: 'baz' - }], - default: { - foo: 'banana' - } - }) - - argv.should.have.property('apple', 'apple') - argv.should.have.property('banana', 42) - argv.should.have.property('foo', 'baz') - }) - - it('should use value from config object to all aliases', function () { - var argv = parser([], { - configObjects: [{ - apple: 'apple', - banana: 42, - foo: 'baz' - }], - alias: { - a: ['apple'], - banana: ['b'] - } - }) - - argv.should.have.property('apple', 'apple') - argv.should.have.property('a', 'apple') - argv.should.have.property('banana', 42) - argv.should.have.property('b', 42) - argv.should.have.property('foo', 'baz') - }) - - it('should load nested options from config object', function () { - var argv = parser(['--nested.foo', 'bar'], { - configObjects: [{ - a: 'a', - nested: { - foo: 'baz', - bar: 'bar' - }, - b: 'b' - }] - }) - - argv.should.have.property('a', 'a') - argv.should.have.property('b', 'b') - argv.should.have.property('nested').and.deep.equal({ - foo: 'bar', - bar: 'bar' - }) - }) - - it('should use nested value from config object, if argv value is using default value', function () { - var argv = parser([], { - configObjects: [{ - a: 'a', - nested: { - foo: 'baz', - bar: 'bar' - }, - b: 'b' - }], - default: { - 'nested.foo': 'banana' - } - }) - - argv.should.have.property('a', 'a') - argv.should.have.property('b', 'b') - argv.should.have.property('nested').and.deep.equal({ - foo: 'baz', - bar: 'bar' - }) - }) - }) - - describe('dot notation', function () { - it('should allow object graph traversal via dot notation', function () { - var argv = parser([ - '--foo.bar', '3', '--foo.baz', '4', - '--foo.quux.quibble', '5', '--foo.quux.o_O', - '--beep.boop' - ]) - argv.should.have.property('foo').and.deep.equal({ - bar: 3, - baz: 4, - quux: { - quibble: 5, - o_O: true - } - }) - argv.should.have.property('beep').and.deep.equal({ boop: true }) - }) - - it('should apply defaults to dot notation arguments', function () { - var argv = parser([], { - default: { - 'foo.bar': 99 - } - }) - - argv.foo.bar.should.eql(99) - }) - - // see #279 - it('should allow default to be overridden when an alias is provided', function () { - var argv = parser(['--foo.bar', '200'], { - default: { - 'foo.bar': 99 - } - }) - - argv.foo.bar.should.eql(200) - }) - - // see #279 - it('should also override alias', function () { - var argv = parser(['--foo.bar', '200'], { - alias: { - 'foo.bar': ['f'] - }, - default: { - 'foo.bar': 99 - } - }) - - argv.f.should.eql(200) - }) - - // see #279 - it('should not set an undefined dot notation key', function () { - var argv = parser(['--foo.bar', '200'], { - default: { - 'foo.bar': 99 - }, - alias: { - 'foo.bar': ['f'] - } - }) - - ;('foo.bar' in argv).should.equal(false) - }) - - it('should respect .string() for dot notation arguments', function () { - var argv = parser(['--foo.bar', '99', '--bar.foo=99'], { - string: ['foo.bar'] - }) - - argv.foo.bar.should.eql('99') - argv.bar.foo.should.eql(99) - }) - - it('should populate aliases when dot notation is used', function () { - var argv = parser(['--foo.bar', '99'], { - alias: { - foo: ['f'] - } - }) - - argv.f.bar.should.eql(99) - }) - - it('should populate aliases when nested dot notation is used', function () { - var argv = parser(['--foo.bar.snuh', '99', '--foo.apple', '33', '--foo.bar.cool', '11'], { - alias: { - foo: ['f'] - } - }) - - argv.f.bar.snuh.should.eql(99) - argv.foo.bar.snuh.should.eql(99) - - argv.f.apple.should.eql(33) - argv.foo.apple.should.eql(33) - - argv.f.bar.cool.should.eql(11) - argv.foo.bar.cool.should.eql(11) - }) - - it("should allow flags to use dot notation, when seperated by '='", function () { - var argv = parser(['-f.foo=99']) - argv.f.foo.should.eql(99) - }) - - it("should allow flags to use dot notation, when seperated by ' '", function () { - var argv = parser(['-f.foo', '99']) - argv.f.foo.should.eql(99) - }) - - it('should allow flags to use dot notation when no right-hand-side is given', function () { - var argv = parser(['-f.foo', '99', '-f.bar']) - argv.f.foo.should.eql(99) - argv.f.bar.should.eql(true) - }) - }) - - it('should set boolean and alias using explicit true', function () { - var aliased = [ '-h', 'true' ] - var aliasedArgv = parser(aliased, { - boolean: ['h'], - alias: { - h: ['herp'] - } - }) - - aliasedArgv.should.have.property('herp', true) - aliasedArgv.should.have.property('h', true) - aliasedArgv.should.have.property('_').with.length(0) - }) - - // regression, see https://github.com/substack/node-optimist/issues/71 - it('should set boolean and --x=true', function () { - var parsed = parser(['--boool', '--other=true'], { - boolean: ['boool'] - }) - parsed.should.have.property('boool', true) - parsed.should.have.property('other', 'true') - parsed = parser(['--boool', '--other=false'], { - boolean: ['boool'] - }) - parsed.should.have.property('boool', true) - parsed.should.have.property('other', 'false') - }) - - // regression, see https://github.com/chevex/yargs/issues/66 - it('should set boolean options values if next value is "true" or "false" with = as separator', function () { - var argv = parser(['--bool=false'], { - boolean: ['b'], - alias: { - b: ['bool'] - }, - default: { - b: true - } - }) - - argv.bool.should.eql(false) - }) - - describe('short options', function () { - it('should set the value of multiple single short options to the next supplied values relative to each', function () { - var parse = parser(['-h', 'localhost', '-p', '555']) - parse.should.have.property('h', 'localhost') - parse.should.have.property('p', 555) - parse.should.have.property('_').with.length(0) - }) - - it('should set the value of a single short option to the next supplied value', function () { - var parse = parser(['-h', 'localhost']) - parse.should.have.property('h', 'localhost') - parse.should.have.property('_').with.length(0) - }) - - it('should expand grouped short options to a hash with a key for each', function () { - var parse = parser(['-cats']) - parse.should.have.property('c', true) - parse.should.have.property('a', true) - parse.should.have.property('t', true) - parse.should.have.property('s', true) - parse.should.have.property('_').with.length(0) - }) - - it('should set n to the numeric value 123', function () { - var argv = parser([ '-n123' ]) - argv.should.have.property('n', 123) - }) - - it('should set n to the numeric value 123, with n at the end of a group', function () { - var argv = parser([ '-ab5n123' ]) - argv.should.have.property('a', true) - argv.should.have.property('b', true) - argv.should.have.property('5', true) - argv.should.have.property('n', 123) - argv.should.have.property('_').with.length(0) - }) - - it('should set n to the numeric value 123, with = as separator', function () { - var argv = parser([ '-n=123' ]) - argv.should.have.property('n', 123) - }) - - it('should set n to the numeric value 123, with n at the end of a group and = as separator', function () { - var argv = parser([ '-ab5n=123' ]) - argv.should.have.property('a', true) - argv.should.have.property('b', true) - argv.should.have.property('5', true) - argv.should.have.property('n', 123) - argv.should.have.property('_').with.length(0) - }) - }) - - describe('whitespace', function () { - it('should be whitespace', function () { - var argv = parser([ '-x', '\t' ]) - argv.should.have.property('x', '\t') - }) - }) - - describe('boolean modifier function', function () { - it('should prevent yargs from sucking in the next option as the value of the first option', function () { - // Arrange & Act - var result = parser(['-b', '123'], { - boolean: ['b'] - }) - // Assert - result.should.have.property('b').that.is.a('boolean').and.is.true - result.should.have.property('_').and.deep.equal([123]) - }) - }) - - describe('defaults', function () { - function checkNoArgs (opts, hasAlias) { - it('should set defaults if no args', function () { - var result = parser([], opts) - result.should.have.property('flag', true) - if (hasAlias) { - result.should.have.property('f', true) - } - }) - } - - function checkExtraArg (opts, hasAlias) { - it('should set defaults if one extra arg', function () { - var result = parser(['extra'], opts) - result.should.have.property('flag', true) - result.should.have.property('_').and.deep.equal(['extra']) - if (hasAlias) { - result.should.have.property('f', true) - } - }) - } - - function checkStringArg (opts, hasAlias) { - it('should set defaults even if arg looks like a string', function () { - var result = parser([ '--flag', 'extra' ], opts) - result.should.have.property('flag', true) - result.should.have.property('_').and.deep.equal(['extra']) - if (hasAlias) { - result.should.have.property('f', true) - } - }) - } - - describe('for options with aliases', function () { - var opts = { - alias: { - flag: ['f'] - }, - default: { - flag: true - } - } - - checkNoArgs(opts, true) - checkExtraArg(opts, true) - }) - - describe('for typed options without aliases', function () { - var opts = { - boolean: ['flag'], - default: { - flag: true - } - } - - checkNoArgs(opts) - checkExtraArg(opts) - checkStringArg(opts) - }) - - describe('for typed options with aliases', function () { - var opts = { - alias: { - flag: ['f'] - }, - boolean: ['flag'], - default: { - flag: true - } - } - - checkNoArgs(opts, true) - checkExtraArg(opts, true) - checkStringArg(opts, true) - }) - - describe('for boolean options', function () { - [true, false, undefined, null].forEach(function (def) { - describe('with explicit ' + def + ' default', function () { - var opts = { - default: { - flag: def - }, - boolean: ['flag'] - } - - it('should set true if --flag in arg', function () { - parser(['--flag'], opts).flag.should.be.true - }) - - it('should set false if --no-flag in arg', function () { - parser(['--no-flag'], opts).flag.should.be.false - }) - - it('should set ' + def + ' if no flag in arg', function () { - expect(parser([], opts).flag).to.equal(def) - }) - }) - }) - - describe('with implied false default', function () { - var opts = null - - beforeEach(function () { - opts = { - boolean: ['flag'] - } - }) - - it('should set true if --flag in arg', function () { - parser(['--flag'], opts).flag.should.be.true - }) - - it('should set false if --no-flag in arg', function () { - parser(['--no-flag'], opts).flag.should.be.false - }) - - it('should set false if no flag in arg', function () { - parser([], opts).flag.should.be.false - }) - }) - - // Fixes: https://github.com/bcoe/yargs/issues/341 - it('should apply defaults to camel-case form of argument', function () { - var argv = parser([], { - default: { - 'foo-bar': 99 - } - }) - - argv.fooBar.should.equal(99) - }) - }) - - it('should define option as boolean and set default to true', function () { - var argv = parser([], { - boolean: ['sometrue'], - default: { - sometrue: true - } - }) - argv.should.have.property('sometrue', true) - }) - - it('should define option as boolean and set default to false', function () { - var argv = parser([], { - default: { - somefalse: false - }, - boolean: ['somefalse'] - }) - argv.should.have.property('somefalse', false) - }) - - it('should set boolean options to false by default', function () { - var parse = parser(['moo'], { - boolean: ['t', 'verbose'], - default: { - verbose: false, - t: false - } - }) - parse.should.have.property('verbose', false).and.be.a('boolean') - parse.should.have.property('t', false).and.be.a('boolean') - parse.should.have.property('_').and.deep.equal(['moo']) - }) - }) - - describe('camelCase', function () { - function runTests (strict) { - if (!strict) { - // Skip this test in strict mode because this option is not specified - it('should provide options with dashes as camelCase properties', function () { - var result = parser(['--some-option']) - - result.should.have.property('some-option').that.is.a('boolean').and.is.true - result.should.have.property('someOption').that.is.a('boolean').and.is.true - }) - } - - it('should provide count options with dashes as camelCase properties', function () { - var result = parser([ '--some-option', '--some-option', '--some-option' ], { - count: ['some-option'] - }) - - result.should.have.property('some-option', 3) - result.should.have.property('someOption', 3) - }) - - it('should provide options with dashes and aliases as camelCase properties', function () { - var result = parser(['--some-option'], { - alias: { - 'some-horse': 'o' - } - }) - - result.should.have.property('some-option').that.is.a('boolean').and.is.true - result.should.have.property('someOption').that.is.a('boolean').and.is.true - }) - - it('should provide defaults of options with dashes as camelCase properties', function () { - var result = parser([], { - default: { - 'some-option': 'asdf' - } - }) - - result.should.have.property('some-option', 'asdf') - result.should.have.property('someOption', 'asdf') - }) - - it('should provide aliases of options with dashes as camelCase properties', function () { - var result = parser([], { - default: { - 'some-option': 'asdf' - }, - alias: { - 'some-option': ['o'] - } - }) - - result.should.have.property('o', 'asdf') - result.should.have.property('some-option', 'asdf') - result.should.have.property('someOption', 'asdf') - }) - - it('should provide aliases of options with dashes as camelCase properties', function () { - var result = parser([], { - alias: { - o: ['some-option'] - }, - default: { - o: 'asdf' - } - }) - - result.should.have.property('o', 'asdf') - result.should.have.property('some-option', 'asdf') - result.should.have.property('someOption', 'asdf') - }) - - it('should provide aliases with dashes as camelCase properties', function () { - var result = parser(['--some-option', 'val'], { - alias: { - o: 'some-option' - } - }) - - result.should.have.property('o').that.is.a('string').and.equals('val') - result.should.have.property('some-option').that.is.a('string').and.equals('val') - result.should.have.property('someOption').that.is.a('string').and.equals('val') - }) - } - - describe('dashes and camelCase', function () { - runTests() - }) - - describe('dashes and camelCase (strict)', function () { - runTests(true) - }) - }) - - describe('-', function () { - it('should set - as value of n', function () { - var argv = parser(['-n', '-']) - argv.should.have.property('n', '-') - argv.should.have.property('_').with.length(0) - }) - - it('should set - as a non-hyphenated value', function () { - var argv = parser(['-']) - argv.should.have.property('_').and.deep.equal(['-']) - }) - - it('should set - as a value of f', function () { - var argv = parser(['-f-']) - argv.should.have.property('f', '-') - argv.should.have.property('_').with.length(0) - }) - - it('should set b to true and set - as a non-hyphenated value when b is set as a boolean', function () { - var argv = parser(['-b', '-'], { - boolean: ['b'] - }) - - argv.should.have.property('b', true) - argv.should.have.property('_').and.deep.equal(['-']) - }) - - it('should set - as the value of s when s is set as a string', function () { - var argv = parser([ '-s', '-' ], { - string: ['s'] - }) - - argv.should.have.property('s', '-') - argv.should.have.property('_').with.length(0) - }) - }) - - describe('count', function () { - it('should count the number of times a boolean is present', function () { - var parsed - - parsed = parser(['-x'], { - count: ['verbose'] - }) - parsed.verbose.should.equal(0) - - parsed = parser(['--verbose'], { - count: ['verbose'] - }) - parsed.verbose.should.equal(1) - - parsed = parser(['--verbose', '--verbose'], { - count: ['verbose'] - }) - parsed.verbose.should.equal(2) - - parsed = parser(['-vvv'], { - alias: { - v: ['verbose'] - }, - count: ['verbose'] - }) - parsed.verbose.should.equal(3) - - parsed = parser(['--verbose', '--verbose', '-v', '--verbose'], { - count: ['verbose'], - alias: { - v: ['verbose'] - } - }) - parsed.verbose.should.equal(4) - - parsed = parser(['--verbose', '--verbose', '-v', '-vv'], { - count: ['verbose'], - alias: { - v: ['verbose'] - } - }) - parsed.verbose.should.equal(5) - }) - - it('should not consume the next argument', function () { - var parsed = parser([ '-v', 'moo' ], { - count: 'v' - }) - parsed.v.should.equal(1) - parsed.should.have.property('_').and.deep.equal(['moo']) - - parsed = parser([ '--verbose', 'moomoo', '--verbose' ], { - count: 'verbose' - }) - parsed.verbose.should.equal(2) - parsed.should.have.property('_').and.deep.equal(['moomoo']) - }) - - it('should use a default value as is when no arg given', function () { - var parsed = parser([], { - count: 'v', - default: { v: 3 } - }) - parsed.v.should.equal(3) - - parsed = parser([], { - count: 'v', - default: { v: undefined } - }) - expect(parsed.v).to.be.undefined - - parsed = parser([], { - count: 'v', - default: { v: null } - }) - expect(parsed.v).to.be.null - - parsed = parser([], { - count: 'v', - default: { v: false } - }) - parsed.v.should.equal(false) - - parsed = parser([], { - count: 'v', - default: { v: 'hello' } - }) - parsed.v.should.equal('hello') - }) - - it('should ignore a default value when arg given', function () { - var parsed = parser(['-vv', '-v', '-v'], { - count: 'v', - default: { v: 1 } - }) - parsed.v.should.equal(4) - }) - - it('should increment regardless of arg value', function () { - var parsed = parser([ - '-v', - '-v=true', - '-v', 'true', - '-v=false', - '-v', 'false', - '--no-v', - '-v=999', - '-v=foobar' - ], { count: 'v' }) - parsed.v.should.equal(8) - }) - }) - - describe('array', function () { - it('should group values into an array if the same option is specified multiple times (duplicate-arguments-array=true)', function () { - var parse = parser(['-v', 'a', '-v', 'b', '-v', 'c'], {configuration: {'duplicate-arguments-array': true}}) - parse.should.have.property('v').and.deep.equal(['a', 'b', 'c']) - parse.should.have.property('_').with.length(0) - }) - it('should keep only the last value if the same option is specified multiple times (duplicate-arguments-false)', function () { - var parse = parser(['-v', 'a', '-v', 'b', '-v', 'c'], {configuration: {'duplicate-arguments-array': false}}) - parse.should.have.property('v').and.equal('c') - parse.should.have.property('_').with.length(0) - }) - - it('should default an array to an empty array if passed as first option followed by another', function () { - var result = parser(['-a', '-b'], { - array: 'a' - }) - result.should.have.property('a').and.deep.equal([]) - }) - - it('should not attempt to default array if an element has already been populated', function () { - var result = parser(['-a', 'foo', 'bar', '-b'], { - array: 'a' - }) - result.should.have.property('a').and.deep.equal(['foo', 'bar']) - }) - - it('should default argument to empty array if no value given', function () { - var result = parser(['-b'], { - array: 'b' - }) - result.should.have.property('b').and.deep.equal([]) - }) - - it('should place value of argument in array, when one argument provided', function () { - var result = parser(['-b', '33'], { - array: ['b'] - }) - Array.isArray(result.b).should.equal(true) - result.b[0].should.equal(33) - }) - - it('should add multiple argument values to the array', function () { - var result = parser(['-b', '33', '-b', 'hello'], { - array: 'b' - }) - Array.isArray(result.b).should.equal(true) - result.b.should.include(33) - result.b.should.include('hello') - }) - - it('should allow array: true, to be set inside an option block', function () { - var result = parser(['-b', '33'], { - array: 'b' - }) - Array.isArray(result.b).should.equal(true) - result.b.should.include(33) - }) - - // issue #103 - it('should default camel-case alias to array type', function () { - var result = parser(['--ca-path', 'http://www.example.com'], { - array: ['ca-path'] - }) - - Array.isArray(result['ca-path']).should.equal(true) - Array.isArray(result.caPath).should.equal(true) - }) - - it('should default alias to array type', function () { - var result = parser(['--ca-path', 'http://www.example.com'], { - array: 'ca-path', - alias: { - 'ca-path': 'c' - } - }) - - Array.isArray(result['ca-path']).should.equal(true) - Array.isArray(result.caPath).should.equal(true) - Array.isArray(result.c).should.equal(true) - }) - - // see: https://github.com/bcoe/yargs/issues/162 - it('should eat non-hyphenated arguments until hyphenated option is hit', function () { - var result = parser(['-a=hello', 'world', '-b', - '33', '22', '--foo', 'red', 'green', - '--bar=cat', 'dog'], { - array: ['a', 'b', 'foo', 'bar'] - }) - - Array.isArray(result.a).should.equal(true) - result.a.should.include('hello') - result.a.should.include('world') - - Array.isArray(result.b).should.equal(true) - result.b.should.include(33) - result.b.should.include(22) - - Array.isArray(result.foo).should.equal(true) - result.foo.should.include('red') - result.foo.should.include('green') - - Array.isArray(result.bar).should.equal(true) - result.bar.should.include('cat') - result.bar.should.include('dog') - }) - - // see: https://github.com/yargs/yargs-parser/pull/13 - it('should support array for --foo= format when the key is a number', function () { - var result = parser(['--1=a', 'b'], { - array: ['1'] - }) - - Array.isArray(result['1']).should.equal(true) - result['1'][0].should.equal('a') - result['1'][1].should.equal('b') - }) - }) - - describe('nargs', function () { - it('should allow the number of arguments following a key to be specified', function () { - var result = parser([ '--foo', 'apple', 'bar' ], { - narg: { - foo: 2 - } - }) - - Array.isArray(result.foo).should.equal(true) - result.foo[0].should.equal('apple') - result.foo[1].should.equal('bar') - }) - - it('should raise an exception if there are not enough arguments following key', function () { - var argv = parser.detailed('--foo apple', { - narg: { - foo: 2 - } - }) - argv.error.message.should.equal('Not enough arguments following: foo') - }) - - it('nargs is applied to aliases', function () { - var result = parser(['--bar', 'apple', 'bar'], { - narg: { - foo: 2 - }, - alias: { - foo: 'bar' - } - }) - Array.isArray(result.foo).should.equal(true) - result.foo[0].should.equal('apple') - result.foo[1].should.equal('bar') - }) - - it('should apply nargs to flag arguments', function () { - var result = parser([ '-f', 'apple', 'bar', 'blerg' ], { - narg: { - f: 2 - } - }) - - result.f[0].should.equal('apple') - result.f[1].should.equal('bar') - result._[0].should.equal('blerg') - }) - - it('should support nargs for -f= and --bar= format arguments', function () { - var result = parser(['-f=apple', 'bar', 'blerg', '--bar=monkey', 'washing', 'cat'], { - narg: { - f: 2, - bar: 2 - } - }) - - result.f[0].should.equal('apple') - result.f[1].should.equal('bar') - result._[0].should.equal('blerg') - - result.bar[0].should.equal('monkey') - result.bar[1].should.equal('washing') - result._[1].should.equal('cat') - }) - - it('should not modify the input args if an = was used', function () { - var expected = ['-f=apple', 'bar', 'blerg', '--bar=monkey', 'washing', 'cat'] - var args = expected.slice() - parser(args, { - narg: { - f: 2, - bar: 2 - } - }) - args.should.deep.equal(expected) - - parser.detailed(args, { - narg: { - f: 2, - bar: 2 - } - }) - args.should.deep.equal(expected) - }) - - it('allows multiple nargs to be set at the same time', function () { - var result = parser([ '--foo', 'apple', 'bar', '--bar', 'banana', '-f' ], { - narg: { - foo: 2, - bar: 1 - } - }) - - Array.isArray(result.foo).should.equal(true) - result.foo[0].should.equal('apple') - result.foo[1].should.equal('bar') - result.bar.should.equal('banana') - result.f.should.equal(true) - }) - - // see: https://github.com/yargs/yargs-parser/pull/13 - it('should support nargs for --foo= format when the key is a number', function () { - var result = parser(['--1=a', 'b'], { - narg: { - 1: 2 - } - }) - - Array.isArray(result['1']).should.equal(true) - result['1'][0].should.equal('a') - result['1'][1].should.equal('b') - }) - }) - - describe('env vars', function () { - it('should apply all env vars if prefix is empty', function () { - process.env.ONE_FISH = 'twofish' - process.env.RED_FISH = 'bluefish' - var result = parser([], { - envPrefix: '' - }) - - result.oneFish.should.equal('twofish') - result.redFish.should.equal('bluefish') - }) - - it('should apply only env vars matching prefix if prefix is valid string', function () { - process.env.ONE_FISH = 'twofish' - process.env.RED_FISH = 'bluefish' - process.env.GREEN_EGGS = 'sam' - process.env.GREEN_HAM = 'iam' - var result = parser([], { - envPrefix: 'GREEN' - }) - - result.eggs.should.equal('sam') - result.ham.should.equal('iam') - expect(result.oneFish).to.be.undefined - expect(result.redFish).to.be.undefined - }) - - it('should set aliases for options defined by env var', function () { - process.env.AIRFORCE_ONE = 'two' - var result = parser([], { - envPrefix: 'AIRFORCE', - alias: { - '1': ['one', 'uno'] - } - }) - - result['1'].should.equal('two') - result.one.should.equal('two') - result.uno.should.equal('two') - }) - - it('should prefer command line value over env var', function () { - process.env.FOO_BAR = 'ignore' - var result = parser(['--foo-bar', 'baz'], { - envPrefix: '' - }) - - result.fooBar.should.equal('baz') - }) - - it('should respect type for args defined by env var', function () { - process.env.MY_TEST_STRING = '1' - process.env.MY_TEST_NUMBER = '2' - var result = parser([], { - string: 'string', - envPrefix: 'MY_TEST_' - }) - - result.string.should.equal('1') - result.number.should.equal(2) - }) - - it('should set option from aliased env var', function () { - process.env.SPACE_X = 'awesome' - var result = parser([], { - alias: { - xactly: 'x' - }, - envPrefix: 'SPACE' - }) - - result.xactly.should.equal('awesome') - }) - - it('should prefer env var value over configured default', function () { - process.env.FOO_BALL = 'wut' - process.env.FOO_BOOL = 'true' - var result = parser([], { - envPrefix: 'FOO', - default: { - ball: 'baz', - bool: false - }, - boolean: 'bool', - string: 'ball' - }) - - result.ball.should.equal('wut') - result.bool.should.equal(true) - }) - - var jsonPath = path.resolve(__dirname, './fixtures/config.json') - it('should prefer environment variables over config file', function () { - process.env.CFG_HERP = 'zerp' - var result = parser(['--cfg', jsonPath], { - envPrefix: 'CFG', - config: 'cfg', - string: 'herp', - default: { - herp: 'nerp' - } - }) - - result.herp.should.equal('zerp') - }) - - it('should support an env var value as config file option', function () { - process.env.TUX_CFG = jsonPath - var result = parser([], { - envPrefix: 'TUX', - config: ['cfg'], - default: { - z: 44 - } - }) - - result.should.have.property('herp') - result.should.have.property('foo') - result.should.have.property('version') - result.should.have.property('truthy') - result.z.should.equal(55) - }) - - it('should prefer cli config file option over env var config file option', function () { - process.env.MUX_CFG = path.resolve(__dirname, '../package.json') - var result = parser(['--cfg', jsonPath], { - envPrefix: 'MUX', - config: 'cfg' - }) - - result.should.have.property('herp') - result.should.have.property('foo') - result.should.have.property('version') - result.should.have.property('truthy') - result.z.should.equal(55) - }) - - it('should apply all nested env vars', function () { - process.env.TEST_A = 'a' - process.env.TEST_NESTED_OPTION__FOO = 'baz' - process.env.TEST_NESTED_OPTION__BAR = 'bar' - var result = parser(['--nestedOption.foo', 'bar'], { - envPrefix: 'TEST' - }) - - result.should.have.property('a', 'a') - result.should.have.property('nestedOption').and.deep.equal({ - foo: 'bar', - bar: 'bar' - }) - }) - - it('should apply nested env var if argv value is using default value', function () { - process.env.TEST_A = 'a' - process.env.TEST_NESTED_OPTION__FOO = 'baz' - process.env.TEST_NESTED_OPTION__BAR = 'bar' - var result = parser([], { - envPrefix: 'TEST', - default: { - 'nestedOption.foo': 'banana' - } - }) - - result.should.have.property('a', 'a') - result.should.have.property('nestedOption').and.deep.equal({ - foo: 'baz', - bar: 'bar' - }) - }) - }) - - describe('configuration', function () { - describe('short option groups', function () { - it('allows short-option-groups to be disabled', function () { - var parse = parser(['-cats=meow'], { - configuration: { - 'short-option-groups': false - } - }) - parse.cats.should.equal('meow') - parse = parser(['-cats', 'meow'], { - configuration: { - 'short-option-groups': false - } - }) - parse.cats.should.equal('meow') - }) - }) - - describe('camel-case expansion', function () { - it('does not expand camel-case aliases', function () { - var parsed = parser.detailed([], { - alias: { - 'foo-bar': ['x'] - }, - configuration: { - 'camel-case-expansion': false - } - }) - - expect(parsed.newAliases.fooBar).to.equal(undefined) - expect(parsed.aliases.fooBar).to.equal(undefined) - }) - - it('does not expand camel-case keys', function () { - var parsed = parser.detailed(['--foo-bar=apple'], { - configuration: { - 'camel-case-expansion': false - } - }) - - expect(parsed.argv.fooBar).to.equal(undefined) - expect(parsed.argv['foo-bar']).to.equal('apple') - }) - }) - - describe('dot notation', function () { - it('does not expand dot notation defaults', function () { - var parsed = parser([], { - default: { - 'foo.bar': 'x' - }, - configuration: { - 'dot-notation': false - } - }) - - expect(parsed['foo.bar']).to.equal('x') - }) - - it('does not expand dot notation arguments', function () { - var parsed = parser(['--foo.bar', 'banana'], { - configuration: { - 'dot-notation': false - } - }) - expect(parsed['foo.bar']).to.equal('banana') - parsed = parser(['--foo.bar=banana'], { - configuration: { - 'dot-notation': false - } - }) - expect(parsed['foo.bar']).to.equal('banana') - }) - - it('should use value from cli, if cli overrides dot notation default', function () { - var parsed = parser(['--foo.bar', 'abc'], { - default: { - 'foo.bar': 'default' - }, - configuration: { - 'dot-notation': false - } - }) - - expect(parsed['foo.bar']).to.equal('abc') - }) - - it('should also override dot notation alias', function () { - var parsed = parser(['--foo.bar', 'abc'], { - alias: { - 'foo.bar': ['alias.bar'] - }, - default: { - 'foo.bar': 'default' - }, - configuration: { - 'dot-notation': false - } - }) - - expect(parsed['alias.bar']).to.equal('abc') - }) - - it('does not expand alias of first element of dot notation arguments', function () { - var parsed = parser(['--foo.bar', 'banana'], { - alias: { - 'foo': ['f'] - }, - configuration: { - 'dot-notation': false - } - }) - expect(parsed['foo.bar']).to.equal('banana') - expect(parsed).not.to.include.keys('f.bar') - }) - - // addresses https://github.com/yargs/yargs/issues/716 - it('does not append nested-object keys from config to top-level key', function () { - var parsed = parser([], { - alias: { - 'foo': ['f'] - }, - configuration: { - 'dot-notation': false - }, - configObjects: [ - { - 'website.com': { - a: 'b', - b: 'c' - } - } - ] - }) - - parsed['website.com'].should.deep.equal({ - a: 'b', - b: 'c' - }) - }) - }) - - describe('parse numbers', function () { - it('does not coerce defaults into numbers', function () { - var parsed = parser([], { - default: { - 'foo': '5' - }, - configuration: { - 'parse-numbers': false - } - }) - - expect(parsed['foo']).to.equal('5') - }) - - it('does not coerce arguments into numbers', function () { - var parsed = parser(['--foo', '5'], { - configuration: { - 'parse-numbers': false - } - }) - expect(parsed['foo']).to.equal('5') - }) - - it('does not coerce positional arguments into numbers', function () { - var parsed = parser(['5'], { - configuration: { - 'parse-numbers': false - } - }) - expect(parsed._[0]).to.equal('5') - }) - }) - - describe('boolean negation', function () { - it('does not negate arguments prefixed with --no-', function () { - var parsed = parser(['--no-dice'], { - configuration: { - 'boolean-negation': false - } - }) - - parsed['no-dice'].should.equal(true) - expect(parsed.dice).to.equal(undefined) - }) - }) - - describe('duplicate arguments array', function () { - it('adds duplicate argument to array', function () { - var parsed = parser('-x a -x b', { - configuration: { - 'duplicate-arguments-array': true - } - }) - - parsed['x'].should.deep.equal(['a', 'b']) - }) - it('keeps only last argument', function () { - var parsed = parser('-x a -x b', { - configuration: { - 'duplicate-arguments-array': false - } - }) - - parsed['x'].should.equal('b') - }) - }) - - describe('flatten duplicate arrays', function () { - it('flattens duplicate array type', function () { - var parsed = parser('-x a b -x c d', { - array: ['x'], - configuration: { - 'flatten-duplicate-arrays': true - } - }) - - parsed['x'].should.deep.equal(['a', 'b', 'c', 'd']) - }) - it('nests duplicate array types', function () { - var parsed = parser('-x a b -x c d', { - array: ['x'], - configuration: { - 'flatten-duplicate-arrays': false - } - }) - - parsed['x'].should.deep.equal([['a', 'b'], ['c', 'd']]) - }) - it('doesn\'t nests single arrays', function () { - var parsed = parser('-x a b', { - array: ['x'], - configuration: { - 'flatten-duplicate-arrays': false - } - }) - - parsed['x'].should.deep.equal(['a', 'b']) - }) - }) - - describe('duplicate-arguments-array VS flatten-duplicate-arrays', function () { - /* - duplicate=false, flatten=false - type=array - [-x 1 2 3] => [1, 2, 3] - [-x 1 2 3 -x 2 3 4] => [2, 3, 4] - type=string/number/etc - [-x 1 -x 2 -x 3] => 3 - - duplicate=false, flatten=true - type=array - [-x 1 2 3] => [1, 2, 3] - [-x 1 2 3 -x 2 3 4] => [2, 3, 4] - type=string/number/etc - [-x 1 -x 2 -x 3] => 3 - - duplicate=true, flatten=true - type=array - [-x 1 2 3] => [1, 2, 3] - [-x 1 2 3 -x 2 3 4] => [1, 2, 3, 2, 3, 4] - type=string/number/etc - [-x 1 -x 2 -x 3] => [1, 2, 3] - - duplicate=true, flatten=false - type=array - [-x 1 2 3] => [1, 2, 3] - [-x 1 2 3 -x 2 3 4] => [[1, 2, 3], [2, 3, 4]] - type=string/number/etc - [-x 1 -x 2 -x 3] => [1, 2, 3] - */ - describe('duplicate=false, flatten=false,', function () { - describe('type=array', function () { - it('[-x 1 2 3] => [1, 2, 3]', function () { - var parsed = parser('-x 1 2 3', { - array: ['x'], - configuration: { - 'duplicate-arguments-array': false, - 'flatten-duplicate-arrays': false - } - }) - parsed['x'].should.deep.equal([1, 2, 3]) - }) - it('[-x 1 2 3 -x 2 3 4] => [2, 3, 4]', function () { - var parsed = parser('-x 1 2 3 -x 2 3 4', { - array: ['x'], - configuration: { - 'duplicate-arguments-array': false, - 'flatten-duplicate-arrays': false - } - }) - parsed['x'].should.deep.equal([2, 3, 4]) - }) - }) - describe('type=number', function () { - it('[-x 1 -x 2 -x 3] => 3', function () { - var parsed = parser('-x 1 -x 2 -x 3', { - number: 'x', - configuration: { - 'duplicate-arguments-array': false, - 'flatten-duplicate-arrays': false - } - }) - parsed['x'].should.deep.equal(3) - }) - }) - }) - describe('duplicate=false, flatten=true,', function () { - describe('type=array', function () { - it('[-x 1 2 3] => [1, 2, 3]', function () { - var parsed = parser('-x 1 2 3', { - array: ['x'], - configuration: { - 'duplicate-arguments-array': false, - 'flatten-duplicate-arrays': true - } - }) - parsed['x'].should.deep.equal([1, 2, 3]) - }) - it('[-x 1 2 3 -x 2 3 4] => [2, 3, 4]', function () { - var parsed = parser('-x 1 2 3 -x 2 3 4', { - array: ['x'], - configuration: { - 'duplicate-arguments-array': false, - 'flatten-duplicate-arrays': true - } - }) - parsed['x'].should.deep.equal([2, 3, 4]) - }) - }) - describe('type=number', function () { - it('[-x 1 -x 2 -x 3] => 3', function () { - var parsed = parser('-x 1 -x 2 -x 3', { - number: 'x', - configuration: { - 'duplicate-arguments-array': false, - 'flatten-duplicate-arrays': true - } - }) - parsed['x'].should.deep.equal(3) - }) - }) - }) - describe('duplicate=true, flatten=true,', function () { - describe('type=array', function () { - it('[-x 1 2 3] => [1, 2, 3]', function () { - var parsed = parser('-x 1 2 3', { - array: ['x'], - configuration: { - 'duplicate-arguments-array': true, - 'flatten-duplicate-arrays': true - } - }) - parsed['x'].should.deep.equal([1, 2, 3]) - }) - it('[-x 1 2 3 -x 2 3 4] => [1, 2, 3, 2, 3, 4]', function () { - var parsed = parser('-x 1 2 3 -x 2 3 4', { - array: ['x'], - configuration: { - 'duplicate-arguments-array': true, - 'flatten-duplicate-arrays': true - } - }) - parsed['x'].should.deep.equal([1, 2, 3, 2, 3, 4]) - }) - }) - describe('type=number', function () { - it('[-x 1 -x 2 -x 3] => [1, 2, 3]', function () { - var parsed = parser('-x 1 -x 2 -x 3', { - number: 'x', - configuration: { - 'duplicate-arguments-array': true, - 'flatten-duplicate-arrays': true - } - }) - parsed['x'].should.deep.equal([1, 2, 3]) - }) - }) - }) - describe('duplicate=true, flatten=false,', function () { - describe('type=array', function () { - it('[-x 1 -x 2 -x 3] => [1, 2, 3]', function () { - var parsed = parser('-x 1 -x 2 -x 3', { - array: ['x'], - configuration: { - 'duplicate-arguments-array': true, - 'flatten-duplicate-arrays': false - } - }) - parsed['x'].should.deep.equal([1, 2, 3]) - }) - it('[-x 1 2 3 -x 2 3 4] => [[1, 2, 3], [ 2, 3, 4]]', function () { - var parsed = parser('-x 1 2 3 -x 2 3 4', { - array: ['x'], - configuration: { - 'duplicate-arguments-array': true, - 'flatten-duplicate-arrays': false - } - }) - parsed['x'].should.deep.equal([[1, 2, 3], [2, 3, 4]]) - }) - }) - describe('type=number', function () { - it('[-x 1 -x 2 -x 3] => [1, 2, 3]', function () { - var parsed = parser('-x 1 -x 2 -x 3', { - number: 'x', - configuration: { - 'duplicate-arguments-array': true, - 'flatten-duplicate-arrays': false - } - }) - parsed['x'].should.deep.equal([1, 2, 3]) - }) - }) - }) - }) - }) - - // addresses: https://github.com/yargs/yargs-parser/issues/41 - it('defaults to empty array if array option is provided no values', function () { - var parsed = parser(['-f'], { - 'alias': { - 'f': 'files' - }, - 'array': ['files'] - }) - parsed.f.should.deep.equal([]) - parsed.files.should.deep.equal([]) - - parsed = parser(['--files'], { - 'alias': { - 'f': 'files' - }, - 'array': ['files'] - }) - parsed.f.should.deep.equal([]) - parsed.files.should.deep.equal([]) - - parsed = parser(['-f', '-y'], { - 'alias': { - 'f': 'files' - }, - 'array': ['files'] - }) - parsed.f.should.deep.equal([]) - parsed.files.should.deep.equal([]) - }) - - describe('coerce', function () { - it('applies coercion function to simple arguments', function () { - var parsed = parser(['--foo', '99'], { - coerce: { - foo: function (arg) { - return arg * -1 - } - } - }) - parsed.foo.should.equal(-99) - }) - - it('applies coercion function to aliases', function () { - var parsed = parser(['--foo', '99'], { - coerce: { - f: function (arg) { - return arg * -1 - } - }, - alias: { - f: ['foo'] - } - }) - parsed.foo.should.equal(-99) - parsed.f.should.equal(-99) - }) - - it('applies coercion function to all dot options', function () { - var parsed = parser(['--foo.bar', 'nananana'], { - coerce: { - foo: function (val) { - val.bar += ', batman!' - return val - } - } - }) - parsed.foo.bar.should.equal('nananana, batman!') - }) - - it('applies coercion to defaults', function () { - var parsed = parser([], { - default: { foo: 'bar' }, - coerce: { - foo: function (val) { - return val.toUpperCase() - } - } - }) - parsed.foo.should.equal('BAR') - }) - - it('applies coercion function to an implicit array', function () { - var parsed = parser(['--foo', '99', '-f', '33'], { - coerce: { - f: function (arg) { - return arg.map(function (a) { - return a * -1 - }) - } - }, - alias: { - f: ['foo'] - } - }) - parsed.f.should.deep.equal([-99, -33]) - parsed.foo.should.deep.equal([-99, -33]) - }) - - it('applies coercion function to an explicit array', function () { - var parsed = parser(['--foo', '99', '-f', '33'], { - coerce: { - f: function (arg) { - return arg.map(function (a) { - return a * -1 - }) - } - }, - array: ['foo'], - alias: { - f: ['foo'] - } - }) - parsed.f.should.deep.equal([-99, -33]) - parsed.foo.should.deep.equal([-99, -33]) - }) - - it('applies coercion function to _', function () { - var parsed = parser(['99', '33'], { - coerce: { - _: function (arg) { - return arg.map(function (a) { - return a * -1 - }) - } - } - }) - parsed._.should.deep.equal([-99, -33]) - }) - - // see: https://github.com/yargs/yargs/issues/550 - it('coercion function can be used to parse large #s', function () { - var fancyNumberParser = function (arg) { - if (arg.length > 10) return arg - else return parseInt(arg) - } - var parsed = parser(['--foo', '88888889999990000998989898989898', '--bar', '998'], { - coerce: { - foo: fancyNumberParser, - bar: fancyNumberParser - } - }) - ;(typeof parsed.foo).should.equal('string') - parsed.foo.should.equal('88888889999990000998989898989898') - ;(typeof parsed.bar).should.equal('number') - parsed.bar.should.equal(998) - }) - - it('populates argv.error, if an error is thrown', function () { - var parsed = parser.detailed(['--foo', '99'], { - coerce: { - foo: function (arg) { - throw Error('banana') - } - } - }) - parsed.error.message.should.equal('banana') - }) - - it('populates argv.error, if an error is thrown for an explicit array', function () { - var parsed = parser.detailed(['--foo', '99'], { - array: ['foo'], - coerce: { - foo: function (arg) { - throw Error('foo is array: ' + Array.isArray(arg)) - } - } - }) - parsed.error.message.should.equal('foo is array: true') - }) - }) - - // see: https://github.com/yargs/yargs-parser/issues/37 - it('normalizes all paths in array when provided via config object', function () { - var argv = parser([ '--foo', 'bar' ], { - array: ['a'], - normalize: ['a'], - configObjects: [{'a': ['bin/../a.txt', 'bin/../b.txt']}] - }) - argv.a.should.deep.equal(['a.txt', 'b.txt']) - }) -}) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..9cf3eed3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "./node_modules/gts/tsconfig-google.json", + "compilerOptions": { + "outDir": "build", + "rootDir": ".", + "sourceMap": false, + "target": "es2017", + "moduleResolution": "node", + "module": "es2015" + }, + "include": [ + "lib/**/*.ts" + ] +} \ No newline at end of file diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 00000000..0a884940 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "sourceMap": true + }, + "include": [ + "test/typescript/*.ts" + ] +} \ No newline at end of file