1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-24 08:09:08 -05:00

fix(ext/node): add util.parseArgs (#21342)

This commit is contained in:
Yoshiya Hinosawa 2023-11-29 15:42:58 +09:00 committed by GitHub
parent 75ec650f08
commit e332fa4a83
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 1595 additions and 1 deletions

View file

@ -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",

View file

@ -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);
}

View file

@ -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,

File diff suppressed because it is too large Load diff

View file

@ -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",

View file

@ -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 " +

View file

@ -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;
};

View file

@ -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,
};

View file

@ -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,
};

View file

@ -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,