appcenter-cli
Version:
Command line tool for Visual Studio App Center
241 lines (196 loc) • 8.56 kB
text/typescript
// Help system - displays help for categories and commands
import * as _ from "lodash";
import * as os from "os";
import { isatty } from "tty";
import * as chalk from "chalk";
const debug = require("debug")("appcenter-cli:util:commandline:help");
const Table = require("cli-table3");
import {
getClassHelpText, getOptionsDescription, getPositionalOptionsDescription
} from "./option-decorators";
import {
OptionDescription, PositionalOptionDescription
} from "./option-parser";
import { out } from "../interaction";
import { scriptName } from "../misc";
const usageConst = "Usage: ";
export function runHelp(commandPrototype: any, commandObj: any): void {
const commandExample: string = getCommandExample(commandPrototype, commandObj);
const commandHelp: string = getCommandHelp(commandObj);
const optionsHelpTable: any = getOptionsHelpTable(commandPrototype);
const commonSwitchOptionsHelpTable: any = getCommonSwitchOptionsHelpTable(commandPrototype);
out.help();
out.help(commandHelp);
out.help();
out.help(usageConst + chalk.bold(commandExample));
if (optionsHelpTable.length > 0) {
out.help();
out.help("Options:");
out.help(optionsHelpTable.toString());
}
if (commonSwitchOptionsHelpTable.length > 0) {
out.help();
out.help("Common Options (works on all commands):");
out.help(commonSwitchOptionsHelpTable.toString());
}
}
function getCommandHelp(commandObj: any): string {
const helpString = getClassHelpText(commandObj.constructor);
return !!helpString ? helpString : "No help text for command. Dev, fix it!";
}
interface SwitchOptionHelp {
shortName: string;
longName: string;
helpText: string;
argName: string;
}
function toSwitchOptionHelp(option: OptionDescription): SwitchOptionHelp {
return {
shortName: option.shortName ? `-${option.shortName}` : "",
longName: option.longName ? `--${option.longName}` : "",
helpText: option.helpText || "",
argName: option.hasArg ? "<arg>" : ""
};
}
function getOptionsHelpTable(commandPrototype: any): any {
const nonCommonSwitchOpts = getSwitchOptionsHelp(commandPrototype, false);
const posOpts = getPositionalOptionsHelp(commandPrototype);
return getStyledOptionsHelpTable(nonCommonSwitchOpts.concat(posOpts));
}
function getCommonSwitchOptionsHelpTable(commandPrototype: any): any {
const commonSwitchOpts = getSwitchOptionsHelp(commandPrototype, true);
return getStyledOptionsHelpTable(commonSwitchOpts);
}
function getStyledOptionsHelpTable(options: string[][]): any {
const opts = styleOptsTable(options);
// Calculate max length of the strings from the first column (switches/positional parameters) - it will be a width for the first column;
const firstColumnWidth = opts.reduce((contenderMaxWidth, optRow) => Math.max(optRow[0].length, contenderMaxWidth), 0);
// Creating a help table object
const helpTableObject = new Table(out.getOptionsForTwoColumnTableWithNoBorders(firstColumnWidth));
opts.forEach((opt) => helpTableObject.push(opt));
return helpTableObject;
}
function getSwitchOptionsHelp(commandPrototype: any, isCommon: boolean): string[][] {
const switchOptions = getOptionsDescription(commandPrototype);
const filteredSwitchOptions = filterOptionDescriptions(_.values(switchOptions), isCommon);
const options = sortOptionDescriptions(filteredSwitchOptions).map(toSwitchOptionHelp);
debug(`Command has ${options.length} switch options:`);
debug(options.map((o) => `${o.shortName}|${o.longName}`).join("/"));
return options.map((optionHelp) => [` ${switchText(optionHelp)} `, optionHelp.helpText]);
}
interface PositionalOptionHelp {
name: string;
helpText: string;
}
function toPositionalOptionHelp(option: PositionalOptionDescription): PositionalOptionHelp {
return {
name: option.name,
helpText: option.helpText
};
}
function getPositionalOptionsHelp(commandPrototype: any): string[][] {
const options: PositionalOptionHelp[] = getPositionalOptionsDescription(commandPrototype).map(toPositionalOptionHelp);
debug(`Command has ${options.length} positional options:`);
debug(options.map((o) => o.name).join("/"));
return options.map((optionsHelp) => [` ${optionsHelp.name} `, optionsHelp.helpText]);
}
function switchText(switchOption: SwitchOptionHelp): string {
// Desired formats look like:
//
// -x
// -x|--xopt
// --xopt
// -y <arg>
// -y|--yopt <arg>
// --yopt <arg>
const start = switchOption.shortName ? [ switchOption.shortName ] : [ " " ];
const sep = switchOption.shortName && switchOption.longName ? [ "|" ] : [ " " ];
const long = switchOption.longName ? [ switchOption.longName ] : [];
const arg = switchOption.argName ? [ " " + switchOption.argName ] : [];
return start.concat(sep).concat(long).concat(arg).join("");
}
function terminalWidth(): number {
// If stdout is a terminal, return the width
if (isatty(1)) {
return (process.stdout as any).columns;
}
// Otherwise return something useful.
return 80;
}
function getCommandExample(commandPrototype: any, commandObj: any): string {
const commandName = getCommandName(commandObj);
const lines: string[] = [];
const lastLinesLeftMargin = " "; // 2 spaces
const linesSeparator = os.EOL + lastLinesLeftMargin;
let currentLine = `${scriptName} ${commandName}`;
const maxWidth = terminalWidth();
const separatorLength = os.EOL.length;
const firstLineFreeSpace = maxWidth - usageConst.length - separatorLength;
const freeSpace = firstLineFreeSpace - lastLinesLeftMargin.length;
const leftMargin = _.repeat(" ", usageConst.length);
getAllOptionExamples(commandPrototype)
.forEach((example) => {
if (currentLine.length + example.length + 1 > (lines.length ? freeSpace : firstLineFreeSpace)) {
lines.push(currentLine);
currentLine = leftMargin + example;
} else {
currentLine += ` ${example}`;
}
});
lines.push(currentLine);
return lines.join(linesSeparator);
}
function getCommandName(commandObj: any): string {
const commandParts: string[] = commandObj.command;
let script = commandParts[commandParts.length - 1];
const extIndex = script.lastIndexOf(".");
if (extIndex > -1) {
script = script.slice(0, extIndex);
}
commandParts[commandParts.length - 1] = script;
return commandParts.join(" ");
}
function getAllOptionExamples(commandPrototype: any): string[] {
return getSwitchOptionExamples(commandPrototype, false)
.concat(getPositionalOptionExamples(commandPrototype));
}
function getSwitchOptionExamples(commandPrototype: any, includeCommon: boolean = true): string[] {
const switchOptions = getOptionsDescription(commandPrototype);
const switchOptionDescriptions = includeCommon ? _.values(switchOptions) : filterOptionDescriptions(_.values(switchOptions), false);
return sortOptionDescriptions(switchOptionDescriptions)
.map((description: OptionDescription): string => {
const result: string[] = [];
result.push(description.shortName ? `-${description.shortName}` : "");
result.push(description.shortName && description.longName ? "|" : "");
result.push(description.longName ? `--${description.longName}` : "");
result.push(description.hasArg ? " <arg>" : "");
if (!description.required) {
result.unshift("[");
result.push("]");
}
return result.join("");
});
}
function getPositionalOptionExamples(commandPrototype: any): string[] {
const positionalOptions = getPositionalOptionsDescription(commandPrototype);
return _.sortBy(positionalOptions, "position")
.map((description): string => {
if (description.position !== null) {
return `<${description.name}>`;
}
// Output for "rest" parameter. sortBy will push it to the end.
return `<${description.name}...>`;
});
}
function styleOptsTable(table: string[][]): string[][] {
return table.map((row) => [chalk.bold(row[0])].concat(row.slice(1)));
}
function sortOptionDescriptions(options: OptionDescription[]): OptionDescription[] {
return _(options)
.reverse() // options from a top prototype are added first, reversing order
.sortBy([(opt: OptionDescription) => opt.required ? 0 : 1]) // required options should be shown first
.value();
}
function filterOptionDescriptions(options: OptionDescription[], isCommon: boolean): OptionDescription[] {
return isCommon ? options.filter((option) => { return option.common; }) : options.filter((option) => { return !option.common; });
}