/* tslint:disable:forin cli only run in node */
const MaxColumns = 100;
const argRegex = /^(-+)?(.+)$/;
export type FlagType = "boolean" | "string" | "number";
export interface CommandFlag {
description: string;
argument?: string;
type?: FlagType;
aliases?: string[];
possibleValues?: string[];
deprecated?: boolean;
}
export interface Command {
name: string;
help: string;
onlineHelp?: boolean;
priority?: number;
advanced?: boolean;
argString?: string;
flags?: { [index: string]: CommandFlag };
aliases?: string[];
numArgs?: number;
anyArgs?: boolean; // pass all arguments as is
/* @internal */
_aliasMap?: { [index: string]: string };
/* @internal */
_callback?: (c?: ParsedCommand) => Promise;
}
export interface ParsedCommand {
name: string;
args: string[];
flags: { [index: string]: boolean | string | number };
}
export class CommandParser {
private commands: Command[] = [];
public defineCommand(c: Command, callback: (c?: ParsedCommand) => Promise) {
const aliasMap: { [index: string]: string } = {};
for (const flag in c.flags) {
const def = c.flags[flag];
recordAlias(flag, flag);
const aliases = c.flags[flag].aliases;
if (aliases) {
aliases.forEach(alias => {
recordAlias(flag, alias);
});
}
}
c._aliasMap = aliasMap;
c._callback = callback;
this.commands.push(c);
function recordAlias(flag: string, alias: string) {
if (aliasMap[alias]) {
throw new Error(`Alias ${alias} for flag ${flag} duplicates the alias for flag ${aliasMap[alias]}`);
}
aliasMap[alias] = flag;
}
}
public parseCommand(args: string[]): Promise {
if (!args[0])
args = ["help"];
const name = args[0];
const parsedArgs: string[] = [];
const flags: { [index: string]: string | boolean | number } = {};
const filtered = this.commands.filter(c => c.name === name || c.aliases && c.aliases.indexOf(name) !== -1);
if (!filtered.length)
pxt.U.userError(`Command '${name}' not found, use "pxt help all" to see available commands.`);
const command = filtered[0];
if (command.anyArgs)
return command._callback({
name: command.name,
args: args.slice(1),
flags
});
let currentFlag: string;
let currentFlagDef: CommandFlag;
for (let i = 1; i < args.length; i++) {
const match = argRegex.exec(args[i]);
if (!match) {
continue;
}
if (match[1]) {
if (currentFlag)
pxt.U.userError(`Expected value to follow flag '${currentFlag}'`);
const flagName = command._aliasMap[match[2]];
const debugFlag = flagName || match[2];
if (debugFlag == "debug" || debugFlag == "d" || debugFlag == "dbg") {
pxt.options.debug = true;
pxt.debug = console.log;
pxt.log(`debug mode`);
if (!flagName)
continue;
}
if (!flagName)
pxt.U.userError(`Unrecognized flag '${match[2]}' for command '${command.name}'`)
const flagDefinition = command.flags[flagName];
if (flagDefinition.argument) {
currentFlag = flagName;
currentFlagDef = flagDefinition;
}
else {
flags[flagName] = true;
}
}
else if (currentFlag) {
if (currentFlagDef.possibleValues && currentFlagDef.possibleValues.length && currentFlagDef.possibleValues.indexOf(match[2]) === -1) {
pxt.U.userError(`Unknown value for flag '${currentFlag}', '${match[2]}'`);
}
if (!currentFlagDef.type || currentFlagDef.type === "string") {
flags[currentFlag] = match[2];
}
else if (currentFlagDef.type === "boolean") {
flags[currentFlag] = match[2].toLowerCase() === "true";
}
else {
try {
flags[currentFlag] = parseFloat(match[2])
}
catch (e) {
throw new Error(`Flag '${currentFlag}' expected an argument of type number but received '${match[2]}'`)
}
}
currentFlag = undefined;
currentFlagDef = undefined;
}
else {
parsedArgs.push(match[2]);
}
}
if (currentFlag) {
pxt.U.userError(`Expected value to follow flag '${currentFlag}'`)
}
else if (!command.argString && parsedArgs.length) {
pxt.U.userError(`Command '${command.name}' expected exactly 0 argument(s) but received ${parsedArgs.length}`);
}
else if (command.numArgs && parsedArgs.length !== command.numArgs) {
pxt.U.userError(`Command '${command.name}' expected exactly ${command.numArgs} argument(s) but received ${parsedArgs.length}`);
}
return command._callback({
name: command.name,
args: parsedArgs,
flags
});
}
public printHelp(args: string[], print: (s: string) => void) {
if (args && args.length === 1) {
const name = args[0];
if (name === "all") {
this.printTopLevelHelp(true, print);
}
else {
const filtered = this.commands.filter(c => c.name === name || c.aliases && c.aliases.indexOf(name) !== -1);
if (filtered) {
this.printCommandHelp(filtered[0], print);
}
}
}
else {
this.printTopLevelHelp(false, print);
}
}
private printCommandHelp(c: Command, print: (s: string) => void) {
let usage = ` pxt ${c.name}`
if (c.argString) {
usage += " " + c.argString;
}
if (c.flags) {
for (const flag in c.flags) {
const def = c.flags[flag];
if (def.possibleValues && def.possibleValues.length) {
usage += ` [${dash(flag)} ${def.possibleValues.join("|")}]`
}
else if (def.argument) {
usage += ` [${dash(flag)} ${def.argument}]`
}
else {
usage += ` [${dash(flag)}]`;
}
}
}
print("");
print("Usage:")
print(usage);
print("")
print(c.help);
if (c.aliases && c.aliases.length) {
print("");
print("Aliases:")
c.aliases.forEach(a => print(" " + a));
}
const flagNames: string[] = [];
const flagDescriptions: string[] = [];
let maxWidth = 0;
for (const flag in c.flags) {
const def = c.flags[flag];
if (def.deprecated) continue;
let usage = dash(flag);
if (def.aliases && def.aliases.length) {
usage += " " + def.aliases.map(dash).join(" ");
}
if (def.argument) {
if (def.possibleValues && def.possibleValues.length) {
usage += ` <${def.possibleValues.join("|")}>`;
}
else {
usage += def.type && def.type === "number" ? " " : " "
}
}
maxWidth = Math.max(maxWidth, usage.length);
flagNames.push(usage);
flagDescriptions.push(def.description);
}
if (flagNames.length) {
print("");
print("Flags:")
for (let i = 0; i < flagNames.length; i++) {
printLine(flagNames[i], maxWidth, flagDescriptions[i], print);
}
}
if (c.onlineHelp)
print(`More information at ${"https://makecode.com/cli/" + c.name} .`);
}
private printTopLevelHelp(advanced: boolean, print: (s: string) => void) {
print("");
print("Usage: pxt ");
print("");
print("Commands:")
this.commands.sort((a, b) => a.priority - b.priority);
const toPrint = advanced ? this.commands : this.commands.filter(c => !c.advanced);
const cmdDescriptions: string[] = [];
let maxNameWidth = 0;
const names: string[] = toPrint.map(command => {
maxNameWidth = Math.max(maxNameWidth, command.name.length);
cmdDescriptions.push(command.help);
return command.name;
});
for (let i = 0; i < names.length; i++) {
printLine(names[i], maxNameWidth, cmdDescriptions[i], print);
}
print("");
print("For more information on a command, try 'pxt help '")
}
}
function printLine(name: string, maxNameWidth: number, description: string, print: (s: string) => void) {
// Lines are of the format: name ...... description
let line = pad(` ${name} `, maxNameWidth - name.length + 3, false, ".");
const prefixLength = line.length;
// Split the description into words so that we can try and do some naive wrapping
const dWords = description.split(" ");
dWords.forEach(w => {
if (line.length + w.length < MaxColumns) {
line += " " + w
}
else {
print(line);
line = pad(w, prefixLength + 1, true);
}
});
print(line);
}
function pad(str: string, len: number, left: boolean, char = " ") {
for (let i = 0; i < len; i++) {
if (left) {
str = char + str;
}
else {
str += char;
}
}
return str;
}
function dash(flag: string) {
if (flag.length === 1) {
return "-" + flag;
}
return "--" + flag;
}