Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tried to add multiple arguments to options #33

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
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
146 changes: 94 additions & 52 deletions src/arg_parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,43 +62,26 @@ export function extractArgumentsFromDenoArgs(
export function extractOptionsFromDenoArgs(
denoArgs: string[],
optionsMap: Map<string, IOption>,
): string[] {
): string[] | undefined {
const errors: string[] = [];
const passedInOptions = getOptionsPassedIntoDenoArgs(denoArgs, optionsMap);

let isValue = false;
let lastOptionObject: IOption | null = null;
const optionsProcessed: string[] = [];

passedInOptions.forEach((option: string) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dXu23 , again sorry for being late. don't get much time to look at the lower priority modules. anyways, any reason you went with different loops as opposed to keeping the .forEeach here? i ask because from a readability point of view, it's difficult for me to keep track of what's being iterated. im all for different loops, but reason i chose for each was so that i didn't have to add [i] to retrieve values. wondering if you found this approach to be better

// If this option is a value, then set it on the last option that was
// processed
if (lastOptionObject && isValue) {
lastOptionObject.value = option;
// Reset the variables used to set values on a last option processed
lastOptionObject = null;
isValue = false;
return;
}
const optionsProcessed = new Set();
let i = 0;

// If this option is not in the options map, then we have no idea what it
// is. The user made a mistake, so tell them they passed in an unknown item.
if (!optionsMap.has(option)) {
optionsProcessed.push(option);
errors.push(`Unknown item '${option}' provided`);
return;
while (i < passedInOptions.length) {
if (!optionsMap.has(passedInOptions[i])) {
optionsProcessed.add(passedInOptions[i]);
errors.push(`Unknown item '${passedInOptions[i]}' provided`);
i++;
continue;
}

// If we get here, then we have an option that is defined in this CLI. Let's
// track it and proceed to check if it requires a value.
const optionObject = optionsMap.get(option)!;
lastOptionObject = optionObject;
const optionObject = optionsMap.get(passedInOptions[i])!;

// If we already processed this option, then tell the user it was provided
// more than once. Options should only be passed in once.
let alreadyProcessed = false;
optionObject.signatures.forEach((signature: string) => {
if (optionsProcessed.indexOf(signature) !== -1) {
if (optionsProcessed.has(signature)) {
alreadyProcessed = true;
}
});
Expand All @@ -109,24 +92,33 @@ export function extractOptionsFromDenoArgs(
optionObject.signatures.join(", ")
}' was provided more than once`,
);
return;

i++;
continue;
}

optionsProcessed.push(option);
optionsProcessed.add(passedInOptions[i]);

optionObject.value = passedInOptions.slice(
i + 1,
i + optionObject.arg_count + 1,
);

let ndx = -1;
if (
(ndx = optionObject.value.findIndex((optArg: string) =>
optionsMap.has(optArg)
)) !== -1
) {
errors.push(
`Option ${passedInOptions[i]}, has the wrong number of arguments`,
);

// If this option takes a value, then set the flag to say that the next
// option is the value
if (optionObject.takes_value) {
isValue = true;
return;
i += ndx + 1;
}

// If we get here, then the option currently being processed does not
// require a value. It just needs to "exist" in the command line, so we set
// its value to true. We set it to true so when `this.option("option")` is
// called from the command or subcommand class, it evaluates to true.
lastOptionObject.value = true;
});
i += optionObject.arg_count + 1;
}

return errors;
}
Expand Down Expand Up @@ -189,6 +181,7 @@ export function setOptionsMapInitialValues(
const optionObject: IOption = {
description: options[optionSignatures],
signatures: [],
arg_count: 0,
// deno-lint-ignore camelcase
takes_value: false,
};
Expand All @@ -200,20 +193,67 @@ export function setOptionsMapInitialValues(
// For each option signature specified ...
split.forEach((signature: string) => {
// ... trim leading/trailing spaces that might be in the signature ...
signature = signature.trim();
const openBracketNdx = signature.indexOf("[");

// ... and check to see if it takes in a value
if (signature.includes("[value]")) {
// If it takes in a value, then remove the `[value]` portion ...
signature = signature.replace(/\s+\[value\]/, "").trim();
// ... and set the option object's `takes_value` flag to true
if (openBracketNdx === -1) {
signature = signature.trim();
} else {
optionObject.takes_value = true;
const sig = signature.substring(0, openBracketNdx).trim();
Copy link
Member

@crookse crookse Jun 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question here. what's the reason behind tokenizing the string in this else block? is it mainly for validating that the option is written properly so it can be parsed? simply trying to get context here so i can better understand the goal and if there may be another way than tokenizing

const argStr = signature.substring(openBracketNdx);
signature = sig;
let switchFlag = false;
let argCount = 0;

for (const c of argStr) {
switch (c) {
case "[":
if (switchFlag === true) {
throw new Error(
"Left open bracket after another left open bracket found.",
);
}

switchFlag = !switchFlag;
break;
case "]":
if (switchFlag === false) {
throw new Error(
"Right open bracket has no matching left open bracket",
);
}

switchFlag = !switchFlag;
argCount++;

break;
case " ":
case "\t":
case "\n":
if (switchFlag === true) {
throw new Error("White space char inside brackets");
}

break;
default:
if (switchFlag === false) {
throw new Error("Non white-space character outside bracket");
}

break;
}
}

// ... and check to see if it takes in a value

// Once done, add all signatures that this option has ...
optionObject.arg_count = argCount;

// ... and set this option -- identifiable by signature -- in the
// command's options Map
}

// Once done, add all signatures that this option has ...
optionObject.signatures.push(signature);
// ... and set this option -- identifiable by signature -- in the
// command's options Map
optionsMap.set(signature, optionObject);
});
}
Expand Down Expand Up @@ -257,9 +297,11 @@ function getLastOptionIndex(
}

lastOptionIndex = index;
if (optionsMap.get(arg)!.takes_value) {
lastOptionIndex = index + 1;
const optionObject = optionsMap.get(arg);
if (optionObject!.takes_value) {
lastOptionIndex += optionObject!.arg_count;
}

optionsProcessed.push(arg);
}
});
Expand Down
5 changes: 3 additions & 2 deletions src/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export abstract class Command {
* @returns The value of the option in the command line or undefined if the
* option does not exist.
*/
public option(optionName: string): string | boolean | undefined {
public option(optionName: string): string[] | boolean | undefined {
const optionObject = this.options_map.get(optionName);

if (optionObject) {
Expand Down Expand Up @@ -172,6 +172,7 @@ export abstract class Command {
alreadyProcessed = true;
}
});

if (!alreadyProcessed) {
help += `\n ${optionObject.signatures.join(", ")}\n`;
help += ` ${optionObject.description}`;
Expand All @@ -197,6 +198,6 @@ export abstract class Command {
);

// Combine all the errors and remove any duplicates
return [...new Set(optionsErrors.concat(argsErrors))].sort();
return [...new Set(optionsErrors!.concat(argsErrors) ?? null)].sort();
}
}
3 changes: 2 additions & 1 deletion src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@ export interface IOption {
/** Does this option take in a value? */
// deno-lint-ignore camelcase
takes_value: boolean;
arg_count: number;
/**
* The option's value. Initially set to `false` and set to `true` if provided
* through the command line. Value is the provided value through the command
* line if this option takes in a value.
*/
value?: boolean | string;
value?: boolean | string[];
}
4 changes: 2 additions & 2 deletions tests/integration/file_manager_with_everything/cli_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,14 @@ Deno.test("should show contents with extra log value: read -L file_to_read.txt",
const stdout = await run(
"read -L SomeValue tests/integration/file_manager_with_everything/file_to_read.txt",
);
asserts.assertEquals(stdout, "SomeValue\nYOU SHALL NOT PASS");
asserts.assertEquals(stdout, '[ "SomeValue" ]\nYOU SHALL NOT PASS');
});

Deno.test("should do a dry run and add an extra log value: read -L --dry-run file_to_read.txt", async () => {
const stdout = await run(
"read -L SomeValue --dry-run tests/integration/file_manager_with_everything/file_to_read.txt",
);
asserts.assertEquals(stdout, "SomeValue\nFile exists.");
asserts.assertEquals(stdout, '[ "SomeValue" ]\nFile exists.');
});

Deno.test("should show file does not exist: read -D read", async () => {
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/file_manager_with_options/cli.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as Line from "../../../mod.ts";

const decoder = new TextDecoder();
const encoder = new TextEncoder();
const _encoder = new TextEncoder();

class Main extends Line.MainCommand {
public subcommands = [
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/file_manager_with_options/cli_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,14 @@ Deno.test("should show contents with extra log value: read -L file_to_read.txt",
const stdout = await run(
"read -L SomeValue tests/integration/file_manager_with_options/file_to_read.txt",
);
asserts.assertEquals(stdout, "SomeValue\nYOU SHALL NOT PASS");
asserts.assertEquals(stdout, '[ "SomeValue" ]\nYOU SHALL NOT PASS');
});

Deno.test("should do a dry run and add an extra log value: read -L --dry-run file_to_read.txt", async () => {
const stdout = await run(
"read -L SomeValue --dry-run tests/integration/file_manager_with_options/file_to_read.txt",
);
asserts.assertEquals(stdout, "SomeValue\nFile exists.");
asserts.assertEquals(stdout, '[ "SomeValue" ]\nFile exists.');
});

Deno.test("should show file does not exist: read -D read", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,14 +125,14 @@ Deno.test("should show contents with extra log value: read -L file_to_read.txt",
const stdout = await run(
"read -L SomeValue tests/integration/file_manager_with_too_many_options/file_to_read.txt",
);
asserts.assertEquals(stdout, "SomeValue\nYOU SHALL NOT PASS");
asserts.assertEquals(stdout, '[ "SomeValue" ]\nYOU SHALL NOT PASS');
});

Deno.test("should do a dry run and add an extra log value: read -L --dry-run file_to_read.txt", async () => {
const stdout = await run(
"read -L SomeValue --dry-run tests/integration/file_manager_with_too_many_options/file_to_read.txt",
);
asserts.assertEquals(stdout, "SomeValue\nFile exists.");
asserts.assertEquals(stdout, '[ "SomeValue" ]\nFile exists.');
});

Deno.test("should show file does not exist: read -D read", async () => {
Expand Down
39 changes: 39 additions & 0 deletions tests/integration/n_values_for_options/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as Line from "../../../mod.ts";

class MainCommand extends Line.MainCommand {
public signature = "main";
public description = "The main command.";

public options = {
"-L [log_value], --log-value [log_value]": "Log value",
"--some-option [arg_1] [arg_2]": "some description",
"--some-option-1 [arg_1] [arg_2] [arg_3]": "some description 1",
};

public handle(): void {
const logValue = this.option("-L");
const someOption = this.option("--some-option");
const someOption1 = this.option("--some-option-1");

if (logValue) {
console.log(`logValue: ${logValue}`);
}

if (someOption) {
console.log(`someOption: ${someOption}`);
}

if (someOption1) {
console.log(`someOption1: ${someOption1}`);
}
}
}

const cli = new Line.CLI({
name: "A main command with multiple arguments for options.",
description: "Options with multiple arguments",
version: "v1.0.0",
command: MainCommand,
});

cli.run();
Loading