Skip to content

Commit 6131ef5

Browse files
authored
fix(wrangler): enforce single value on non-array arguments (#7005)
1 parent f9e9134 commit 6131ef5

File tree

17 files changed

+121
-45
lines changed

17 files changed

+121
-45
lines changed

.changeset/sour-buses-develop.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
fix: prevent users from passing multiple arguments to non array options

packages/wrangler/src/__tests__/core/command-registration.test.ts

+11
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,17 @@ describe("Command Registration", () => {
484484
`[Error: Missing namespace definition for 'wrangler missing-namespace']`
485485
);
486486
});
487+
488+
test("throws upon duplicated arguments on non-array options", async () => {
489+
await expect(
490+
runWrangler(
491+
"my-test-command --str foo --str bar --num 123 --bool --arr first second --str baz"
492+
)
493+
).rejects.toThrowErrorMatchingInlineSnapshot(
494+
`[Error: The argument "--str" expects a single value, but received multiple: ["foo","bar","baz"].]`
495+
);
496+
});
497+
487498
test("throws upon alias to undefined command", async () => {
488499
defineAlias({
489500
command: "wrangler my-alias-command",

packages/wrangler/src/__tests__/index.test.ts

+16
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,22 @@ describe("wrangler", () => {
140140
});
141141
});
142142

143+
describe("global options", () => {
144+
it("should display an error if duplicated --env or --config arguments are provided", async () => {
145+
await expect(
146+
runWrangler("--env prod -e prod")
147+
).rejects.toThrowErrorMatchingInlineSnapshot(
148+
`[Error: The argument "--env" expects a single value, but received multiple: ["prod","prod"].]`
149+
);
150+
151+
await expect(
152+
runWrangler("--config=wrangler.toml -c example")
153+
).rejects.toThrowErrorMatchingInlineSnapshot(
154+
`[Error: The argument "--config" expects a single value, but received multiple: ["wrangler.toml","example"].]`
155+
);
156+
});
157+
});
158+
143159
describe("preview", () => {
144160
it("should throw an error if the deprecated command is used with positional arguments", async () => {
145161
await expect(runWrangler("preview GET")).rejects

packages/wrangler/src/core/helpers.ts

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { CommandLineArgsError } from "../errors";
2+
3+
/**
4+
* A helper to demand one of a set of options
5+
* via https://github.com/yargs/yargs/issues/1093#issuecomment-491299261
6+
*/
7+
export function demandOneOfOption(...options: string[]) {
8+
return function (argv: { [key: string]: unknown }) {
9+
const count = options.filter((option) => argv[option]).length;
10+
const lastOption = options.pop();
11+
12+
if (count === 0) {
13+
throw new CommandLineArgsError(
14+
`Exactly one of the arguments ${options.join(
15+
", "
16+
)} and ${lastOption} is required`
17+
);
18+
} else if (count > 1) {
19+
throw new CommandLineArgsError(
20+
`Arguments ${options.join(
21+
", "
22+
)} and ${lastOption} are mutually exclusive`
23+
);
24+
}
25+
26+
return true;
27+
};
28+
}
29+
30+
/**
31+
* A helper to ensure that an argument only receives a single value.
32+
*
33+
* This is a workaround for a limitation in yargs where non-array arguments can still receive multiple values
34+
* even though the inferred type is not an array.
35+
*
36+
* @see https://github.com/yargs/yargs/issues/1318
37+
*/
38+
export function demandSingleValue(key: string) {
39+
return function (argv: { [key: string]: unknown }) {
40+
if (Array.isArray(argv[key])) {
41+
throw new CommandLineArgsError(
42+
`The argument "--${key}" expects a single value, but received multiple: ${JSON.stringify(argv[key])}.`
43+
);
44+
}
45+
46+
return true;
47+
};
48+
}

packages/wrangler/src/core/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { defineAlias, defineCommand, defineNamespace } from "./define-command";
2+
export { demandOneOfOption, demandSingleValue } from "./helpers";

packages/wrangler/src/core/register-commands.ts

+12
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { FatalError, UserError } from "../errors";
66
import { logger } from "../logger";
77
import { printWranglerBanner } from "../update-check";
88
import { CommandRegistrationError, DefinitionTreeRoot } from "./define-command";
9+
import { demandSingleValue } from "./helpers";
910
import type { CommonYargsArgv, SubHelp } from "../yargs-types";
1011
import type {
1112
Command,
@@ -125,6 +126,17 @@ function walkTreeAndRegister(
125126
if (def.type === "command") {
126127
yargs.options(def.args);
127128

129+
// Yargs offers an `array: true` option that will always coerces the value to an array
130+
// e.g. `--name foo` becomes `{ name: ["foo"] }` instead of `{ name: "foo" }`
131+
// However, non-array arguments can still receives multiple values
132+
// e.g. `--name foo --name bar` becomes `{ name: ["foo", "bar"] }` regardless of the `array` setting
133+
// @see https://github.com/yargs/yargs/issues/1318
134+
for (const [key, opt] of Object.entries(def.args)) {
135+
if (!opt.array) {
136+
yargs.check(demandSingleValue(key));
137+
}
138+
}
139+
128140
for (const key of def.positionalArgs ?? []) {
129141
yargs.positional(key, def.args[key]);
130142
}

packages/wrangler/src/errors.ts

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export class FatalError extends UserError {
2828
}
2929
}
3030

31+
export class CommandLineArgsError extends UserError {}
32+
3133
/**
3234
* JsonFriendlyFatalError is used to output JSON when wrangler crashes, useful for --json mode.
3335
*

packages/wrangler/src/generate/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import fs from "node:fs";
22
import path from "node:path";
33
import { execa } from "execa";
44
import { getC3CommandFromEnv } from "../environment-variables/misc-variables";
5-
import { UserError } from "../errors";
5+
import { CommandLineArgsError, UserError } from "../errors";
66
import { cloneIntoDirectory, initializeGit } from "../git-client";
7-
import { CommandLineArgsError, printWranglerBanner } from "../index";
7+
import { printWranglerBanner } from "../index";
88
import { initHandler } from "../init";
99
import { logger } from "../logger";
1010
import { getPackageManager } from "../package-manager";

packages/wrangler/src/index.ts

+8-30
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,17 @@ import {
3838
import { devHandler, devOptions } from "./dev";
3939
import { workerNamespaceCommands } from "./dispatch-namespace";
4040
import { docsHandler, docsOptions } from "./docs";
41-
import { JsonFriendlyFatalError, UserError } from "./errors";
41+
import {
42+
CommandLineArgsError,
43+
JsonFriendlyFatalError,
44+
UserError,
45+
} from "./errors";
4246
import { generateHandler, generateOptions } from "./generate";
4347
import { hyperdrive } from "./hyperdrive/index";
4448
import { initHandler, initOptions } from "./init";
4549
import "./kv";
4650
import "./workflows";
51+
import { demandSingleValue } from "./core";
4752
import { logBuildFailure, logger, LOGGER_LEVELS } from "./logger";
4853
import * as metrics from "./metrics";
4954
import { mTlsCertificateCommands } from "./mtls-certificate/cli";
@@ -159,35 +164,6 @@ export function getLegacyScriptName(
159164
: args.name ?? config.name;
160165
}
161166

162-
/**
163-
* A helper to demand one of a set of options
164-
* via https://github.com/yargs/yargs/issues/1093#issuecomment-491299261
165-
*/
166-
export function demandOneOfOption(...options: string[]) {
167-
return function (argv: { [key: string]: unknown }) {
168-
const count = options.filter((option) => argv[option]).length;
169-
const lastOption = options.pop();
170-
171-
if (count === 0) {
172-
throw new CommandLineArgsError(
173-
`Exactly one of the arguments ${options.join(
174-
", "
175-
)} and ${lastOption} is required`
176-
);
177-
} else if (count > 1) {
178-
throw new CommandLineArgsError(
179-
`Arguments ${options.join(
180-
", "
181-
)} and ${lastOption} are mutually exclusive`
182-
);
183-
}
184-
185-
return true;
186-
};
187-
}
188-
189-
export class CommandLineArgsError extends UserError {}
190-
191167
export function createCLIParser(argv: string[]) {
192168
const experimentalGradualRollouts =
193169
// original flag -- using internal product name (Gradual Rollouts) -- kept for temp back-compat
@@ -230,12 +206,14 @@ export function createCLIParser(argv: string[]) {
230206
type: "string",
231207
requiresArg: true,
232208
})
209+
.check(demandSingleValue("config"))
233210
.option("env", {
234211
alias: "e",
235212
describe: "Environment to use for operations and .env files",
236213
type: "string",
237214
requiresArg: true,
238215
})
216+
.check(demandSingleValue("env"))
239217
.option("experimental-json-config", {
240218
alias: "j",
241219
describe: `Experimental: support wrangler.json`,

packages/wrangler/src/init.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { readConfig } from "./config";
1212
import { getDatabaseInfoFromId } from "./d1/utils";
1313
import { confirm, select } from "./dialogs";
1414
import { getC3CommandFromEnv } from "./environment-variables/misc-variables";
15-
import { FatalError, UserError } from "./errors";
15+
import { CommandLineArgsError, FatalError, UserError } from "./errors";
1616
import { getGitVersioon, initializeGit, isInsideGitRepo } from "./git-client";
1717
import { logger } from "./logger";
1818
import { getPackageManager } from "./package-manager";
@@ -21,7 +21,7 @@ import { getBasePath } from "./paths";
2121
import { requireAuth } from "./user";
2222
import { createBatches } from "./utils/create-batches";
2323
import * as shellquote from "./utils/shell-quote";
24-
import { CommandLineArgsError, printWranglerBanner } from "./index";
24+
import { printWranglerBanner } from "./index";
2525
import type { RawConfig } from "./config";
2626
import type {
2727
CustomDomainRoute,

packages/wrangler/src/kv/index.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ import { Blob } from "node:buffer";
22
import { arrayBuffer } from "node:stream/consumers";
33
import { StringDecoder } from "node:string_decoder";
44
import { readConfig } from "../config";
5-
import { defineAlias, defineCommand, defineNamespace } from "../core";
5+
import {
6+
defineAlias,
7+
defineCommand,
8+
defineNamespace,
9+
demandOneOfOption,
10+
} from "../core";
611
import { confirm } from "../dialogs";
7-
import { UserError } from "../errors";
8-
import { CommandLineArgsError, demandOneOfOption } from "../index";
12+
import { CommandLineArgsError, UserError } from "../errors";
913
import { logger } from "../logger";
1014
import * as metrics from "../metrics";
1115
import { parseJSON, readFileSync, readFileSyncToBuffer } from "../parse";

packages/wrangler/src/pubsub/pubsub-commands.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { readConfig } from "../config";
22
import { confirm } from "../dialogs";
3-
import { CommandLineArgsError } from "../index";
3+
import { CommandLineArgsError } from "../errors";
44
import { logger } from "../logger";
55
import * as metrics from "../metrics";
66
import { parseHumanDuration } from "../parse";

packages/wrangler/src/queues/cli/commands/consumer/http-pull/add.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { readConfig } from "../../../../../config";
2-
import { CommandLineArgsError } from "../../../../../index";
2+
import { CommandLineArgsError } from "../../../../../errors";
33
import { logger } from "../../../../../logger";
44
import { postConsumer } from "../../../../client";
55
import type {

packages/wrangler/src/queues/cli/commands/consumer/worker/add.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { readConfig } from "../../../../../config";
2-
import { CommandLineArgsError } from "../../../../../index";
2+
import { CommandLineArgsError } from "../../../../../errors";
33
import { logger } from "../../../../../logger";
44
import { postConsumer } from "../../../../client";
55
import type {

packages/wrangler/src/queues/cli/commands/create.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { readConfig } from "../../../config";
2-
import { CommandLineArgsError } from "../../../index";
2+
import { CommandLineArgsError } from "../../../errors";
33
import { logger } from "../../../logger";
44
import { createQueue } from "../../client";
55
import { handleFetchError } from "../../utils";

packages/wrangler/src/r2/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import * as stream from "node:stream";
55
import { ReadableStream } from "node:stream/web";
66
import prettyBytes from "pretty-bytes";
77
import { readConfig } from "../config";
8-
import { FatalError, UserError } from "../errors";
9-
import { CommandLineArgsError, printWranglerBanner } from "../index";
8+
import { CommandLineArgsError, FatalError, UserError } from "../errors";
9+
import { printWranglerBanner } from "../index";
1010
import { logger } from "../logger";
1111
import * as metrics from "../metrics";
1212
import { requireAuth } from "../user";

packages/wrangler/src/type-generation/index.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ import { getNodeCompat } from "miniflare";
66
import { findWranglerToml, readConfig } from "../config";
77
import { getEntry } from "../deployment-bundle/entry";
88
import { getVarsForDev } from "../dev/dev-vars";
9-
import { UserError } from "../errors";
10-
import { CommandLineArgsError } from "../index";
9+
import { CommandLineArgsError, UserError } from "../errors";
1110
import { logger } from "../logger";
1211
import { parseJSONC } from "../parse";
1312
import { printWranglerBanner } from "../update-check";

0 commit comments

Comments
 (0)