All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sumologic.shellbase.ShellCommand.scala Maven / Gradle / Ivy

The newest version!
/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package com.sumologic.shellbase

import com.sumologic.shellbase.cmdline.ArgumentTrackingOptions

import jline.console.completer.{Completer, NullCompleter, StringsCompleter}
import org.apache.commons.cli.{CommandLine, GnuParser, HelpFormatter, Options, ParseException, UnrecognizedOptionException}
import org.slf4j.LoggerFactory
import scala.collection.JavaConverters._
import scala.concurrent.{ExecutionContext, Future}

class ExitShellCommandException(message: String) extends RuntimeException(message)

/**
  * Definition of a shell command.
  */
abstract class ShellCommand(val name: String,
                            val helpText: String,
                            val aliases: List[String] = List[String](),
                            val deprecated: Boolean = false,
                            val hiddenInHelp: Boolean = false,
                            val usageText: Option[String] = None)
                           (implicit e: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global) {

  protected val _logger = LoggerFactory.getLogger(getClass)
  protected lazy val prompter = new ShellPrompter()

  private val _currentCommand = new ThreadLocal[String]()

  protected def currentCommand: String = _currentCommand.get()

  def executeLine(argumentsInput: List[String], commandPath: List[String] = List()): Boolean = {
    val (arguments, comments) = extractPossibleComments(argumentsInput)

    _currentCommand.set {
      val cmd = commandPath.mkString(" ").trim
      val args = arguments.mkString(" ").trim
      if (args.nonEmpty) {
        s"$cmd $args"
      } else {
        s"$cmd"
      }
    }
    var commandResult: Boolean = false
    var slackTsOpt: Option[String] = None
    val timeNow = System.currentTimeMillis()

    try {
      val cmdLine = parseOptions(arguments)

      validate(cmdLine) match {
        case Some(error) => {
          println(error)
          false
        }
        case None => {
          // do NOT block the command from executing
          Future {
            slackTsOpt = postCommandToSlack(commandPath, arguments, comments)
          }

          commandResult = execute(cmdLine)

          commandResult
        }
      }
    } catch {
      case e: IllegalArgumentException => {
        println(ShellColors.red(e.getMessage))
        _logger.debug("", e)
        false
      }
      case e: ExitShellCommandException => {
        println(ShellColors.red(e.getMessage))
        _logger.debug("", e)
        false
      }
      case e: UnrecognizedOptionException => {
        println(ShellColors.red(e.getMessage))
        println("To set negative numbers as property value then quote and prefixing it with a space. For ex. \" -1\"")
        help
        false
      }
      case e: ParseException => {
        println(ShellColors.red(e.getMessage))
        help
        false
      }
      case e: Throwable => {
        _logger.debug("", e)
        e.printStackTrace()
        false
      }
    } finally {
      Future{
        // we calculate how long it takes from starting the command to finishing the command
        val commandExecuteTimeDuration = (System.currentTimeMillis() - timeNow) / 60000
        slackTsOpt.foreach { ts =>
          postInformationToSlackThread(ts, commandExecuteTimeDuration, commandResult)
        }
      }

      _currentCommand.remove()
    }
  }

  // VisibleForTesting
  private[shellbase] def extractPossibleComments(arguments: List[String]): (List[String], Option[String]) = {
    val argumentStartingAComment = arguments.indexWhere(_.startsWith("#"))
    if (argumentStartingAComment > -1) {
      val split = arguments.splitAt(argumentStartingAComment)
      (split._1, Some(split._2.mkString(" ").substring(1).trim))
    } else {
      (arguments, None)
    }
  }

  /**
    * Execute the command. The command line passed in contains everything the user specified - options
    * and other arguments. Returns true if the command succeeded, false otherwise.
    */
  def execute(cmdLine: CommandLine): Boolean

  final def parseOptions(arguments: Seq[String]): CommandLine = {
    val options = new Options()
    addOptions(options)
    new GnuParser().parse(options, arguments.toArray)
  }

  def validate(cmdLine: CommandLine): Option[String] = {
    if (maxNumberOfArguments >= 0 && cmdLine.getArgs.size > maxNumberOfArguments) {
      if (maxNumberOfArguments == 0) {
        Some(s"Unexpected command line arguments: ${cmdLine.getArgs.mkString(" ")}")
      } else {
        Some(s"At most $maxNumberOfArguments arguments expected, but ${cmdLine.getArgs.size} received.")
      }
    } else {
      None
    }
  }

  def postCommandToSlack(cmdPath: List[String], args: List[String], comments: Option[String]): Option[String] = {
    None
  }

  def postInformationToSlackThread(ts: String,
                                   commandExecuteTimeDuration: Long,
                                   commandResult: Boolean): Option[String] = {
    None
  }

  // NOTE(stefan, 2012-03-01): -1 for unlimited.
  def maxNumberOfArguments = 0

  def addOptions(opts: Options) = {}

  /**
    * Returns a completer that jline can use for tab completion.
    */
  def argCompleter: Completer = new NullCompleter

  /**
    * Returns `true` iff notifications should be run for this command
    */
  def shouldRunNotifications(arguments: List[String], commandPath: List[String] = List()): Boolean = true

  // -----------------------------------------------------------------------------------------------
  // Internals.
  // -----------------------------------------------------------------------------------------------

  final def help = {
    val options = new ArgumentTrackingOptions
    addOptions(options)

    val arguments = options.getArguments.sortBy(_.index).map {
      opt => {
        if (opt.isRequired) {
          opt.name
        } else {
          s"[${opt.name}]"
        }
      }
    }

    val txt = s"$name ${arguments.mkString(" ")}"

    val fullHelpText: String = usageText match {
      case Some(text) => helpText + "\n\n" + text
      case _ => helpText
    }

    new HelpFormatter().printHelp(HelpFormatter.DEFAULT_WIDTH, txt, fullHelpText, options,
      null, // footer
      true) // print an automatically generated usage statement, instead of:
      //  usage: sleep period
      // It prints:
      //  usage: sleep period [-v]
  }

  def basicVariants: List[String] = List(name) ++ aliases

  final def completer = {
    new NestedCompleter(new StringsCompleter(basicVariants.asJavaCollection), argCompleter)
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy