├── .npmignore ├── .gitignore ├── .github ├── FUNDING.yml └── workflows │ └── release.yml ├── LICENSE.md ├── tests ├── 01-sanity.js └── 09-regression.js ├── package.json ├── src ├── flag.js └── index.js └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | images 2 | .* 3 | _* 4 | test 5 | docs 6 | *.md 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .* 3 | !.gitignore 4 | !.npmignore 5 | !.github 6 | node_modules 7 | test/output/**/* 8 | /**/*.old 9 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [coreybutler] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Author.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/01-sanity.js: -------------------------------------------------------------------------------- 1 | import { Parser } from '@author.io/arg' 2 | import test from 'tappedout' 3 | 4 | test('Basic Tests', t => { 5 | const Args = new Parser() 6 | 7 | t.ok(Args !== undefined, 'Parser is defined.') 8 | 9 | t.end() 10 | }) 11 | 12 | test('Escaped parameters', t => { 13 | const Args = new Parser('-r -hdr "CF-IPCountry=US" -kv "test=something" -kv "a=test" demo.js true') 14 | const data = Args.data 15 | 16 | t.ok( 17 | data.r === true && 18 | data.hdr === 'CF-IPCountry=US' && 19 | data['demo.js'] === true && 20 | data.true === true 21 | , 'Parsed complex input' 22 | ) 23 | 24 | // console.log(new Parser('cfw run -r -hdr "CF-IPCountry=US" -kv "test=something" -kv "a=test" demo.js', { 25 | // kv: { 26 | // allowMultipleValues: true 27 | // }, 28 | // header: { 29 | // alias: 'hdr', 30 | // allowMultipleValues: true 31 | // } 32 | // }).data) 33 | 34 | t.end() 35 | }) 36 | 37 | test('Support custom validation methods', t => { 38 | const input = 'test -v ok -b notok' 39 | const cfg = { 40 | value: { 41 | alias: 'v', 42 | validate: vl => vl === 'ok' 43 | } 44 | } 45 | 46 | let Args = new Parser(input, cfg) 47 | 48 | t.ok(Args.violations.length === 0, `Expected no violations, recognized ${Args.violations.length}.`) 49 | if (Args.violations.length > 0) { console.log(Args.violations) } 50 | 51 | cfg.bad = { 52 | alias: 'b', 53 | validate: value => value === 'ok' 54 | } 55 | 56 | Args = new Parser(input, cfg) 57 | t.ok(Args.violations.length === 1, `Expected 1 violation, recognized ${Args.violations.length}.`) 58 | if (Args.violations.length > 1) { console.log(Args.violations) } 59 | 60 | Args = new Parser('test --pass abbbbc', { 61 | pass: { 62 | validate: /^a.*c$/gi 63 | } 64 | }) 65 | 66 | t.ok(Args.violations.length === 0, `Expected no violations, recognized ${Args.violations.length}.`) 67 | if (Args.violations.length > 0) { console.log(Args.violations) } 68 | 69 | t.end() 70 | }) 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@author.io/arg", 3 | "version": "1.3.22", 4 | "description": "An argument parser for CLI applications.", 5 | "main": "./src/index.js", 6 | "exports": { 7 | "import": "./index.js", 8 | "default": "./index.js" 9 | }, 10 | "scripts": { 11 | "start": "dev workspace", 12 | "test": "npm run test:node && npm run test:deno && npm run test:browser && npm run report:syntax && npm run report:size", 13 | "test:node": "dev test -rt node tests/*.js", 14 | "test:node:sanity": "dev test -rt node tests/01-sanity.js", 15 | "test:node:regression": "dev test -rt node tests/09-regression.js", 16 | "test:browser": "dev test -rt browser tests/*.js", 17 | "test:browser:sanity": "dev test -rt browser tests/01-sanity.js", 18 | "test:deno": "dev test -rt deno tests/*.js", 19 | "test:deno:sanity": "dev test -rt deno tests/01-sanity.js", 20 | "manually": "dev test -rt manual tests/*.js", 21 | "build": "dev build", 22 | "report:syntax": "dev report syntax --pretty", 23 | "report:size": "dev report size ./.dist/**/*.js ./.dist/**/*.js.map", 24 | "report:compat": "dev report compatibility ./src/**/*.js", 25 | "report:preview": "npm pack --dry-run && echo \"==============================\" && echo \"This report shows what will be published to the module registry. Pay attention to the tarball contents and assure no sensitive files will be published.\"", 26 | "ci": "dev test --verbose --mode ci --peer -rt node tests/*.js && dev test --mode ci -rt deno tests/*.js && dev test --mode ci -rt browser tests/*.js" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/author/arg.git" 31 | }, 32 | "keywords": [ 33 | "argv", 34 | "arg", 35 | "cli", 36 | "parser", 37 | "argument", 38 | "command" 39 | ], 40 | "author": "Corey Butler", 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/author/arg/issues" 44 | }, 45 | "engines": { 46 | "node": ">=12.0.0" 47 | }, 48 | "type": "module", 49 | "files": [ 50 | "*.js" 51 | ], 52 | "homepage": "https://github.com/author/arg#readme", 53 | "devDependencies": { 54 | "@author.io/dev": "^1.1.5" 55 | }, 56 | "dev": { 57 | "alias": { 58 | "@author.io/arg": "/app/.dist/arg/index.js" 59 | } 60 | }, 61 | "standard": { 62 | "parser": "babel-eslint", 63 | "ignore": [ 64 | "_*", 65 | "_**/*", 66 | ".**/*", 67 | "node_modules", 68 | "karma.conf.js", 69 | "karma.conf.cjs", 70 | "build.js" 71 | ], 72 | "globals": [ 73 | "window", 74 | "global", 75 | "globalThis" 76 | ] 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Tag, Release, & Publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | # Checkout updated source code 13 | - uses: actions/checkout@v2 14 | 15 | # If the version has changed, create a new git tag for it. 16 | - name: Tag 17 | id: autotagger 18 | uses: butlerlogic/action-autotag@stable 19 | with: 20 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 21 | 22 | # The remaining steps all depend on whether or not 23 | # a new tag was created. There is no need to release/publish 24 | # updates until the code base is in a releaseable state. 25 | 26 | # If the new version/tag is a pre-release (i.e. 1.0.0-beta.1), create 27 | # an environment variable indicating it is a prerelease. 28 | - name: Pre-release 29 | if: steps.autotagger.outputs.tagname != '' 30 | run: | 31 | if [[ "${{ steps.autotagger.output.version }}" == *"-"* ]]; then echo "::set-env IS_PRERELEASE=true";else echo "::set-env IS_PRERELEASE=''";fi 32 | 33 | # Create a github release 34 | # This will create a snapshot of the module, 35 | # available in the "Releases" section on Github. 36 | - name: Release 37 | id: create_release 38 | if: steps.autotagger.outputs.tagname != '' 39 | uses: actions/create-release@v1.0.0 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | with: 43 | tag_name: ${{ steps.autotagger.outputs.tagname }} 44 | release_name: ${{ steps.autotagger.outputs.tagname }} 45 | body: ${{ steps.autotagger.outputs.tagmessage }} 46 | draft: false 47 | prerelease: env.IS_PRERELEASE != '' 48 | 49 | - name: Publish 50 | id: publish 51 | if: steps.autotagger.outputs.tagname != '' 52 | uses: author/action-publish@stable 53 | env: 54 | REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} 55 | 56 | - name: Rollback 57 | if: failure() 58 | uses: author/action-rollback@stable 59 | with: 60 | tag: ${{ steps.autotagger.outputs.tagname }} 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | 64 | - name: Failure Notification 65 | if: failure() && steps.autotagger.outputs.tagname != '' 66 | env: 67 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} 68 | SLACK_USERNAME: Github # Optional. (defaults to webhook app) 69 | SLACK_CHANNEL: author # Optional. (defaults to webhook) 70 | SLACK_AVATAR: https://upload.wikimedia.org/wikipedia/commons/thumb/d/db/Npm-logo.svg/320px-Npm-logo.svg.png 71 | uses: Ilshidur/action-slack@master 72 | with: 73 | args: '@author.io/arg ${{ steps.autotagger.outputs.tagname }} failed to publish and was rolled back.' # Optional 74 | 75 | - name: Success Notification 76 | if: success() && steps.autotagger.outputs.tagname != '' 77 | env: 78 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} 79 | SLACK_USERNAME: Github # Optional. (defaults to webhook app) 80 | SLACK_CHANNEL: author # Optional. (defaults to webhook) 81 | SLACK_AVATAR: https://upload.wikimedia.org/wikipedia/commons/thumb/d/db/Npm-logo.svg/320px-Npm-logo.svg.png 82 | uses: Ilshidur/action-slack@master 83 | with: 84 | args: '@author.io/arg ${{ steps.autotagger.outputs.tagname }} published to npm.' # Optional 85 | 86 | - name: Inaction Notification 87 | if: steps.autotagger.outputs.tagname == '' 88 | env: 89 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} 90 | SLACK_USERNAME: Github # Optional. (defaults to webhook app) 91 | SLACK_CHANNEL: author # Optional. (defaults to webhook) 92 | SLACK_AVATAR: "https://cdn.freebiesupply.com/logos/large/2x/nodejs-icon-logo-png-transparent.png" # Optional. can be (repository, sender, an URL) (defaults to webhook app avatar) 93 | uses: Ilshidur/action-slack@master 94 | with: 95 | args: "New code was added to @author/arg's master branch." # Optional -------------------------------------------------------------------------------- /tests/09-regression.js: -------------------------------------------------------------------------------- 1 | import { Parser } from '@author.io/arg' 2 | import test from 'tappedout' 3 | 4 | test('Boolean Defaults Regression Test', t => { 5 | const input = '-r --environment development index.js' 6 | const cfg = { 7 | port: { 8 | alias: 'p', 9 | type: 'number', 10 | required: true, 11 | default: 8787, 12 | description: 'Port' 13 | }, 14 | cache: { 15 | alias: 'c', 16 | description: 'Enable the cache', 17 | type: Boolean, 18 | default: true 19 | }, 20 | reload: { 21 | alias: 'r', 22 | type: Boolean, 23 | description: 'Automatically reload when files change.' 24 | }, 25 | environment: { 26 | description: 'The Wrangler environment to load.' 27 | }, 28 | toml: { 29 | alias: 'f', 30 | description: 'Path to the wrangler.toml configuration file.', 31 | default: './' 32 | } 33 | } 34 | 35 | const Args = new Parser(input, cfg) 36 | const d = Args.data 37 | 38 | t.ok(d.reload === true, 'Recognized boolean flags by alias.') 39 | t.ok(d.cache === true, 'Recognize boolean defaults when they are not specified in the input.') 40 | t.end() 41 | }) 42 | 43 | // This test assures that non-boolean flags positioned 44 | // immediately after a boolean flag are treated separately. 45 | test('Non-Boolean Regression Test', t => { 46 | const input = '--more t' 47 | const cfg = { 48 | test: { 49 | alias: 't', 50 | allowMultipleValues: true, 51 | description: 'test of multiples.' 52 | }, 53 | more: { 54 | alias: 'm', 55 | description: 'more stuff', 56 | type: Boolean 57 | } 58 | } 59 | 60 | const Args = new Parser(input, cfg) 61 | const d = Args.data 62 | t.expect(true, d.more, 'Recognized boolean flag.') 63 | t.expect(true, d.t, 'Treat unrecognized flags separtely from boolean flag. Expected a flag called "t" to exist. Recognized: ' + d.hasOwnProperty('t')) 64 | t.end() 65 | }) 66 | 67 | test('Spaces in flag values', t => { 68 | const input = 'test -c "my connection"' 69 | const cfg = { 70 | connection: { 71 | alias: 'c' 72 | } 73 | } 74 | const { data } = new Parser(input, cfg) 75 | 76 | t.expect('my connection', data.connection, 'Extract escaped values with spaces') 77 | t.end() 78 | }) 79 | 80 | test('Multi-value flags', t => { 81 | const input = 'test -f a.js -f b.js' 82 | const cfg = { 83 | file: { 84 | alias: 'f', 85 | allowMultipleValues: true 86 | } 87 | } 88 | const { data } = new Parser(input, cfg) 89 | 90 | t.expect(2, data.file.length, 'Extract multiple values') 91 | t.end() 92 | }) 93 | 94 | test('Multi-value quoted and unquoted arguments', t => { 95 | const input = 'me@domain.com "John Doe" empty -name \'Jill Doe\'' 96 | const cfg = { 97 | name: { alias: 'n' } 98 | } 99 | const { data } = new Parser(input, cfg) 100 | 101 | t.expect('Jill Doe', data.name, 'recognized single quoted flag') 102 | t.ok(data['me@domain.com'], 'recognized unquoted string with special characters') 103 | t.ok(data['John Doe'], 'recognized double quoted argument with space in the value') 104 | t.ok(data.empty, 'recognized unquoted argument') 105 | 106 | t.end() 107 | }) 108 | 109 | test('Boolean flags followed by unnamed string argument', t => { 110 | const input = '-rt deno@1.7.5 -dm --verbose ./tests/*-*.js' 111 | const cfg = { 112 | runtime: { 113 | alias: 'rt', 114 | description: 'The runtime to build for. This does not do anything by default, but will provide an environment variable called RUNTIME to the internal build process (which can be overridden).', 115 | options: ['node', 'browser', 'deno'], 116 | default: 'node' 117 | }, 118 | debugmodule: { 119 | alias: 'dm', 120 | description: 'Generate a debugging module containing sourcemaps', 121 | type: 'boolean' 122 | }, 123 | verbose: { 124 | description: 'Add verbose logging. This usually displays the command used to launch the container.', 125 | type: 'boolean' 126 | }, 127 | other: { 128 | alias: 'o', 129 | description: 'other bool', 130 | type: 'boolean' 131 | }, 132 | more: { 133 | alias: 'm', 134 | description: 'other bool', 135 | type: 'boolean', 136 | default: true 137 | } 138 | } 139 | const { data } = new Parser(input, cfg) 140 | 141 | t.expect('deno@1.7.5', data.runtime, 'Recognized string flag') 142 | t.expect(true, data.debugmodule, 'Recognized first boolean flag') 143 | t.expect(true, data.verbose, 'Recognized second boolean flag') 144 | t.expect('./tests/*-*.js', data.unknown1, 'Recognized unnamed string argument') 145 | t.expect(false, data.other, 'Unspecified boolean defaults to appropriate value') 146 | t.ok(!data.unknown2, 'No additional unknown values') 147 | t.expect(true, data.more, 'Non-specified boolean defaults to defined value') 148 | 149 | t.end() 150 | }) 151 | 152 | test('Flags with hyphens should not strip hypen', t => { 153 | const input = `session account api_profiles remove "-d1pkFXOnEmvbi-xwDJ8H"` 154 | const { data } = new Parser(input) 155 | 156 | t.expect(true, data['-d1pkFXOnEmvbi-xwDJ8H'], 'hyphenated argument recognized') 157 | 158 | t.end() 159 | }) 160 | -------------------------------------------------------------------------------- /src/flag.js: -------------------------------------------------------------------------------- 1 | export default class Flag { 2 | #name 3 | #rawName 4 | #description 5 | #default = null 6 | #alias = new Set() 7 | #required = false 8 | #type = String 9 | #allowMultipleValues = false 10 | #strictTypes = true 11 | #enum = new Set() 12 | #value = null 13 | #violations = new Set() 14 | #recognized = false 15 | #validator = null 16 | 17 | constructor (cfg = {}) { 18 | if (typeof cfg === 'string') { 19 | cfg = { name: cfg } 20 | } 21 | 22 | if (!cfg.name) { 23 | throw new Error('Flag name is required.') 24 | } 25 | 26 | this.#rawName = cfg.name 27 | this.#name = cfg.name // .replace(/^-+/gi, '').trim() 28 | 29 | if (cfg.hasOwnProperty('description')) { // eslint-disable-line no-prototype-builtins 30 | this.#description = cfg.description 31 | } 32 | 33 | if (cfg.hasOwnProperty('default')) { // eslint-disable-line no-prototype-builtins 34 | this.#default = cfg.default 35 | } 36 | 37 | if (cfg.hasOwnProperty('alias')) { // eslint-disable-line no-prototype-builtins 38 | this.createAlias(cfg.alias) 39 | } 40 | 41 | if (cfg.hasOwnProperty('aliases')) { // eslint-disable-line no-prototype-builtins 42 | this.createAlias(cfg.aliases) 43 | } 44 | 45 | if (cfg.hasOwnProperty('required')) { // eslint-disable-line no-prototype-builtins 46 | this.#required = cfg.required 47 | } 48 | 49 | if (cfg.hasOwnProperty('type')) { // eslint-disable-line no-prototype-builtins 50 | this.type = cfg.type 51 | } else if (cfg.hasOwnProperty('default')) { // eslint-disable-line no-prototype-builtins 52 | if (this.#default) { 53 | this.type = typeof this.#default 54 | } 55 | } 56 | 57 | if (cfg.hasOwnProperty('allowMultipleValues')) { // eslint-disable-line no-prototype-builtins 58 | this.#allowMultipleValues = cfg.allowMultipleValues 59 | } 60 | 61 | if (cfg.hasOwnProperty('strictTypes')) { // eslint-disable-line no-prototype-builtins 62 | this.#strictTypes = cfg.strictTypes 63 | } 64 | 65 | if (cfg.hasOwnProperty('options')) { // eslint-disable-line no-prototype-builtins 66 | this.options = cfg.options 67 | } 68 | 69 | if (cfg.hasOwnProperty('validate')) { // eslint-disable-line no-prototype-builtins 70 | if (!(cfg.validate instanceof RegExp || typeof cfg.validate === 'function')) { 71 | throw new Error(`The "validate" configuration attribute for ${this.#rawName} is invalid. Only RegExp and functions are supported (received ${typeof cfg.validate})`) 72 | } 73 | 74 | this.#validator = cfg.validate 75 | } 76 | } 77 | 78 | get inputName () { 79 | return this.#rawName 80 | } 81 | 82 | get recognized () { 83 | return this.#recognized 84 | } 85 | 86 | set recognized (value) { 87 | this.#recognized = value 88 | } 89 | 90 | get required () { 91 | return this.#required 92 | } 93 | 94 | set required (value) { 95 | this.#required = value 96 | } 97 | 98 | get valid () { 99 | const value = this.value 100 | this.#violations = new Set() 101 | 102 | if (this.#required) { 103 | if (this.#allowMultipleValues ? this.value.length === 0 : this.value === null) { 104 | this.#violations = new Set([`"${this.#name}" is required.`]) 105 | return false 106 | } 107 | } 108 | 109 | if (this.#enum.size > 0) { 110 | if (this.#allowMultipleValues) { 111 | const invalid = value.filter(item => !this.#enum.has(item)) 112 | 113 | if (invalid.length > 0) { 114 | invalid.forEach(v => this.#violations.add(`"${v}" is invalid. Expected one of: ${Array.from(this.#enum).join(', ')}`)) 115 | return false 116 | } 117 | } else if (!this.#enum.has(value)) { 118 | this.#violations.add(`"${value}" is invalid. Expected one of: ${Array.from(this.#enum).join(', ')}`) 119 | return false 120 | } 121 | } 122 | 123 | if (this.#strictTypes) { 124 | const type = this.type 125 | 126 | if (type !== 'any' && type !== '*' && this.recognized) { 127 | if (this.#allowMultipleValues) { 128 | const invalidTypes = value.filter(item => typeof item !== type) // eslint-disable-line valid-typeof 129 | 130 | if (invalidTypes.length > 0) { 131 | invalidTypes.forEach(v => this.#violations.add(`"${this.name}" (${v}) should be a ${type}, not ${typeof v}.`)) 132 | return false 133 | } 134 | } else if (value !== null && typeof value !== type) { // eslint-disable-line valid-typeof 135 | this.#violations.add(`"${this.name}" should be a ${type}, not ${typeof value}.`) 136 | return false 137 | } 138 | } 139 | } 140 | 141 | if (this.#validator !== null) { 142 | if (typeof this.#validator === 'function') { 143 | if (!this.#validator(value)) { 144 | this.#violations.add(`"${value}" is invalid (failed custom validation).`) 145 | return false 146 | } 147 | } else { 148 | if (typeof value !== 'string' && this.#validator instanceof RegExp) { 149 | this.#violations.add(`"${value}" is invalid (failed custom validation).`) 150 | return false 151 | } 152 | 153 | if (!this.#validator.test(value)) { 154 | this.#violations.add(`"${value}" is invalid (failed custom validation).`) 155 | return false 156 | } 157 | } 158 | } 159 | 160 | return true 161 | } 162 | 163 | get violations () { 164 | if (this.valid) { 165 | return [] 166 | } 167 | 168 | return Array.from(this.#violations) 169 | } 170 | 171 | get type () { 172 | return this.#type.name.split(/\s+/)[0].toLowerCase() 173 | } 174 | 175 | set type (value) { 176 | if (typeof value === 'string') { 177 | switch (value.trim().toLowerCase()) { 178 | case 'number': 179 | case 'integer': 180 | case 'float': 181 | case 'double': 182 | this.#type = Number 183 | break 184 | case 'bigint': 185 | this.#type = BigInt // eslint-disable-line no-undef 186 | break 187 | case 'boolean': 188 | this.#type = Boolean 189 | break 190 | default: 191 | this.#type = String 192 | } 193 | } else { 194 | this.#type = value 195 | } 196 | } 197 | 198 | get strictTypes () { 199 | return this.#strictTypes 200 | } 201 | 202 | set strictTypes (value) { 203 | if (typeof value !== 'boolean') { 204 | throw new Error('strictTypes must be a boolean value.') 205 | } 206 | 207 | this.#strictTypes = value 208 | } 209 | 210 | get name () { 211 | return this.#name 212 | } 213 | 214 | set name (value) { 215 | this.#name = value.trim() 216 | } 217 | 218 | get description () { 219 | return this.#name 220 | } 221 | 222 | set description (value) { 223 | this.#description = value.trim() 224 | } 225 | 226 | get value () { 227 | if (this.#allowMultipleValues && (this.#value === null)) { 228 | if (this.#default === null) { 229 | return [] 230 | } 231 | 232 | if (!Array.isArray(this.#default)) { 233 | return [this.#default] 234 | } 235 | } 236 | 237 | return this.#value || this.#default 238 | } 239 | 240 | set value (value) { 241 | if (this.#allowMultipleValues) { 242 | if (Array.isArray(value)) { 243 | this.#value = value 244 | return 245 | } 246 | 247 | this.#value = this.#value || [] 248 | this.#value.push(value) 249 | } else { 250 | this.#value = value 251 | } 252 | } 253 | 254 | get options () { 255 | return Array.from(this.#enum) 256 | } 257 | 258 | set options (value) { 259 | if (typeof value === 'string') { 260 | value = value.split(',').map(option => option.trim()) 261 | } 262 | 263 | this.#enum = new Set(value) 264 | } 265 | 266 | get aliases () { 267 | return Array.from(this.#alias) 268 | } 269 | 270 | get multipleValuesAllowed () { 271 | return this.#allowMultipleValues 272 | } 273 | 274 | hasAlias (alias) { 275 | return this.#alias.has(alias) 276 | } 277 | 278 | createAlias () { 279 | for (let alias of arguments) { 280 | // Convert set to array 281 | if (alias instanceof Set) { 282 | alias = Array.from(alias) 283 | } 284 | 285 | if (Array.isArray(alias)) { 286 | this.createAlias(...alias) 287 | } else if (typeof alias === 'string') { 288 | this.#alias.add(alias.replace(/^-+/gi, '')) 289 | } else { 290 | throw new Error(`Cannot create an alias for a ${typeof alias} element. Please specify a string instead.`) 291 | } 292 | } 293 | } 294 | 295 | allowMultipleValues () { 296 | if (!this.#allowMultipleValues) { 297 | if (this.#value !== null) { 298 | this.#value = [this.#value] 299 | } 300 | 301 | if (this.#default !== null) { 302 | this.#default = [this.#default] 303 | } 304 | 305 | this.#allowMultipleValues = true 306 | } 307 | } 308 | 309 | preventMultipleValues () { 310 | if (this.#allowMultipleValues) { 311 | if (this.#value !== null) { 312 | this.#value = this.#value.pop() 313 | } 314 | 315 | if (this.#default !== null) { 316 | this.#default = this.#default.pop() 317 | } 318 | 319 | this.#allowMultipleValues = false 320 | } 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Argument Parser ![Version](https://img.shields.io/github/v/tag/author/arg?label=Latest&style=for-the-badge) 2 | 3 | There are many CLI argument parsers for Node.js. This differs in the following ways: 4 | 5 | 1. Uses modern [ES Modules](https://nodejs.org/api/esm.html#esm_ecmascript_modules) with private properties (easy to read). 6 | 1. No dependencies. 7 | 1. Separation of Concerns.* 8 | 1. Also works in browsers. 9 | 10 | After writing countless CLI utilities, it became clear that the majority of most existing libraries contain a ton of code that really isn't necessary 99% of the time. This library is still very powerful, but works very transparently, with a minimal API. 11 | 12 | > **This tool is just a parser.** It parses arguments and optionally enforces developer-defined rules. It exposes all relevant aspects of the arguments so developers can use the parsed content in any manner. It does not attempt to autogenerate help screens or apply any other "blackbox" functionality. WYSIWYG.

13 | **If your tool needs more management/organization features, see the [@author.io/shell](https://github.com/author/shell) micro-framework** _(which is built atop this library)_**.** 14 | 15 | > This library is part of the [CLI-First development initiative](https://github.com/coreybutler/clifirst). 16 | 17 | ## Verbose Example 18 | 19 | **Install:** `npm install @author.io/arg` 20 | 21 | The following example automatically parses the `process.argv` variable (i.e. flags passed to a Node script) and strips the first two arguments (the executable name (node) and the script name). It then enforces several rules. 22 | 23 | ```javascript 24 | #!/usr/bin/env node --experimental-modules 25 | import Args from '@author.io/arg' 26 | 27 | // Require specific flags 28 | Args.require('a', 'b', 'input') 29 | 30 | // Optionally specify flag types 31 | Args.types({ 32 | a: Boolean, // accepts primitives or strings, such as 'boolean' 33 | 34 | }) 35 | 36 | // Optionally specify default values for specific flags 37 | Args.defaults({ 38 | c: 'test' 39 | }) 40 | 41 | // Optionally alias flags 42 | Args.alias({ 43 | input: 'in' 44 | }) 45 | 46 | // Optionally specify a list of possible flag values (autocreates the flag if it does not already exist, updates options if it does) 47 | Args.setOptions('name', 'john', 'jane') 48 | 49 | // Allow a flag to accept multiple values (applies when the same flag is defined multiple times). 50 | Args.allowMultipleValues('c') 51 | 52 | // Do not allow unrecognized flags 53 | Args.disallowUnrecognized() 54 | 55 | // Enforce all of the rules specified above, by exiting with an error when an invalid configuration is identified. 56 | Args.enforceRules() 57 | 58 | console.log(Args.data) 59 | ``` 60 | 61 | Using the script above in a `mycli.js` file could be executed as follows: 62 | 63 | ```sh 64 | ./mycli.js -a false -b "some value" -in testfile.txt -c "ignored" -c "accepted" -name jane 65 | ``` 66 | 67 | _Output:_ 68 | ```json 69 | { 70 | "a": false, 71 | "b": "some value", 72 | "c": "accepted", 73 | "input": "testfile.txt", 74 | "name": "jane" 75 | } 76 | ``` 77 | 78 | ## Simpler Syntax 79 | 80 | For brevity, there is also a `configure` method which will automatically do all of the things the first script does, but with minimal code. 81 | 82 | ```javascript 83 | #!/usr/bin/env node --experimental-modules 84 | import Args from '@author.io/arg' 85 | 86 | Args.configure({ 87 | a: { 88 | required: true, 89 | type: 'boolean' 90 | }, 91 | b: { 92 | required: true 93 | }, 94 | c: { 95 | default: 'test', 96 | allowMultipleValues: true 97 | }, 98 | input: { 99 | alias: 'in' 100 | }, 101 | name: { 102 | options: ['john', 'jane'] 103 | } 104 | }) 105 | 106 | // Do not allow unrecognized flags 107 | Args.disallowUnrecognized() 108 | 109 | // Enforce all of the rules specified above, by exiting with an error when an invalid configuration is identified. 110 | Args.enforceRules() 111 | 112 | console.log(Args.data) 113 | ``` 114 | 115 | It is also possible to parse something other than than the `process.argv` variable. An alternative is to provide an array of arguments. 116 | 117 | _Notice the change in the `import` and the optional configuration._ 118 | 119 | ```javascript 120 | #!/usr/bin/env node --experimental-modules 121 | import { Parser } from '@author.io/arg' 122 | 123 | let Args = new Parser(myArgs [,cfg]) 124 | 125 | console.log(Args.data) 126 | ``` 127 | 128 | ## API/Usage 129 | 130 | The source code is pretty easy to figure out, but here's an overview: 131 | 132 | ## Configuring Parser Logic 133 | 134 | There are two ways to configure the parser. A single `configure()` method can describe everything, or individual methods can be used to dynamically define the parsing logic. 135 | 136 | ### Using `configure()` 137 | 138 | The `configure()` method accepts a shorthand (yet-easily-understood) configuration object. 139 | 140 | ```javascript 141 | Args.configure({ 142 | flagname: { 143 | required: true/false, 144 | default: value, 145 | type: string_or_primitive, // example: 'boolean' or Boolean 146 | alias: string, 147 | allowMultipleValues: true/false, 148 | options: [...], 149 | validate: function(){}/RegExp 150 | }, { 151 | ... 152 | } 153 | }) 154 | ``` 155 | 156 | _Purpose:_ 157 | 158 | - `required` - Indicates the flag must be present in the command. 159 | - `default` - A value to use when the flag is not specified. 160 | - `type` - The data type. Supports primitives like `Boolean` or their text (typeof) equivalent (i.e. "`boolean`"). 161 | - `alias` - A string representing an alternative name for the flag. 162 | - `aliases` - Support for multiple aliases. 163 | - `allowMultipleValues` - If a flag is specified more than once, capture all values (instead of only the last one specified). 164 | - `options` - An array of valid values for the flag. 165 | - `validate` - This is a function or regular expression that determines whether the value of the flag is valid or not. A function receives the value as the only argument and is expected to return `true` or `false` (where `true` means the value is valid). If a RegExp is provided, the [RegExp.test()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test) method is executed against the flag value. The validate feature is used **in addition** to other validation mechanisms (options, typing, etc). 166 | 167 | ### Using Individual Methods 168 | 169 | The following methods can be used to dynamically construct the parsing logic, or modify existing logic. 170 | 171 | #### require('flag1', 'flag2', ...) 172 | 173 | Require the presence of specific flags amongst the arguments. Automatically executes `recognize` for all required flags. 174 | 175 | #### recognize('flag1', 'flag2', ...) 176 | 177 | Register "known" flags. This is useful when you want to prevent unrecognized flags from being passed to the application. 178 | 179 | #### types({...}) 180 | 181 | Identify the data type of a flag or series of flags. Automatically executes `recognize` for any flags specified amongst the data types. 182 | 183 | #### defaults({...}) 184 | 185 | Identify default values for flags. 186 | 187 | Automatically executes `recognize` for any flags specified amongst the defaults. 188 | 189 | #### alias({...}) 190 | 191 | Identify aliases for recognized flags. 192 | 193 | Automatically executes `recognize` for any flags specified amongst the defaults. 194 | 195 | #### allowMultipleValues('flag1', 'flag2', ...) 196 | 197 | By default, if the same flag is defined multiple times, only the last value is recognized. Setting `allowMultiple` on a flag will capture all values (as an array). 198 | 199 | Automatically executes `recognize` for any flags specified amongst the defaults. 200 | 201 | #### setOptions('flag', 'optionA', 'optionB') 202 | 203 | A list/enumeration of values will be enforced _if_ the flag is set. If a flag contains a value not present in the list, a violation will be recognized. 204 | 205 | Automatically executes `recognize` for any flags specified amongst the defaults. 206 | 207 | --- 208 | 209 | ## Enforcement Methods 210 | 211 | Enforcement methods are designed to help toggle rules on/off as needed. 212 | 213 | There is no special method to enforce a flag value to be within a list of valid options (enumerability), _because this is enforced automatically_. 214 | 215 | #### disallowUnrecognized() 216 | 217 | Sets a rule to prevent the presence of any unrecognized flags. 218 | 219 | #### allowUnrecognized() 220 | 221 | Sets a rule to allow the presence of unrecognized flags (this is the default behavior). 222 | 223 | #### ignoreDataTypes() 224 | 225 | This will ignore data type checks, even if the `types` method has been used to enforce data types. 226 | 227 | #### enforceDataTypes() 228 | 229 | This will enforce data type checks. This is the default behavior. 230 | 231 | --- 232 | 233 | ## Helper Methods 234 | 235 | The following helper methods are made available for developers who need quick access to flags and enforcement functionality. 236 | 237 | #### enforceRules() 238 | 239 | This method can be used within a process to validate flags and exit with error when validation fails. 240 | 241 | #### value(flagname) 242 | 243 | Retrieve the value of a flag. This accepts flags or aliases. If the specified flag does not exist, a value of `undefined` is returned. 244 | 245 | #### exists(flagname) 246 | 247 | Returns a boolean value indicating the flag exists. 248 | 249 | --- 250 | ## Defining Metadata 251 | 252 | The following methods are made available to manage metadata about flags. 253 | 254 | #### describe(flagname, description) 255 | 256 | Use this message to store a description of the flag. This will throw an error if the flag does not exist. 257 | 258 | #### description(flagname) 259 | 260 | Retrieve the description of a flag. Returns `null` if no description is found. 261 | 262 | --- 263 | 264 | ## Parser Properties 265 | 266 | These are readable properties of the parser. For example: 267 | 268 | ```javascript 269 | import Args from '@author.io/arg' 270 | 271 | Args.configure({...}) 272 | 273 | console.log(Args.flags, Args.data, ...) 274 | ``` 275 | 276 | - `flags`: An array of the unique flag names passed to the application. 277 | - `data`: A key/value object representing all data passed to the application. If a flag is passed in more than once and duplicates are _not_ suppressed, the value will be an array. 278 | - `length` The total number of arguments passed to the application. 279 | - `valid` A boolean representing whether all of the validation rules passed or not. 280 | - `violations` An array of violations (this is an empty array when everything is valid). 281 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Flag from './flag.js' 2 | 3 | // const PARSER = /\s*(?:((?:(?:"(?:\\.|[^"])*")|(?:'[^']*')|(?:\\.)|\S)+)\s*)/gi 4 | const PARSER = /((-+(?[^\s\"\']+))(\s+((?[\"\'](?((\\\"|\\\')|[^\"\'])+)[\"\']|[^-][^\s]+)))?|(([\"\'](?((\\\"|\\\')|[^\"\'])+)[\"\']))|(?[^\s]+))/gi // eslint-disable-line no-useless-escape 5 | const BOOLS = new Set(['true', 'false']) 6 | 7 | class Parser { 8 | #args = [] 9 | #flags = new Map() 10 | #unknownFlags = new Map() 11 | #allowUnrecognized = true 12 | #violations = new Set() 13 | #ignoreTypes = false 14 | #aliases = new Set() 15 | #validFlags = null 16 | #length = 0 17 | #quotedFlags = new Set() 18 | 19 | #cleanFlag = flag => { 20 | return flag.replace(/^-+/g, '').trim().toLowerCase() 21 | } 22 | 23 | #flagRef = flag => { 24 | return (this.getFlag(flag) || this.addFlag(flag)) 25 | } 26 | 27 | constructor (argList = null, cfg = null) { 28 | if (argList !== null && typeof argList === 'object' && !Array.isArray(argList)) { 29 | cfg = argList 30 | argList = null 31 | } 32 | 33 | if (cfg !== null) { 34 | this.configure(cfg) 35 | } 36 | 37 | if (globalThis.hasOwnProperty('argv')) { // eslint-disable-line no-prototype-builtins 38 | this.parse(process.argv.slice(2)) 39 | } else if (argList !== null) { 40 | this.parse(argList) 41 | } 42 | } 43 | 44 | get length () { 45 | return this.#length 46 | } 47 | 48 | get valid () { 49 | this.#validFlags = true 50 | this.#violations = new Set() 51 | 52 | this.#flags.forEach((flag, flagname) => { 53 | if (!this.#aliases.has(flagname)) { 54 | flag.strictTypes = !this.#ignoreTypes 55 | 56 | if (!flag.valid) { 57 | this.#validFlags = false 58 | this.#violations = new Set([...this.#violations, ...flag.violations]) 59 | } 60 | 61 | if (!this.#allowUnrecognized && !flag.recognized) { 62 | this.#validFlags = false 63 | this.#violations.add(`"${flagname}" is unrecognized.`) 64 | } 65 | } 66 | }) 67 | 68 | if (!this.#allowUnrecognized && this.#unknownFlags.size > 0) { 69 | this.#validFlags = false 70 | this.#unknownFlags.forEach(flag => this.#violations.add(`"${flag.name}" is unrecognized.`)) 71 | } 72 | 73 | return this.#validFlags 74 | } 75 | 76 | get violations () { 77 | this.#validFlags = this.#validFlags || this.valid // Helps prevent unnecessarily rerunning the validity getter 78 | return Array.from(this.#violations) 79 | } 80 | 81 | get unrecognizedFlags () { 82 | const result = new Set() 83 | this.#flags.forEach((flag, flagname) => { 84 | if (!this.#aliases.has(flagname)) { 85 | if (!flag.recognized) { 86 | result.add(flag.name) 87 | } 88 | } 89 | }) 90 | 91 | this.#unknownFlags.forEach(flag => result.add(flag.name)) 92 | 93 | return Array.from(result) 94 | } 95 | 96 | get recognizedFlags () { 97 | const result = new Set() 98 | this.#flags.forEach((flag, flagname) => { 99 | if (!this.#aliases.has(flagname)) { 100 | if (flag.recognized) { 101 | result.add(flagname) 102 | } 103 | } 104 | }) 105 | 106 | return Array.from(result) 107 | } 108 | 109 | get flags () { 110 | return Array.from(this.#flags.keys()).concat(Array.from(this.#unknownFlags.keys())) 111 | } 112 | 113 | get data () { 114 | const data = {} 115 | const sources = {} 116 | 117 | this.#flags.forEach((flag, name) => { 118 | if (!this.#aliases.has(name)) { 119 | if (flag.type === 'boolean' && flag.value === null) { 120 | data[flag.name] = false 121 | } else { 122 | data[flag.name] = flag.value 123 | } 124 | 125 | Object.defineProperty(sources, flag.name, { 126 | enumerable: true, 127 | get () { 128 | return flag 129 | } 130 | }) 131 | } 132 | }) 133 | 134 | this.#unknownFlags.forEach((flag, name) => { 135 | let unknownName = flag.name 136 | let count = 0 137 | while (data.hasOwnProperty(unknownName)) { // eslint-disable-line no-prototype-builtins 138 | count++ 139 | unknownName = `${unknownName}${count}` 140 | } 141 | 142 | data[unknownName] = flag.value !== null ? flag.value : true 143 | Object.defineProperty(sources, unknownName, { 144 | enumerable: true, 145 | get () { 146 | return flag 147 | } 148 | }) 149 | }) 150 | 151 | Object.defineProperty(data, 'flagSource', { 152 | enumerable: false, 153 | writable: false, 154 | configurable: false, 155 | value: sources 156 | }) 157 | 158 | return data 159 | } 160 | 161 | configure (config = {}) { 162 | for (const [name, cfg] of Object.entries(config)) { 163 | cfg.name = name 164 | this.addFlag(cfg).recognized = true 165 | } 166 | } 167 | 168 | parse (input) { 169 | if (!input) { 170 | return 171 | } 172 | 173 | // Normalize the input 174 | // If an array is provided, assume the input has been split into 175 | // arguments. Otherwise use the parser RegEx pattern to split 176 | // into arguments. 177 | input = Array.isArray(input) ? input.join(' ') : input 178 | 179 | // Parse using regular expression 180 | const args = [] 181 | const flags = [] 182 | 183 | // Normalize each flag/value pairing 184 | Array.from([...input.matchAll(PARSER)]).forEach(parsedArg => { 185 | let { flag, value, unquoted_value, quoted_arg, arg } = parsedArg.groups // eslint-disable-line camelcase 186 | 187 | // If the arg attribute is present, add the 188 | // value to the arguments placeholder instead 189 | // of the flags 190 | if (arg) { 191 | args.push(arg) 192 | } else if (quoted_arg) { // eslint-disable-line camelcase 193 | args.push(quoted_arg) 194 | this.#quotedFlags.add(this.#cleanFlag(quoted_arg)) 195 | } else { 196 | value = unquoted_value || value // eslint-disable-line camelcase 197 | // Flags without values are considered boolean "true" 198 | value = value !== undefined ? value : true 199 | 200 | // Remove surrounding quotes in string values 201 | // and convert true/false strings to boolean values. 202 | if (typeof value === 'string' && BOOLS.has(value.toLowerCase())) { 203 | value = value.toLowerCase() === 'true' 204 | } 205 | 206 | flags.push({ flag, value }) 207 | } 208 | }) 209 | 210 | // Make the length available via private variable 211 | this.#length = flags.length + args.length 212 | 213 | for (const arg of flags) { 214 | let ref = this.#flagRef(arg.flag) 215 | if (ref.aliasOf) { 216 | ref = ref.aliasOf 217 | } 218 | ref.value = arg.value 219 | } 220 | 221 | for (const arg of args) { 222 | if (!this.exists(arg)) { 223 | this.addFlag(arg).value = true 224 | } else { 225 | // This clause exists in case an alias 226 | // conflicts with the value of an unrecognized flag. 227 | const uflag = new Flag(this.#cleanFlag(arg)) 228 | uflag.strictTypes = !this.#ignoreTypes 229 | // this.#flags.set(this.#cleanFlag(arg), uflag) 230 | this.#unknownFlags.set(this.#cleanFlag(arg), uflag) 231 | } 232 | } 233 | 234 | this.#flags.forEach((flag, name) => { 235 | if (this.#aliases.has(name)) { 236 | if (flag.value !== undefined && !flag.aliasOf.multipleValuesAllowed) { 237 | flag.aliasOf.value = flag.value 238 | } 239 | } 240 | 241 | if (typeof flag.value !== flag.type) { // eslint-disable-line valid-typeof 242 | if (flag.type === 'boolean') { 243 | if (flag.value === null) { 244 | flag.value = false 245 | } else { 246 | const unknownFlag = new Flag(this.#cleanFlag(`unknown${this.#unknownFlags.size + 1}`)) 247 | unknownFlag.strictTypes = !this.#ignoreTypes 248 | unknownFlag.value = flag.value 249 | 250 | if (!this.#unknownFlags.has(unknownFlag.name)) { 251 | this.#unknownFlags.set(unknownFlag.name, unknownFlag) 252 | } 253 | 254 | flag.value = true 255 | } 256 | } 257 | } 258 | }) 259 | } 260 | 261 | getFlag (flag) { 262 | const f = this.#flags.get(this.#cleanFlag(flag)) 263 | if (f) { 264 | return f 265 | } 266 | 267 | return this.#unknownFlags.get(this.#cleanFlag(flag)) 268 | } 269 | 270 | addFlag (cfg) { 271 | cfg = typeof cfg === 'object' ? cfg : { name: cfg } 272 | 273 | const preclean = this.#cleanFlag(cfg.name) 274 | const clean = this.#quotedFlags.has(preclean) ? cfg.name : preclean 275 | 276 | if (this.#flags.has(clean)) { 277 | throw new Error(`"${cfg.name}" flag already exists.`) 278 | } 279 | 280 | const flag = new Flag(cfg) 281 | 282 | flag.strictTypes = !this.#ignoreTypes 283 | 284 | this.#flags.set(clean, flag) 285 | 286 | if (flag.aliases.length > 0) { 287 | flag.aliases.forEach(alias => { 288 | this.#flags.set(this.#cleanFlag(alias), { aliasOf: this.#flags.get(clean) }) 289 | this.#aliases.add(this.#cleanFlag(alias)) 290 | }) 291 | } 292 | 293 | return this.#flags.get(clean) 294 | } 295 | 296 | exists (flag) { 297 | return this.#flags.has(this.#cleanFlag(flag)) || this.#unknownFlags.has(this.#cleanFlag(flag)) 298 | } 299 | 300 | typeof (flag) { 301 | if (!this.exists(flag)) { 302 | if (this.#unknownFlags.has(this.#cleanFlag(flag))) { 303 | return 'boolean' 304 | } 305 | 306 | return 'undefined' 307 | } 308 | 309 | return this.getFlag(flag).type 310 | } 311 | 312 | value (flag = null) { 313 | if (!this.exists(flag)) { 314 | if (this.#unknownFlags.has(this.#cleanFlag(flag))) { 315 | return true 316 | } 317 | 318 | return undefined 319 | } 320 | 321 | return this.getFlag(flag).value 322 | } 323 | 324 | getFlagAliases (flag) { 325 | if (!this.exists(flag)) { 326 | return new Set() 327 | } 328 | 329 | return new Set(this.getFlag(flag).aliases) 330 | } 331 | 332 | require () { 333 | Array.from(arguments).forEach(arg => { 334 | if (!this.#aliases.has(arg)) { 335 | const flag = this.#flagRef(arg) 336 | flag.required = true 337 | flag.recognized = true 338 | } 339 | }) 340 | } 341 | 342 | recognize () { 343 | Array.from(arguments).forEach(arg => { 344 | if (!this.getFlag(arg)) { 345 | this.addFlag(arg).recognized = true 346 | } 347 | }) 348 | } 349 | 350 | disallowUnrecognized () { 351 | this.#allowUnrecognized = false 352 | } 353 | 354 | allowUnrecognized () { 355 | this.#allowUnrecognized = true 356 | } 357 | 358 | ignoreDataTypes () { 359 | this.#ignoreTypes = false 360 | 361 | this.#flags.forEach((flag, name) => { 362 | flag.strictTypes = false 363 | this.#flags.set(name, flag) 364 | }) 365 | } 366 | 367 | enforceDataTypes () { 368 | this.#ignoreTypes = true 369 | 370 | this.#flags.forEach((flag, name) => { 371 | flag.strictTypes = true 372 | this.#flags.set(name, flag) 373 | }) 374 | } 375 | 376 | defaults (obj = {}) { 377 | for (const [name, value] of Object.entries(obj)) { 378 | const flag = this.#flagRef(name) 379 | flag.default = value 380 | flag.recognized = true 381 | } 382 | } 383 | 384 | alias (obj = {}) { 385 | for (const [flagname, alias] of Object.entries(obj)) { 386 | const flag = this.#flagRef(flagname) 387 | 388 | if (this.#aliases.has(alias) && flagname.toLowerCase() !== flag.name.toLowerCase()) { 389 | throw new Error(`The "${alias}" alias is already associated to the "${this.getFlag(alias).name}" flag.`) 390 | } 391 | 392 | if (!flag.hasAlias(alias)) { 393 | flag.createAlias.apply(flag, alias) 394 | } 395 | 396 | flag.recognized = true 397 | } 398 | } 399 | 400 | // In case of duplicate flag, ignore all but last flag value 401 | allowMultipleValues () { 402 | for (const flag of arguments) { 403 | this.#flagRef(flag).allowMultipleValues() 404 | } 405 | } 406 | 407 | preventMultipleValues () { 408 | for (const flag of arguments) { 409 | this.#flagRef(flag).preventMultipleValues() 410 | } 411 | } 412 | 413 | // Set enumerable options for a flag 414 | setOptions () { 415 | if (arguments.length < 2) { 416 | throw new Error('setOptions method requires the flag name and at least one value (i.e. minimum 2 arguments).') 417 | } 418 | 419 | const enums = Array.from(arguments) 420 | const flag = this.#flagRef(enums.shift()) 421 | 422 | flag.recognized = true 423 | flag.options = enums 424 | } 425 | 426 | // Set a description for a flag 427 | describe (flag, desc) { 428 | this.#flagRef(flag).description = desc 429 | } 430 | 431 | // Retrieve a description of the flag. 432 | description (flagname) { 433 | const flag = this.getFlag(flagname) 434 | return flag ? flag.description : 'undefined' 435 | } 436 | 437 | enforceRules () { 438 | this.#validFlags = this.valid 439 | 440 | if (!this.#validFlags) { 441 | if (globalThis.hasOwnProperty('process')) { // eslint-disable-line no-prototype-builtins 442 | console.error('InvalidFlags: Process exited with error.\n * ' + this.violations.join('\n * ')) 443 | return globalThis.process.exit(1) 444 | } else { 445 | throw new Error('InvalidFlags: Process exited with error.') 446 | } 447 | } 448 | 449 | return this.#validFlags 450 | } 451 | } 452 | 453 | const DefaultArgumentParser = new Parser() 454 | 455 | export { DefaultArgumentParser as default, Parser, Flag } 456 | --------------------------------------------------------------------------------