diff --git a/command/command.ts b/command/command.ts index c2a80014..5261ad67 100644 --- a/command/command.ts +++ b/command/command.ts @@ -2148,16 +2148,20 @@ export class Command< }; if (expectedArg.variadic) { - arg = args.splice(0, args.length).map((value) => - parseArgValue(value) - ); + arg = args + .splice(0, args.length) + .filter((arg) => !(expectedArg.optional && arg === "")) + .map((value) => parseArgValue(value)); } else { - arg = parseArgValue(args.shift() as string); + const value = args.shift() as string; + if (!(expectedArg.optional && value === "")) { + arg = parseArgValue(value); + } } if (expectedArg.variadic && Array.isArray(arg)) { params.push(...arg); - } else if (typeof arg !== "undefined") { + } else { params.push(arg); } } diff --git a/command/test/command/arguments_test.ts b/command/test/command/arguments_test.ts index c3aa6563..897d04b5 100644 --- a/command/test/command/arguments_test.ts +++ b/command/test/command/arguments_test.ts @@ -94,6 +94,20 @@ test("should parse multi argument option", async () => { assertEquals(args, ["mod.ts"]); }); +test("should ignore optional arguments with an empty string as value", async () => { + const { args } = await new Command() + .arguments("[foo] [bar] [baz] [...beep]") + .parse(["", "bar-value", "", "", "", "beep-value-3", "", "beep-value-5"]); + + assertEquals(args, [ + undefined, + "bar-value", + undefined, + "beep-value-3", + "beep-value-5", + ]); +}); + test("should throw an error for invalid number types", async () => { await assertRejects( async () => { diff --git a/command/test/command/option_test.ts b/command/test/command/option_test.ts index 76c99939..5cd9b644 100644 --- a/command/test/command/option_test.ts +++ b/command/test/command/option_test.ts @@ -275,3 +275,63 @@ test("command - option - global option value handler", async () => { .parse(["foo", "--foo", "bar"]); assertEquals(options, { foo: { value: "bar" } }); }); + +test("command - option - should skip optional arguments with an empty value", async () => { + const { options, args } = await new Command() + .noExit() + .option("--foo [value:number]", "...") + .option("--bar ", "...") + .option("--baz ", "...") + .option("--beep ", "...", { collect: true }) + .option("--boop ", "...") + .option("--multi [value:string] [value:string] [value:string]", "...") + .option("--variadic [...value:string]", "...") + .parse([ + "--foo", + "", + "--bar", + "", + "--baz", + "", + "--boop", + "1", + "--beep", + "", + "--beep", + "beep-value-2", + "--beep", + "", + "--beep", + "beep-value-4", + "--multi", + "", + "multi-value-2", + "", + "--variadic", + "", + "variadic-value-2", + "", + "variadic-value-4", + "", + ]); + + assertEquals({ options, args }, { + options: { + beep: [ + "beep-value-2", + "beep-value-4", + ], + boop: 1, + multi: [ + undefined, + "multi-value-2", + undefined, + ], + variadic: [ + "variadic-value-2", + "variadic-value-4", + ], + }, + args: [], + }); +}); diff --git a/flags/flags.ts b/flags/flags.ts index 23612dde..d38a89a4 100644 --- a/flags/flags.ts +++ b/flags/flags.ts @@ -148,6 +148,7 @@ function parseArgs( let current: string = args[argsIndex]; let currentValue: string | undefined; let negate = false; + let skipArgument = false; // literal args after -- if (inLiteral) { @@ -188,7 +189,7 @@ function parseArgs( // split value: --foo="bar=baz" => --foo bar=baz const equalSignIndex = current.indexOf("="); if (equalSignIndex !== -1) { - currentValue = current.slice(equalSignIndex + 1) || undefined; + currentValue = current.slice(equalSignIndex + 1); current = current.slice(0, equalSignIndex); } @@ -257,6 +258,10 @@ function parseArgs( parseNext(option); + if (skipArgument) { + continue; + } + if (typeof ctx.flags[propName] === "undefined") { if (option.args?.length && !option.args?.[optionArgsIndex].optional) { throw new MissingOptionValueError(option.name); @@ -289,6 +294,8 @@ function parseArgs( /** Parse next argument for current option. */ // deno-lint-ignore no-inner-declarations function parseNext(option: FlagOptions): void { + let skipOptionArgument = false; + if (negate) { setFlagValue(false); return; @@ -360,6 +367,16 @@ function parseArgs( } } + if (skipOptionArgument) { + if (hasNext(arg)) { + parseNext(option); + } + return; + } + if (skipArgument) { + return; + } + if ( typeof result !== "undefined" && (option.args.length > 1 || arg.variadic) @@ -367,6 +384,9 @@ function parseArgs( if (!ctx.flags[propName]) { setFlagValue([]); } + if (result === "") { + result = undefined; + } (ctx.flags[propName] as Array).push(result); @@ -383,7 +403,7 @@ function parseArgs( return false; } const nextValue = currentValue ?? args[argsIndex + 1]; - if (!nextValue) { + if (nextValue === undefined) { return false; } if (option.args.length > 1 && optionArgsIndex >= option.args.length) { @@ -414,17 +434,33 @@ function parseArgs( arg: ArgumentOptions, value: string, ): unknown { - const result: unknown = opts.parse - ? opts.parse({ - label: "Option", - type: arg.type || OptionType.STRING, - name: `--${option.name}`, - value, - }) - : parseDefaultType(option, arg, value); - - if (typeof result !== "undefined") { + let result: unknown; + + if ((!option.required || arg.optional) && value === "") { + // if the value is empty and the argument is optional, + // we can skip the argument. + if (arg.variadic) { + skipOptionArgument = true; + } else if (option.args?.length === 1) { + skipArgument = true; + } else { + // will be mapped to undefined later. + result = ""; + } increase = true; + } else { + result = opts.parse + ? opts.parse({ + label: "Option", + type: arg.type || OptionType.STRING, + name: `--${option.name}`, + value, + }) + : parseDefaultType(option, arg, value); + + if (typeof result !== "undefined") { + increase = true; + } } return result; diff --git a/flags/test/flags/empty_options_test.ts b/flags/test/flags/empty_options_test.ts new file mode 100644 index 00000000..2f08fe08 --- /dev/null +++ b/flags/test/flags/empty_options_test.ts @@ -0,0 +1,92 @@ +import { test } from "@cliffy/internal/testing/test"; +import { assertEquals } from "@std/assert"; +import { parseFlags } from "../../flags.ts"; + +test("[flags] should skip optional arguments with an empty value", () => { + const { flags, unknown, literal } = parseFlags( + [ + "--foo", + "", + "--bar", + "", + "--baz", + "", + "--boop", + "1", + "--beep", + "", + "--beep", + "beep-value-2", + "--beep", + "", + "--beep", + "beep-value-4", + "--multi", + "", + "multi-value-2", + "", + "--variadic", + "", + "variadic-value-2", + "", + "variadic-value-4", + "", + ], + { + flags: [{ + name: "foo", + type: "number", + optionalValue: true, + }, { + name: "bar", + type: "boolean", + }, { + name: "baz", + type: "string", + }, { + name: "beep", + type: "string", + collect: true, + }, { + name: "boop", + type: "number", + }, { + name: "multi", + args: [{ + type: "string", + optional: true, + }, { + type: "string", + optional: true, + }, { + type: "string", + optional: true, + }], + }, { + name: "variadic", + type: "string", + optional: true, + variadic: true, + }], + }, + ); + + assertEquals(flags, { + beep: [ + "beep-value-2", + "beep-value-4", + ], + boop: 1, + multi: [ + undefined, + "multi-value-2", + undefined, + ], + variadic: [ + "variadic-value-2", + "variadic-value-4", + ], + }); + assertEquals(unknown, []); + assertEquals(literal, []); +}); diff --git a/flags/types/number.ts b/flags/types/number.ts index f1d8ae7c..5efcb746 100644 --- a/flags/types/number.ts +++ b/flags/types/number.ts @@ -3,9 +3,11 @@ import { InvalidTypeError } from "../_errors.ts"; /** Number type handler. Excepts any numeric value. */ export const number: TypeHandler = (type: ArgumentValue): number => { - const value = Number(type.value); - if (Number.isFinite(value)) { - return value; + if (type.value) { + const value = Number(type.value); + if (Number.isFinite(value)) { + return value; + } } throw new InvalidTypeError(type);