├── .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 
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 |
--------------------------------------------------------------------------------