cmr1/node-cli

View on GitHub
lib/cli.js

Summary

Maintainability
A
25 mins
Test Coverage
// 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