Skip to content

Commit

Permalink
Rebrand ICliNode as ICliTree
Browse files Browse the repository at this point in the history
Node is too overloaded for a Node.js CLI framework
  • Loading branch information
carnesen committed Jul 4, 2020
1 parent b222bf7 commit 9d807d4
Show file tree
Hide file tree
Showing 12 changed files with 58 additions and 58 deletions.
6 changes: 3 additions & 3 deletions main/src/cli-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ export type TCliRoot =
| ICliCommand<ICliArgGroup, any, ICliArgGroup>;

/**
* A node in a command tree
* Data structure representing a node in a command tree
* */
export interface ICliNode {
export interface ICliTree {
current: TCliRoot;
parents: ICliBranch[];
}

/**
* A leaf node in a command tree
* Data structure representing a leaf node in a command tree
* */
export interface ICliLeaf {
current: ICliCommand;
Expand Down
10 changes: 5 additions & 5 deletions main/src/cli-usage-error.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ICliNode } from './cli-tree';
import { ICliTree } from './cli-tree';

/** "code" of a {@link CliUsageError} */
export const CLI_USAGE_ERROR = 'CLI_USAGE_ERROR';
Expand All @@ -11,17 +11,17 @@ export class CliUsageError extends Error {
public readonly code: typeof CLI_USAGE_ERROR;

/** Used internally for constructing the command-line usage string */
public node?: ICliNode;
public tree?: ICliTree;

/**
* @param message If provided, [[`runCliAndExit`]] will also print "Error: \<your
* message\>"
* @param node Used internally for constructing the command-line usage string
* @param tree Used internally for constructing the command-line usage string
*/
constructor(message?: string, node?: ICliNode) {
constructor(message?: string, tree?: ICliTree) {
super(message);
this.code = CLI_USAGE_ERROR;
this.node = node;
this.tree = tree;
Object.setPrototypeOf(this, new.target.prototype);
}
}
2 changes: 1 addition & 1 deletion main/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ describe(Cli.name, () => {
commandWithNoArguments.name,
);
expect(exception.code).toBe(CLI_USAGE_ERROR);
expect((exception as CliUsageError).node!.current).toBe(root);
expect((exception as CliUsageError).tree!.current).toBe(root);
expect(exception.message).toBeFalsy();
});

Expand Down
20 changes: 10 additions & 10 deletions main/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { partitionArgs } from './partition-args';
import { findCliNode } from './find-cli-node';
import { findCliTree } from './find-cli-tree';
import { parseArgs } from './parse-args';
import { parseNamedArgs } from './parse-named-args';
import { CliUsageError, CLI_USAGE_ERROR } from './cli-usage-error';
Expand All @@ -26,26 +26,26 @@ export interface ICli {
*/
export function Cli(root: TCliRoot): ICli {
return async function cli(...args: string[]) {
const node = findCliNode(root, args);
const tree = findCliTree(root, args);

if (node.message || node.current.kind !== CLI_COMMAND) {
throw new CliUsageError(node.message, node);
if (tree.message || tree.current.kind !== CLI_COMMAND) {
throw new CliUsageError(tree.message, tree);
}

const { positionalArgs, namedArgs, escapedArgs } = partitionArgs(node.args);
const { positionalArgs, namedArgs, escapedArgs } = partitionArgs(tree.args);

// We found "--help" among the arguments
if (namedArgs.help) {
throw new CliUsageError(undefined, node);
throw new CliUsageError(undefined, tree);
}

const command = node.current;
const command = tree.current;

// Pre-validation for positional argument group
if (!command.positionalArgGroup && positionalArgs.length > 0) {
throw new CliUsageError(
`Unexpected argument "${positionalArgs[0]}" : Command "${command.name}" does not accept positional arguments`,
node,
tree,
);
}

Expand All @@ -59,7 +59,7 @@ export function Cli(root: TCliRoot): ICli {
}

// Calls to `parseArgs` and `action` in this try/catch block could throw
// `CliUsageError`, which we catch and enhance with the current `TCliNode`
// `CliUsageError`, which we catch and enhance with the current `TCliTree`
// context.
try {
let argsValue: any;
Expand Down Expand Up @@ -95,7 +95,7 @@ export function Cli(root: TCliRoot): ICli {
return result;
} catch (exception) {
if (exception && exception.code === CLI_USAGE_ERROR) {
exception.node = node;
exception.tree = tree;
}
throw exception;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { runAndCatch } from '@carnesen/run-and-catch';
import { findCliNode } from './find-cli-node';
import { findCliTree } from './find-cli-tree';
import { CliBranch } from './cli-branch';
import { CliCommand } from './cli-command';

Expand All @@ -15,17 +15,17 @@ const branch = CliBranch({
subcommands: [command],
});

describe(findCliNode.name, () => {
describe(findCliTree.name, () => {
it('finds a command in a tree based on command-line arguments', () => {
const { current, parents, args } = findCliNode(branch, ['echo', 'foo']);
const { current, parents, args } = findCliTree(branch, ['echo', 'foo']);
expect(current).toBe(command);
expect(parents[0]).toBe(branch);
expect(args).toEqual(['foo']);
});

it('throws an error if passed an unexpected kind', async () => {
const exception = await runAndCatch(
findCliNode,
findCliTree,
{ current: {} } as any,
[],
);
Expand Down
16 changes: 8 additions & 8 deletions main/src/find-cli-node.ts → main/src/find-cli-tree.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { ICliNode, TCliRoot } from './cli-tree';
import { ICliTree, TCliRoot } from './cli-tree';
import { CLI_COMMAND } from './cli-command';
import { CLI_BRANCH } from './cli-branch';

/**
* The result of calling [[`findCliNode`]]
* The result of calling [[`findCliTree`]]
*/
export interface IFindCliNodeResult extends ICliNode {
export interface IFindCliTreeResult extends ICliTree {
/** Passed args past those used during navigation */
args: string[];
/** An error message describing why navigation stopped */
Expand All @@ -18,14 +18,14 @@ export interface IFindCliNodeResult extends ICliNode {
* @param args - An array of command-line arguments
* @returns The result of the search
*/
export function findCliNode(
export function findCliTree(
root: TCliRoot,
args: string[],
): IFindCliNodeResult {
return recursiveFindCliNode({ current: root, parents: [], args });
): IFindCliTreeResult {
return recursiveFindCliTree({ current: root, parents: [], args });
}

function recursiveFindCliNode(result: IFindCliNodeResult): IFindCliNodeResult {
function recursiveFindCliTree(result: IFindCliTreeResult): IFindCliTreeResult {
// Terminate recursion if current is a command
if (result.current.kind === CLI_COMMAND) {
return result;
Expand All @@ -52,7 +52,7 @@ function recursiveFindCliNode(result: IFindCliNodeResult): IFindCliNodeResult {
return { ...result, message: `Bad command "${result.args[0]}"` };
}

return recursiveFindCliNode({
return recursiveFindCliTree({
parents: [...result.parents, result.current],
current: next,
args: result.args.slice(1),
Expand Down
2 changes: 1 addition & 1 deletion main/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,4 @@ export { CliUsageError, CLI_USAGE_ERROR } from './cli-usage-error';
export { ICliArgGroup, TCliArgGroupArgs } from './cli-arg-group';

// Advanced: Command tree
export { TCliRoot, ICliNode, ICliLeaf } from './cli-tree';
export { TCliRoot, ICliTree, ICliLeaf } from './cli-tree';
6 changes: 3 additions & 3 deletions main/src/run-cli-and-exit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,15 @@ export async function runCliAndExit(
);
} else if (exception.code === CLI_USAGE_ERROR) {
const exceptionAsUsageError: CliUsageError = exception;
if (exceptionAsUsageError.node) {
const usageString = UsageString(exception.node, columns, ' ');
if (exceptionAsUsageError.tree) {
const usageString = UsageString(exception.tree, columns, ' ');
if (exception.message) {
consoleError(`${usageString}\n\n${RED_ERROR} ${exception.message}`);
} else {
consoleError(usageString);
}
} else {
// Handle case where "code" is CLI_USAGE_ERROR but "node" is undefined. Surely
// Handle case where "code" is CLI_USAGE_ERROR but "tree" is undefined. Surely
// this is a coding mistake on our part.
consoleError(exceptionAsUsageError);
}
Expand Down
6 changes: 3 additions & 3 deletions main/src/usage-string.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { UsageString } from './usage-string';
import { CliBranch } from './cli-branch';
import { CliStringArgGroup } from './arg-group-factories/cli-string-arg-group';
import { CliCommand } from './cli-command';
import { ICliNode } from './cli-tree';
import { ICliTree } from './cli-tree';

const messageArgGroup = CliStringArgGroup({
description: 'A string message please',
Expand Down Expand Up @@ -42,7 +42,7 @@ describe(UsageString.name, () => {

it('Creates a usage string for a command without a parent', () => {
const usageString = UsageString({
current: current as ICliNode['current'],
current: current as ICliTree['current'],
parents: [],
});
expect(usageString).toMatch(messageArgGroup.description!);
Expand All @@ -51,7 +51,7 @@ describe(UsageString.name, () => {

it('Creates a usage string for a command without a parent branch', () => {
const usageString = UsageString({
current: current as ICliNode['current'],
current: current as ICliTree['current'],
parents: [branch],
});
expect(usageString).toMatchSnapshot();
Expand Down
6 changes: 3 additions & 3 deletions main/src/usage-string.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { ICliNode } from './cli-tree';
import { ICliTree } from './cli-tree';
import { CLI_BRANCH } from './cli-branch';
import { UsageForBranch } from './usage-for-branch';
import { CLI_COMMAND } from './cli-command';
import { UsageForCommand } from './usage-for-command';

export function UsageString(
node: ICliNode,
tree: ICliTree,
maxLineLength = +Infinity,
indentation = '',
): string {
const { current, parents } = node;
const { current, parents } = tree;
let lines: string[] = [];
if (current.kind === CLI_BRANCH) {
lines = UsageForBranch(current, parents, maxLineLength, indentation);
Expand Down
8 changes: 4 additions & 4 deletions main/src/usage-subcommand-rows.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ICliNode } from './cli-tree';
import { ICliTree } from './cli-tree';
import { CLI_COMMAND } from './cli-command';
import { ICliBranch, CLI_BRANCH } from './cli-branch';
import { TTwoColumnTableRow } from './two-column-table';
Expand All @@ -10,12 +10,12 @@ export function UsageSubcommandRows(branch: ICliBranch): TTwoColumnTableRow[] {
}

function RecursiveUsageSubcommandRows(
current: ICliNode['current'],
current: ICliTree['current'],
path: string,
): TTwoColumnTableRow[] {
if (current.hidden && path.length > 0) {
// We've walked to a hidden node. When path.length === 0 the user has
// specifically invoked a hidden node in which case we still want to show
// We've walked to a hidden tree. When path.length === 0 the user has
// specifically invoked a hidden tree in which case we still want to show
// them the usage.
return [];
}
Expand Down
26 changes: 13 additions & 13 deletions website/src/autocomplete.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CLI_COMMAND, TCliRoot, CLI_BRANCH, ICliArgGroup } from '@carnesen/cli';
import { findCliNode } from '@carnesen/cli/lib/find-cli-node';
import { findCliTree } from '@carnesen/cli/lib/find-cli-tree';

import { LongestLeadingSubstring } from './longest-leading-substring';

Expand All @@ -18,24 +18,24 @@ export function autocomplete(
if (args.includes('--help')) {
return [];
}
const node = findCliNode(root, args);
switch (node.current.kind) {
const tree = findCliTree(root, args);
switch (tree.current.kind) {
case CLI_BRANCH: {
if (node.args.length > 0) {
// findCliNode stopped at a branch node with args still remaining.
if (tree.args.length > 0) {
// findCliTree stopped at a branch with args still remaining.
// There's no way for us to autocomplete from that state.
return [];
}

const subcommandNames = node.current.subcommands.map(({ name }) => name);
const subcommandNames = tree.current.subcommands.map(({ name }) => name);
return autocompleteFromWordList([...subcommandNames], search);
}

case CLI_COMMAND: {
// E.g. The command line was "cloud users list --all --v"

// E.g. The command invoked e.g. "listCommand"
const command = node.current;
const command = tree.current;

// E.g. ["--all", "--verbose", "--version", "--help"]
const namedArgGroupSeparators = [
Expand All @@ -46,19 +46,19 @@ export function autocomplete(
];

// The argument _before_ the search term
const previousArg = node.args.slice(-1)[0];
const previousArg = tree.args.slice(-1)[0];

// This is perhaps an obscure sub-case to start with, but if the previous
// arg is "--", we can be sure we are currently searching at the start of
// the "escaped" argument group e.g. "cloud users list -- "
if (previousArg === '--') {
// We are in the escaped arg group
return autocompleteArgGroup(node.current.escapedArgGroup, [], search);
return autocompleteArgGroup(tree.current.escapedArgGroup, [], search);
}

// Otherwise if there's a "--" but we're not at the start of the argument
// group, just give up e.g. "cloud users list -- chris "
if (node.args.includes('--')) {
if (tree.args.includes('--')) {
return [];
}
// Now we know we are NOT in the escaped args group
Expand All @@ -79,7 +79,7 @@ export function autocomplete(
}

// We are AT a command e.g. "cloud users list "
if (node.args.length === 0) {
if (tree.args.length === 0) {
const { positionalArgGroup } = command;
const completions = autocompleteArgGroup(
positionalArgGroup,
Expand All @@ -104,12 +104,12 @@ export function autocomplete(

// E.g. "cloud users list --email chr"
if (previousArg.startsWith('--')) {
if (!node.current.namedArgGroups) {
if (!tree.current.namedArgGroups) {
return [];
}
// OK if undefined
const argGroup: ICliArgGroup | undefined =
node.current.namedArgGroups[previousArg.slice(2)];
tree.current.namedArgGroups[previousArg.slice(2)];
return autocompleteArgGroup(argGroup, [], search);
}

Expand Down

0 comments on commit 9d807d4

Please sign in to comment.