fix(cli): a negative number after a flag is its value, not an unknown flag (#6549)

This commit is contained in:
Miguel Ángel
2026-10-27 10:49:32 +00:00
committed by GitHub
parent 5a92fe7e5e
commit 24d49637d3
2 changed files with 50 additions and 2 deletions
@@ -61,6 +61,18 @@ describe("assertKnownFlags", () => {
expect(ok(["-ow"])).not.toThrow(); // both known aliases
expect(ok(["-ox"])).toThrow(/Unknown flag: -x/); // x unknown
});
it("takes a negative number as a value, in the space and equals forms", () => {
expect(ok(["--workers", "-18"])).not.toThrow();
expect(ok(["--workers=-18"])).not.toThrow();
expect(ok(["--workers", "-0.5", "--output", "-.25"])).not.toThrow();
expect(ok(["-w", "-3"])).not.toThrow();
});
it("still rejects a dash token that only starts like a number", () => {
expect(ok(["--workers", "-1x"])).toThrow(/Unknown flag: -1/);
expect(ok(["--workers", "-1-2"])).toThrow(/Unknown flag: -1/);
});
});
describe("guardSwallowedFlagValues", () => {
@@ -102,6 +114,11 @@ describe("guardSwallowedFlagValues", () => {
expect(guard(["--output", "-out.mp4"])).not.toThrow();
});
it("does not take a negative number for a swallowed flag", () => {
expect(guard(["--output", "-18"])).not.toThrow();
expect(guard(["--output", "-0.5"])).not.toThrow();
});
it("does not reject a trailing bare string flag with nothing after it", () => {
expect(guard(["--output"])).not.toThrow();
});
@@ -183,6 +200,27 @@ describe("guardSwallowedFlagValues end-to-end (via citty's real runCommand + the
expect(capturedArgs).toBeUndefined();
});
it("passes a negative value in both forms through the real pipeline", async () => {
let captured: Record<string, unknown> | undefined;
const testCommand: CommandDef<any> = {
meta: { name: "test-cmd" },
args: { lufs: { type: "string" }, json: { type: "boolean" } },
setup: ({ rawArgs, cmd }) => assertKnownFlags({ args: cmd.args }, rawArgs),
run: ({ args }) => {
captured = args;
},
};
const wrapped = await wrapTestCommand(testCommand);
for (const rawArgs of [
["--lufs=-18", "--json"],
["--lufs", "-18", "--json"],
]) {
captured = undefined;
await runCommand(wrapped, { rawArgs });
expect(captured).toEqual(expect.objectContaining({ lufs: "-18", json: true }));
}
});
it("rewrites correctly with a positional argument ahead of the flags (check's own opt-in)", async () => {
let capturedArgs: Record<string, unknown> | undefined;
const testCommand: CommandDef<any> = {
+12 -2
View File
@@ -43,8 +43,18 @@ function longFlagName(tok: string): string {
return name.startsWith("no-") ? name.slice(3) : name;
}
// A negative number (`-18`, `-0.5`) is a value: no flag is spelled with digits.
function isNegativeNumber(tok: string): boolean {
return /^-(\d+\.?\d*|\.\d+)$/.test(tok);
}
// Dash-prefixed and not a bare `-` (stdin) or a negative number: the only tokens that can name a flag.
function canNameFlag(tok: string): boolean {
return tok.startsWith("-") && tok !== "-" && !isNegativeNumber(tok);
}
function unknownFlagIn(tok: string, known: Set<string>): string | null {
if (tok === "-" || !tok.startsWith("-")) return null; // positional or flag value
if (!canNameFlag(tok)) return null; // positional or flag value
if (tok.startsWith("--")) {
const name = longFlagName(tok);
return name && !known.has(name) ? `--${name}` : null;
@@ -135,7 +145,7 @@ export interface SwallowGuardResult {
// End of argv and a bare `-` (stdin) are never a swallow; `--` or a known flag spelling is.
function looksLikeSwallowedFlag(next: string | undefined, known: Set<string>): boolean {
if (next === undefined || next === "-") return false;
if (next === undefined || next === "-" || isNegativeNumber(next)) return false;
return next === "--" || (next.startsWith("-") && unknownFlagIn(next, known) === null);
}