summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYoshiya Hinosawa <stibium121@gmail.com>2023-11-29 15:42:58 +0900
committerGitHub <noreply@github.com>2023-11-29 15:42:58 +0900
commite332fa4a83d36605771844745738c001f276b390 (patch)
tree9d21beacdcad06bb49cc31539ed3d8374e27506f
parent75ec650f080ac66e98d8b848118dc2349ca70aa8 (diff)
fix(ext/node): add util.parseArgs (#21342)
-rw-r--r--cli/tests/node_compat/config.jsonc1
-rw-r--r--cli/tests/node_compat/runner.ts8
-rw-r--r--cli/tests/node_compat/test.ts1
-rw-r--r--cli/tests/node_compat/test/parallel/test-parse-args.mjs1001
-rw-r--r--ext/node/lib.rs2
-rw-r--r--ext/node/polyfills/internal/errors.ts30
-rw-r--r--ext/node/polyfills/internal/util/parse_args/parse_args.js345
-rw-r--r--ext/node/polyfills/internal/util/parse_args/utils.js187
-rw-r--r--ext/node/polyfills/internal/validators.mjs18
-rw-r--r--ext/node/polyfills/util.ts3
10 files changed, 1595 insertions, 1 deletions
diff --git a/cli/tests/node_compat/config.jsonc b/cli/tests/node_compat/config.jsonc
index c60e4ca00..ea4e723fc 100644
--- a/cli/tests/node_compat/config.jsonc
+++ b/cli/tests/node_compat/config.jsonc
@@ -430,6 +430,7 @@
"test-nodeeventtarget.js",
"test-outgoing-message-destroy.js",
"test-outgoing-message-pipe.js",
+ "test-parse-args.mjs",
"test-path-basename.js",
"test-path-dirname.js",
"test-path-extname.js",
diff --git a/cli/tests/node_compat/runner.ts b/cli/tests/node_compat/runner.ts
index 93fca6548..c531efd1f 100644
--- a/cli/tests/node_compat/runner.ts
+++ b/cli/tests/node_compat/runner.ts
@@ -1,8 +1,14 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
import "./polyfill_globals.js";
import { createRequire } from "node:module";
+import { toFileUrl } from "../../../test_util/std/path/mod.ts";
const file = Deno.args[0];
if (!file) {
throw new Error("No file provided");
}
-createRequire(import.meta.url)(file);
+
+if (file.endsWith(".mjs")) {
+ await import(toFileUrl(file).href);
+} else {
+ createRequire(import.meta.url)(file);
+}
diff --git a/cli/tests/node_compat/test.ts b/cli/tests/node_compat/test.ts
index 52bb6810c..3f3d7bd21 100644
--- a/cli/tests/node_compat/test.ts
+++ b/cli/tests/node_compat/test.ts
@@ -83,6 +83,7 @@ async function runTest(t: Deno.TestContext, path: string): Promise<void> {
"--quiet",
"--unstable",
//"--unsafely-ignore-certificate-errors",
+ "--unstable-bare-node-builtins",
"--v8-flags=" + v8Flags.join(),
"runner.ts",
testCase,
diff --git a/cli/tests/node_compat/test/parallel/test-parse-args.mjs b/cli/tests/node_compat/test/parallel/test-parse-args.mjs
new file mode 100644
index 000000000..ae8332fa7
--- /dev/null
+++ b/cli/tests/node_compat/test/parallel/test-parse-args.mjs
@@ -0,0 +1,1001 @@
+// deno-fmt-ignore-file
+// deno-lint-ignore-file
+
+// Copyright Joyent and Node contributors. All rights reserved. MIT license.
+// Taken from Node 18.12.1
+// This file is automatically generated by `tools/node_compat/setup.ts`. Do not modify this file manually.
+
+import '../common/index.mjs';
+import assert from 'node:assert';
+import { test } from 'node:test';
+import { parseArgs } from 'node:util';
+
+test('when short option used as flag then stored as flag', () => {
+ const args = ['-f'];
+ const expected = { values: { __proto__: null, f: true }, positionals: [] };
+ const result = parseArgs({ strict: false, args });
+ assert.deepStrictEqual(result, expected);
+});
+
+test('when short option used as flag before positional then stored as flag and positional (and not value)', () => {
+ const args = ['-f', 'bar'];
+ const expected = { values: { __proto__: null, f: true }, positionals: [ 'bar' ] };
+ const result = parseArgs({ strict: false, args });
+ assert.deepStrictEqual(result, expected);
+});
+
+test('when short option `type: "string"` used with value then stored as value', () => {
+ const args = ['-f', 'bar'];
+ const options = { f: { type: 'string' } };
+ const expected = { values: { __proto__: null, f: 'bar' }, positionals: [] };
+ const result = parseArgs({ args, options });
+ assert.deepStrictEqual(result, expected);
+});
+
+test('when short option listed in short used as flag then long option stored as flag', () => {
+ const args = ['-f'];
+ const options = { foo: { short: 'f', type: 'boolean' } };
+ const expected = { values: { __proto__: null, foo: true }, positionals: [] };
+ const result = parseArgs({ args, options });
+ assert.deepStrictEqual(result, expected);
+});
+
+test('when short option listed in short and long listed in `type: "string"` and ' +
+ 'used with value then long option stored as value', () => {
+ const args = ['-f', 'bar'];
+ const options = { foo: { short: 'f', type: 'string' } };
+ const expected = { values: { __proto__: null, foo: 'bar' }, positionals: [] };
+ const result = parseArgs({ args, options });
+ assert.deepStrictEqual(result, expected);
+});
+
+test('when short option `type: "string"` used without value then stored as flag', () => {
+ const args = ['-f'];
+ const options = { f: { type: 'string' } };
+ const expected = { values: { __proto__: null, f: true }, positionals: [] };
+ const result = parseArgs({ strict: false, args, options });
+ assert.deepStrictEqual(result, expected);
+});
+
+test('short option group behaves like multiple short options', () => {
+ const args = ['-rf'];
+ const options = { };
+ const expected = { values: { __proto__: null, r: true, f: true }, positionals: [] };
+ const result = parseArgs({ strict: false, args, options });
+ assert.deepStrictEqual(result, expected);
+});
+
+test('short option group does not consume subsequent positional', () => {
+ const args = ['-rf', 'foo'];
+ const options = { };
+ const expected = { values: { __proto__: null, r: true, f: true }, positionals: ['foo'] };
+ const result = parseArgs({ strict: false, args, options });
+ assert.deepStrictEqual(result, expected);
+});
+
+// See: Guideline 5 https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html
+test('if terminal of short-option group configured `type: "string"`, subsequent positional is stored', () => {
+ const args = ['-rvf', 'foo'];
+ const options = { f: { type: 'string' } };
+ const expected = { values: { __proto__: null, r: true, v: true, f: 'foo' }, positionals: [] };
+ const result = parseArgs({ strict: false, args, options });
+ assert.deepStrictEqual(result, expected);
+});
+
+test('handles short-option groups in conjunction with long-options', () => {
+ const args = ['-rf', '--foo', 'foo'];
+ const options = { foo: { type: 'string' } };
+ const expected = { values: { __proto__: null, r: true, f: true, foo: 'foo' }, positionals: [] };
+ const result = parseArgs({ strict: false, args, options });
+ assert.deepStrictEqual(result, expected);
+});
+
+test('handles short-option groups with "short" alias configured', () => {
+ const args = ['-rf'];
+ const options = { remove: { short: 'r', type: 'boolean' } };
+ const expected = { values: { __proto__: null, remove: true, f: true }, positionals: [] };
+ const result = parseArgs({ strict: false, args, options });
+ assert.deepStrictEqual(result, expected);
+});
+
+test('handles short-option followed by its value', () => {
+ const args = ['-fFILE'];
+ const options = { foo: { short: 'f', type: 'string' } };
+ const expected = { values: { __proto__: null, foo: 'FILE' }, positionals: [] };
+ const result = parseArgs({ strict: false, args, options });
+ assert.deepStrictEqual(result, expected);
+});
+
+test('Everything after a bare `--` is considered a positional argument', () => {
+ const args = ['--', 'barepositionals', 'mopositionals'];
+ const expected = { values: { __proto__: null }, positionals: ['barepositionals', 'mopositionals'] };
+ const result = parseArgs({ allowPositionals: true, args });
+ assert.deepStrictEqual(result, expected, Error('testing bare positionals'));
+});
+
+test('args are true', () => {
+ const args = ['--foo', '--bar'];
+ const expected = { values: { __proto__: null, foo: true, bar: true }, positionals: [] };
+ const result = parseArgs({ strict: false, args });
+ assert.deepStrictEqual(result, expected, Error('args are true'));
+});
+
+test('arg is true and positional is identified', () => {
+ const args = ['--foo=a', '--foo', 'b'];
+ const expected = { values: { __proto__: null, foo: true }, positionals: ['b'] };
+ const result = parseArgs({ strict: false, args });
+ assert.deepStrictEqual(result, expected, Error('arg is true and positional is identified'));
+});
+
+test('args equals are passed `type: "string"`', () => {
+ const args = ['--so=wat'];
+ const options = { so: { type: 'string' } };
+ const expected = { values: { __proto__: null, so: 'wat' }, positionals: [] };
+ const result = parseArgs({ args, options });
+ assert.deepStrictEqual(result, expected, Error('arg value is passed'));
+});
+
+test('when args include single dash then result stores dash as positional', () => {
+ const args = ['-'];
+ const expected = { values: { __proto__: null }, positionals: ['-'] };
+ const result = parseArgs({ allowPositionals: true, args });
+ assert.deepStrictEqual(result, expected);
+});
+
+test('zero config args equals are parsed as if `type: "string"`', () => {
+ const args = ['--so=wat'];
+ const options = { };
+ const expected = { values: { __proto__: null, so: 'wat' }, positionals: [] };
+ const result = parseArgs({ strict: false, args, options });
+ assert.deepStrictEqual(result, expected, Error('arg value is passed'));
+});
+
+test('same arg is passed twice `type: "string"` and last value is recorded', () => {
+ const args = ['--foo=a', '--foo', 'b'];
+ const options = { foo: { type: 'string' } };
+ const expected = { values: { __proto__: null, foo: 'b' }, positionals: [] };
+ const result = parseArgs({ args, options });
+ assert.deepStrictEqual(result, expected, Error('last arg value is passed'));
+});
+
+test('args equals pass string including more equals', () => {
+ const args = ['--so=wat=bing'];
+ const options = { so: { type: 'string' } };
+ const expected = { values: { __proto__: null, so: 'wat=bing' }, positionals: [] };
+ const result = parseArgs({ args, options });
+ assert.deepStrictEqual(result, expected, Error('arg value is passed'));
+});
+
+test('first arg passed for `type: "string"` and "multiple" is in array', () => {
+ const args = ['--foo=a'];
+ const options = { foo: { type: 'string', multiple: true } };
+ const expected = { values: { __proto__: null, foo: ['a'] }, positionals: [] };
+ const result = parseArgs({ args, options });
+ assert.deepStrictEqual(result, expected, Error('first multiple in array'));
+});
+
+test('args are passed `type: "string"` and "multiple"', () => {
+ const args = ['--foo=a', '--foo', 'b'];
+ const options = {
+ foo: {
+ type: 'string',
+ multiple: true,
+ },
+ };
+ const expected = { values: { __proto__: null, foo: ['a', 'b'] }, positionals: [] };
+ const result = parseArgs({ args, options });
+ assert.deepStrictEqual(result, expected, Error('both arg values are passed'));
+});
+
+test('when expecting `multiple:true` boolean option and option used multiple times then result includes array of ' +
+ 'booleans matching usage', () => {
+ const args = ['--foo', '--foo'];
+ const options = {
+ foo: {
+ type: 'boolean',
+ multiple: true,
+ },
+ };
+ const expected = { values: { __proto__: null, foo: [true, true] }, positionals: [] };
+ const result = parseArgs({ args, options });
+ assert.deepStrictEqual(result, expected);
+});
+
+test('order of option and positional does not matter (per README)', () => {
+ const args1 = ['--foo=bar', 'baz'];
+ const args2 = ['baz', '--foo=bar'];
+ const options = { foo: { type: 'string' } };
+ const expected = { values: { __proto__: null, foo: 'bar' }, positionals: ['baz'] };
+ assert.deepStrictEqual(
+ parseArgs({ allowPositionals: true, args: args1, options }),
+ expected,
+ Error('option then positional')
+ );
+ assert.deepStrictEqual(
+ parseArgs({ allowPositionals: true, args: args2, options }),
+ expected,
+ Error('positional then option')
+ );
+});
+
+test('correct default args when use node -p', () => {
+ const holdArgv = process.argv;
+ process.argv = [process.argv0, '--foo'];
+ const holdExecArgv = process.execArgv;
+ process.execArgv = ['-p', '0'];
+ const result = parseArgs({ strict: false });
+
+ const expected = { values: { __proto__: null, foo: true },
+ positionals: [] };
+ assert.deepStrictEqual(result, expected);
+ process.argv = holdArgv;
+ process.execArgv = holdExecArgv;
+});
+
+test('correct default args when use node --print', () => {
+ const holdArgv = process.argv;
+ process.argv = [process.argv0, '--foo'];
+ const holdExecArgv = process.execArgv;
+ process.execArgv = ['--print', '0'];
+ const result = parseArgs({ strict: false });
+
+ const expected = { values: { __proto__: null, foo: true },
+ positionals: [] };
+ assert.deepStrictEqual(result, expected);
+ process.argv = holdArgv;
+ process.execArgv = holdExecArgv;
+});
+
+test('correct default args when use node -e', () => {
+ const holdArgv = process.argv;
+ process.argv = [process.argv0, '--foo'];
+ const holdExecArgv = process.execArgv;
+ process.execArgv = ['-e', '0'];
+ const result = parseArgs({ strict: false });
+
+ const expected = { values: { __proto__: null, foo: true },
+ positionals: [] };
+ assert.deepStrictEqual(result, expected);
+ process.argv = holdArgv;
+ process.execArgv = holdExecArgv;
+});
+
+test('correct default args when use node --eval', () => {
+ const holdArgv = process.argv;
+ process.argv = [process.argv0, '--foo'];
+ const holdExecArgv = process.execArgv;
+ process.execArgv = ['--eval', '0'];
+ const result = parseArgs({ strict: false });
+ const expected = { values: { __proto__: null, foo: true },
+ positionals: [] };
+ assert.deepStrictEqual(result, expected);
+ process.argv = holdArgv;
+ process.execArgv = holdExecArgv;
+});
+
+test('correct default args when normal arguments', () => {
+ const holdArgv = process.argv;
+ process.argv = [process.argv0, 'script.js', '--foo'];
+ const holdExecArgv = process.execArgv;
+ process.execArgv = [];
+ const result = parseArgs({ strict: false });
+
+ const expected = { values: { __proto__: null, foo: true },
+ positionals: [] };
+ assert.deepStrictEqual(result, expected);
+ process.argv = holdArgv;
+ process.execArgv = holdExecArgv;
+});
+
+test('excess leading dashes on options are retained', () => {
+ // Enforce a design decision for an edge case.
+ const args = ['---triple'];
+ const options = { };
+ const expected = {
+ values: { '__proto__': null, '-triple': true },
+ positionals: []
+ };
+ const result = parseArgs({ strict: false, args, options });
+ assert.deepStrictEqual(result, expected, Error('excess option dashes are retained'));
+});
+
+test('positional arguments are allowed by default in strict:false', () => {
+ const args = ['foo'];
+ const options = { };
+ const expected = {
+ values: { __proto__: null },
+ positionals: ['foo']
+ };
+ const result = parseArgs({ strict: false, args, options });
+ assert.deepStrictEqual(result, expected);
+});
+
+test('positional arguments may be explicitly disallowed in strict:false', () => {
+ const args = ['foo'];
+ const options = { };
+ assert.throws(() => { parseArgs({ strict: false, allowPositionals: false, args, options }); }, {
+ code: 'ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL'
+ });
+});
+
+// Test bad inputs
+
+test('invalid argument passed for options', () => {
+ const args = ['--so=wat'];
+ const options = 'bad value';
+ assert.throws(() => { parseArgs({ args, options }); }, {
+ code: 'ERR_INVALID_ARG_TYPE'
+ });
+});
+
+test('type property missing for option then throw', () => {
+ const knownOptions = { foo: { } };
+ assert.throws(() => { parseArgs({ options: knownOptions }); }, {
+ code: 'ERR_INVALID_ARG_TYPE'
+ });
+});
+
+test('boolean passed to "type" option', () => {
+ const args = ['--so=wat'];
+ const options = { foo: { type: true } };
+ assert.throws(() => { parseArgs({ args, options }); }, {
+ code: 'ERR_INVALID_ARG_TYPE'
+ });
+});
+
+test('invalid union value passed to "type" option', () => {
+ const args = ['--so=wat'];
+ const options = { foo: { type: 'str' } };
+ assert.throws(() => { parseArgs({ args, options }); }, {
+ code: 'ERR_INVALID_ARG_TYPE'
+ });
+});
+
+// Test strict mode
+
+test('unknown long option --bar', () => {
+ const args = ['--foo', '--bar'];
+ const options = { foo: { type: 'boolean' } };
+ assert.throws(() => { parseArgs({ args, options }); }, {
+ code: 'ERR_PARSE_ARGS_UNKNOWN_OPTION'
+ });
+});
+
+test('unknown short option -b', () => {
+ const args = ['--foo', '-b'];
+ const options = { foo: { type: 'boolean' } };
+ assert.throws(() => { parseArgs({ args, options }); }, {
+ code: 'ERR_PARSE_ARGS_UNKNOWN_OPTION'
+ });
+});
+
+test('unknown option -r in short option group -bar', () => {
+ const args = ['-bar'];
+ const options = { b: { type: 'boolean' }, a: { type: 'boolean' } };
+ assert.throws(() => { parseArgs({ args, options }); }, {
+ code: 'ERR_PARSE_ARGS_UNKNOWN_OPTION'
+ });
+});
+
+test('unknown option with explicit value', () => {
+ const args = ['--foo', '--bar=baz'];
+ const options = { foo: { type: 'boolean' } };
+ assert.throws(() => { parseArgs({ args, options }); }, {
+ code: 'ERR_PARSE_ARGS_UNKNOWN_OPTION'
+ });
+});
+
+test('unexpected positional', () => {
+ const args = ['foo'];
+ const options = { foo: { type: 'boolean' } };
+ assert.throws(() => { parseArgs({ args, options }); }, {
+ code: 'ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL'
+ });
+});
+
+test('unexpected positional after --', () => {
+ const args = ['--', 'foo'];
+ const options = { foo: { type: 'boolean' } };
+ assert.throws(() => { parseArgs({ args, options }); }, {
+ code: 'ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL'
+ });
+});
+
+test('-- by itself is not a positional', () => {
+ const args = ['--foo', '--'];
+ const options = { foo: { type: 'boolean' } };
+ const result = parseArgs({ args, options });
+ const expected = { values: { __proto__: null, foo: true },
+ positionals: [] };
+ assert.deepStrictEqual(result, expected);
+});
+
+test('string option used as boolean', () => {
+ const args = ['--foo'];
+ const options = { foo: { type: 'string' } };
+ assert.throws(() => { parseArgs({ args, options }); }, {
+ code: 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE'
+ });
+});
+
+test('boolean option used with value', () => {
+ const args = ['--foo=bar'];
+ const options = { foo: { type: 'boolean' } };
+ assert.throws(() => { parseArgs({ args, options }); }, {
+ code: 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE'
+ });
+});
+
+test('invalid short option length', () => {
+ const args = [];
+ const options = { foo: { short: 'fo', type: 'boolean' } };
+ assert.throws(() => { parseArgs({ args, options }); }, {
+ code: 'ERR_INVALID_ARG_VALUE'
+ });
+});
+
+test('null prototype: when no options then values.toString is undefined', () => {
+ const result = parseArgs({ args: [] });
+ assert.strictEqual(result.values.toString, undefined);
+});
+
+test('null prototype: when --toString then values.toString is true', () => {
+ const args = ['--toString'];
+ const options = { toString: { type: 'boolean' } };
+ const expectedResult = { values: { __proto__: null, toString: true }, positionals: [] };
+
+ const result = parseArgs({ args, options });
+ assert.deepStrictEqual(result, expectedResult);
+});
+
+const candidateGreedyOptions = [
+ '',
+ '-',
+ '--',
+ 'abc',
+ '123',
+ '-s',
+ '--foo',
+];
+
+candidateGreedyOptions.forEach((value) => {
+ test(`greedy: when short option with value '${value}' then eaten`, () => {
+ const args = ['-w', value];
+ const options = { with: { type: 'string', short: 'w' } };
+ const expectedResult = { values: { __proto__: null, with: value }, positionals: [] };
+
+ const result = parseArgs({ args, options, strict: false });
+ assert.deepStrictEqual(result, expectedResult);
+ });
+
+ test(`greedy: when long option with value '${value}' then eaten`, () => {
+ const args = ['--with', value];
+ const options = { with: { type: 'string', short: 'w' } };
+ const expectedResult = { values: { __proto__: null, with: value }, positionals: [] };
+
+ const result = parseArgs({ args, options, strict: false });
+ assert.deepStrictEqual(result, expectedResult);
+ });
+});
+
+test('strict: when candidate option value is plain text then does not throw', () => {
+ const args = ['--with', 'abc'];
+ const options = { with: { type: 'string' } };
+ const expectedResult = { values: { __proto__: null, with: 'abc' }, positionals: [] };
+
+ const result = parseArgs({ args, options, strict: true });
+ assert.deepStrictEqual(result, expectedResult);
+});
+
+test("strict: when candidate option value is '-' then does not throw", () => {
+ const args = ['--with', '-'];
+ const options = { with: { type: 'string' } };
+ const expectedResult = { values: { __proto__: null, with: '-' }, positionals: [] };
+
+ const result = parseArgs({ args, options, strict: true });
+ assert.deepStrictEqual(result, expectedResult);
+});
+
+test("strict: when candidate option value is '--' then throws", () => {
+ const args = ['--with', '--'];
+ const options = { with: { type: 'string' } };
+
+ assert.throws(() => {
+ parseArgs({ args, options });
+ }, {
+ code: 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE'
+ });
+});
+
+test('strict: when candidate option value is short option then throws', () => {
+ const args = ['--with', '-a'];
+ const options = { with: { type: 'string' } };
+
+ assert.throws(() => {
+ parseArgs({ args, options });
+ }, {
+ code: 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE'
+ });
+});
+
+test('strict: when candidate option value is short option digit then throws', () => {
+ const args = ['--with', '-1'];
+ const options = { with: { type: 'string' } };
+
+ assert.throws(() => {
+ parseArgs({ args, options });
+ }, {
+ code: 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE'
+ });
+});
+
+test('strict: when candidate option value is long option then throws', () => {
+ const args = ['--with', '--foo'];
+ const options = { with: { type: 'string' } };
+
+ assert.throws(() => {
+ parseArgs({ args, options });
+ }, {
+ code: 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE'
+ });
+});
+
+test('strict: when short option and suspect value then throws with short option in error message', () => {
+ const args = ['-w', '--foo'];
+ const options = { with: { type: 'string', short: 'w' } };
+
+ assert.throws(() => {
+ parseArgs({ args, options });
+ }, /for '-w'/
+ );
+});
+
+test('strict: when long option and suspect value then throws with long option in error message', () => {
+ const args = ['--with', '--foo'];
+ const options = { with: { type: 'string' } };
+
+ assert.throws(() => {
+ parseArgs({ args, options });
+ }, /for '--with'/
+ );
+});
+
+test('strict: when short option and suspect value then throws with whole expected message', () => {
+ const args = ['-w', '--foo'];
+ const options = { with: { type: 'string', short: 'w' } };
+
+ try {
+ parseArgs({ args, options });
+ } catch (err) {
+ console.info(err.message);
+ }
+
+ assert.throws(() => {
+ parseArgs({ args, options });
+ }, /To specify an option argument starting with a dash use '--with=-XYZ' or '-w-XYZ'/
+ );
+});
+
+test('strict: when long option and suspect value then throws with whole expected message', () => {
+ const args = ['--with', '--foo'];
+ const options = { with: { type: 'string', short: 'w' } };
+
+ assert.throws(() => {
+ parseArgs({ args, options });
+ }, /To specify an option argument starting with a dash use '--with=-XYZ'/
+ );
+});
+
+test('tokens: positional', () => {
+ const args = ['one'];
+ const expectedTokens = [
+ { kind: 'positional', index: 0, value: 'one' },
+ ];
+ const { tokens } = parseArgs({ strict: false, args, tokens: true });
+ assert.deepStrictEqual(tokens, expectedTokens);
+});
+
+test('tokens: -- followed by option-like', () => {
+ const args = ['--', '--foo'];
+ const expectedTokens = [
+ { kind: 'option-terminator', index: 0 },
+ { kind: 'positional', index: 1, value: '--foo' },
+ ];
+ const { tokens } = parseArgs({ strict: false, args, tokens: true });
+ assert.deepStrictEqual(tokens, expectedTokens);
+});
+
+test('tokens: strict:true boolean short', () => {
+ const args = ['-f'];
+ const options = {
+ file: { short: 'f', type: 'boolean' }
+ };
+ const expectedTokens = [
+ { kind: 'option', name: 'file', rawName: '-f',
+ index: 0, value: undefined, inlineValue: undefined },
+ ];
+ const { tokens } = parseArgs({ strict: true, args, options, tokens: true });
+ assert.deepStrictEqual(tokens, expectedTokens);
+});
+
+test('tokens: strict:true boolean long', () => {
+ const args = ['--file'];
+ const options = {
+ file: { short: 'f', type: 'boolean' }
+ };
+ const expectedTokens = [
+ { kind: 'option', name: 'file', rawName: '--file',
+ index: 0, value: undefined, inlineValue: undefined },
+ ];
+ const { tokens } = parseArgs({ strict: true, args, options, tokens: true });
+ assert.deepStrictEqual(tokens, expectedTokens);
+});
+
+test('tokens: strict:false boolean short', () => {
+ const args = ['-f'];
+ const expectedTokens = [
+ { kind: 'option', name: 'f', rawName: '-f',
+ index: 0, value: undefined, inlineValue: undefined },
+ ];
+ const { tokens } = parseArgs({ strict: false, args, tokens: true });
+ assert.deepStrictEqual(tokens, expectedTokens);
+});
+
+test('tokens: strict:false boolean long', () => {
+ const args = ['--file'];
+ const expectedTokens = [
+ { kind: 'option', name: 'file', rawName: '--file',
+ index: 0, value: undefined, inlineValue: undefined },
+ ];
+ const { tokens } = parseArgs({ strict: false, args, tokens: true });
+ assert.deepStrictEqual(tokens, expectedTokens);
+});
+
+test('tokens: strict:false boolean option group', () => {
+ const args = ['-ab'];
+ const expectedTokens = [
+ { kind: 'option', name: 'a', rawName: '-a',
+ index: 0, value: undefined, inlineValue: undefined },
+ { kind: 'option', name: 'b', rawName: '-b',
+ index: 0, value: undefined, inlineValue: undefined },
+ ];
+ const { tokens } = parseArgs({ strict: false, args, tokens: true });
+ assert.deepStrictEqual(tokens, expectedTokens);
+});
+
+test('tokens: strict:false boolean option group with repeated option', () => {
+ // Also positional to check index correct after grouop
+ const args = ['-aa', 'pos'];
+ const expectedTokens = [
+ { kind: 'option', name: 'a', rawName: '-a',
+ index: 0, value: undefined, inlineValue: undefined },
+ { kind: 'option', name: 'a', rawName: '-a',
+ index: 0, value: undefined, inlineValue: undefined },
+ { kind: 'positional', index: 1, value: 'pos' },
+ ];
+ const { tokens } = parseArgs({ strict: false, allowPositionals: true, args, tokens: true });
+ assert.deepStrictEqual(tokens, expectedTokens);
+});
+
+test('tokens: strict:true string short with value after space', () => {
+ // Also positional to check index correct after out-of-line.
+ const args = ['-f', 'bar', 'ppp'];
+ const options = {
+ file: { short: 'f', type: 'string' }
+ };
+ const expectedTokens = [
+ { kind: 'option', name: 'file', rawName: '-f',
+ index: 0, value: 'bar', inlineValue: false },
+ { kind: 'positional', index: 2, value: 'ppp' },
+ ];
+ const { tokens } = parseArgs({ strict: true, allowPositionals: true, args, options, tokens: true });
+ assert.deepStrictEqual(tokens, expectedTokens);
+});
+
+test('tokens: strict:true string short with value inline', () => {
+ const args = ['-fBAR'];
+ const options = {
+ file: { short: 'f', type: 'string' }
+ };
+ const expectedTokens = [
+ { kind: 'option', name: 'file', rawName: '-f',
+ index: 0, value: 'BAR', inlineValue: true },
+ ];
+ const { tokens } = parseArgs({ strict: true, args, options, tokens: true });
+ assert.deepStrictEqual(tokens, expectedTokens);
+});
+
+test('tokens: strict:false string short missing value', () => {
+ const args = ['-f'];
+ const options = {
+ file: { short: 'f', type: 'string' }
+ };
+ const expectedTokens = [
+ { kind: 'option', name: 'file', rawName: '-f',
+ index: 0, value: undefined, inlineValue: undefined },
+ ];
+ const { tokens } = parseArgs({ strict: false, args, options, tokens: true });
+ assert.deepStrictEqual(tokens, expectedTokens);
+});
+
+test('tokens: strict:true string long with value after space', () => {
+ // Also positional to check index correct after out-of-line.
+ const args = ['--file', 'bar', 'ppp'];
+ const options = {
+ file: { short: 'f', type: 'string' }
+ };
+ const expectedTokens = [
+ { kind: 'option', name: 'file', rawName: '--file',
+ index: 0, value: 'bar', inlineValue: false },
+ { kind: 'positional', index: 2, value: 'ppp' },
+ ];
+ const { tokens } = parseArgs({ strict: true, allowPositionals: true, args, options, tokens: true });
+ assert.deepStrictEqual(tokens, expectedTokens);
+});
+
+test('tokens: strict:true string long with value inline', () => {
+ // Also positional to check index correct after out-of-line.
+ const args = ['--file=bar', 'pos'];
+ const options = {
+ file: { short: 'f', type: 'string' }
+ };
+ const expectedTokens = [
+ { kind: 'option', name: 'file', rawName: '--file',
+ index: 0, value: 'bar', inlineValue: true },
+ { kind: 'positional', index: 1, value: 'pos' },
+ ];
+ const { tokens } = parseArgs({ strict: true, allowPositionals: true, args, options, tokens: true });
+ assert.deepStrictEqual(tokens, expectedTokens);
+});
+
+test('tokens: strict:false string long with value inline', () => {
+ const args = ['--file=bar'];
+ const expectedTokens = [
+ { kind: 'option', name: 'file', rawName: '--file',
+ index: 0, value: 'bar', inlineValue: true },
+ ];
+ const { tokens } = parseArgs({ strict: false, args, tokens: true });
+ assert.deepStrictEqual(tokens, expectedTokens);
+});
+
+test('tokens: strict:false string long missing value', () => {
+ const args = ['--file'];
+ const options = {
+ file: { short: 'f', type: 'string' }
+ };
+ const expectedTokens = [
+ { kind: 'option', name: 'file', rawName: '--file',
+ index: 0, value: undefined, inlineValue: undefined },
+ ];
+ const { tokens } = parseArgs({ strict: false, args, options, tokens: true });
+ assert.deepStrictEqual(tokens, expectedTokens);
+});
+
+test('tokens: strict:true complex option group with value after space', () => {
+ // Also positional to check index correct afterwards.
+ const args = ['-ab', 'c', 'pos'];
+ const options = {
+ alpha: { short: 'a', type: 'boolean' },
+ beta: { short: 'b', type: 'string' },
+ };
+ const expectedTokens = [
+ { kind: 'option', name: 'alpha', rawName: '-a',
+ index: 0, value: undefined, inlineValue: undefined },
+ { kind: 'option', name: 'beta', rawName: '-b',
+ index: 0, value: 'c', inlineValue: false },
+ { kind: 'positional', index: 2, value: 'pos' },
+ ];
+ const { tokens } = parseArgs({ strict: true, allowPositionals: true, args, options, tokens: true });
+ assert.deepStrictEqual(tokens, expectedTokens);
+});
+
+test('tokens: strict:true complex option group with inline value', () => {
+ // Also positional to check index correct afterwards.
+ const args = ['-abc', 'pos'];
+ const options = {
+ alpha: { short: 'a', type: 'boolean' },
+ beta: { short: 'b', type: 'string' },
+ };
+ const expectedTokens = [
+ { kind: 'option', name: 'alpha', rawName: '-a',
+ index: 0, value: undefined, inlineValue: undefined },
+ { kind: 'option', name: 'beta', rawName: '-b',
+ index: 0, value: 'c', inlineValue: true },
+ { kind: 'positional', index: 1, value: 'pos' },
+ ];
+ const { tokens } = parseArgs({ strict: true, allowPositionals: true, args, options, tokens: true });
+ assert.deepStrictEqual(tokens, expectedTokens);
+});
+
+test('tokens: strict:false with single dashes', () => {
+ const args = ['--file', '-', '-'];
+ const options = {
+ file: { short: 'f', type: 'string' },
+ };
+ const expectedTokens = [
+ { kind: 'option', name: 'file', rawName: '--file',
+ index: 0, value: '-', inlineValue: false },
+ { kind: 'positional', index: 2, value: '-' },
+ ];
+ const { tokens } = parseArgs({ strict: false, args, options, tokens: true });
+ assert.deepStrictEqual(tokens, expectedTokens);
+});
+
+test('tokens: strict:false with -- --', () => {
+ const args = ['--', '--'];
+ const expectedTokens = [
+ { kind: 'option-terminator', index: 0 },
+ { kind: 'positional', index: 1, value: '--' },
+ ];
+ const { tokens } = parseArgs({ strict: false, args, tokens: true });
+ assert.deepStrictEqual(tokens, expectedTokens);
+});
+
+test('default must be a boolean when option type is boolean', () => {
+ const args = [];
+ const options = { alpha: { type: 'boolean', default: 'not a boolean' } };
+ assert.throws(() => {
+ parseArgs({ args, options });
+ }, /"options\.alpha\.default" property must be of type boolean/
+ );
+});
+
+test('default must accept undefined value', () => {
+ const args = [];
+ const options = { alpha: { type: 'boolean', default: undefined } };
+ const result = parseArgs({ args, options });
+ const expected = {
+ values: {
+ __proto__: null,
+ },
+ positionals: []
+ };
+ assert.deepStrictEqual(result, expected);
+});
+
+test('default must be a boolean array when option type is boolean and multiple', () => {
+ const args = [];
+ const options = { alpha: { type: 'boolean', multiple: true, default: 'not an array' } };
+ assert.throws(() => {
+ parseArgs({ args, options });
+ }, /"options\.alpha\.default" property must be an instance of Array/
+ );
+});
+
+test('default must be a boolean array when option type is string and multiple is true', () => {
+ const args = [];
+ const options = { alpha: { type: 'boolean', multiple: true, default: [true, true, 42] } };
+ assert.throws(() => {
+ parseArgs({ args, options });
+ }, /"options\.alpha\.default\[2\]" property must be of type boolean/
+ );
+});
+
+test('default must be a string when option type is string', () => {
+ const args = [];
+ const options = { alpha: { type: 'string', default: true } };
+ assert.throws(() => {
+ parseArgs({ args, options });
+ }, /"options\.alpha\.default" property must be of type string/
+ );
+});
+
+test('default must be an array when option type is string and multiple is true', () => {
+ const args = [];
+ const options = { alpha: { type: 'string', multiple: true, default: 'not an array' } };
+ assert.throws(() => {
+ parseArgs({ args, options });
+ }, /"options\.alpha\.default" property must be an instance of Array/
+ );
+});
+
+test('default must be a string array when option type is string and multiple is true', () => {
+ const args = [];
+ const options = { alpha: { type: 'string', multiple: true, default: ['str', 42] } };
+ assert.throws(() => {
+ parseArgs({ args, options });
+ }, /"options\.alpha\.default\[1\]" property must be of type string/
+ );
+});
+
+test('default accepted input when multiple is true', () => {
+ const args = ['--inputStringArr', 'c', '--inputStringArr', 'd', '--inputBoolArr', '--inputBoolArr'];
+ const options = {
+ inputStringArr: { type: 'string', multiple: true, default: ['a', 'b'] },
+ emptyStringArr: { type: 'string', multiple: true, default: [] },
+ fullStringArr: { type: 'string', multiple: true, default: ['a', 'b'] },
+ inputBoolArr: { type: 'boolean', multiple: true, default: [false, true, false] },
+ emptyBoolArr: { type: 'boolean', multiple: true, default: [] },
+ fullBoolArr: { type: 'boolean', multiple: true, default: [false, true, false] },
+ };
+ const expected = { values: { __proto__: null,
+ inputStringArr: ['c', 'd'],
+ inputBoolArr: [true, true],
+ emptyStringArr: [],
+ fullStringArr: ['a', 'b'],
+ emptyBoolArr: [],
+ fullBoolArr: [false, true, false] },
+ positionals: [] };
+ const result = parseArgs({ args, options });
+ assert.deepStrictEqual(result, expected);
+});
+
+test('when default is set, the option must be added as result', () => {
+ const args = [];
+ const options = {
+ a: { type: 'string', default: 'HELLO' },
+ b: { type: 'boolean', default: false },
+ c: { type: 'boolean', default: true }
+ };
+ const expected = { values: { __proto__: null, a: 'HELLO', b: false, c: true }, positionals: [] };
+
+ const result = parseArgs({ args, options });
+ assert.deepStrictEqual(result, expected);
+});
+
+test('when default is set, the args value takes precedence', () => {
+ const args = ['--a', 'WORLD', '--b', '-c'];
+ const options = {
+ a: { type: 'string', default: 'HELLO' },
+ b: { type: 'boolean', default: false },
+ c: { type: 'boolean', default: true }
+ };
+ const expected = { values: { __proto__: null, a: 'WORLD', b: true, c: true }, positionals: [] };
+
+ const result = parseArgs({ args, options });
+ assert.deepStrictEqual(result, expected);
+});
+
+test('tokens should not include the default options', () => {
+ const args = [];
+ const options = {
+ a: { type: 'string', default: 'HELLO' },
+ b: { type: 'boolean', default: false },
+ c: { type: 'boolean', default: true }
+ };
+
+ const expectedTokens = [];
+
+ const { tokens } = parseArgs({ args, options, tokens: true });
+ assert.deepStrictEqual(tokens, expectedTokens);
+});
+
+test('tokens:true should not include the default options after the args input', () => {
+ const args = ['--z', 'zero', 'positional-item'];
+ const options = {
+ z: { type: 'string' },
+ a: { type: 'string', default: 'HELLO' },
+ b: { type: 'boolean', default: false },
+ c: { type: 'boolean', default: true }
+ };
+
+ const expectedTokens = [
+ { kind: 'option', name: 'z', rawName: '--z', index: 0, value: 'zero', inlineValue: false },
+ { kind: 'positional', index: 2, value: 'positional-item' },
+ ];
+
+ const { tokens } = parseArgs({ args, options, tokens: true, allowPositionals: true });
+ assert.deepStrictEqual(tokens, expectedTokens);
+});
+
+test('proto as default value must be ignored', () => {
+ const args = [];
+ const options = Object.create(null);
+
+ // eslint-disable-next-line no-proto
+ options.__proto__ = { type: 'string', default: 'HELLO' };
+
+ const result = parseArgs({ args, options, allowPositionals: true });
+ const expected = { values: { __proto__: null }, positionals: [] };
+ assert.deepStrictEqual(result, expected);
+});
+
+
+test('multiple as false should expect a String', () => {
+ const args = [];
+ const options = { alpha: { type: 'string', multiple: false, default: ['array'] } };
+ assert.throws(() => {
+ parseArgs({ args, options });
+ }, /"options\.alpha\.default" property must be of type string/
+ );
+});
diff --git a/ext/node/lib.rs b/ext/node/lib.rs
index 149efe6f4..af8aabd1f 100644
--- a/ext/node/lib.rs
+++ b/ext/node/lib.rs
@@ -463,6 +463,8 @@ deno_core::extension!(deno_node,
"internal/util/comparisons.ts",
"internal/util/debuglog.ts",
"internal/util/inspect.mjs",
+ "internal/util/parse_args/parse_args.js",
+ "internal/util/parse_args/utils.js",
"internal/util/types.ts",
"internal/validators.mjs",
"path/_constants.ts",
diff --git a/ext/node/polyfills/internal/errors.ts b/ext/node/polyfills/internal/errors.ts
index 7f7ce9d3a..8d10e1857 100644
--- a/ext/node/polyfills/internal/errors.ts
+++ b/ext/node/polyfills/internal/errors.ts
@@ -2472,6 +2472,36 @@ export class ERR_PACKAGE_PATH_NOT_EXPORTED extends NodeError {
}
}
+export class ERR_PARSE_ARGS_INVALID_OPTION_VALUE extends NodeTypeError {
+ constructor(x: string) {
+ super("ERR_PARSE_ARGS_INVALID_OPTION_VALUE", x);
+ }
+}
+
+export class ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL extends NodeTypeError {
+ constructor(x: string) {
+ super(
+ "ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL",
+ `Unexpected argument '${x}'. This ` +
+ `command does not take positional arguments`,
+ );
+ }
+}
+
+export class ERR_PARSE_ARGS_UNKNOWN_OPTION extends NodeTypeError {
+ constructor(option, allowPositionals) {
+ const suggestDashDash = allowPositionals
+ ? ". To specify a positional " +
+ "argument starting with a '-', place it at the end of the command after " +
+ `'--', as in '-- ${JSONStringify(option)}`
+ : "";
+ super(
+ "ERR_PARSE_ARGS_UNKNOWN_OPTION",
+ `Unknown option '${option}'${suggestDashDash}`,
+ );
+ }
+}
+
export class ERR_INTERNAL_ASSERTION extends NodeError {
constructor(message?: string) {
const suffix = "This is caused by either a bug in Node.js " +
diff --git a/ext/node/polyfills/internal/util/parse_args/parse_args.js b/ext/node/polyfills/internal/util/parse_args/parse_args.js
new file mode 100644
index 000000000..798039620
--- /dev/null
+++ b/ext/node/polyfills/internal/util/parse_args/parse_args.js
@@ -0,0 +1,345 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+// Copyright Joyent and Node contributors. All rights reserved. MIT license.
+
+const primordials = globalThis.__bootstrap.primordials;
+const {
+ ArrayPrototypeForEach,
+ ArrayPrototypeIncludes,
+ ArrayPrototypePushApply,
+ ArrayPrototypeShift,
+ ArrayPrototypeSlice,
+ ArrayPrototypePush,
+ ArrayPrototypeUnshiftApply,
+ ObjectHasOwn,
+ ObjectEntries,
+ StringPrototypeCharAt,
+ StringPrototypeIndexOf,
+ StringPrototypeSlice,
+} = primordials;
+
+import {
+ validateArray,
+ validateBoolean,
+ validateObject,
+ validateString,
+ validateUnion,
+} from "ext:deno_node/internal/validators.mjs";
+
+import {
+ findLongOptionForShort,
+ isLoneLongOption,
+ isLoneShortOption,
+ isLongOptionAndValue,
+ isOptionLikeValue,
+ isOptionValue,
+ isShortOptionAndValue,
+ isShortOptionGroup,
+ objectGetOwn,
+ optionsGetOwn,
+} from "ext:deno_node/internal/util/parse_args/utils.js";
+
+import { codes } from "ext:deno_node/internal/error_codes.ts";
+const {
+ ERR_INVALID_ARG_VALUE,
+ ERR_PARSE_ARGS_INVALID_OPTION_VALUE,
+ ERR_PARSE_ARGS_UNKNOWN_OPTION,
+ ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL,
+} = codes;
+
+function getMainArgs() {
+ // Work out where to slice process.argv for user supplied arguments.
+
+ // Check node options for scenarios where user CLI args follow executable.
+ const execArgv = process.execArgv;
+ if (
+ ArrayPrototypeIncludes(execArgv, "-e") ||
+ ArrayPrototypeIncludes(execArgv, "--eval") ||
+ ArrayPrototypeIncludes(execArgv, "-p") ||
+ ArrayPrototypeIncludes(execArgv, "--print")
+ ) {
+ return ArrayPrototypeSlice(process.argv, 1);
+ }
+
+ // Normally first two arguments are executable and script, then CLI arguments
+ return ArrayPrototypeSlice(process.argv, 2);
+}
+
+/**
+ * In strict mode, throw for possible usage errors like --foo --bar
+ *
+ * @param {string} longOption - long option name e.g. 'foo'
+ * @param {string|undefined} optionValue - value from user args
+ * @param {string} shortOrLong - option used, with dashes e.g. `-l` or `--long`
+ * @param {boolean} strict - show errors, from parseArgs({ strict })
+ */
+function checkOptionLikeValue(longOption, optionValue, shortOrLong, strict) {
+ if (strict && isOptionLikeValue(optionValue)) {
+ // Only show short example if user used short option.
+ const example = (shortOrLong.length === 2)
+ ? `'--${longOption}=-XYZ' or '${shortOrLong}-XYZ'`
+ : `'--${longOption}=-XYZ'`;
+ const errorMessage = `Option '${shortOrLong}' argument is ambiguous.
+Did you forget to specify the option argument for '${shortOrLong}'?
+To specify an option argument starting with a dash use ${example}.`;
+ throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(errorMessage);
+ }
+}
+
+/**
+ * In strict mode, throw for usage errors.
+ *
+ * @param {string} longOption - long option name e.g. 'foo'
+ * @param {string|undefined} optionValue - value from user args
+ * @param {object} options - option configs, from parseArgs({ options })
+ * @param {string} shortOrLong - option used, with dashes e.g. `-l` or `--long`
+ * @param {boolean} strict - show errors, from parseArgs({ strict })
+ * @param {boolean} allowPositionals - from parseArgs({ allowPositionals })
+ */
+function checkOptionUsage(
+ longOption,
+ optionValue,
+ options,
+ shortOrLong,
+ strict,
+ allowPositionals,
+) {
+ // Strict and options are used from local context.
+ if (!strict) return;
+
+ if (!ObjectHasOwn(options, longOption)) {
+ throw new ERR_PARSE_ARGS_UNKNOWN_OPTION(shortOrLong, allowPositionals);
+ }
+
+ const short = optionsGetOwn(options, longOption, "short");
+ const shortAndLong = short ? `-${short}, --${longOption}` : `--${longOption}`;
+ const type = optionsGetOwn(options, longOption, "type");
+ if (type === "string" && typeof optionValue !== "string") {
+ throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(
+ `Option '${shortAndLong} <value>' argument missing`,
+ );
+ }
+ // (Idiomatic test for undefined||null, expecting undefined.)
+ if (type === "boolean" && optionValue != null) {
+ throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(
+ `Option '${shortAndLong}' does not take an argument`,
+ );
+ }
+}
+
+/**
+ * Store the option value in `values`.
+ *
+ * @param {string} longOption - long option name e.g. 'foo'
+ * @param {string|undefined} optionValue - value from user args
+ * @param {object} options - option configs, from parseArgs({ options })
+ * @param {object} values - option values returned in `values` by parseArgs
+ */
+function storeOption(longOption, optionValue, options, values) {
+ if (longOption === "__proto__") {
+ return; // No. Just no.
+ }
+
+ // We store based on the option value rather than option type,
+ // preserving the users intent for author to deal with.
+ const newValue = optionValue ?? true;
+ if (optionsGetOwn(options, longOption, "multiple")) {
+ // Always store value in array, including for boolean.
+ // values[longOption] starts out not present,
+ // first value is added as new array [newValue],
+ // subsequent values are pushed to existing array.
+ // (note: values has null prototype, so simpler usage)
+ if (values[longOption]) {
+ ArrayPrototypePush(values[longOption], newValue);
+ } else {
+ values[longOption] = [newValue];
+ }
+ } else {
+ values[longOption] = newValue;
+ }
+}
+
+export const parseArgs = (config = { __proto__: null }) => {
+ const args = objectGetOwn(config, "args") ?? getMainArgs();
+ const strict = objectGetOwn(config, "strict") ?? true;
+ const allowPositionals = objectGetOwn(config, "allowPositionals") ?? !strict;
+ const options = objectGetOwn(config, "options") ?? { __proto__: null };
+
+ // Validate input configuration.
+ validateArray(args, "args");
+ validateBoolean(strict, "strict");
+ validateBoolean(allowPositionals, "allowPositionals");
+ validateObject(options, "options");
+ ArrayPrototypeForEach(
+ ObjectEntries(options),
+ ({ 0: longOption, 1: optionConfig }) => {
+ validateObject(optionConfig, `options.${longOption}`);
+
+ // type is required
+ validateUnion(
+ objectGetOwn(optionConfig, "type"),
+ `options.${longOption}.type`,
+ ["string", "boolean"],
+ );
+
+ if (ObjectHasOwn(optionConfig, "short")) {
+ const shortOption = optionConfig.short;
+ validateString(shortOption, `options.${longOption}.short`);
+ if (shortOption.length !== 1) {
+ throw new ERR_INVALID_ARG_VALUE(
+ `options.${longOption}.short`,
+ shortOption,
+ "must be a single character",
+ );
+ }
+ }
+
+ if (ObjectHasOwn(optionConfig, "multiple")) {
+ validateBoolean(
+ optionConfig.multiple,
+ `options.${longOption}.multiple`,
+ );
+ }
+ },
+ );
+
+ const result = {
+ values: { __proto__: null },
+ positionals: [],
+ };
+
+ const remainingArgs = ArrayPrototypeSlice(args);
+ while (remainingArgs.length > 0) {
+ const arg = ArrayPrototypeShift(remainingArgs);
+ const nextArg = remainingArgs[0];
+
+ // Check if `arg` is an options terminator.
+ // Guideline 10 in https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html
+ if (arg === "--") {
+ if (!allowPositionals && remainingArgs.length > 0) {
+ throw new ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL(nextArg);
+ }
+
+ // Everything after a bare '--' is considered a positional argument.
+ ArrayPrototypePushApply(
+ result.positionals,
+ remainingArgs,
+ );
+ break; // Finished processing args, leave while loop.
+ }
+
+ if (isLoneShortOption(arg)) {
+ // e.g. '-f'
+ const shortOption = StringPrototypeCharAt(arg, 1);
+ const longOption = findLongOptionForShort(shortOption, options);
+ let optionValue;
+ if (
+ optionsGetOwn(options, longOption, "type") === "string" &&
+ isOptionValue(nextArg)
+ ) {
+ // e.g. '-f', 'bar'
+ optionValue = ArrayPrototypeShift(remainingArgs);
+ checkOptionLikeValue(longOption, optionValue, arg, strict);
+ }
+ checkOptionUsage(
+ longOption,
+ optionValue,
+ options,
+ arg,
+ strict,
+ allowPositionals,
+ );
+ storeOption(longOption, optionValue, options, result.values);
+ continue;
+ }
+
+ if (isShortOptionGroup(arg, options)) {
+ // Expand -fXzy to -f -X -z -y
+ const expanded = [];
+ for (let index = 1; index < arg.length; index++) {
+ const shortOption = StringPrototypeCharAt(arg, index);
+ const longOption = findLongOptionForShort(shortOption, options);
+ if (
+ optionsGetOwn(options, longOption, "type") !== "string" ||
+ index === arg.length - 1
+ ) {
+ // Boolean option, or last short in group. Well formed.
+ ArrayPrototypePush(expanded, `-${shortOption}`);
+ } else {
+ // String option in middle. Yuck.
+ // Expand -abfFILE to -a -b -fFILE
+ ArrayPrototypePush(expanded, `-${StringPrototypeSlice(arg, index)}`);
+ break; // finished short group
+ }
+ }
+ ArrayPrototypeUnshiftApply(remainingArgs, expanded);
+ continue;
+ }
+
+ if (isShortOptionAndValue(arg, options)) {
+ // e.g. -fFILE
+ const shortOption = StringPrototypeCharAt(arg, 1);
+ const longOption = findLongOptionForShort(shortOption, options);
+ const optionValue = StringPrototypeSlice(arg, 2);
+ checkOptionUsage(
+ longOption,
+ optionValue,
+ options,
+ `-${shortOption}`,
+ strict,
+ allowPositionals,
+ );
+ storeOption(longOption, optionValue, options, result.values);
+ continue;
+ }
+
+ if (isLoneLongOption(arg)) {
+ // e.g. '--foo'
+ const longOption = StringPrototypeSlice(arg, 2);
+ let optionValue;
+ if (
+ optionsGetOwn(options, longOption, "type") === "string" &&
+ isOptionValue(nextArg)
+ ) {
+ // e.g. '--foo', 'bar'
+ optionValue = ArrayPrototypeShift(remainingArgs);
+ checkOptionLikeValue(longOption, optionValue, arg, strict);
+ }
+ checkOptionUsage(
+ longOption,
+ optionValue,
+ options,
+ arg,
+ strict,
+ allowPositionals,
+ );
+ storeOption(longOption, optionValue, options, result.values);
+ continue;
+ }
+
+ if (isLongOptionAndValue(arg)) {
+ // e.g. --foo=bar
+ const index = StringPrototypeIndexOf(arg, "=");
+ const longOption = StringPrototypeSlice(arg, 2, index);
+ const optionValue = StringPrototypeSlice(arg, index + 1);
+ checkOptionUsage(
+ longOption,
+ optionValue,
+ options,
+ `--${longOption}`,
+ strict,
+ allowPositionals,
+ );
+ storeOption(longOption, optionValue, options, result.values);
+ continue;
+ }
+
+ // Anything left is a positional
+ if (!allowPositionals) {
+ throw new ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL(arg);
+ }
+
+ ArrayPrototypePush(result.positionals, arg);
+ }
+
+ return result;
+};
diff --git a/ext/node/polyfills/internal/util/parse_args/utils.js b/ext/node/polyfills/internal/util/parse_args/utils.js
new file mode 100644
index 000000000..d101dd3fd
--- /dev/null
+++ b/ext/node/polyfills/internal/util/parse_args/utils.js
@@ -0,0 +1,187 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+// Copyright Joyent and Node contributors. All rights reserved. MIT license.
+
+const primordials = globalThis.__bootstrap.primordials;
+const {
+ ArrayPrototypeFind,
+ ObjectEntries,
+ ObjectHasOwn,
+ StringPrototypeCharAt,
+ StringPrototypeIncludes,
+ StringPrototypeStartsWith,
+} = primordials;
+
+import { validateObject } from "ext:deno_node/internal/validators.mjs";
+
+// These are internal utilities to make the parsing logic easier to read, and
+// add lots of detail for the curious. They are in a separate file to allow
+// unit testing, although that is not essential (this could be rolled into
+// main file and just tested implicitly via API).
+//
+// These routines are for internal use, not for export to client.
+
+/**
+ * Return the named property, but only if it is an own property.
+ */
+function objectGetOwn(obj, prop) {
+ if (ObjectHasOwn(obj, prop)) {
+ return obj[prop];
+ }
+}
+
+/**
+ * Return the named options property, but only if it is an own property.
+ */
+function optionsGetOwn(options, longOption, prop) {
+ if (ObjectHasOwn(options, longOption)) {
+ return objectGetOwn(options[longOption], prop);
+ }
+}
+
+/**
+ * Determines if the argument may be used as an option value.
+ * @example
+ * isOptionValue('V') // returns true
+ * isOptionValue('-v') // returns true (greedy)
+ * isOptionValue('--foo') // returns true (greedy)
+ * isOptionValue(undefined) // returns false
+ */
+function isOptionValue(value) {
+ if (value == null) return false;
+
+ // Open Group Utility Conventions are that an option-argument
+ // is the argument after the option, and may start with a dash.
+ return true; // greedy!
+}
+
+/**
+ * Detect whether there is possible confusion and user may have omitted
+ * the option argument, like `--port --verbose` when `port` of type:string.
+ * In strict mode we throw errors if value is option-like.
+ */
+function isOptionLikeValue(value) {
+ if (value == null) return false;
+
+ return value.length > 1 && StringPrototypeCharAt(value, 0) === "-";
+}
+
+/**
+ * Determines if `arg` is just a short option.
+ * @example '-f'
+ */
+function isLoneShortOption(arg) {
+ return arg.length === 2 &&
+ StringPrototypeCharAt(arg, 0) === "-" &&
+ StringPrototypeCharAt(arg, 1) !== "-";
+}
+
+/**
+ * Determines if `arg` is a lone long option.
+ * @example
+ * isLoneLongOption('a') // returns false
+ * isLoneLongOption('-a') // returns false
+ * isLoneLongOption('--foo') // returns true
+ * isLoneLongOption('--foo=bar') // returns false
+ */
+function isLoneLongOption(arg) {
+ return arg.length > 2 &&
+ StringPrototypeStartsWith(arg, "--") &&
+ !StringPrototypeIncludes(arg, "=", 3);
+}
+
+/**
+ * Determines if `arg` is a long option and value in the same argument.
+ * @example
+ * isLongOptionAndValue('--foo') // returns false
+ * isLongOptionAndValue('--foo=bar') // returns true
+ */
+function isLongOptionAndValue(arg) {
+ return arg.length > 2 &&
+ StringPrototypeStartsWith(arg, "--") &&
+ StringPrototypeIncludes(arg, "=", 3);
+}
+
+/**
+ * Determines if `arg` is a short option group.
+ *
+ * See Guideline 5 of the [Open Group Utility Conventions](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html).
+ * One or more options without option-arguments, followed by at most one
+ * option that takes an option-argument, should be accepted when grouped
+ * behind one '-' delimiter.
+ * @example
+ * isShortOptionGroup('-a', {}) // returns false
+ * isShortOptionGroup('-ab', {}) // returns true
+ * // -fb is an option and a value, not a short option group
+ * isShortOptionGroup('-fb', {
+ * options: { f: { type: 'string' } }
+ * }) // returns false
+ * isShortOptionGroup('-bf', {
+ * options: { f: { type: 'string' } }
+ * }) // returns true
+ * // -bfb is an edge case, return true and caller sorts it out
+ * isShortOptionGroup('-bfb', {
+ * options: { f: { type: 'string' } }
+ * }) // returns true
+ */
+function isShortOptionGroup(arg, options) {
+ if (arg.length <= 2) return false;
+ if (StringPrototypeCharAt(arg, 0) !== "-") return false;
+ if (StringPrototypeCharAt(arg, 1) === "-") return false;
+
+ const firstShort = StringPrototypeCharAt(arg, 1);
+ const longOption = findLongOptionForShort(firstShort, options);
+ return optionsGetOwn(options, longOption, "type") !== "string";
+}
+
+/**
+ * Determine if arg is a short string option followed by its value.
+ * @example
+ * isShortOptionAndValue('-a', {}); // returns false
+ * isShortOptionAndValue('-ab', {}); // returns false
+ * isShortOptionAndValue('-fFILE', {
+ * options: { foo: { short: 'f', type: 'string' }}
+ * }) // returns true
+ */
+function isShortOptionAndValue(arg, options) {
+ validateObject(options, "options");
+
+ if (arg.length <= 2) return false;
+ if (StringPrototypeCharAt(arg, 0) !== "-") return false;
+ if (StringPrototypeCharAt(arg, 1) === "-") return false;
+
+ const shortOption = StringPrototypeCharAt(arg, 1);
+ const longOption = findLongOptionForShort(shortOption, options);
+ return optionsGetOwn(options, longOption, "type") === "string";
+}
+
+/**
+ * Find the long option associated with a short option. Looks for a configured
+ * `short` and returns the short option itself if a long option is not found.
+ * @example
+ * findLongOptionForShort('a', {}) // returns 'a'
+ * findLongOptionForShort('b', {
+ * options: { bar: { short: 'b' } }
+ * }) // returns 'bar'
+ */
+function findLongOptionForShort(shortOption, options) {
+ validateObject(options, "options");
+ const longOptionEntry = ArrayPrototypeFind(
+ ObjectEntries(options),
+ ({ 1: optionConfig }) =>
+ objectGetOwn(optionConfig, "short") === shortOption,
+ );
+ return longOptionEntry?.[0] ?? shortOption;
+}
+
+export {
+ findLongOptionForShort,
+ isLoneLongOption,
+ isLoneShortOption,
+ isLongOptionAndValue,
+ isOptionLikeValue,
+ isOptionValue,
+ isShortOptionAndValue,
+ isShortOptionGroup,
+ objectGetOwn,
+ optionsGetOwn,
+};
diff --git a/ext/node/polyfills/internal/validators.mjs b/ext/node/polyfills/internal/validators.mjs
index 285603d06..b5b539088 100644
--- a/ext/node/polyfills/internal/validators.mjs
+++ b/ext/node/polyfills/internal/validators.mjs
@@ -9,6 +9,12 @@ import { hideStackFrames } from "ext:deno_node/internal/hide_stack_frames.ts";
import { isArrayBufferView } from "ext:deno_node/internal/util/types.ts";
import { normalizeEncoding } from "ext:deno_node/internal/normalize_encoding.mjs";
+const primordials = globalThis.__bootstrap.primordials;
+const {
+ ArrayPrototypeIncludes,
+ ArrayPrototypeJoin,
+} = primordials;
+
/**
* @param {number} value
* @returns {boolean}
@@ -282,6 +288,16 @@ const validateArray = hideStackFrames(
},
);
+function validateUnion(value, name, union) {
+ if (!ArrayPrototypeIncludes(union, value)) {
+ throw new ERR_INVALID_ARG_TYPE(
+ name,
+ `('${ArrayPrototypeJoin(union, "|")}')`,
+ value,
+ );
+ }
+}
+
export default {
isInt32,
isUint32,
@@ -299,6 +315,7 @@ export default {
validatePort,
validateString,
validateUint32,
+ validateUnion,
};
export {
isInt32,
@@ -317,4 +334,5 @@ export {
validatePort,
validateString,
validateUint32,
+ validateUnion,
};
diff --git a/ext/node/polyfills/util.ts b/ext/node/polyfills/util.ts
index 2e25746ef..68c9626c9 100644
--- a/ext/node/polyfills/util.ts
+++ b/ext/node/polyfills/util.ts
@@ -15,6 +15,7 @@ import { Buffer } from "node:buffer";
import { isDeepStrictEqual } from "ext:deno_node/internal/util/comparisons.ts";
import process from "node:process";
import { validateString } from "ext:deno_node/internal/validators.mjs";
+import { parseArgs } from "ext:deno_node/internal/util/parse_args/parse_args.js";
const primordials = globalThis.__bootstrap.primordials;
const {
ArrayIsArray,
@@ -48,6 +49,7 @@ export {
format,
formatWithOptions,
inspect,
+ parseArgs,
promisify,
stripVTControlCharacters,
types,
@@ -312,6 +314,7 @@ export default {
getSystemErrorName,
deprecate,
callbackify,
+ parseArgs,
promisify,
inherits,
types,