appcenter-cli
Version:
Command line tool for Visual Studio App Center
162 lines (133 loc) • 5.63 kB
text/typescript
import * as minimist from "minimist";
import * as util from "util";
const debug = require("debug")("appcenter-cli:util:commandline:option-parser");
// Flag arguments
export interface OptionDescription {
shortName?: string; // Short flag for option, single character
longName?: string; // long name for option
required?: boolean; // Is this is a required parameter, if not present defaults to false
defaultValue?: string | string[]; // Default value for this option if it's not present
hasArg?: boolean; // Does this option take an argument?
helpText?: string; // Help text for this parameter
common?: boolean; // Is this option an common option?
}
export interface OptionsDescription {
[field: string]: OptionDescription;
}
function optionKey(option: OptionDescription): string {
return option.shortName || option.longName;
}
function optionDisplayName(options: OptionDescription): string {
const short = options.shortName ? "-" + options.shortName : null;
const long = options.longName ? "--" + options.longName : null;
return [ short, long ].filter((x) => !!x).join(" / ");
}
// Positional arguments
export interface PositionalOptionDescription {
name: string; // Name used in error messages & help
propertyName: string; // Name of property in target object this goes it
position: number; // if null, is the "rest" argument and consumes any leftover positional args
required?: boolean; // Is this a required parameter?
defaultValue?: string; // Default value for this arg if not present
helpText?: string; // Help text for this parameter
}
export type PositionalOptionsDescription = PositionalOptionDescription[];
function descriptionToMinimistOpts(options: OptionsDescription): minimist.Opts {
const parseOpts: minimist.Opts = {
boolean: [] as string[],
string: [] as string[],
alias: {},
default: {},
unknown: (arg: string): boolean => {
if (arg.charAt(0) === "-") {
throw new Error(`Unknown argument ${arg}`);
}
return true;
}
};
Object.keys(options)
.map((key) => options[key])
.forEach((option) => {
const key = optionKey(option);
// Is option a boolean or has a value?
if (option.hasArg) {
(parseOpts.string as string[]).push(key);
} else {
(parseOpts.boolean as string[]).push(key);
}
// If both names are given, set up alias
if (option.shortName && option.longName) {
parseOpts.alias[option.shortName] = option.longName;
}
if (option.defaultValue !== undefined) {
parseOpts.default[key] = option.defaultValue;
}
});
return parseOpts;
}
export function parseOptions(flagOptions: OptionsDescription,
positionalOptions: PositionalOptionsDescription,
target: any, args: string[]): void;
export function parseOptions(flagOptions: OptionsDescription, target: any, args: string[]): void;
export function parseOptions(...params: any[]): void {
let flagOptions: OptionsDescription;
let positionalOptions: PositionalOptionsDescription;
let target: any;
let args: string[];
if (params.length === 4) {
debug(`Parser called with 4 args: ${util.inspect(params)}`);
[flagOptions, positionalOptions, target, args] = params;
} else {
debug(`Parser called with 3 args: ${util.inspect(params)}`);
[flagOptions, target, args] = params;
positionalOptions = [];
}
debug(`Parsing command line ${args.join(" ")}`);
const minimistOptions = descriptionToMinimistOpts(flagOptions);
const parsed = minimist(args, minimistOptions);
debug(`Raw parsed command line = ${util.inspect(parsed)}`);
// handle flag args
Object.keys(flagOptions).forEach((targetPropertyName) => {
const option = flagOptions[targetPropertyName];
const optKey = optionKey(option);
// Skip required args if help or version have been invoked
if (!parsed["help"] && !parsed["version"] && option.required && !parsed[optKey]) {
// TODO: Replace this with auto-prompting
throw new Error(`Missing required option ${optionDisplayName(option)}`);
}
target[targetPropertyName] = parsed[optKey];
});
// Handle positional args
const positionalArgs = parsed["_"] || [];
positionalOptions.sort((a, b) => {
if (a.position === null) { return +1; }
if (b.position === null) { return -1; }
return b.position - a.position;
});
// Check for leftover positional parameters, fail if found
const hasRestOption = positionalOptions.some((o) => o.position === null);
if (!hasRestOption && positionalArgs.length > positionalOptions.length) {
const unknownArgs = positionalArgs.slice(positionalOptions.length);
throw new Error(`Unknown arguments: ${unknownArgs.join(" ")}`);
}
positionalOptions.forEach((opt, index) => {
debug(`Checking for ${opt.required ? "required" : "optional" } option ${opt.name} at position ${opt.position}`);
if (positionalArgs.length - 1 < opt.position) {
if (!parsed["help"] && opt.required) {
throw new Error(`Missing required positional argument ${opt.name}`);
} else if (opt.defaultValue) {
target[opt.propertyName] = opt.defaultValue;
return;
} else {
return;
}
}
if (opt.position !== null) {
target[opt.propertyName] = positionalArgs[opt.position];
positionalArgs[opt.position] = null;
} else {
// This is the rest argument, pick up whatever's left
target[opt.propertyName] = positionalArgs.filter((opt) => opt !== null);
}
});
}