Implement basic options; directives; handling
This commit is contained in:
parent
13aab00c1b
commit
fb85e42dee
@ -8,12 +8,12 @@
|
|||||||
"lib": "lib"
|
"lib": "lib"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@extollo/util": "file:../util",
|
|
||||||
"@extollo/lib": "file:../lib",
|
|
||||||
"@extollo/di": "file:../di",
|
"@extollo/di": "file:../di",
|
||||||
|
"@extollo/lib": "file:../lib",
|
||||||
|
"@extollo/util": "file:../util",
|
||||||
|
"colors": "^1.4.0",
|
||||||
"typescript": "^4.1.3"
|
"typescript": "^4.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"build": "tsc"
|
"build": "tsc"
|
||||||
|
@ -2,9 +2,16 @@ dependencies:
|
|||||||
'@extollo/di': link:../di
|
'@extollo/di': link:../di
|
||||||
'@extollo/lib': link:../lib
|
'@extollo/lib': link:../lib
|
||||||
'@extollo/util': link:../util
|
'@extollo/util': link:../util
|
||||||
|
colors: 1.4.0
|
||||||
typescript: 4.2.3
|
typescript: 4.2.3
|
||||||
lockfileVersion: 5.2
|
lockfileVersion: 5.2
|
||||||
packages:
|
packages:
|
||||||
|
/colors/1.4.0:
|
||||||
|
dev: false
|
||||||
|
engines:
|
||||||
|
node: '>=0.1.90'
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
|
||||||
/typescript/4.2.3:
|
/typescript/4.2.3:
|
||||||
dev: false
|
dev: false
|
||||||
engines:
|
engines:
|
||||||
@ -16,4 +23,5 @@ specifiers:
|
|||||||
'@extollo/di': file:../di
|
'@extollo/di': file:../di
|
||||||
'@extollo/lib': file:../lib
|
'@extollo/lib': file:../lib
|
||||||
'@extollo/util': file:../util
|
'@extollo/util': file:../util
|
||||||
|
colors: ^1.4.0
|
||||||
typescript: ^4.1.3
|
typescript: ^4.1.3
|
||||||
|
337
src/Directive.ts
Normal file
337
src/Directive.ts
Normal file
@ -0,0 +1,337 @@
|
|||||||
|
import {AppClass, Logging} from "@extollo/lib"
|
||||||
|
import {Injectable, Inject} from "@extollo/di"
|
||||||
|
import {infer, ErrorWithContext} from "@extollo/util"
|
||||||
|
import {CLIOption} from "./directive/options/CLIOption"
|
||||||
|
import {PositionalOption} from "./directive/options/PositionalOption";
|
||||||
|
import {FlagOption} from "./directive/options/FlagOption";
|
||||||
|
|
||||||
|
export type OptionDefinition = CLIOption<any> | string
|
||||||
|
|
||||||
|
export class OptionValidationError extends ErrorWithContext {}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export abstract class Directive extends AppClass {
|
||||||
|
@Inject()
|
||||||
|
protected readonly logging!: Logging
|
||||||
|
|
||||||
|
private _optionValues: any
|
||||||
|
|
||||||
|
public abstract getKeywords(): string | string[]
|
||||||
|
|
||||||
|
public abstract getDescription(): string
|
||||||
|
|
||||||
|
public getHelpText(): string {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract handle(argv: string[]): void | Promise<void>
|
||||||
|
|
||||||
|
private _setOptionValues(optionValues: any) {
|
||||||
|
this._optionValues = optionValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
public option(name: string, defaultValue?: any) {
|
||||||
|
if ( name in this._optionValues ) {
|
||||||
|
return this._optionValues[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
async invoke(argv: string[]) {
|
||||||
|
const options = this.getResolvedOptions()
|
||||||
|
|
||||||
|
if ( this.didRequestUsage(argv) ) {
|
||||||
|
// @ts-ignore
|
||||||
|
const positionalArguments: PositionalOption<any>[] = options.filter(opt => opt instanceof PositionalOption)
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const flagArguments: FlagOption<any>[] = options.filter(opt => opt instanceof FlagOption)
|
||||||
|
|
||||||
|
const positionalDisplay: string = positionalArguments.map(x => `<${x.getArgumentName()}>`).join(' ')
|
||||||
|
const flagDisplay: string = flagArguments.length ? ' [...flags]' : ''
|
||||||
|
|
||||||
|
console.log([
|
||||||
|
'',
|
||||||
|
`DIRECTIVE: ${this.getMainKeyword()} - ${this.getDescription()}`,
|
||||||
|
'',
|
||||||
|
`USAGE: ${this.getMainKeyword()} ${positionalDisplay}${flagDisplay}`,
|
||||||
|
].join('\n'))
|
||||||
|
|
||||||
|
if ( positionalArguments.length ) {
|
||||||
|
console.log([
|
||||||
|
'',
|
||||||
|
`POSITIONAL ARGUMENTS:`,
|
||||||
|
...(positionalArguments.map(arg => {
|
||||||
|
return ` ${arg.getArgumentName()}${arg.message ? ' - ' + arg.message : ''}`
|
||||||
|
})),
|
||||||
|
].join('\n'))
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( flagArguments.length ) {
|
||||||
|
console.log([
|
||||||
|
'',
|
||||||
|
`FLAGS:`,
|
||||||
|
...(flagArguments.map(arg => {
|
||||||
|
return ` ${arg.shortFlag ? arg.shortFlag + ', ' : ''}${arg.longFlag}${arg.argumentDescription ? ' {' + arg.argumentDescription + '}' : ''}${arg.message ? ' - ' + arg.message : ''}`
|
||||||
|
})),
|
||||||
|
].join('\n'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const help = this.getHelpText()
|
||||||
|
if ( help ) {
|
||||||
|
console.log('\n' + help)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n')
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const optionValues = this.parseOptions(options, argv)
|
||||||
|
this._setOptionValues(optionValues)
|
||||||
|
await this.handle(argv)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e.message)
|
||||||
|
if ( e instanceof OptionValidationError ) {
|
||||||
|
// expecting, value, requirements
|
||||||
|
if ( e.context.expecting ) {
|
||||||
|
console.error(` - Expecting: ${e.context.expecting}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( e.context.requirements && Array.isArray(e.context.requirements) ) {
|
||||||
|
for ( const req of e.context.requirements ) {
|
||||||
|
console.error(` - ${req}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( e.context.value ) {
|
||||||
|
console.error(` - ${e.context.value}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getOptions(): OptionDefinition[] {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
public getResolvedOptions(): CLIOption<any>[] {
|
||||||
|
return this.getOptions().map(option => {
|
||||||
|
if ( typeof option === 'string' ) {
|
||||||
|
return this.instantiateOptionFromString(option)
|
||||||
|
} else {
|
||||||
|
return option
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMainKeyword(): string {
|
||||||
|
const kws = this.getKeywords()
|
||||||
|
|
||||||
|
if ( Array.isArray(kws) ) {
|
||||||
|
return kws[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return kws
|
||||||
|
}
|
||||||
|
|
||||||
|
public matchesKeyword(name: string) {
|
||||||
|
let kws = this.getKeywords()
|
||||||
|
if ( !Array.isArray(kws) ) kws = [kws]
|
||||||
|
return kws.includes(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
success(output: any) {
|
||||||
|
this.logging.success(output, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
error(output: any) {
|
||||||
|
this.logging.error(output, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(output: any) {
|
||||||
|
this.logging.warn(output, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
info(output: any) {
|
||||||
|
this.logging.info(output, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(output: any) {
|
||||||
|
this.logging.debug(output, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
verbose(output: any) {
|
||||||
|
this.logging.verbose(output, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the flag option that signals help. Usually, this is named 'help'
|
||||||
|
* and supports the flags '--help' and '-?'.
|
||||||
|
*/
|
||||||
|
getHelpOption() {
|
||||||
|
return new FlagOption('--help', '-?', 'usage information about this directive')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the raw CLI arguments using an array of option class instances to build
|
||||||
|
* a mapping of option names to provided values.
|
||||||
|
*/
|
||||||
|
parseOptions(options: CLIOption<any>[], args: string[]) {
|
||||||
|
// @ts-ignore
|
||||||
|
let positionalArguments: PositionalOption<any>[] = options.filter(cls => cls instanceof PositionalOption)
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const flagArguments: FlagOption<any>[] = options.filter(cls => cls instanceof FlagOption)
|
||||||
|
const optionValue: any = {}
|
||||||
|
|
||||||
|
flagArguments.push(this.getHelpOption())
|
||||||
|
|
||||||
|
let expectingFlagArgument = false
|
||||||
|
let positionalFlagName = ''
|
||||||
|
for ( const value of args ) {
|
||||||
|
if ( value.startsWith('--') ) {
|
||||||
|
if ( expectingFlagArgument ) {
|
||||||
|
throw new OptionValidationError(`Unexpected flag argument. Expecting argument for flag: ${positionalFlagName}`, {
|
||||||
|
expecting: positionalFlagName,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const flagArgument = flagArguments.filter(x => x.longFlag === value)
|
||||||
|
if ( flagArgument.length < 1 ) {
|
||||||
|
throw new OptionValidationError(`Unknown flag argument: ${value}`, {
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
if ( flagArgument[0].argumentDescription ) {
|
||||||
|
positionalFlagName = flagArgument[0].getArgumentName()
|
||||||
|
expectingFlagArgument = true
|
||||||
|
} else {
|
||||||
|
optionValue[flagArgument[0].getArgumentName()] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ( value.startsWith('-') ) {
|
||||||
|
if ( expectingFlagArgument ) {
|
||||||
|
throw new OptionValidationError(`Unknown flag argument: ${value}`, {
|
||||||
|
expecting: positionalFlagName,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const flagArgument = flagArguments.filter(x => x.shortFlag === value)
|
||||||
|
if ( flagArgument.length < 1 ) {
|
||||||
|
throw new OptionValidationError(`Unknown flag argument: ${value}`, {
|
||||||
|
value
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
if ( flagArgument[0].argumentDescription ) {
|
||||||
|
positionalFlagName = flagArgument[0].getArgumentName()
|
||||||
|
expectingFlagArgument = true
|
||||||
|
} else {
|
||||||
|
optionValue[flagArgument[0].getArgumentName()] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ( expectingFlagArgument ) {
|
||||||
|
const inferredValue = infer(value)
|
||||||
|
const optionInstance = flagArguments.filter(x => x.getArgumentName() === positionalFlagName)[0]
|
||||||
|
if ( !optionInstance.validate(inferredValue) ) {
|
||||||
|
throw new OptionValidationError(`Invalid value for argument: ${positionalFlagName}`, {
|
||||||
|
requirements: optionInstance.getRequirementDisplays(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
optionValue[positionalFlagName] = inferredValue
|
||||||
|
expectingFlagArgument = false
|
||||||
|
} else {
|
||||||
|
if ( positionalArguments.length < 1 ) {
|
||||||
|
throw new OptionValidationError(`Unknown positional argument: ${value}`, {
|
||||||
|
value
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const inferredValue = infer(value)
|
||||||
|
if ( !positionalArguments[0].validate(inferredValue) ) {
|
||||||
|
throw new OptionValidationError(`Invalid value for argument: ${positionalArguments[0].getArgumentName()}`, {
|
||||||
|
requirements: positionalArguments[0].getRequirementDisplays(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
optionValue[positionalArguments[0].getArgumentName()] = infer(value)
|
||||||
|
positionalArguments = positionalArguments.slice(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( expectingFlagArgument ) {
|
||||||
|
throw new OptionValidationError(`Missing argument for flag: ${positionalFlagName}`, {
|
||||||
|
expecting: positionalFlagName
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( positionalArguments.length > 0 ) {
|
||||||
|
throw new OptionValidationError(`Missing required argument: ${positionalArguments[0].getArgumentName()}`, {
|
||||||
|
expecting: positionalArguments[0].getArgumentName()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return optionValue
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an instance of CLIOption based on a string definition of a particular format.
|
||||||
|
*
|
||||||
|
* e.g. '{file name} | canonical name of the resource to create'
|
||||||
|
* e.g. '--push -p {value} | the value to be pushed'
|
||||||
|
* e.g. '--force -f | do a force push'
|
||||||
|
*
|
||||||
|
* @param string
|
||||||
|
*/
|
||||||
|
protected instantiateOptionFromString(string: string): CLIOption<any> {
|
||||||
|
if ( string.startsWith('{') ) {
|
||||||
|
// The string is a positional argument
|
||||||
|
const stringParts = string.split('|').map(x => x.trim())
|
||||||
|
const name = stringParts[0].replace(/\{|\}/g, '')
|
||||||
|
return stringParts.length > 1 ? (new PositionalOption(name, stringParts[1])) : (new PositionalOption(name))
|
||||||
|
} else {
|
||||||
|
// The string is a flag argument
|
||||||
|
const stringParts = string.split('|').map(x => x.trim())
|
||||||
|
|
||||||
|
// Parse the flag parts first
|
||||||
|
const hasArgument = stringParts[0].indexOf('{') >= 0
|
||||||
|
const flagString = hasArgument ? stringParts[0].substr(0, stringParts[0].indexOf('{')).trim() : stringParts[0].trim()
|
||||||
|
const flagParts = flagString.split(' ')
|
||||||
|
|
||||||
|
let longFlag = flagParts[0].startsWith('--') ? flagParts[0] : undefined
|
||||||
|
if ( !longFlag && flagParts.length > 1 ) {
|
||||||
|
if ( flagParts[1].startsWith('--') ) {
|
||||||
|
longFlag = flagParts[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let shortFlag = flagParts[0].length === 2 ? flagParts[0] : undefined
|
||||||
|
if ( !shortFlag && flagParts.length > 1 ) {
|
||||||
|
if ( flagParts[1].length === 2 ) {
|
||||||
|
shortFlag = flagParts[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const argumentDescription = hasArgument ? stringParts[0].substring(stringParts[0].indexOf('{')+1, stringParts[0].indexOf('}')) : undefined
|
||||||
|
const description = stringParts.length > 1 ? stringParts[1] : undefined
|
||||||
|
|
||||||
|
return new FlagOption(longFlag, shortFlag, description, argumentDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if, at any point in the arguments, the help option's short or long flag appears.
|
||||||
|
* @returns {boolean} - true if the help flag appeared
|
||||||
|
*/
|
||||||
|
didRequestUsage(argv: string[]) {
|
||||||
|
const help_option = this.getHelpOption()
|
||||||
|
for ( const arg of argv ) {
|
||||||
|
if ( arg.trim() === help_option.longFlag || arg.trim() === help_option.shortFlag ) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
39
src/directive/ShellDirective.ts
Normal file
39
src/directive/ShellDirective.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import {Directive} from "../Directive"
|
||||||
|
import * as colors from "colors/safe"
|
||||||
|
import * as repl from 'repl'
|
||||||
|
|
||||||
|
export class ShellDirective extends Directive {
|
||||||
|
protected options: any = {
|
||||||
|
welcome: `powered by Extollo, © ${(new Date).getFullYear()} Garrett Mills\nAccess your application using the "app" global.`,
|
||||||
|
prompt: `${colors.blue('(')}extollo${colors.blue(') ➤ ')}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
protected repl?: repl.REPLServer
|
||||||
|
|
||||||
|
getDescription(): string {
|
||||||
|
return 'launch an interactive shell inside your application'
|
||||||
|
}
|
||||||
|
|
||||||
|
getKeywords(): string | string[] {
|
||||||
|
return ['shell']
|
||||||
|
}
|
||||||
|
|
||||||
|
getHelpText(): string {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle(): Promise<void> {
|
||||||
|
const state: any = {
|
||||||
|
app: this.app(),
|
||||||
|
make: this.make,
|
||||||
|
container: this.container,
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise<void>(res => {
|
||||||
|
console.log(this.options.welcome)
|
||||||
|
this.repl = repl.start(this.options.prompt)
|
||||||
|
Object.assign(this.repl.context, state)
|
||||||
|
this.repl.on('exit', () => res())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
50
src/directive/UsageDirective.ts
Normal file
50
src/directive/UsageDirective.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import {Directive} from "../Directive"
|
||||||
|
import {Injectable, Inject} from "@extollo/di"
|
||||||
|
import {padRight} from "@extollo/util"
|
||||||
|
import {CommandLine} from "../service"
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UsageDirective extends Directive {
|
||||||
|
@Inject()
|
||||||
|
protected readonly cli!: CommandLine
|
||||||
|
|
||||||
|
public getKeywords(): string | string[] {
|
||||||
|
return 'help'
|
||||||
|
}
|
||||||
|
|
||||||
|
public getDescription(): string {
|
||||||
|
return 'print information about available commands'
|
||||||
|
}
|
||||||
|
|
||||||
|
public handle(argv: string[]): void | Promise<void> {
|
||||||
|
const directiveStrings = this.cli.getDirectives()
|
||||||
|
.map(cls => this.make<Directive>(cls))
|
||||||
|
.map<[string, string]>(dir => {
|
||||||
|
return [dir.getMainKeyword(), dir.getDescription()]
|
||||||
|
})
|
||||||
|
|
||||||
|
const maxLen = directiveStrings.max<number>(x => x[0].length)
|
||||||
|
|
||||||
|
const printStrings = directiveStrings.map(grp => {
|
||||||
|
return [padRight(grp[0], maxLen + 1), grp[1]]
|
||||||
|
})
|
||||||
|
.map(grp => {
|
||||||
|
return ` ${grp[0]}: ${grp[1]}`
|
||||||
|
})
|
||||||
|
.toArray()
|
||||||
|
|
||||||
|
console.log(this.cli.getASCIILogo())
|
||||||
|
console.log([
|
||||||
|
'',
|
||||||
|
'Welcome to Extollo! Specify a command to get started.',
|
||||||
|
'',
|
||||||
|
`USAGE: ex <directive> [..options]`,
|
||||||
|
'',
|
||||||
|
...printStrings,
|
||||||
|
'',
|
||||||
|
'For usage information about a particular command, pass the --help flag.',
|
||||||
|
'-------------------------------------------',
|
||||||
|
`powered by Extollo, © ${(new Date).getFullYear()} Garrett Mills`,
|
||||||
|
].join('\n'))
|
||||||
|
}
|
||||||
|
}
|
244
src/directive/options/CLIOption.ts
Normal file
244
src/directive/options/CLIOption.ts
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
/**
|
||||||
|
* A CLI option. Supports basic comparative, and set-based validation.
|
||||||
|
* @class
|
||||||
|
*/
|
||||||
|
export abstract class CLIOption<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do we use the whitelist?
|
||||||
|
* @type {boolean}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
protected _useWhitelist: boolean = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do we use the blacklist?
|
||||||
|
* @type {boolean}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
protected _useBlacklist: boolean = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do we use the less-than comparison?
|
||||||
|
* @type {boolean}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
protected _useLessThan: boolean = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do we use the greater-than comparison?
|
||||||
|
* @type {boolean}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
protected _useGreaterThan: boolean = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do we use the equality operator?
|
||||||
|
* @type {boolean}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
protected _useEquality: boolean = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is this option optional?
|
||||||
|
* @type {boolean}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
protected _optional: boolean = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whitelisted values.
|
||||||
|
* @type {Array<*>}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
protected _whitelist: T[] = []
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blacklisted values.
|
||||||
|
* @type {Array<*>}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
protected _blacklist: T[] = []
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value to be compared in less than.
|
||||||
|
* @type {*}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
protected _lessThanValue?: T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, the less than will be less than or equal to.
|
||||||
|
* @type {boolean}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
protected _lessThanBit: boolean = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value to be compared in greater than.
|
||||||
|
* @type {*}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
protected _greaterThanValue?: T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, the greater than will be greater than or equal to.
|
||||||
|
* @type {boolean}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
protected _greateerThanBit: boolean = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value to be used to check equality.
|
||||||
|
* @type {*}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
protected _equalityValue?: T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whitelist the specified item or items and enable the whitelist.
|
||||||
|
* @param {...*} items - the items to whitelist
|
||||||
|
*/
|
||||||
|
whitelist(...items: T[]) {
|
||||||
|
this._useWhitelist = true
|
||||||
|
items.forEach(item => this._whitelist.push(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blacklist the specified item or items and enable the blacklist.
|
||||||
|
* @param {...*} items - the items to blacklist
|
||||||
|
*/
|
||||||
|
blacklist(...items: T[]) {
|
||||||
|
this._useBlacklist = true
|
||||||
|
items.forEach(item => this._blacklist.push(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the value to be used in less-than comparison and enables less-than comparison.
|
||||||
|
* @param {*} value
|
||||||
|
*/
|
||||||
|
lessThan(value: T) {
|
||||||
|
this._useLessThan = true
|
||||||
|
this._lessThanValue = value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the value to be used in less-than or equal-to comparison and enables that comparison.
|
||||||
|
* @param {*} value
|
||||||
|
*/
|
||||||
|
lessThanOrEqualTo(value: T) {
|
||||||
|
this._lessThanBit = true
|
||||||
|
this.lessThan(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the value to be used in greater-than comparison and enables that comparison.
|
||||||
|
* @param {*} value
|
||||||
|
*/
|
||||||
|
greaterThan(value: T) {
|
||||||
|
this._useGreaterThan = true
|
||||||
|
this._greaterThanValue = value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the value to be used in greater-than or equal-to comparison and enables that comparison.
|
||||||
|
* @param {*} value
|
||||||
|
*/
|
||||||
|
greaterThanOrEqualTo(value: T) {
|
||||||
|
this._greateerThanBit = true
|
||||||
|
this.greaterThan(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the value to be used in equality comparison and enables that comparison.
|
||||||
|
* @param {*} value
|
||||||
|
*/
|
||||||
|
equals(value: T) {
|
||||||
|
this._useEquality = true
|
||||||
|
this._equalityValue = value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the specified value passes the configured comparisons.
|
||||||
|
* @param {*} value
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
validate(value: any) {
|
||||||
|
let is_valid = true
|
||||||
|
if ( this._useEquality ) {
|
||||||
|
is_valid = is_valid && (this._equalityValue === value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( this._useLessThan && typeof this._lessThanValue !== 'undefined' ) {
|
||||||
|
if ( this._lessThanBit ) {
|
||||||
|
is_valid = is_valid && (value <= this._lessThanValue)
|
||||||
|
} else {
|
||||||
|
is_valid = is_valid && (value < this._lessThanValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( this._useGreaterThan && typeof this._greaterThanValue !== 'undefined' ) {
|
||||||
|
if ( this._greateerThanBit ) {
|
||||||
|
is_valid = is_valid && (value >= this._greaterThanValue)
|
||||||
|
} else {
|
||||||
|
is_valid = is_valid && (value > this._greaterThanValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( this._useWhitelist ) {
|
||||||
|
is_valid = is_valid && this._whitelist.some(x => {
|
||||||
|
return x === value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( this._useBlacklist ) {
|
||||||
|
is_valid = is_valid && !(this._blacklist.some(x => x === value))
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_valid
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the Option as optional.
|
||||||
|
*/
|
||||||
|
optional() {
|
||||||
|
this._optional = true
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the argument name. Should be overridden by child classes.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
abstract getArgumentName(): string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an array of strings denoting the human-readable requirements for this option to be valid.
|
||||||
|
* @returns {Array<string>}
|
||||||
|
*/
|
||||||
|
getRequirementDisplays() {
|
||||||
|
const clauses = []
|
||||||
|
|
||||||
|
if ( this._useBlacklist ) {
|
||||||
|
clauses.push(`must not be one of: ${this._blacklist.map(x => String(x)).join(', ')}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( this._useWhitelist ) {
|
||||||
|
clauses.push(`must be one of: ${this._whitelist.map(x => String(x)).join(', ')}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( this._useGreaterThan ) {
|
||||||
|
clauses.push(`must be greater than${this._greateerThanBit ? ' or equal to' : ''}: ${String(this._greaterThanValue)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( this._useLessThan ) {
|
||||||
|
clauses.push(`must be less than${this._lessThanBit ? ' or equal to' : ''}: ${String(this._lessThanValue)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( this._useEquality ) {
|
||||||
|
clauses.push(`must be equal to: ${String(this._equalityValue)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return clauses
|
||||||
|
}
|
||||||
|
}
|
30
src/directive/options/FlagOption.ts
Normal file
30
src/directive/options/FlagOption.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import {CLIOption} from "./CLIOption"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Non-positional, flag-based CLI option.
|
||||||
|
*/
|
||||||
|
export class FlagOption<T> extends CLIOption<T> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public readonly longFlag?: string,
|
||||||
|
public readonly shortFlag?: string,
|
||||||
|
public readonly message?: string,
|
||||||
|
public readonly argumentDescription?: string
|
||||||
|
) { super() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the referential name for this option.
|
||||||
|
* Defaults to the long flag (without the '--'). If this cannot
|
||||||
|
* be found, the short flag (without the '-') is used.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getArgumentName() {
|
||||||
|
if ( this.longFlag ) {
|
||||||
|
return this.longFlag.replace('--', '')
|
||||||
|
} else if ( this.shortFlag ) {
|
||||||
|
return this.shortFlag.replace('-', '')
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Missing either a long- or short-flag for FlagOption.')
|
||||||
|
}
|
||||||
|
}
|
25
src/directive/options/PositionalOption.ts
Normal file
25
src/directive/options/PositionalOption.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import {CLIOption} from "./CLIOption"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A positional CLI option. Defined without a flag.
|
||||||
|
*/
|
||||||
|
export class PositionalOption<T> extends CLIOption<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiate the option.
|
||||||
|
* @param {string} name - the name of the option
|
||||||
|
* @param {string} message - message describing the option
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
public readonly name: string,
|
||||||
|
public readonly message: string = ''
|
||||||
|
) { super() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the name of the option.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getArgumentName () {
|
||||||
|
return this.name
|
||||||
|
}
|
||||||
|
}
|
4
src/index.ts
Normal file
4
src/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from './Directive'
|
||||||
|
|
||||||
|
export * from './service/CommandLineApplication'
|
||||||
|
export * from './service/CommandLine'
|
39
src/service/CommandLine.ts
Normal file
39
src/service/CommandLine.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import {Unit} from "@extollo/lib"
|
||||||
|
import {Singleton, Instantiable} from "@extollo/di"
|
||||||
|
import {Collection} from "@extollo/util"
|
||||||
|
import {CommandLineApplication} from "./CommandLineApplication"
|
||||||
|
import {Directive} from "../Directive";
|
||||||
|
|
||||||
|
@Singleton()
|
||||||
|
export class CommandLine extends Unit {
|
||||||
|
protected directives: Collection<Instantiable<Directive>> = new Collection<Instantiable<Directive>>()
|
||||||
|
|
||||||
|
constructor() { super() }
|
||||||
|
|
||||||
|
public isCLI() {
|
||||||
|
return this.app().hasUnit(CommandLineApplication)
|
||||||
|
}
|
||||||
|
|
||||||
|
public getASCIILogo() {
|
||||||
|
return ` ______ _ _ _
|
||||||
|
| ____| | | | | |
|
||||||
|
| |__ __ _| |_ ___ | | | ___
|
||||||
|
| __| \\ \\/ / __/ _ \\| | |/ _ \\
|
||||||
|
| |____ > <| || (_) | | | (_) |
|
||||||
|
|______/_/\\_\\\\__\\___/|_|_|\\___/`
|
||||||
|
}
|
||||||
|
|
||||||
|
public registerDirective(directiveClass: Instantiable<Directive>) {
|
||||||
|
if ( !this.directives.includes(directiveClass) ) {
|
||||||
|
this.directives.push(directiveClass)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasDirective(directiveClass: Instantiable<Directive>) {
|
||||||
|
return this.directives.includes(directiveClass)
|
||||||
|
}
|
||||||
|
|
||||||
|
public getDirectives() {
|
||||||
|
return this.directives.clone()
|
||||||
|
}
|
||||||
|
}
|
39
src/service/CommandLineApplication.ts
Normal file
39
src/service/CommandLineApplication.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import {Unit, Logging} from "@extollo/lib"
|
||||||
|
import {Singleton, Inject} from "@extollo/di"
|
||||||
|
import {CommandLine} from "./CommandLine"
|
||||||
|
import {UsageDirective} from "../directive/UsageDirective";
|
||||||
|
import {Directive} from "../Directive";
|
||||||
|
import {ShellDirective} from "../directive/ShellDirective";
|
||||||
|
|
||||||
|
@Singleton()
|
||||||
|
export class CommandLineApplication extends Unit {
|
||||||
|
private static replacement?: typeof Unit
|
||||||
|
public static setReplacement(unitClass?: typeof Unit) {
|
||||||
|
this.replacement = unitClass
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
protected readonly cli!: CommandLine
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
protected readonly logging!: Logging
|
||||||
|
|
||||||
|
constructor() { super() }
|
||||||
|
|
||||||
|
public async up() {
|
||||||
|
this.cli.registerDirective(UsageDirective)
|
||||||
|
this.cli.registerDirective(ShellDirective)
|
||||||
|
|
||||||
|
const argv = process.argv.slice(2)
|
||||||
|
const match = this.cli.getDirectives()
|
||||||
|
.map(dirCls => this.make<Directive>(dirCls))
|
||||||
|
.firstWhere(dir => dir.matchesKeyword(argv[0]))
|
||||||
|
|
||||||
|
if ( match ) {
|
||||||
|
await match.invoke(argv.slice(1))
|
||||||
|
} else {
|
||||||
|
const usage = this.make<UsageDirective>(UsageDirective)
|
||||||
|
await usage.handle(process.argv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2
src/service/index.ts
Normal file
2
src/service/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './CommandLine'
|
||||||
|
export * from './CommandLineApplication'
|
Loading…
Reference in New Issue
Block a user