lib/cli.js
// Require extend for merging settings
const extend = require('extend')
// Require basic Logger class to extend from
const Logger = require('cmr1-logger')
// Require command-line-args to parse options from process.argv
const clArgs = require('command-line-args')
// Require command-line-usage to pair with clArgs to display usage info
const getUsage = require('command-line-usage')
// Require readline-sync for easy use of CLI input
const readLineSync = require('readline-sync')
/**
* Cli Helper Class
*/
class Cli extends Logger {
/**
* Create a Cli object
* @param {object} settings - The settings for this Cli object
*/
constructor (settings = {}) {
super()
// Most things happen here, wrap it all up
try {
// Initialize options and settings
this.options = {}
this.settings = Object.assign({}, Cli.DEFAULT_SETTINGS) // Copys defaults settings into a NEW OBEJCT
// Create Cli "alias" functions for reading input
this.select = readLineSync.keyInSelect
this.prompt = readLineSync.question
this.confirm = readLineSync.keyInYN
// Merge provided settings with defaults
this.mergeSettings(settings)
// Enable the logging based on settings
this.enableLogging(this.settings.logging)
// Parse and set options based on settings
this.setOptions()
// Catch an exception (Error)
} catch (e) {
// If we've already failed, force error output without throwing another Error
this.options.force = true
// Reference to logger function (
// Verify function exists - thrown Error may have been from enableLogging() function
const logger = (typeof this.error === 'function' ? this.error : console.error)
// Print message using logger (or entire error object if message is undefined)
logger(e.message || e)
// Show the help menu & exit the process
this.showHelp()
}
// Show the help menu & exit the process
if (this.options.help) {
this.showHelp()
} else if (this.options.version) {
this.showVersion()
}
}
/**
* Merge settings from constructor with defaults
* @param {object} settings - The settings for this Cli object
*/
mergeSettings (settings = {}) {
// Only attempt to merge objects
if (typeof settings === 'object') {
// Loop through all keys in the provided settings object
Object.keys(settings).forEach(key => {
// Obtain the value from the current key
const val = settings[key]
// If the types of the values match, merge them
if (typeof this.settings[key] === typeof val) {
switch (typeof val) {
// If the value is an object...
case 'object':
// If the both the default and the provided value are arrays, concat the two
if (Array.isArray(this.settings[key]) && Array.isArray(val)) {
this.settings[key] = this.settings[key].concat(val)
// If neither of the values are arrays, extend then (recursively)
} else if (!Array.isArray(this.settings[key]) && !Array.isArray(val)) {
this.settings[key] = extend(true, this.settings[key], val)
// Otherwise, we're attempting to merge incompatible settings!
} else {
throw new Error(`Cannot override setting: '${key}' ... Type did not match!`)
}
break
// Otherwise, overwrite the value
default:
this.settings[key] = val
break
}
// If there was no default, copy the new setting value!
} else if (typeof this.settings[key] === 'undefined') {
this.settings[key] = val
}
})
} else {
// Throw an Error if settings argument is not an object
throw new Error('Invalid settings provided to merge function!')
}
}
/**
* Set the options from the process argument list (process.argv)
*/
setOptions () {
// Set options on this instance using clArgs (command-line-args)
this.options = clArgs(this.settings.optionDefinitions)
}
/**
* Get parsed CLI options
*/
getOptions () {
// Return the options object
return this.options
}
/**
* Return the JSON structure for the usage message
*/
getHelpSections () {
const sections = [
{
header: this.getVersionString(), // String - Main usage screen header
content: this.settings.description // String - Main usage screen description
}
]
Object.keys(this.settings.helpSections).forEach(header => {
let content = this.settings.helpSections[header]
if (Array.isArray(content)) {
content = content.join('\n')
}
sections.push({
header: header.charAt(0).toUpperCase() + header.slice(1),
content
})
})
sections.push({
header: this.settings.helpHeader, // String - Options header
optionList: this.settings.optionDefinitions // Array - Option definitions
})
// Build simple config for command-line-usage module
return sections
}
/**
* Show the help menu & exit
*/
showHelp () {
// Use console.log() for help (in case logging hasn't been enabled)
console.log(getUsage(this.getHelpSections()))
// Exit the process
process.exit(0)
}
getVersionString () {
return `${this.settings.name} v${this.settings.version}`
}
/**
* Show the name and current version
*/
showVersion () {
// Show name and version
console.log(this.getVersionString())
// Exit the process
process.exit(0)
}
/**
* Return the default settings from JSON file
*/
static get DEFAULT_SETTINGS () {
return require('../settings.json')
}
}
// Export Cli class
module.exports = Cli