Skip to content

Commit

Permalink
fix(flags,command): values starting with '-' not supported (#251)
Browse files Browse the repository at this point in the history
  • Loading branch information
c4spar committed Jul 22, 2021
1 parent ce794cf commit ab598bf
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 76 deletions.
11 changes: 11 additions & 0 deletions flags/_errors.ts
Expand Up @@ -62,6 +62,17 @@ export class DuplicateOption extends ValidationError {
}
}

export class InvalidOption extends ValidationError {
constructor(option: string, options: Array<IFlagOptions>) {
super(
`Invalid option "${getFlag(option)}".${
didYouMeanOption(option, options)
}`,
);
Object.setPrototypeOf(this, InvalidOption.prototype);
}
}

export class UnknownOption extends ValidationError {
constructor(option: string, options: Array<IFlagOptions>) {
super(
Expand Down
91 changes: 74 additions & 17 deletions flags/flags.ts
Expand Up @@ -2,6 +2,7 @@ import { getDefaultValue, getOption, paramCaseToCamelCase } from "./_utils.ts";
import {
ArgumentFollowsVariadicArgument,
DuplicateOption,
InvalidOption,
InvalidOptionValue,
MissingOptionValue,
RequiredArgumentFollowsOptionalArgument,
Expand All @@ -10,7 +11,6 @@ import {
UnknownRequiredOption,
UnknownType,
} from "./_errors.ts";
import { normalize } from "./normalize.ts";
import type {
IFlagArgument,
IFlagOptions,
Expand Down Expand Up @@ -64,7 +64,7 @@ export function parseFlags<O extends Record<string, any> = Record<string, any>>(
): IFlagsResult<O> {
!opts.flags && (opts.flags = []);

const normalized = normalize(args);
const normalized = splitValues(args);

let inLiteral = false;
let negate = false;
Expand All @@ -91,7 +91,7 @@ export function parseFlags<O extends Record<string, any> = Record<string, any>>(
for (let i = 0; i < normalized.length; i++) {
let option: IFlagOptions | undefined;
let args: IFlagArgument[] | undefined;
const current = normalized[i];
let current: string = normalized[i];

// literal args after --
if (inLiteral) {
Expand All @@ -108,17 +108,22 @@ export function parseFlags<O extends Record<string, any> = Record<string, any>>(
const next = () => normalized[i + 1];

if (isFlag) {
if (current[2] === "-" || (current[1] === "-" && current.length === 3)) {
throw new UnknownOption(current, opts.flags);
const isShort = current[1] !== "-";
const isLong = isShort ? false : current.length > 3 && current[2] !== "-";

if (!isShort && !isLong) {
throw new InvalidOption(current, opts.flags);
}

negate = current.startsWith("--no-");
// normalize short hand flags e.g: -abc => -a -b -c
if (isShort && current.length > 2 && current[2] !== ".") {
normalized.splice(i, 1, ...splitFlags(current));
current = normalized[i];
} else if (isLong && current.startsWith("--no-")) {
negate = true;
}

option = getOption(opts.flags, current);
// if (option && negate) {
// const positiveName: string = current.replace(/^-+(no-)?/, "");
// option = getOption(opts.flags, positiveName) ?? option;
// }

if (!option) {
if (opts.flags.length) {
Expand Down Expand Up @@ -280,14 +285,21 @@ export function parseFlags<O extends Record<string, any> = Record<string, any>>(

/** Check if current option should have an argument. */
function hasNext(arg: IFlagArgument): boolean {
return !!(
normalized[i + 1] &&
(arg.optionalValue || arg.requiredValue || arg.variadic) &&
(normalized[i + 1][0] !== "-" ||
if (!normalized[i + 1]) {
return false;
}

if (arg.requiredValue) {
return true;
}

if (arg.optionalValue || arg.variadic) {
return (normalized[i + 1][0] !== "-" ||
(arg.type === OptionType.NUMBER &&
!isNaN(Number(normalized[i + 1])))) &&
arg
);
!isNaN(Number(normalized[i + 1]))));
}

return false;
}

/** Parse argument value. */
Expand Down Expand Up @@ -376,6 +388,51 @@ export function parseFlags<O extends Record<string, any> = Record<string, any>>(
return { flags: result as O, unknown, literal };
}

function splitValues(args: string[]) {
const normalized = [];
let inLiteral = false;

for (const arg of args) {
if (inLiteral) {
normalized.push(arg);
} else if (arg === "--") {
inLiteral = true;
normalized.push(arg);
} else if (arg.length > 1 && arg[0] === "-") {
if (arg.includes("=")) {
const parts = arg.split("=");
const flag = parts.shift() as string;
normalized.push(flag);
if (parts.length) {
normalized.push(parts.join("="));
}
} else {
normalized.push(arg);
}
} else {
normalized.push(arg);
}
}

return normalized;
}

function splitFlags(flag: string): Array<string> {
const normalized: Array<string> = [];
const flags = flag.slice(1).split("");

if (isNaN(Number(flag[flag.length - 1]))) {
flags.forEach((val) => normalized.push(`-${val}`));
} else {
normalized.push(`-${flags.shift()}`);
if (flags.length) {
normalized.push(flags.join(""));
}
}

return normalized;
}

function parseFlagValue(
option: IFlagOptions,
arg: IFlagArgument,
Expand Down
1 change: 0 additions & 1 deletion flags/mod.ts
@@ -1,5 +1,4 @@
export * from "./flags.ts";
export * from "./normalize.ts";
export * from "./types.ts";
export * from "./validate_flags.ts";
export * from "./types/boolean.ts";
Expand Down
55 changes: 0 additions & 55 deletions flags/normalize.ts

This file was deleted.

102 changes: 100 additions & 2 deletions flags/test/type/string_test.ts
Expand Up @@ -14,7 +14,7 @@ const optionalValueOptions = <IParseOptions> {
}],
};

const requiredValueOptions = <IParseOptions> {
const requiredStringValueOptions = <IParseOptions> {
stopEarly: false,
allowEmpty: false,
flags: [{
Expand All @@ -24,6 +24,16 @@ const requiredValueOptions = <IParseOptions> {
}],
};

const requiredNumberValueOptions = <IParseOptions> {
stopEarly: false,
allowEmpty: false,
flags: [{
name: "flag",
aliases: ["f"],
type: OptionType.NUMBER,
}],
};

Deno.test("flags - type - string - with no value", () => {
const { flags, unknown, literal } = parseFlags(["-f"], optionalValueOptions);

Expand Down Expand Up @@ -56,8 +66,96 @@ Deno.test("flags - type - string - with special chars", () => {

Deno.test("flags - type - string - with missing value", () => {
assertThrows(
() => parseFlags(["-f"], requiredValueOptions),
() => parseFlags(["-f"], requiredStringValueOptions),
Error,
`Missing value for option "--flag".`,
);
});

Deno.test("flags - type - string - value starting with '-'", () => {
const { flags, unknown, literal } = parseFlags(
["-f", "-a", "unknown"],
requiredStringValueOptions,
);

assertEquals(flags, { flag: "-a" });
assertEquals(unknown, ["unknown"]);
assertEquals(literal, []);
});

Deno.test("flags - type - string - with negative numeric value", () => {
const { flags, unknown, literal } = parseFlags(
["-f", "-1", "unknown"],
requiredNumberValueOptions,
);

assertEquals(flags, { flag: -1 });
assertEquals(unknown, ["unknown"]);
assertEquals(literal, []);
});

Deno.test("flags - type - string - with negative numeric value 2", () => {
const { flags, unknown, literal } = parseFlags(
["-f", "-123", "unknown"],
requiredNumberValueOptions,
);

assertEquals(flags, { flag: -123 });
assertEquals(unknown, ["unknown"]);
assertEquals(literal, []);
});

Deno.test("flags - type - string - with negative decimal value", () => {
const { flags, unknown, literal } = parseFlags(
["-f", "-12.2", "unknown"],
requiredNumberValueOptions,
);

assertEquals(flags, { flag: -12.2 });
assertEquals(unknown, ["unknown"]);
assertEquals(literal, []);
});

Deno.test("flags - type - string - with negative numeric string value", () => {
const { flags, unknown, literal } = parseFlags(
["-f", "-1", "unknown"],
requiredStringValueOptions,
);

assertEquals(flags, { flag: "-1" });
assertEquals(unknown, ["unknown"]);
assertEquals(literal, []);
});

Deno.test("flags - type - string - value starting with hyphen", () => {
const { flags, unknown, literal } = parseFlags(
["-f", "-foo", "unknown"],
requiredStringValueOptions,
);

assertEquals(flags, { flag: "-foo" });
assertEquals(unknown, ["unknown"]);
assertEquals(literal, []);
});

Deno.test("flags - type - string - with long flag as value", () => {
const { flags, unknown, literal } = parseFlags(
["-f", "--foo", "unknown"],
requiredStringValueOptions,
);

assertEquals(flags, { flag: "--foo" });
assertEquals(unknown, ["unknown"]);
assertEquals(literal, []);
});

Deno.test("flags - type - string - with only hyphens as value", () => {
const { flags, unknown, literal } = parseFlags(
["-f", "-------------", "unknown"],
requiredStringValueOptions,
);

assertEquals(flags, { flag: "-------------" });
assertEquals(unknown, ["unknown"]);
assertEquals(literal, []);
});
1 change: 0 additions & 1 deletion mod.ts
Expand Up @@ -2,7 +2,6 @@ export * from "./ansi/mod.ts";
export * from "./command/mod.ts";
export {
boolean,
normalize,
number,
// Already exported by command module.
// ValidationError,
Expand Down

0 comments on commit ab598bf

Please sign in to comment.