cli-flags: Improve output (see PR screenshots)
MODIFY
packages/@romejs/cli-flags/Parser.ts
packages/@romejs/cli-flags/Parser.ts
@@ -20,16 +20,25 @@ import {toKebabCase, toCamelCase} from '@romejs/string-utils';
import {DiagnosticsError} from '@romejs/diagnostics';
import {createUnknownFilePath} from '@romejs/path';
import {Dict} from '@romejs/typescript-helpers';
-import {escapeMarkup} from '@romejs/string-markup';
+import {escapeMarkup, markup} from '@romejs/string-markup';
+
+export type Examples = Array<{
+ description: string;
+ command: string;
+}>;
type CommandOptions<T extends Dict<unknown>> = {
name: string;
category?: string;
description?: string;
+ usage?: string;
+ examples?: Examples;
defineFlags?: (consumer: Consumer) => T;
callback: (flags: T) => void | Promise<void>;
};
+type AnyCommandOptions = CommandOptions<Dict<unknown>>;
+
type ArgDeclaration = {
definition: ConsumePropertyDefinition;
name: string;
@@ -38,12 +47,12 @@ type ArgDeclaration = {
type DefinedCommand = {
flags: Dict<unknown>;
- command: CommandOptions<Dict<unknown>>;
+ command: AnyCommandOptions;
};
export type ParserOptions<T> = {
- examples?: Array<string>;
- programName?: string;
+ examples?: Examples;
+ programName: string;
usage?: string;
description?: string;
version?: string;
@@ -72,7 +81,7 @@ export default class Parser<T> {
this.consumeRawArgs(rawArgs);
this.commands = new Map();
- this.ranCommand = false;
+ this.ranCommand = undefined;
this.currentCommand = undefined;
if (opts.version !== undefined) {
@@ -116,8 +125,8 @@ export default class Parser<T> {
defaultFlags: Map<string, unknown>;
declaredFlags: Map<string, ArgDeclaration>;
- ranCommand: boolean;
- commands: Map<string, CommandOptions<Dict<unknown>>>;
+ ranCommand: undefined | string;
+ commands: Map<string, AnyCommandOptions>;
args: Array<string>;
currentCommand: undefined | string;
@@ -208,12 +217,8 @@ export default class Parser<T> {
keys: ConsumePath,
target: ConsumeSourceLocationRequestTarget,
) => {
- let prefixParts = [];
-
const {programName} = this.opts;
- if (programName !== undefined) {
- prefixParts.push(programName);
- }
+ const prefixParts = [programName];
if (this.currentCommand !== undefined) {
prefixParts.push(this.currentCommand);
}
@@ -279,12 +284,6 @@ export default class Parser<T> {
}
async init(): Promise<T> {
- // Show help for --help
- if (this.helpMode) {
- await this.showHelp();
- process.exit(1);
- }
-
// Show help for --version
if (this.flags.has('version')) {
this.reporter.logAll(String(this.opts.version));
@@ -342,8 +341,16 @@ export default class Parser<T> {
throw new DiagnosticsError('CLI flag parsing diagnostics', diagnostics);
}
+ // Show help for --help
+ if (this.helpMode) {
+ await this.showHelp(
+ definedCommand === undefined ? undefined : definedCommand.command.name,
+ );
+ process.exit(1);
+ }
+
if (definedCommand !== undefined) {
- this.ranCommand = true;
+ this.ranCommand = definedCommand.command.name;
await definedCommand.command.callback(definedCommand.flags);
}
@@ -420,81 +427,80 @@ export default class Parser<T> {
// Output options
for (const {arg, description} of optionOutput) {
lines.push(
- ` <brightBlack>${rightPad(
- escapeMarkup(arg),
+ markup`<brightBlack>${rightPad(
+ arg,
argColumnLength,
' ',
- )}</brightBlack> ${escapeMarkup(description)}`,
+ )}</brightBlack> ${description}`,
);
}
return lines;
}
- showCommandsHelp(heading: string, rawCommandNames: Array<string>) {
- if (rawCommandNames.length === 0) {
- return undefined;
- }
-
+ showUsageHelp(
+ description?: string,
+ usage: string = '[flags]',
+ prefix?: string,
+ ) {
const {reporter} = this;
- reporter.spacer();
- reporter.logAll(`<emphasis>${heading}</emphasis>`);
- reporter.spacer();
+ const {programName} = this.opts;
- // Build output
- const commandNames: Array<string> = rawCommandNames.sort();
- const cmdOutput: Array<[string, string, Array<string>]> = [];
- let nameColumnLength: number = 0;
- for (const cmd of commandNames) {
- const opts = this.commands.get(cmd);
- if (opts === undefined) {
- throw new Error('Expected command options');
+ reporter.section(`Usage`, () => {
+ if (description !== undefined) {
+ reporter.logAll(description);
+ reporter.forceSpacer();
}
- if (cmd[0] === '_') {
- continue;
+ const commandParts = [programName];
+ if (prefix !== undefined) {
+ commandParts.push(prefix);
}
+ commandParts.push(usage);
- // Set arg col length if we'll be longer
- if (nameColumnLength < cmd.length) {
- nameColumnLength = cmd.length;
+ const command = commandParts.join(' ');
+ reporter.command(command);
+ });
+ }
+
+ showFocusedCommandHelp(commandName: string) {
+ const command = this.commands.get(commandName);
+ if (command === undefined) {
+ throw new Error(`Unknown command ${commandName}`);
}
+ const {reporter} = this;
+ const {name, usage, description, examples} = command;
+
+ reporter.forceSpacer();
+ this.showUsageHelp(description, usage, name);
+ this.showHelpExamples(examples, name);
+
// Find arguments that belong to this command
const argKeys = [];
for (const [key, decl] of this.declaredFlags) {
- if (decl.command === cmd) {
+ if (decl.command === name) {
argKeys.push(key);
}
}
- const desc =
- opts.description === undefined
- ? 'no description available'
- : opts.description;
const optLines = this.buildOptionsHelp(argKeys);
- cmdOutput.push([cmd, desc, optLines]);
- }
-
- for (const [nameCol, descCol, optLines] of cmdOutput) {
- reporter.logAll(
- ` <brightBlack>${rightPad(
- nameCol,
- nameColumnLength,
- ' ',
- )}</brightBlack> ${descCol}`,
- );
-
- reporter.indent();
+ if (optLines.length > 0) {
+ reporter.section('Command Flags', () => {
for (const line of optLines) {
reporter.logAll(line);
}
- reporter.dedent();
+ });
+ }
}
+
+ async showHelp(commandName: undefined | string = this.ranCommand) {
+ if (commandName !== undefined) {
+ this.showFocusedCommandHelp(commandName);
+ return;
}
- async showHelp() {
- const {description, usage, programName, examples} = this.opts;
+ const {description, usage, examples, programName} = this.opts;
const consumer = this.getFlagsConsumer();
await this.opts.defineFlags(consumer);
@@ -503,25 +509,10 @@ export default class Parser<T> {
await this.defineCommandFlags(key, consumer);
}
- const {reporter} = this;
- reporter.indent();
-
- reporter.spacer();
- reporter.logAll(
- `<emphasis>Usage:</emphasis> ${programName} ${
- usage === undefined ? '[flags]' : usage
- }`,
- );
- reporter.spacer();
-
- if (description !== undefined) {
- reporter.logAll(description);
- reporter.spacer();
- }
-
- reporter.logAll('<emphasis>Options</emphasis>');
- reporter.spacer();
+ this.showUsageHelp(description, usage);
+ const {reporter} = this;
+ reporter.section('Global Flags', () => {
// Show options not attached to any commands
const lonerArgKeys = [];
for (const [key, decl] of this.declaredFlags) {
@@ -529,64 +520,99 @@ export default class Parser<T> {
lonerArgKeys.push(key);
}
}
+
for (const line of this.buildOptionsHelp(lonerArgKeys)) {
reporter.logAll(line);
}
+ });
// Sort commands into their appropriate categories for output
- const commandNames = new Set(this.commands.keys());
- const commandsByCategory: Map<string, Array<string>> = new Map();
- for (const name of commandNames) {
- const command = this.commands.get(name);
- if (command === undefined) {
- throw new Error('Expected command');
+ const commandsByCategory: Map<
+ undefined | string,
+ Array<AnyCommandOptions>
+ > = new Map();
+ const categoryNames: Set<string | undefined> = new Set();
+ for (const [name, command] of this.commands) {
+ if (name[0] === '_') {
+ continue;
}
const {category} = command;
- if (category === undefined) {
- continue;
+ let commandsForCategory = commandsByCategory.get(category);
+ if (commandsForCategory === undefined) {
+ commandsForCategory = [];
+ commandsByCategory.set(category, commandsForCategory);
}
-
- let categoryNames = commandsByCategory.get(category);
- if (categoryNames === undefined) {
- categoryNames = [];
- commandsByCategory.set(category, categoryNames);
+ commandsForCategory.push(command);
+ categoryNames.add(category);
}
- categoryNames.push(name);
- commandNames.delete(name);
+ reporter.section('Commands', () => {
+ const sortedCategoryNames: Array<string | undefined> = Array.from(
+ categoryNames,
+ ).sort();
+
+ // Always make sure categoryless commands are displayed first
+ if (sortedCategoryNames.includes(undefined)) {
+ sortedCategoryNames.splice(sortedCategoryNames.indexOf(undefined), 1);
+ sortedCategoryNames.unshift(undefined);
}
- // Display commands by category
- const categoryNames = Array.from(commandsByCategory.keys()).sort();
- for (const category of categoryNames) {
- const commandNames = commandsByCategory.get(category);
- if (commandNames === undefined) {
- throw new Error('Expected command names');
+ for (const category of sortedCategoryNames) {
+ const commands = commandsByCategory.get(category);
+ if (commands === undefined) {
+ throw new Error('Impossible. Should always be populated.');
}
- this.showCommandsHelp(`${category} Commands`, commandNames);
+
+ if (category !== undefined) {
+ reporter.logAll(`<emphasis>${category} Commands</emphasis>`);
}
- // Display rest of the commands
- this.showCommandsHelp(
- commandsByCategory.size > 0 ? 'Other Commands' : 'Commands',
- Array.from(commandNames),
+ reporter.list(
+ commands.map(cmd => {
+ return `<emphasis>${cmd.name}</emphasis> ${
+ cmd.description === undefined ? '' : cmd.description
+ }`;
+ }),
);
-
- // Output examples
- if (examples !== undefined) {
- reporter.spacer();
- reporter.logAll('<emphasis>Examples:</emphasis>');
reporter.spacer();
+ }
+
+ reporter.info('To view help for a specific command run');
+ reporter.command(`${programName} command_name --help`);
+ });
+
+ this.showHelpExamples(examples);
+ }
+
+ showHelpExamples(examples?: Examples, prefix?: string) {
+ const {programName} = this.opts;
+ const {reporter} = this;
+
+ if (examples === undefined) {
+ return;
+ }
- reporter.indent();
- for (const cmd of examples) {
- reporter.command(cmd);
+ reporter.section('Examples', () => {
+ for (const {description, command} of examples) {
+ const commandParts = [];
+ if (programName !== undefined) {
+ commandParts.push(programName);
}
- reporter.dedent();
+ if (prefix !== undefined) {
+ commandParts.push(prefix);
}
+ commandParts.push(command);
+
+ const builtCommand = commandParts.join(' ');
reporter.spacer();
+ if (description !== undefined) {
+ reporter.logAll(description);
+ }
+ reporter.command(builtCommand);
+ }
+ });
}
commandRequired() {
@@ -611,7 +637,7 @@ export default class Parser<T> {
process.exit(1);
}
- addCommand(opts: CommandOptions<Dict<unknown>>) {
+ addCommand(opts: AnyCommandOptions) {
if (this.currentCommand !== undefined) {
throw new Error("Nested commands aren't allowed");
}
@@ -637,7 +663,10 @@ export default class Parser<T> {
this.currentCommand = undefined;
- return {flags, command: opts};
+ return {
+ flags,
+ command: opts,
+ };
}
}
@@ -652,6 +681,10 @@ export class ParserInterface<T> {
return this.parser.init();
}
+ showHelp(): Promise<void> {
+ return this.parser.showHelp();
+ }
+
getArgs(): Array<string> {
return this.parser.args;
}
@@ -660,7 +693,7 @@ export class ParserInterface<T> {
this.parser.commandRequired();
}
- command(opts: CommandOptions<Dict<unknown>>) {
+ command(opts: AnyCommandOptions) {
this.parser.addCommand(opts);
}
}

cli-flags: Improve output (see PR screenshots)