123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269 |
- import CAC from "./CAC.ts";
- import Option, { OptionConfig } from "./Option.ts";
- import { removeBrackets, findAllBrackets, findLongest, padRight, CACError } from "./utils.ts";
- import { platformInfo } from "./deno.ts";
- interface CommandArg {
- required: boolean;
- value: string;
- variadic: boolean;
- }
- interface HelpSection {
- title?: string;
- body: string;
- }
- interface CommandConfig {
- allowUnknownOptions?: boolean;
- ignoreOptionDefaultValue?: boolean;
- }
- type HelpCallback = (sections: HelpSection[]) => void | HelpSection[];
- type CommandExample = ((bin: string) => string) | string;
- class Command {
- options: Option[];
- aliasNames: string[];
- /* Parsed command name */
- name: string;
- args: CommandArg[];
- commandAction?: (...args: any[]) => any;
- usageText?: string;
- versionNumber?: string;
- examples: CommandExample[];
- helpCallback?: HelpCallback;
- globalCommand?: GlobalCommand;
- constructor(public rawName: string, public description: string, public config: CommandConfig = {}, public cli: CAC) {
- this.options = [];
- this.aliasNames = [];
- this.name = removeBrackets(rawName);
- this.args = findAllBrackets(rawName);
- this.examples = [];
- }
- usage(text: string) {
- this.usageText = text;
- return this;
- }
- allowUnknownOptions() {
- this.config.allowUnknownOptions = true;
- return this;
- }
- ignoreOptionDefaultValue() {
- this.config.ignoreOptionDefaultValue = true;
- return this;
- }
- version(version: string, customFlags = '-v, --version') {
- this.versionNumber = version;
- this.option(customFlags, 'Display version number');
- return this;
- }
- example(example: CommandExample) {
- this.examples.push(example);
- return this;
- }
- /**
- * Add a option for this command
- * @param rawName Raw option name(s)
- * @param description Option description
- * @param config Option config
- */
- option(rawName: string, description: string, config?: OptionConfig) {
- const option = new Option(rawName, description, config);
- this.options.push(option);
- return this;
- }
- alias(name: string) {
- this.aliasNames.push(name);
- return this;
- }
- action(callback: (...args: any[]) => any) {
- this.commandAction = callback;
- return this;
- }
- /**
- * Check if a command name is matched by this command
- * @param name Command name
- */
- isMatched(name: string) {
- return this.name === name || this.aliasNames.includes(name);
- }
- get isDefaultCommand() {
- return this.name === '' || this.aliasNames.includes('!');
- }
- get isGlobalCommand(): boolean {
- return this instanceof GlobalCommand;
- }
- /**
- * Check if an option is registered in this command
- * @param name Option name
- */
- hasOption(name: string) {
- name = name.split('.')[0];
- return this.options.find(option => {
- return option.names.includes(name);
- });
- }
- outputHelp() {
- const {
- name,
- commands
- } = this.cli;
- const {
- versionNumber,
- options: globalOptions,
- helpCallback
- } = this.cli.globalCommand;
- let sections: HelpSection[] = [{
- body: `${name}${versionNumber ? `/${versionNumber}` : ''}`
- }];
- sections.push({
- title: 'Usage',
- body: ` $ ${name} ${this.usageText || this.rawName}`
- });
- const showCommands = (this.isGlobalCommand || this.isDefaultCommand) && commands.length > 0;
- if (showCommands) {
- const longestCommandName = findLongest(commands.map(command => command.rawName));
- sections.push({
- title: 'Commands',
- body: commands.map(command => {
- return ` ${padRight(command.rawName, longestCommandName.length)} ${command.description}`;
- }).join('\n')
- });
- sections.push({
- title: `For more info, run any command with the \`--help\` flag`,
- body: commands.map(command => ` $ ${name}${command.name === '' ? '' : ` ${command.name}`} --help`).join('\n')
- });
- }
- let options = this.isGlobalCommand ? globalOptions : [...this.options, ...(globalOptions || [])];
- if (!this.isGlobalCommand && !this.isDefaultCommand) {
- options = options.filter(option => option.name !== 'version');
- }
- if (options.length > 0) {
- const longestOptionName = findLongest(options.map(option => option.rawName));
- sections.push({
- title: 'Options',
- body: options.map(option => {
- return ` ${padRight(option.rawName, longestOptionName.length)} ${option.description} ${option.config.default === undefined ? '' : `(default: ${option.config.default})`}`;
- }).join('\n')
- });
- }
- if (this.examples.length > 0) {
- sections.push({
- title: 'Examples',
- body: this.examples.map(example => {
- if (typeof example === 'function') {
- return example(name);
- }
- return example;
- }).join('\n')
- });
- }
- if (helpCallback) {
- sections = helpCallback(sections) || sections;
- }
- console.log(sections.map(section => {
- return section.title ? `${section.title}:\n${section.body}` : section.body;
- }).join('\n\n'));
- }
- outputVersion() {
- const {
- name
- } = this.cli;
- const {
- versionNumber
- } = this.cli.globalCommand;
- if (versionNumber) {
- console.log(`${name}/${versionNumber} ${platformInfo}`);
- }
- }
- checkRequiredArgs() {
- const minimalArgsCount = this.args.filter(arg => arg.required).length;
- if (this.cli.args.length < minimalArgsCount) {
- throw new CACError(`missing required args for command \`${this.rawName}\``);
- }
- }
- /**
- * Check if the parsed options contain any unknown options
- *
- * Exit and output error when true
- */
- checkUnknownOptions() {
- const {
- options,
- globalCommand
- } = this.cli;
- if (!this.config.allowUnknownOptions) {
- for (const name of Object.keys(options)) {
- if (name !== '--' && !this.hasOption(name) && !globalCommand.hasOption(name)) {
- throw new CACError(`Unknown option \`${name.length > 1 ? `--${name}` : `-${name}`}\``);
- }
- }
- }
- }
- /**
- * Check if the required string-type options exist
- */
- checkOptionValue() {
- const {
- options: parsedOptions,
- globalCommand
- } = this.cli;
- const options = [...globalCommand.options, ...this.options];
- for (const option of options) {
- const value = parsedOptions[option.name.split('.')[0]]; // Check required option value
- if (option.required) {
- const hasNegated = options.some(o => o.negated && o.names.includes(option.name));
- if (value === true || value === false && !hasNegated) {
- throw new CACError(`option \`${option.rawName}\` value is missing`);
- }
- }
- }
- }
- }
- class GlobalCommand extends Command {
- constructor(cli: CAC) {
- super('@@global@@', '', {}, cli);
- }
- }
- export type { HelpCallback, CommandExample, CommandConfig };
- export { GlobalCommand };
- export default Command;
|