swc/crates/swc_bundler/tests/.cache/deno/6f7edb35811a9cf784c67fdeffd2497d8bd4377e.ts
2021-11-09 20:42:49 +09:00

1501 lines
44 KiB
TypeScript

// Loaded from https://deno.land/x/cliffy@v0.12.1/packages/command/lib/command.ts
const { stdout, stderr } = Deno;
import { encode } from 'https://deno.land/std@0.63.0/encoding/utf8.ts';
import { dim, red } from 'https://deno.land/std@0.63.0/fmt/colors.ts';
import { parseFlags } from '../../flags/lib/flags.ts';
import { IFlagArgument, IFlagOptions, IFlagsResult, IFlagValue, IFlagValueHandler, IFlagValueType, ITypeHandler } from '../../flags/lib/types.ts';
import { fill } from '../../flags/lib/utils.ts';
import format from '../../x/format.ts';
import { BooleanType } from '../types/boolean.ts';
import { NumberType } from '../types/number.ts';
import { StringType } from '../types/string.ts';
import { Type } from '../types/type.ts';
import { ArgumentsParser } from './arguments-parser.ts';
import { HelpGenerator } from './help-generator.ts';
import { IAction, IArgumentDetails, ICommandOption, ICompleteHandler, ICompleteOptions, ICompleteSettings, IDescription, IEnvVariable, IEnvVarOption, IExample, IOption, IParseResult, ITypeMap, ITypeOption, ITypeSettings } from './types.ts';
const permissions: any = ( Deno as any ).permissions;
const envPermissionStatus: any = permissions && permissions.query && await permissions.query( { name: 'env' } );
const hasEnvPermissions: boolean = !!envPermissionStatus && envPermissionStatus.state === 'granted';
interface IDefaultOption<O = any, A extends Array<any> = any> {
flags: string;
desc?: string
opts?: ICommandOption<O, A>;
}
/**
* Base command implementation without pre configured command's and option's.
*/
export class Command<O = any, A extends Array<any> = any> {
private types: ITypeMap = new Map<string, ITypeSettings>( [
[ 'string', { name: 'string', handler: new StringType() } ],
[ 'number', { name: 'number', handler: new NumberType() } ],
[ 'boolean', { name: 'boolean', handler: new BooleanType() } ]
] );
private rawArgs: string[] = [];
private literalArgs: string[] = [];
// @TODO: get script name: https://github.com/denoland/deno/pull/5034
// private name: string = location.pathname.split( '/' ).pop() as string;
private _name: string = 'COMMAND';
private _parent?: Command;
private _globalParent?: Command;
private ver: string = '0.0.0';
private desc: IDescription = '';
private fn?: IAction<O, A>;
private options: IOption<O, A>[] = [];
private commands: Map<string, Command> = new Map();
private examples: IExample[] = [];
private envVars: IEnvVariable[] = [];
private aliases: string[] = [];
private completions: Map<string, ICompleteSettings> = new Map();
private cmd: Command = this;
private argsDefinition?: string;
private isExecutable: boolean = false;
private throwOnError: boolean = false;
private _allowEmpty: boolean = true;
private _stopEarly: boolean = false;
private defaultCommand?: string;
private _useRawArgs: boolean = false;
private args: IArgumentDetails[] = [];
private isHidden: boolean = false;
private isGlobal: boolean = false;
private hasDefaults: Boolean = false;
private _versionOption?: IDefaultOption<O, A> | false;
private _helpOption?: IDefaultOption<O, A> | false;
public versionOption( flags: string | false, desc?: string, opts?: IAction<O, A> | ICommandOption<O, A> ): this {
this._versionOption = flags === false ? flags : {
flags,
desc,
opts: typeof opts === 'function' ? { action: opts } : opts
};
return this;
}
public helpOption( flags: string | false, desc?: string, opts?: IAction<O, A> | ICommandOption<O, A> ): this {
this._helpOption = flags === false ? flags : {
flags,
desc,
opts: typeof opts === 'function' ? { action: opts } : opts
};
return this;
}
/**
* Add new sub-command.
*/
public command( nameAndArguments: string, cmd?: Command | string, override?: boolean ): this {
let executableDescription: string | undefined;
if ( typeof cmd === 'string' ) {
executableDescription = cmd;
cmd = undefined;
}
const result = ArgumentsParser.splitArguments( nameAndArguments );
const name: string | undefined = result.args.shift();
const aliases: string[] = result.args;
if ( !name ) {
throw this.error( new Error( 'Missing command name.' ) );
}
if ( this.getBaseCommand( name, true ) ) {
if ( !override ) {
throw this.error( new Error( `Duplicate command: ${ name }` ) );
}
this.removeCommand( name );
}
const subCommand = ( cmd || new Command() ).reset();
subCommand._name = name;
subCommand._parent = this;
if ( executableDescription ) {
subCommand.isExecutable = true;
subCommand.description( executableDescription );
}
if ( result.typeDefinition ) {
subCommand.arguments( result.typeDefinition );
}
// if ( name === '*' && !subCommand.isExecutable ) {
// subCommand.isExecutable = true;
// }
aliases.forEach( alias => subCommand.aliases.push( alias ) );
this.commands.set( name, subCommand );
this.select( name );
return this;
}
// public static async exists( name: string ) {
//
// const proc = Deno.run( {
// cmd: [ 'sh', '-c', 'compgen -c' ],
// stdout: 'piped',
// stderr: 'piped'
// } );
// const output: Uint8Array = await proc.output();
// const commands = new TextDecoder().decode( output )
// .trim()
// .split( '\n' );
//
// return commands.indexOf( name ) !== -1;
// }
/**
* Add new command alias.
*
* @param alias Alias name
*/
public alias( alias: string ): this {
if ( this.cmd === this ) {
throw this.error( new Error( `Failed to add alias '${ alias }'. No sub command selected.` ) );
}
if ( this.cmd.aliases.indexOf( alias ) !== -1 ) {
throw this.error( new Error( `Duplicate alias: ${ alias }` ) );
}
this.cmd.aliases.push( alias );
return this;
}
/**
* Reset internal command reference to main command.
*/
public reset(): this {
return this.cmd = this;
}
/**
* Reset internal command reference to child command with given name.
*
* @param name Sub-command name.
*/
public select( name: string ): this {
const cmd = this.getBaseCommand( name, true );
if ( !cmd ) {
throw this.error( new Error( `Sub-command not found: ${ name }` ) );
}
this.cmd = cmd;
return this;
}
/********************************************************************************
**** SUB HANDLER ***************************************************************
********************************************************************************/
/**
* Get or set command name.
*/
public name( name: string ): this {
this.cmd._name = name;
return this;
}
/**
* Set command version.
*
* @param version Semantic version string.
*/
public version( version: string ): this {
this.cmd.ver = version;
return this;
}
/**
* Set command description.
*
* @param description Short command description.
*/
public description( description: IDescription ): this {
this.cmd.desc = description;
return this;
}
/**
* Hide command from help, completions, etc.
*/
public hidden(): this {
this.cmd.isHidden = true;
return this;
}
/**
* Make command globally available.
*/
public global(): this {
this.cmd.isGlobal = true;
return this;
}
/**
* Set command arguments.
*
* @param args Define required and optional commands like: <requiredArg:string> [optionalArg: number] [...restArgs:string]
*/
public arguments( args: string ): this {
this.cmd.argsDefinition = args;
return this;
}
/**
* Set command handler.
*
* @param fn Callback method.
*/
public action( fn: IAction<O, A> ): this {
this.cmd.fn = fn;
return this;
}
/**
* Don't throw an error if the command was called without arguments.
*
* @param allowEmpty
*/
public allowEmpty( allowEmpty: boolean = true ): this {
this.cmd._allowEmpty = allowEmpty;
return this;
}
/**
* If enabled, all arguments starting from the first non option argument will be interpreted as raw argument.
*
* For example:
* `command --debug-level warning server --port 80`
*
* Will result in:
* - options: `{debugLevel: 'warning'}`
* - args: `['server', '--port', '80']`
*
* @param stopEarly
*/
public stopEarly( stopEarly: boolean = true ): this {
this.cmd._stopEarly = stopEarly;
return this;
}
/**
* Disable parsing arguments. If enabled the raw arguments will be passed to the action handler.
* This has no effect for parent or child commands. Only for the command on which this method was called.
*/
public useRawArgs( useRawArgs: boolean = true ): this {
this.cmd._useRawArgs = useRawArgs;
return this;
}
/**
* Set default command. The default command will be called if no action handler is registered.
*/
public default( name: string ): this {
this.cmd.defaultCommand = name;
return this;
}
/**
* Register command specific custom type.
*/
public type( name: string, handler: Type<any> | ITypeHandler<any>, options?: ITypeOption ): this {
if ( this.cmd.types.get( name ) && !options?.override ) {
throw this.error( new Error( `Type '${ name }' already exists.` ) );
}
this.cmd.types.set( name, { ...options, name, handler } );
if ( handler instanceof Type && typeof handler.complete !== 'undefined' ) {
this.complete( name, ( cmd: Command, parent?: Command ) => handler.complete?.( cmd, parent ) || [], options );
}
return this;
}
/**
* Register command specific custom type.
*/
public complete( name: string, complete: ICompleteHandler, options?: ICompleteOptions ): this {
if ( this.cmd.completions.has( name ) && !options?.override ) {
throw this.error( new Error( `Completion '${ name }' already exists.` ) );
}
this.cmd.completions.set( name, {
name,
complete,
...options
} );
return this;
}
/**
* Throw error's instead of calling `Deno.exit()` to handle error's manually.
* This has no effect for parent commands. Only for the command on which this method was called and all child commands.
*/
public throwErrors(): this {
this.cmd.throwOnError = true;
return this;
}
protected shouldThrowErrors(): boolean {
return this.cmd.throwOnError || !!this.cmd._parent?.shouldThrowErrors();
}
public getCompletions() {
return this.getGlobalCompletions().concat( this.getBaseCompletions() );
}
public getBaseCompletions(): ICompleteSettings[] {
return Array.from( this.completions.values() );
}
public getGlobalCompletions(): ICompleteSettings[] {
const getCompletions = ( cmd: Command | undefined, completions: ICompleteSettings[] = [], names: string[] = [] ): ICompleteSettings[] => {
if ( cmd ) {
if ( cmd.completions.size ) {
cmd.completions.forEach( ( completion: ICompleteSettings ) => {
if (
completion.global &&
!this.completions.has( completion.name ) &&
names.indexOf( completion.name ) === -1
) {
names.push( completion.name );
completions.push( completion );
}
} );
}
return getCompletions( cmd._parent, completions, names );
}
return completions;
};
return getCompletions( this._parent );
}
public getCompletion( name: string ) {
return this.getBaseCompletion( name ) ?? this.getGlobalCompletion( name );
}
public getBaseCompletion( name: string ): ICompleteSettings | undefined {
return this.completions.get( name );
}
public getGlobalCompletion( name: string ): ICompleteSettings | undefined {
if ( !this._parent ) {
return;
}
let completion: ICompleteSettings | undefined = this._parent.getBaseCompletion( name );
if ( !completion?.global ) {
return this._parent.getGlobalCompletion( name );
}
return completion;
}
/**
* Add new option (flag).
*
* @param flags Flags string like: -h, --help, --manual <requiredArg:string> [optionalArg: number] [...restArgs:string]
* @param desc Flag description.
* @param opts Flag options or custom handler for processing flag value.
*/
public option( flags: string, desc: string, opts?: ICommandOption | IFlagValueHandler ): this {
if ( typeof opts === 'function' ) {
return this.option( flags, desc, { value: opts } );
}
const result = ArgumentsParser.splitArguments( flags );
const args: IArgumentDetails[] = result.typeDefinition ? ArgumentsParser.parseArgumentsDefinition( result.typeDefinition ) : [];
const option: IOption = {
name: '',
description: desc,
args,
flags: result.args.join( ', ' ),
typeDefinition: result.typeDefinition,
...opts
};
if ( option.separator ) {
for ( const arg of args ) {
if ( arg.list ) {
arg.separator = option.separator;
}
}
}
for ( const part of result.args ) {
const arg = part.trim();
const isLong = /^--/.test( arg );
const name = isLong ? arg.slice( 2 ) : arg.slice( 1 );
if ( option.name === name || option.aliases && ~option.aliases.indexOf( name ) ) {
throw this.error( new Error( `Duplicate command name: ${ name }` ) );
}
if ( !option.name && isLong ) {
option.name = name;
} else if ( !option.aliases ) {
option.aliases = [ name ];
} else {
option.aliases.push( name );
}
if ( this.cmd.getBaseOption( name, true ) ) {
if ( opts?.override ) {
this.removeOption( name );
} else {
throw this.error( new Error( `Duplicate option name: ${ name }` ) );
}
}
}
if ( option.prepend ) {
this.cmd.options.unshift( option );
} else {
this.cmd.options.push( option );
}
return this;
}
/**
* Add new command example.
*
* @param name Name of the example.
* @param description The content of the example.
*/
public example( name: string, description: string ): this {
if ( this.cmd.hasExample( name ) ) {
throw this.error( new Error( 'Example already exists.' ) );
}
this.cmd.examples.push( { name, description } );
return this;
}
/**
* Add new environment variable.
*
* @param name Name of the environment variable.
* @param description The description of the environment variable.
* @param options Environment variable options.
*/
public env( name: string, description: string, options?: IEnvVarOption ): this {
const result = ArgumentsParser.splitArguments( name );
if ( !result.typeDefinition ) {
result.typeDefinition = '<value:boolean>';
}
if ( result.args.some( envName => this.cmd.getBaseEnvVar( envName, true ) ) ) {
throw this.error( new Error( `Environment variable already exists: ${ name }` ) );
}
const details: IArgumentDetails[] = ArgumentsParser.parseArgumentsDefinition( result.typeDefinition );
if ( details.length > 1 ) {
throw this.error( new Error( `An environment variable can only have one value but got: ${ name }` ) );
} else if ( details.length && details[ 0 ].optionalValue ) {
throw this.error( new Error( `An environment variable can not have an optional value but '${ name }' is defined as optional.` ) );
} else if ( details.length && details[ 0 ].variadic ) {
throw this.error( new Error( `An environment variable can not have an variadic value but '${ name }' is defined as variadic.` ) );
}
this.cmd.envVars.push( {
names: result.args,
description,
type: details[ 0 ].type,
details: details.shift() as IArgumentDetails,
...options
} );
return this;
}
/********************************************************************************
**** MAIN HANDLER **************************************************************
********************************************************************************/
/**
* Parse command line arguments and execute matched command.
*
* @param args Command line args to parse. Ex: `cmd.parse( Deno.args )`
* @param dry Execute command after parsed.
*/
public async parse( args: string[] = Deno.args, dry?: boolean ): Promise<IParseResult<O, A>> {
this.reset()
.registerDefaults();
this.rawArgs = args;
const subCommand = this.rawArgs.length > 0 && this.getCommand( this.rawArgs[ 0 ], true );
if ( subCommand ) {
subCommand._globalParent = this;
return await subCommand.parse( this.rawArgs.slice( 1 ), dry );
}
if ( this.isExecutable ) {
if ( !dry ) {
await this.executeExecutable( this.rawArgs );
}
return { options: {} as O, args: this.rawArgs as any as A, cmd: this, literal: this.literalArgs };
} else if ( this._useRawArgs ) {
if ( dry ) {
return { options: {} as O, args: this.rawArgs as any as A, cmd: this, literal: this.literalArgs };
}
return await this.execute( {} as O, ...this.rawArgs as A );
} else {
const { flags, unknown, literal } = this.parseFlags( this.rawArgs );
this.literalArgs = literal;
const params = this.parseArguments( unknown, flags );
this.validateEnvVars();
if ( dry ) {
return { options: flags, args: params as any as A, cmd: this, literal: this.literalArgs };
}
return await this.execute( flags, ...params );
}
}
private registerDefaults(): this {
if ( this.getParent() || this.hasDefaults ) {
return this;
}
this.hasDefaults = true;
this.reset();
if ( this._versionOption !== false ) {
this.option(
this._versionOption?.flags || '-V, --version',
this._versionOption?.desc || 'Show the version number for this program.',
Object.assign( {
standalone: true,
prepend: true,
action: async function ( this: Command ) {
await Deno.stdout.writeSync( encode( this.getVersion() + '\n' ) );
Deno.exit( 0 );
}
}, this._versionOption?.opts ?? {} ) );
}
if ( this._helpOption !== false ) {
this.option(
this._helpOption?.flags || '-h, --help',
this._helpOption?.desc || 'Show this help.',
Object.assign( {
standalone: true,
global: true,
prepend: true,
action: function ( this: Command ) {
this.help();
Deno.exit( 0 );
}
}, this._helpOption?.opts ?? {} ) );
}
return this;
};
/**
* Execute command.
*
* @param options A map of options.
* @param args Command arguments.
*/
protected async execute( options: O, ...args: A ): Promise<IParseResult<O, A>> {
const actionOption = this.findActionFlag( options );
if ( actionOption && actionOption.action ) {
await actionOption.action.call( this, options, ...args );
return { options, args: args as any as A, cmd: this, literal: this.literalArgs };
}
if ( this.fn ) {
try {
await this.fn( options, ...args );
} catch ( e ) {
throw this.error( e );
}
} else if ( this.defaultCommand ) {
const cmd = this.getCommand( this.defaultCommand, true );
if ( !cmd ) {
throw this.error( new Error( `Default command '${ this.defaultCommand }' not found.` ) );
}
cmd._globalParent = this;
try {
await cmd.execute( options, ...args );
} catch ( e ) {
throw this.error( e );
}
}
return { options, args: args as any as A, cmd: this, literal: this.literalArgs };
}
/**
* Execute external sub-command.
*
* @param args Raw command line arguments.
*/
protected async executeExecutable( args: string[] ) {
const [ main, ...names ] = this.getPath().split( ' ' );
names.unshift( main.replace( /\.ts$/, '' ) );
const executable = names.join( '-' );
try {
// @TODO: create getEnv() method which should return all known environment variables and pass it to Deno.run({env})
await Deno.run( {
cmd: [ executable, ...args ]
} );
return;
} catch ( e ) {
if ( !e.message.match( /No such file or directory/ ) ) {
throw e;
}
}
try {
await Deno.run( {
cmd: [ executable + '.ts', ...args ]
} );
return;
} catch ( e ) {
if ( !e.message.match( /No such file or directory/ ) ) {
throw e;
}
}
throw this.error( new Error( `Sub-command executable not found: ${ executable }${ dim( '(.ts)' ) }` ) );
}
/**
* Parse command line args.
*
* @param args Command line args.
*/
protected parseFlags( args: string[] ): IFlagsResult<O> {
try {
return parseFlags<O>( args, {
stopEarly: this._stopEarly,
// knownFlaks,
allowEmpty: this._allowEmpty,
flags: this.getOptions( true ),
parse: ( type: string, option: IFlagOptions, arg: IFlagArgument, nextValue: string ) =>
this.parseType( type, option, arg, nextValue )
} );
} catch ( e ) {
throw this.error( e );
}
}
protected parseType( name: string, option: IFlagOptions, arg: IFlagArgument, nextValue: string ): any {
const type: ITypeSettings | undefined = this.getType( name );
if ( !type ) {
throw this.error( new Error( `No type registered with name: ${ name }` ) );
}
// @TODO: pass only name & value to .parse() method
return type.handler instanceof Type ? type.handler.parse( option, arg, nextValue ) : type.handler( option, arg, nextValue );
}
/**
* Validate environment variables.
*/
protected validateEnvVars() {
const envVars = this.getEnvVars( true );
if ( !envVars.length ) {
return;
}
if ( hasEnvPermissions ) {
envVars.forEach( ( env: IEnvVariable ) => {
const name = env.names.find( name => !!Deno.env.get( name ) );
if ( name ) {
const value: string | undefined = Deno.env.get( name );
try {
// @TODO: optimize handling for environment variable error message: parseFlag & parseEnv ?
this.parseType( env.type, { name }, env, value || '' );
} catch ( e ) {
throw new Error( `Environment variable '${ name }' must be of type ${ env.type } but got: ${ value }` );
}
}
} );
}
}
/**
* Match commands and arguments from command line arguments.
*/
protected parseArguments( args: string[], flags: O ): A {
const params: IFlagValue[] = [];
// remove array reference
args = args.slice( 0 );
if ( !this.hasArguments() ) {
if ( args.length ) {
if ( this.hasCommands( true ) ) {
throw this.error( new Error( `Unknown command: ${ args.join( ' ' ) }` ) );
} else {
throw this.error( new Error( `No arguments allowed for command: ${ this._name }` ) );
}
}
} else {
if ( !args.length ) {
const required = this.getArguments()
.filter( expectedArg => !expectedArg.optionalValue )
.map( expectedArg => expectedArg.name );
if ( required.length ) {
const flagNames: string[] = Object.keys( flags );
const hasStandaloneOption: boolean = !!flagNames.find( name => this.getOption( name, true )?.standalone );
if ( !hasStandaloneOption ) {
throw this.error( new Error( 'Missing argument(s): ' + required.join( ', ' ) ) );
}
}
return params as A;
}
for ( const expectedArg of this.getArguments() ) {
if ( !expectedArg.optionalValue && !args.length ) {
throw this.error( new Error( `Missing argument: ${ expectedArg.name }` ) );
}
let arg: IFlagValue;
if ( expectedArg.variadic ) {
arg = args.splice( 0, args.length ) as IFlagValueType[];
} else {
arg = args.shift() as IFlagValue;
}
if ( arg ) {
params.push( arg );
}
}
if ( args.length ) {
throw this.error( new Error( `To many arguments: ${ args.join( ' ' ) }` ) );
}
}
return params as A;
}
/**
* Execute help command if help flag is set.
*
* @param flags Command options.
*/
protected findActionFlag( flags: O ): IOption | undefined {
const flagNames = Object.keys( flags );
for ( const flag of flagNames ) {
const option = this.getOption( flag, true );
if ( option?.action ) {
return option;
}
}
return;
}
/********************************************************************************
**** GETTER ********************************************************************
********************************************************************************/
/**
* Get command name.
*/
public getName(): string {
return this._name;
}
/**
* Get parent command.
*/
public getParent(): Command | undefined {
return this._parent;
}
/**
* Get parent command from global executed command.
* Be sure, to call this method only inside an action handler. Unless this or any child command was executed,
* this method returns always undefined.
*/
public getGlobalParent(): Command | undefined {
return this._globalParent;
}
/**
* Get main command.
*/
public getMainCommand(): Command {
return this._parent?.getMainCommand() ?? this;
}
/**
* Get command name aliases.
*/
public getAliases(): string[] {
return this.aliases;
}
/**
* Get full command path of all parent command names's and current command name.
*/
public getPath(): string {
return this._parent ? this._parent.getPath() + ' ' + this._name : this._name;
}
/**
* Get arguments definition.
*/
public getArgsDefinition(): string | undefined {
return this.argsDefinition;
}
/**
* Get argument.
*/
public getArgument( name: string ): IArgumentDetails | undefined {
return this.getArguments().find( arg => arg.name === name );
}
/**
* Get arguments.
*/
public getArguments(): IArgumentDetails[] {
if ( !this.args.length && this.argsDefinition ) {
this.args = ArgumentsParser.parseArgumentsDefinition( this.argsDefinition );
}
return this.args;
}
/**
* Check if command has arguments.
*/
public hasArguments() {
return !!this.argsDefinition;
}
/**
* Get command version.
*/
public getVersion(): string {
return this.ver || ( this._parent?.getVersion() ?? '' );
}
/**
* Get command description.
*/
public getDescription(): string {
// call description method only once
return typeof this.desc === 'function' ? this.desc = this.desc() : this.desc;
}
public getShortDescription(): string {
return this.getDescription()
.trim()
.split( '\n' )
.shift() as string;
}
/**
* Checks whether the command has options or not.
*/
public hasOptions( hidden?: boolean ): boolean {
return this.getOptions( hidden ).length > 0;
}
public getOptions( hidden?: boolean ): IOption[] {
return this.getGlobalOptions( hidden ).concat( this.getBaseOptions( hidden ) );
}
public getBaseOptions( hidden?: boolean ): IOption[] {
if ( !this.options.length ) {
return [];
}
return hidden ? this.options.slice( 0 ) : this.options.filter( opt => !opt.hidden );
}
public getGlobalOptions( hidden?: boolean ): IOption[] {
const getOptions = ( cmd: Command | undefined, options: IOption[] = [], names: string[] = [] ): IOption[] => {
if ( cmd ) {
if ( cmd.options.length ) {
cmd.options.forEach( ( option: IOption ) => {
if (
option.global &&
!this.options.find( opt => opt.name === option.name ) &&
names.indexOf( option.name ) === -1 &&
( hidden || !option.hidden )
) {
names.push( option.name );
options.push( option );
}
} );
}
return getOptions( cmd._parent, options, names );
}
return options;
};
return getOptions( this._parent );
}
/**
* Checks whether the command has an option with given name or not.
*
* @param name Name of the option. Must be in param-case.
* @param hidden Include hidden options.
*/
public hasOption( name: string, hidden?: boolean ): boolean {
return !!this.getOption( name, hidden );
}
/**
* Get option by name.
*
* @param name Name of the option. Must be in param-case.
* @param hidden Include hidden options.
*/
public getOption( name: string, hidden?: boolean ): IOption | undefined {
return this.getBaseOption( name, hidden ) ?? this.getGlobalOption( name, hidden );
}
/**
* Get base option by name.
*
* @param name Name of the option. Must be in param-case.
* @param hidden Include hidden options.
*/
public getBaseOption( name: string, hidden?: boolean ): IOption | undefined {
const option = this.options.find( option => option.name === name );
return option && ( hidden || !option.hidden ) ? option : undefined;
}
/**
* Get global option from parent command's by name.
*
* @param name Name of the option. Must be in param-case.
* @param hidden Include hidden options.
*/
public getGlobalOption( name: string, hidden?: boolean ): IOption | undefined {
if ( !this._parent ) {
return;
}
let option: IOption | undefined = this._parent.getBaseOption( name, hidden );
if ( !option || !option.global ) {
return this._parent.getGlobalOption( name, hidden );
}
return option;
}
/**
* Remove option by name.
*
* @param name Name of the option. Must be in param-case.
*/
public removeOption( name: string ): IOption | undefined {
const index = this.options.findIndex( option => option.name === name );
if ( index === -1 ) {
return;
}
return this.options.splice( index, 1 )[ 0 ];
}
/**
* Checks whether the command has sub-commands or not.
*/
public hasCommands( hidden?: boolean ): boolean {
return this.getCommands( hidden ).length > 0;
}
/**
* Get sub-commands.
*/
public getCommands( hidden?: boolean ): Command[] {
return this.getGlobalCommands( hidden ).concat( this.getBaseCommands( hidden ) );
}
public getBaseCommands( hidden?: boolean ): Command[] {
const commands = Array.from( this.commands.values() );
return hidden ? commands : commands.filter( cmd => !cmd.isHidden );
}
public getGlobalCommands( hidden?: boolean ): Command[] {
const getCommands = ( cmd: Command | undefined, commands: Command[] = [], names: string[] = [] ): Command[] => {
if ( cmd ) {
if ( cmd.commands.size ) {
cmd.commands.forEach( ( cmd: Command ) => {
if (
cmd.isGlobal &&
this !== cmd &&
!this.commands.has( cmd._name ) &&
names.indexOf( cmd._name ) === -1 &&
( hidden || !cmd.isHidden )
) {
names.push( cmd._name );
commands.push( cmd );
}
} );
}
return getCommands( cmd._parent, commands, names );
}
return commands;
};
return getCommands( this._parent );
}
/**
* Checks whether the command has a sub-command with given name or not.
*
* @param name Name of the command.
* @param hidden Include hidden commands.
*/
public hasCommand( name: string, hidden?: boolean ): boolean {
return !!this.getCommand( name, hidden );
}
/**
* Get sub-command with given name.
*
* @param name Name of the sub-command.
* @param hidden Include hidden commands.
*/
public getCommand<O = any>( name: string, hidden?: boolean ): Command<O> | undefined {
return this.getBaseCommand( name, hidden ) ?? this.getGlobalCommand( name, hidden );
}
public getBaseCommand<O = any>( name: string, hidden?: boolean ): Command<O> | undefined {
let cmd: Command | undefined = this.commands.get( name );
return cmd && ( hidden || !cmd.isHidden ) ? cmd : undefined;
}
public getGlobalCommand<O = any>( name: string, hidden?: boolean ): Command<O> | undefined {
if ( !this._parent ) {
return;
}
let cmd: Command | undefined = this._parent.getBaseCommand( name, hidden );
if ( !cmd?.isGlobal ) {
return this._parent.getGlobalCommand( name, hidden );
}
return cmd;
}
/**
* Remove sub-command with given name.
*
* @param name Name of the command.
*/
public removeCommand<O = any>( name: string ): Command<O> | undefined {
const command = this.getBaseCommand( name, true );
if ( command ) {
this.commands.delete( name );
}
return command;
}
public getTypes(): ITypeSettings[] {
return this.getGlobalTypes().concat( this.getBaseTypes() );
}
public getBaseTypes(): ITypeSettings[] {
return Array.from( this.types.values() );
}
public getGlobalTypes(): ITypeSettings[] {
const getTypes = ( cmd: Command | undefined, types: ITypeSettings[] = [], names: string[] = [] ): ITypeSettings[] => {
if ( cmd ) {
if ( cmd.types.size ) {
cmd.types.forEach( ( type: ITypeSettings ) => {
if (
type.global &&
!this.types.has( type.name ) &&
names.indexOf( type.name ) === -1
) {
names.push( type.name );
types.push( type );
}
} );
}
return getTypes( cmd._parent, types, names );
}
return types;
};
return getTypes( this._parent );
}
protected getType( name: string ): ITypeSettings | undefined {
return this.getBaseType( name ) ?? this.getGlobalType( name );
}
protected getBaseType( name: string ): ITypeSettings | undefined {
return this.types.get( name );
}
protected getGlobalType( name: string ): ITypeSettings | undefined {
if ( !this._parent ) {
return;
}
let cmd: ITypeSettings | undefined = this._parent.getBaseType( name );
if ( !cmd?.global ) {
return this._parent.getGlobalType( name );
}
return cmd;
}
/**
* Checks whether the command has environment variables or not.
*/
public hasEnvVars( hidden?: boolean ): boolean {
return this.getEnvVars( hidden ).length > 0;
}
/**
* Get environment variables.
*/
public getEnvVars( hidden?: boolean ): IEnvVariable[] {
return this.getGlobalEnvVars( hidden ).concat( this.getBaseEnvVars( hidden ) );
}
public getBaseEnvVars( hidden?: boolean ): IEnvVariable[] {
if ( !this.envVars.length ) {
return [];
}
return hidden ? this.envVars.slice( 0 ) : this.envVars.filter( env => !env.hidden );
}
public getGlobalEnvVars( hidden?: boolean ): IEnvVariable[] {
const getEnvVars = ( cmd: Command | undefined, envVars: IEnvVariable[] = [], names: string[] = [] ): IEnvVariable[] => {
if ( cmd ) {
if ( cmd.envVars.length ) {
cmd.envVars.forEach( ( envVar: IEnvVariable ) => {
if (
envVar.global &&
!this.envVars.find( env => env.names[ 0 ] === envVar.names[ 0 ] ) &&
names.indexOf( envVar.names[ 0 ] ) === -1 &&
( hidden || !envVar.hidden )
) {
names.push( envVar.names[ 0 ] );
envVars.push( envVar );
}
} );
}
return getEnvVars( cmd._parent, envVars, names );
}
return envVars;
};
return getEnvVars( this._parent );
}
/**
* Checks whether the command has an environment variable with given name or not.
*
* @param name Name of the environment variable.
* @param hidden Include hidden environment variable.
*/
public hasEnvVar( name: string, hidden?: boolean ): boolean {
return !!this.getEnvVar( name, hidden );
}
/**
* Get environment variable with given name.
*
* @param name Name of the environment variable.
* @param hidden Include hidden environment variable.
*/
public getEnvVar( name: string, hidden?: boolean ): IEnvVariable | undefined {
return this.getBaseEnvVar( name, hidden ) ?? this.getGlobalEnvVar( name, hidden );
}
public getBaseEnvVar( name: string, hidden?: boolean ): IEnvVariable | undefined {
const envVar: IEnvVariable | undefined = this.envVars.find( env => env.names.indexOf( name ) !== -1 );
return envVar && ( hidden || !envVar.hidden ) ? envVar : undefined;
}
public getGlobalEnvVar( name: string, hidden?: boolean ): IEnvVariable | undefined {
if ( !this._parent ) {
return;
}
let envVar: IEnvVariable | undefined = this._parent.getBaseEnvVar( name, hidden );
if ( !envVar?.global ) {
return this._parent.getGlobalEnvVar( name, hidden );
}
return envVar;
}
/**
* Checks whether the command has examples or not.
*/
public hasExamples(): boolean {
return this.examples.length > 0;
}
/**
* Get examples.
*/
public getExamples(): IExample[] {
return this.examples;
}
/**
* Checks whether the command has an example with given name or not.
*
* @param name Name of the example.
*/
public hasExample( name: string ): boolean {
return !!this.getExample( name );
}
/**
* Get example with given name.
*
* @param name Name of the example.
*/
public getExample( name: string ): IExample | undefined {
return this.examples.find( example => example.name === name );
}
public getRawArgs(): string[] {
return this.rawArgs;
}
public getLiteralArgs(): string[] {
return this.literalArgs;
}
/********************************************************************************
**** HELPER ********************************************************************
********************************************************************************/
/**
* Write line to stdout without line break.
*
* @param args Data to write to stdout.
*/
public write( ...args: any[] ) {
stdout.writeSync( encode( fill( 2 ) + format( ...args ) ) );
}
/**
* Write line to stderr without line break.
*
* @param args Data to write to stdout.
*/
public writeError( ...args: any[] ) {
stderr.writeSync( encode( fill( 2 ) + red( format( `[ERROR:${ this._name }]`, ...args ) ) ) );
}
/**
* Write line to stdout.
*
* @param args Data to write to stdout.
*/
public log( ...args: any[] ) {
this.write( ...args, '\n' );
}
/**
* Write line to stderr.
*
* @param args Data to write to stderr.
*/
public logError( ...args: any[] ) {
this.writeError( ...args, '\n' );
}
/**
* Handle error. If `.throwErrors()` was called all error's will be thrown, otherwise `Deno.exit(1)` will be called.
*
* @param error Error to handle.
* @param showHelp Show help.
*/
public error( error: Error, showHelp: boolean = true ): Error {
if ( this.shouldThrowErrors() ) {
return error;
}
const CLIFFY_DEBUG: boolean = hasEnvPermissions ? !!Deno.env.get( 'CLIFFY_DEBUG' ) : false;
showHelp && this.help();
this.logError( CLIFFY_DEBUG ? error : error.message );
this.log();
Deno.exit( 1 );
}
/**
* Output generated help without exiting.
*/
public help() {
Deno.stdout.writeSync( encode( this.getHelp() ) );
}
/**
* Get generated help.
*/
public getHelp(): string {
this.registerDefaults();
return HelpGenerator.generate( this );
}
}