/**
* @module flitter-cli/CliAppUnit
*/
const Unit = require('libflitter/Unit')
const PositionalOption = require('./options/PositionalOption')
const FlagOption = require('./options/FlagOption')
/**
* Final unit to run the ./flitter CLI utility. This will replace the default {@link module:libflitter/app/AppUnit~AppUnit}.
*/
class CliAppUnit extends Unit {
/**
* Defines the services required by this unit.
* @returns {Array<string>}
*/
static get services() {
return [...super.services, 'cli', 'utility', 'output']
}
/**
* Get the name of the service provided by this unit: 'CliApp'
* @returns {string} - 'CliApp'
*/
static get name() {
return 'CliApp'
}
/**
* Loads the unit. Retrieves the CLI arguments and passes control off to the appropriate {@link module:flitter-cli/Directive~Directive} handler.
* @param {module:libflitter/app/FlitterApp~FlitterApp} app - the Flitter app
* @param {Array<string>} [argv = process.argv.slice(2)] - the CLI argv to be interpreted
* @returns {Promise<void>}
*/
async go(app){
const argv = process.argv.slice(2)
const directives = this.cli.loaded_directives
let success = false
for ( let class_name in directives ){
if ( argv[0] === directives[class_name].name() ){
await this.handle_directive(app, directives[class_name], argv.slice(1))
// const directive = new directives[class_name]()
// await directive.handle(app, argv.slice(1))
success = true
break
}
}
if ( !success ){
const directive = new directives.UsageDirective()
await directive.handle(app, argv.slice(1))
}
}
/**
* Handles an invocation of the specified directive class with the provided arguments.
* @param {module:libflitter/app/FlitterApp~FlitterAPp} app - the Flitter app
* @param {module:flitter-cli/Directive~Directive} DirectiveClass - the static CLASS of the directive
* @param {Array<string>} args - the raw CLI arguments
* @returns {Promise<void>}
*/
async handle_directive(app, DirectiveClass, args) {
app.make(DirectiveClass)
// Create the option classes for the directive
const options = DirectiveClass.options()
if ( !options ) {
const directive_instance = new DirectiveClass({})
await directive_instance.handle(app, args)
return
}
const option_classes = []
options.forEach(option => {
if ( typeof option === 'string' ) {
// Create option from string
option_classes.push(this.option_from_string(option))
} else {
// Assume the option is already created
option_classes.push(option)
}
})
if ( this.requested_usage(args) ) {
this.output.info(`DIRECTIVE: ${DirectiveClass.name()} - ${DirectiveClass.help()}`, 0)
const positional_arguments = option_classes.filter(Class => Class instanceof PositionalOption)
const flag_arguments = option_classes.filter(Class => Class instanceof FlagOption)
this.output.info('', 0)
this.output.info(`USAGE: ${DirectiveClass.name()} ${positional_arguments.map(x => '{'+x.argument_name+'}').join(' ')}${flag_arguments.length > 0 ? ' [flag arguments]' : ''}`, 0)
this.output.info('', 0)
if ( positional_arguments.length > 0 ) this.output.info(`POSITIONAL ARGUMENTS:`, 0)
for ( const option of positional_arguments ) {
this.output.info(` {${option.argument_name}}${option.message ? ' - '+option.message : ''}`, 0)
}
this.output.info('', 0)
if ( flag_arguments.length > 0 ) this.output.info('FLAG ARGUMENTS:', 0)
for ( const option of flag_arguments ) {
this.output.info(` ${option.short_flag ? option.short_flag+', ' : ''}${option.long_flag ? option.long_flag : ''}${option.argument_description ? ' {'+option.argument_description+'}' : ''}${option.message ? ' - '+option.message : ''}`, 0)
}
} else {
try {
const option_values = this.process_options(option_classes, args)
const directive_instance = new DirectiveClass(option_values)
await directive_instance.handle(app, args)
} catch (e) {
this.output.error(e.message, 0)
if (e.requirements) {
for (const string of e.requirements) {
this.output.error(' - ' + string, 0)
}
}
}
}
}
/**
* Determines if, at any point in the arguments, the help option's short or long flag appears.
* @param {Array<string>} args - the raw CLI args
* @returns {boolean} - true if the help flag appeared
*/
requested_usage(args) {
const help_option = this.get_help_option()
for ( const arg of args ) {
if ( arg.trim() === help_option.long_flag || arg.trim() === help_option.short_flag ) {
return true
}
}
return false
}
/**
* Get the flag option that signals help. Usually, this is named 'help'
* and supports the flags '--help' and '-?'.
* @returns {module:flitter-cli/options/FlagOption~FlagOption}
*/
get_help_option() {
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.
* @param {Array<module:flitter-cli/option/Option~Option>} option_classes - array of option definitions
* @param {Array<string>} args - the raw CLI args
* @returns {object} - the parsed options, by name
*/
process_options(option_classes, args) {
let positional_arguments = option_classes.filter(Class => Class instanceof PositionalOption)
const flag_arguments = option_classes.filter(Class => Class instanceof FlagOption)
const option_values = {}
flag_arguments.push(this.get_help_option())
let expecting_flag_argument = false
let positional_flag_name = false
for ( const value of args ) {
if ( value.startsWith('--') ) {
if ( expecting_flag_argument ) {
throw new Error('Unexpected flag argument. Expecting argument for flag: '+positional_flag_name)
} else {
const flag_argument = flag_arguments.filter(x => x.long_flag === value)
if ( flag_argument.length < 1 ) {
throw new Error('Unknown flag argument: '+value)
} else {
if ( flag_argument[0].argument_description ) {
positional_flag_name = flag_argument[0].argument_name
expecting_flag_argument = true
} else {
option_values[flag_argument[0].argument_name] = true
}
}
}
} else if ( value.startsWith('-') ) {
if ( expecting_flag_argument ) {
throw new Error('Unexpected flag argument. Expecting argument for flag: '+positional_flag_name)
} else {
const flag_argument = flag_arguments.filter(x => x.short_flag === value)
if ( flag_argument.length < 1 ) {
throw new Error('Unknown flag argument: '+value)
} else {
if ( flag_argument[0].argument_description ) {
positional_flag_name = flag_argument[0].argument_name
expecting_flag_argument = true
} else {
option_values[flag_argument[0].argument_name] = true
}
}
}
} else if ( expecting_flag_argument ) {
const inferred_value = this.utility.infer(value)
const option_instance = flag_arguments.filter(x => x.argument_name === positional_flag_name)[0]
if ( !option_instance.validate(inferred_value) ) {
const e = new Error('Invalid value for argument: '+positional_flag_name)
e.requirements = option_instance.requirement_displays
throw e
}
option_values[positional_flag_name] = inferred_value
expecting_flag_argument = false
} else {
if ( positional_arguments.length < 1 ) {
throw new Error('Unknown positional argument: '+value)
} else {
const inferred_value = this.utility.infer(value)
if ( !positional_arguments[0].validate(inferred_value) ) {
const e = new Error('Invalid value for argument: '+positional_arguments[0].argument_name)
e.requirements = positional_arguments[0].requirement_displays
throw e
}
option_values[positional_arguments[0].argument_name] = this.utility.infer(value)
positional_arguments = positional_arguments.slice(1)
}
}
}
if ( expecting_flag_argument ) {
throw new Error('Missing argument for flag: '+positional_flag_name)
}
if ( positional_arguments.length > 0 ) {
throw new Error('Missing required argument: '+positional_arguments[0].argument_name)
}
return option_values
}
/**
* Create an instance of {@link module:flitter-cli/options/Option~Option}
* 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
* @returns {module:flitter-cli/options/FlagOption~FlagOption|module:flitter-cli/options/PositionalOption~PositionalOption}
*/
option_from_string(string) {
if ( string.startsWith('{') ) {
// The string is a positional argument
const string_parts = string.split('|').map(x => x.trim())
const name = string_parts[0].replace(/\{|\}/g, '')
return string_parts.length > 1 ? (new PositionalOption(name, string_parts[1])) : (new PositionalOption(name))
} else {
// The string is a flag argument
const string_parts = string.split('|').map(x => x.trim())
// Parse the flag parts first
const has_argument = string_parts[0].indexOf('{') >= 0
const flag_string = has_argument ? string_parts[0].substr(0, string_parts[0].indexOf('{')).trim() : string_parts[0].trim()
const flag_parts = flag_string.split(' ')
let long_flag = flag_parts[0].startsWith('--') ? flag_parts[0] : false
if ( !long_flag && flag_parts.length > 1 ) {
if ( flag_parts[1].startsWith('--') ) {
long_flag = flag_parts[1]
}
}
let short_flag = flag_parts[0].length === 2 ? flag_parts[0] : false
if ( !short_flag && flag_parts.length > 1 ) {
if ( flag_parts[1].length === 2 ) {
short_flag = flag_parts[1]
}
}
const argument_description = has_argument ? string_parts[0].substring(string_parts[0].indexOf('{')+1, string_parts[0].indexOf('}')) : false
const description = string_parts.length > 1 ? string_parts[1] : false
return new FlagOption(long_flag, short_flag, description, argument_description)
}
}
}
module.exports = exports = CliAppUnit