0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-03-03 17:34:47 -05:00

Improvements to std/flags. (#4279)

Adds JSDoc to module, improves the typing of the return type, uses
iteration instead of Array forEach, uses the dotall support in Regular
Expression which is now supported in JavaScript, uses destructuring and
nullish coalescing where appropriate.
This commit is contained in:
Kitson Kelly 2020-03-08 21:27:23 +11:00 committed by GitHub
parent 0dd131d4a5
commit b9037c86ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 125 additions and 92 deletions

View file

@ -1,29 +1,62 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
import { assert } from "../testing/asserts.ts"; import { assert } from "../testing/asserts.ts";
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. export interface Args {
export interface ArgParsingOptions { /** Contains all the arguments that didn't have an option associated with
unknown: (i: unknown) => unknown; * them. */
boolean: boolean | string | string[]; _: Array<string | number>;
alias: { [key: string]: string | string[] }; // eslint-disable-next-line @typescript-eslint/no-explicit-any
string: string | string[]; [key: string]: any;
default: { [key: string]: unknown };
"--": boolean;
stopEarly: boolean;
} }
const DEFAULT_OPTIONS = { export interface ArgParsingOptions {
unknown: (i: unknown): unknown => i, /** When `true`, populate the result `_` with everything before the `--` and
boolean: false, * the result `['--']` with everything after the `--`. Here's an example:
alias: {}, *
string: [], * const { args } = Deno;
default: {}, * import { parse } from "https://deno.land/std/flags/mod.ts";
"--": false, * // options['--'] is now set to false
stopEarly: false * console.dir(parse(args, { "--": false }));
}; * // $ deno example.ts -- a arg1
* // output: { _: [ "example.ts", "a", "arg1" ] }
* // options['--'] is now set to true
* console.dir(parse(args, { "--": true }));
* // $ deno example.ts -- a arg1
* // output: { _: [ "example.ts" ], --: [ "a", "arg1" ] }
*
* Defaults to `false`.
*/
"--": boolean;
/** An object mapping string names to strings or arrays of string argument
* names to use as aliases */
alias: Record<string, string | string[]>;
/** A boolean, string or array of strings to always treat as booleans. If
* `true` will treat all double hyphenated arguments without equal signs as
* `boolean` (e.g. affects `--foo`, not `-f` or `--foo=bar`) */
boolean: boolean | string | string[];
/** An object mapping string argument names to default values. */
default: Record<string, unknown>;
/** When `true`, populate the result `_` with everything after the first
* non-option. */
stopEarly: boolean;
/** A string or array of strings argument names to always treat as strings. */
string: string | string[];
/** A function which is invoked with a command line parameter not defined in
* the `options` configuration object. If the function returns `false`, the
* unknown option is not added to `parsedArgs`. */
unknown: (i: unknown) => unknown;
}
interface Flags { interface Flags {
bools: { [key: string]: boolean }; bools: Record<string, boolean>;
strings: { [key: string]: boolean }; strings: Record<string, boolean>;
unknownFn: (i: unknown) => unknown; unknownFn: (i: unknown) => unknown;
allBools: boolean; allBools: boolean;
} }
@ -32,12 +65,13 @@ interface NestedMapping {
[key: string]: NestedMapping | unknown; [key: string]: NestedMapping | unknown;
} }
function get<T>(obj: { [s: string]: T }, key: string): T | undefined { function get<T>(obj: Record<string, T>, key: string): T | undefined {
if (Object.prototype.hasOwnProperty.call(obj, key)) { if (Object.prototype.hasOwnProperty.call(obj, key)) {
return obj[key]; return obj[key];
} }
} }
function getForce<T>(obj: { [s: string]: T }, key: string): T {
function getForce<T>(obj: Record<string, T>, key: string): T {
const v = get(obj, key); const v = get(obj, key);
assert(v != null); assert(v != null);
return v; return v;
@ -51,82 +85,80 @@ function isNumber(x: unknown): boolean {
function hasKey(obj: NestedMapping, keys: string[]): boolean { function hasKey(obj: NestedMapping, keys: string[]): boolean {
let o = obj; let o = obj;
keys.slice(0, -1).forEach(function(key: string): void { keys.slice(0, -1).forEach(key => {
o = (get(o, key) || {}) as NestedMapping; o = (get(o, key) ?? {}) as NestedMapping;
}); });
const key = keys[keys.length - 1]; const key = keys[keys.length - 1];
return key in o; return key in o;
} }
/** Take a set of command line arguments, with an optional set of options, and
* return an object representation of those argument.
*
* const parsedArgs = parse(Deno.args);
*/
export function parse( export function parse(
// eslint-disable-next-line @typescript-eslint/no-explicit-any args: string[],
args: any[], {
initialOptions: Partial<ArgParsingOptions> = {} "--": doubleDash = false,
// eslint-disable-next-line @typescript-eslint/no-explicit-any alias = {},
): { [key: string]: any } { boolean = false,
const options: ArgParsingOptions = { default: defaults = {},
...DEFAULT_OPTIONS, stopEarly = false,
...initialOptions string = [],
}; unknown = (i: unknown): unknown => i
}: Partial<ArgParsingOptions> = {}
): Args {
const flags: Flags = { const flags: Flags = {
bools: {}, bools: {},
strings: {}, strings: {},
unknownFn: options.unknown, unknownFn: unknown,
allBools: false allBools: false
}; };
if (options.boolean !== undefined) { if (boolean !== undefined) {
if (typeof options.boolean === "boolean") { if (typeof boolean === "boolean") {
flags.allBools = !!options.boolean; flags.allBools = !!boolean;
} else { } else {
const booleanArgs: string[] = const booleanArgs = typeof boolean === "string" ? [boolean] : boolean;
typeof options.boolean === "string"
? [options.boolean]
: options.boolean;
booleanArgs.filter(Boolean).forEach((key: string): void => { for (const key of booleanArgs.filter(Boolean)) {
flags.bools[key] = true; flags.bools[key] = true;
}); }
} }
} }
const aliases: { [key: string]: string[] } = {}; const aliases: Record<string, string[]> = {};
if (options.alias !== undefined) { if (alias !== undefined) {
for (const key in options.alias) { for (const key in alias) {
const val = getForce(options.alias, key); const val = getForce(alias, key);
if (typeof val === "string") { if (typeof val === "string") {
aliases[key] = [val]; aliases[key] = [val];
} else { } else {
aliases[key] = val; aliases[key] = val;
} }
for (const alias of getForce(aliases, key)) { for (const alias of getForce(aliases, key)) {
aliases[alias] = [key].concat( aliases[alias] = [key].concat(aliases[key].filter(y => alias !== y));
aliases[key].filter((y: string): boolean => alias !== y)
);
} }
} }
} }
if (options.string !== undefined) { if (string !== undefined) {
const stringArgs = const stringArgs = typeof string === "string" ? [string] : string;
typeof options.string === "string" ? [options.string] : options.string;
stringArgs.filter(Boolean).forEach(function(key): void { for (const key of stringArgs.filter(Boolean)) {
flags.strings[key] = true; flags.strings[key] = true;
const alias = get(aliases, key); const alias = get(aliases, key);
if (alias) { if (alias) {
alias.forEach((alias: string): void => { for (const al of alias) {
flags.strings[alias] = true; flags.strings[al] = true;
}); }
} }
}); }
} }
const defaults = options.default; const argv: Args = { _: [] };
const argv: { [key: string]: unknown[] } = { _: [] };
function argDefined(key: string, arg: string): boolean { function argDefined(key: string, arg: string): boolean {
return ( return (
@ -172,25 +204,28 @@ export function parse(
const value = !get(flags.strings, key) && isNumber(val) ? Number(val) : val; const value = !get(flags.strings, key) && isNumber(val) ? Number(val) : val;
setKey(argv, key.split("."), value); setKey(argv, key.split("."), value);
(get(aliases, key) || []).forEach(function(x): void { const alias = get(aliases, key);
setKey(argv, x.split("."), value); if (alias) {
}); for (const x of alias) {
setKey(argv, x.split("."), value);
}
}
} }
function aliasIsBoolean(key: string): boolean { function aliasIsBoolean(key: string): boolean {
return getForce(aliases, key).some(function(x): boolean { return getForce(aliases, key).some(
return typeof get(flags.bools, x) === "boolean"; x => typeof get(flags.bools, x) === "boolean"
}); );
} }
Object.keys(flags.bools).forEach(function(key): void { for (const key of Object.keys(flags.bools)) {
setArg(key, defaults[key] === undefined ? false : defaults[key]); setArg(key, defaults[key] === undefined ? false : defaults[key]);
}); }
let notFlags: string[] = []; let notFlags: string[] = [];
// all args after "--" are not parsed // all args after "--" are not parsed
if (args.indexOf("--") !== -1) { if (args.includes("--")) {
notFlags = args.slice(args.indexOf("--") + 1); notFlags = args.slice(args.indexOf("--") + 1);
args = args.slice(0, args.indexOf("--")); args = args.slice(0, args.indexOf("--"));
} }
@ -199,13 +234,9 @@ export function parse(
const arg = args[i]; const arg = args[i];
if (/^--.+=/.test(arg)) { if (/^--.+=/.test(arg)) {
// Using [\s\S] instead of . because js doesn't support the const m = arg.match(/^--([^=]+)=(.*)$/s);
// 'dotall' regex modifier. See:
// http://stackoverflow.com/a/1068308/13216
const m = arg.match(/^--([^=]+)=([\s\S]*)$/);
assert(m != null); assert(m != null);
const key = m[1]; const [, key, value] = m;
const value = m[2];
if (flags.bools[key]) { if (flags.bools[key]) {
const booleanValue = value !== "false"; const booleanValue = value !== "false";
@ -220,7 +251,7 @@ export function parse(
} else if (/^--.+/.test(arg)) { } else if (/^--.+/.test(arg)) {
const m = arg.match(/^--(.+)/); const m = arg.match(/^--(.+)/);
assert(m != null); assert(m != null);
const key = m[1]; const [, key] = m;
const next = args[i + 1]; const next = args[i + 1];
if ( if (
next !== undefined && next !== undefined &&
@ -273,7 +304,7 @@ export function parse(
} }
} }
const key = arg.slice(-1)[0]; const [key] = arg.slice(-1);
if (!broken && key !== "-") { if (!broken && key !== "-") {
if ( if (
args[i + 1] && args[i + 1] &&
@ -292,34 +323,36 @@ export function parse(
} }
} else { } else {
if (!flags.unknownFn || flags.unknownFn(arg) !== false) { if (!flags.unknownFn || flags.unknownFn(arg) !== false) {
argv._.push(flags.strings["_"] || !isNumber(arg) ? arg : Number(arg)); argv._.push(flags.strings["_"] ?? !isNumber(arg) ? arg : Number(arg));
} }
if (options.stopEarly) { if (stopEarly) {
argv._.push(...args.slice(i + 1)); argv._.push(...args.slice(i + 1));
break; break;
} }
} }
} }
Object.keys(defaults).forEach(function(key): void { for (const key of Object.keys(defaults)) {
if (!hasKey(argv, key.split("."))) { if (!hasKey(argv, key.split("."))) {
setKey(argv, key.split("."), defaults[key]); setKey(argv, key.split("."), defaults[key]);
(aliases[key] || []).forEach(function(x): void { if (aliases[key]) {
setKey(argv, x.split("."), defaults[key]); for (const x of aliases[key]) {
}); setKey(argv, x.split("."), defaults[key]);
}
}
} }
}); }
if (options["--"]) { if (doubleDash) {
argv["--"] = []; argv["--"] = [];
notFlags.forEach(function(key): void { for (const key of notFlags) {
argv["--"].push(key); argv["--"].push(key);
}); }
} else { } else {
notFlags.forEach(function(key): void { for (const key of notFlags) {
argv._.push(key); argv._.push(key);
}); }
} }
return argv; return argv;

View file

@ -33,7 +33,7 @@ Deno.test(function nums(): void {
}); });
Deno.test(function alreadyNumber(): void { Deno.test(function alreadyNumber(): void {
const argv = parse(["-x", 1234, 789]); const argv = parse(["-x", "1234", "789"]);
assertEquals(argv, { x: 1234, _: [789] }); assertEquals(argv, { x: 1234, _: [789] });
assertEquals(typeof argv.x, "number"); assertEquals(typeof argv.x, "number");
assertEquals(typeof argv._[0], "number"); assertEquals(typeof argv._[0], "number");