Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions command/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
14 changes: 14 additions & 0 deletions command/test/command/arguments_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
60 changes: 60 additions & 0 deletions command/test/command/option_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <value:boolean>", "...")
.option("--baz <value:string>", "...")
.option("--beep <value:string>", "...", { collect: true })
.option("--boop <value:number>", "...")
.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: [],
});
});
60 changes: 48 additions & 12 deletions flags/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ function parseArgs<TFlagOptions extends FlagOptions>(
let current: string = args[argsIndex];
let currentValue: string | undefined;
let negate = false;
let skipArgument = false;

// literal args after --
if (inLiteral) {
Expand Down Expand Up @@ -188,7 +189,7 @@ function parseArgs<TFlagOptions extends FlagOptions>(
// 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);
}

Expand Down Expand Up @@ -257,6 +258,10 @@ function parseArgs<TFlagOptions extends FlagOptions>(

parseNext(option);

if (skipArgument) {
continue;
}

if (typeof ctx.flags[propName] === "undefined") {
if (option.args?.length && !option.args?.[optionArgsIndex].optional) {
throw new MissingOptionValueError(option.name);
Expand Down Expand Up @@ -289,6 +294,8 @@ function parseArgs<TFlagOptions extends FlagOptions>(
/** 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;
Expand Down Expand Up @@ -360,13 +367,26 @@ function parseArgs<TFlagOptions extends FlagOptions>(
}
}

if (skipOptionArgument) {
if (hasNext(arg)) {
parseNext(option);
}
return;
}
if (skipArgument) {
return;
}

if (
typeof result !== "undefined" &&
(option.args.length > 1 || arg.variadic)
) {
if (!ctx.flags[propName]) {
setFlagValue([]);
}
if (result === "") {
result = undefined;
}

(ctx.flags[propName] as Array<unknown>).push(result);

Expand All @@ -383,7 +403,7 @@ function parseArgs<TFlagOptions extends FlagOptions>(
return false;
}
const nextValue = currentValue ?? args[argsIndex + 1];
if (!nextValue) {
if (nextValue === undefined) {
return false;
}
if (option.args.length > 1 && optionArgsIndex >= option.args.length) {
Expand Down Expand Up @@ -414,17 +434,33 @@ function parseArgs<TFlagOptions extends FlagOptions>(
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;
Expand Down
92 changes: 92 additions & 0 deletions flags/test/flags/empty_options_test.ts
Original file line number Diff line number Diff line change
@@ -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, []);
});
8 changes: 5 additions & 3 deletions flags/types/number.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { InvalidTypeError } from "../_errors.ts";

/** Number type handler. Excepts any numeric value. */
export const number: TypeHandler<number> = (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);
Expand Down
Loading