Skip to content

Commit

Permalink
cli: Use clikt instead of JCommander for command line parsing
Browse files Browse the repository at this point in the history
In contrast to claims by the maintainer, JCommander is not really
well-maintained anymore [1], see e.g. the number of open PRs and the
recent releases which lack proper release notes and Git tags.

Use clikt [2] instead for its native Kotlin features and maintainer's
responsiveness.

[1] cbeust/jcommander#466
[2] https://kotlin.link/?q=clikt

Signed-off-by: Sebastian Schuberth <[email protected]>
  • Loading branch information
sschuberth committed Jan 23, 2020
1 parent c5b8499 commit 8efd208
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 183 deletions.
6 changes: 3 additions & 3 deletions cli/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
* License-Filename: LICENSE
*/

val cliktVersion: String by project
val config4kVersion: String by project
val jcommanderVersion: String by project
val kotlintestVersion: String by project
val log4jCoreVersion: String by project
val reflectionsVersion: String by project
Expand All @@ -31,7 +31,7 @@ plugins {

application {
applicationName = "ort"
mainClassName = "com.here.ort.OrtMain"
mainClassName = "com.here.ort.OrtMainKt"
}

tasks.named<CreateStartScripts>("startScripts") {
Expand Down Expand Up @@ -60,7 +60,7 @@ dependencies {
implementation(project(":scanner"))
implementation(project(":utils"))

implementation("com.beust:jcommander:$jcommanderVersion")
implementation("com.github.ajalt:clikt:$cliktVersion")
implementation("io.github.config4k:config4k:$config4kVersion")
implementation("org.apache.logging.log4j:log4j-core:$log4jCoreVersion")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
Expand Down
62 changes: 0 additions & 62 deletions cli/src/main/kotlin/CommandWithHelp.kt

This file was deleted.

218 changes: 100 additions & 118 deletions cli/src/main/kotlin/OrtMain.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,24 @@

package com.here.ort

import com.beust.jcommander.DynamicParameter
import com.beust.jcommander.JCommander
import com.beust.jcommander.Parameter
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.context
import com.github.ajalt.clikt.core.findObject
import com.github.ajalt.clikt.core.subcommands
import com.github.ajalt.clikt.output.CliktHelpFormatter
import com.github.ajalt.clikt.output.HelpFormatter
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.multiple
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.pair
import com.github.ajalt.clikt.parameters.options.switch
import com.github.ajalt.clikt.parameters.options.versionOption
import com.github.ajalt.clikt.parameters.types.file

import com.here.ort.commands.*
import com.here.ort.model.Environment
import com.here.ort.model.config.OrtConfiguration
import com.here.ort.utils.PARAMETER_ORDER_LOGGING
import com.here.ort.utils.PARAMETER_ORDER_OPTIONAL
import com.here.ort.utils.getUserOrtDirectory
import com.here.ort.utils.expandTilde
import com.here.ort.utils.printStackTrace
Expand All @@ -36,10 +45,6 @@ import com.typesafe.config.ConfigFactory

import io.github.config4k.extract

import java.io.File

import kotlin.system.exitProcess

import org.apache.logging.log4j.Level
import org.apache.logging.log4j.core.config.Configurator

Expand All @@ -53,126 +58,79 @@ const val ORT_USER_HOME_ENV = "ORT_USER_HOME"
/**
* The main entry point of the application.
*/
object OrtMain : CommandWithHelp() {
@Parameter(
description = "The path to a configuration file.",
names = ["--config", "-c"],
order = PARAMETER_ORDER_OPTIONAL
)
private var configFile: File? = null

@Parameter(
description = "Enable info logging.",
names = ["--info"],
order = PARAMETER_ORDER_LOGGING
)
private var info = false

@Parameter(
description = "Enable debug logging and keep any temporary files.",
names = ["--debug"],
order = PARAMETER_ORDER_LOGGING
)
private var debug = false

@Parameter(
description = "Print out the stacktrace for all exceptions.",
names = ["--stacktrace"],
order = PARAMETER_ORDER_LOGGING
)
private var stacktrace = false

@Parameter(
description = "Show version information and exit.",
names = ["--version", "-v"],
order = PARAMETER_ORDER_OPTIONAL
)
private var version = false

@DynamicParameter(
description = "Allows to override configuration parameters.",
names = ["-P"]
)
private var configArguments = mutableMapOf<String, String>()

/**
* The entry point for the application.
*
* @param args The list of application arguments.
*/
@JvmStatic
fun main(args: Array<String>) {
fixupUserHomeProperty()
exitProcess(run(args))
class OrtMain : CliktCommand(name = TOOL_NAME, epilog = "* denotes required options.") {
private val configFile by option("--config", "-c", help = "The path to a configuration file.")
.file(exists = true, fileOkay = true, folderOkay = false, writable = false, readable = true)

private val logLevel by option(help = "Set the verbosity level of log output.").switch(
"--info" to Level.INFO,
"--debug" to Level.DEBUG
).default(Level.WARN)

private val stacktrace by option(help = "Print out the stacktrace for all exceptions.").flag()

private val configArguments by option("-P", help = "Allows to override configuration parameters.").pair().multiple()

private val env = Environment()

private inner class OrtHelpFormatter : CliktHelpFormatter(requiredOptionMarker = "*", showDefaultValues = true) {
override fun formatHelp(
prolog: String,
epilog: String,
parameters: List<HelpFormatter.ParameterHelp>,
programName: String
) =
buildString {
// If help is invoked without a subcommand, the main run() is not invoked and no header is printed, so
// we need to to that manually here.
if (context.invokedSubcommand == null) appendln(getVersionHeader(env.ortVersion))
append(super.formatHelp(prolog, epilog, parameters, programName))
}
}

/**
* Check if the "user.home" property is set to a sane value and otherwise set it to the value of the ORT_USER_HOME
* environment variable, if set, or to the value of an (OS-specific) environment variable for the user home
* directory. This works around the issue that esp. in certain Docker scenarios "user.home" is set to "?", see
* https://bugs.openjdk.java.net/browse/JDK-8193433 for some background information.
*/
fun fixupUserHomeProperty() {
val userHome = System.getProperty("user.home")
val checkedUserHome = sequenceOf(
userHome,
System.getenv(ORT_USER_HOME_ENV),
System.getenv("HOME"),
System.getenv("USERPROFILE")
).first {
it != null && it.isNotBlank() && it != "?"
init {
context {
expandArgumentFiles = false
helpFormatter = OrtHelpFormatter()
}

if (checkedUserHome != userHome) System.setProperty("user.home", checkedUserHome)
}

/**
* Run the ORT CLI with the provided [args] and return the exit code of [CommandWithHelp.run].
*/
fun run(args: Array<String>): Int {
val jc = JCommander(this).apply {
programName = TOOL_NAME
setExpandAtSign(false)
addCommand(AnalyzerCommand)
addCommand(ClearlyDefinedUploadCommand)
addCommand(DownloaderCommand)
addCommand(EvaluatorCommand)
addCommand(ReporterCommand)
addCommand(RequirementsCommand)
addCommand(ScannerCommand)
parse(*args)
// Make the OrtConfiguration available to subcommands.
findObject {
loadConfig()
}

println(getVersionHeader(jc.parsedCommand))

val config = loadConfig()

return if (version) 0 else run(jc, config)
subcommands(
//AnalyzerCommand(),
//ClearlyDefinedUploadCommand(),
//DownloaderCommand(),
//EvaluatorCommand(),
//ReporterCommand(),
//RequirementsCommand(),
//ScannerCommand()
)

versionOption(
version = env.ortVersion,
names = setOf("--version", "-v"),
help = "Show version information and exit.",
message = ::getVersionHeader
)
}

override fun runCommand(jc: JCommander, config: OrtConfiguration): Int {
when {
debug -> Configurator.setRootLevel(Level.DEBUG)
info -> Configurator.setRootLevel(Level.INFO)
}
override fun run() {
Configurator.setRootLevel(logLevel)

// Make the parameter globally available.
printStackTrace = stacktrace

// JCommander already validates the command names.
val command = jc.commands[jc.parsedCommand]!!
val commandObject = command.objects.first() as CommandWithHelp

// Delegate running actions to the specified command.
return commandObject.run(jc, config)
println(getVersionHeader(env.ortVersion))
}

private fun getVersionHeader(commandName: String?): String {
val env = Environment()

private fun getVersionHeader(version: String): String {
val variables = mutableListOf("$ORT_USER_HOME_ENV = ${getUserOrtDirectory()}")
env.variables.entries.mapTo(variables) { (key, value) -> "$key = $value" }

val commandName = context.invokedSubcommand?.commandName
val command = commandName?.let { " '$commandName'" }.orEmpty()
val with = if (variables.isNotEmpty()) " with" else "."

Expand All @@ -181,7 +139,7 @@ object OrtMain : CommandWithHelp() {
val header = mutableListOf<String>()
"""
________ _____________________
\_____ \\______ \__ ___/ the OSS Review Toolkit, version ${env.ortVersion}.
\_____ \\______ \__ ___/ the OSS Review Toolkit, version $version.
/ | \| _/ | | Running$command under Java ${env.javaVersion} on ${env.os}$with
/ | \ | \ | | ${variables.getOrElse(variableIndex++) { "" }}
\_______ /____|_ / |____| ${variables.getOrElse(variableIndex++) { "" }}
Expand All @@ -198,12 +156,8 @@ object OrtMain : CommandWithHelp() {
}

private fun loadConfig(): OrtConfiguration {
val argsConfig = ConfigFactory.parseMap(configArguments, "Command line").withOnlyPath("ort")
val argsConfig = ConfigFactory.parseMap(configArguments.toMap(), "Command line").withOnlyPath("ort")
val fileConfig = configFile?.expandTilde()?.let {
require(it.isFile) {
"The provided configuration file '$it' is not actually a file."
}

ConfigFactory.parseFile(it).withOnlyPath("ort")
}
val defaultConfig = ConfigFactory.parseResources("default.conf")
Expand All @@ -217,3 +171,31 @@ object OrtMain : CommandWithHelp() {
return combinedConfig.extract("ort")
}
}

/**
* Check if the "user.home" property is set to a sane value and otherwise set it to the value of the ORT_USER_HOME
* environment variable, if set, or to the value of an (OS-specific) environment variable for the user home
* directory. This works around the issue that esp. in certain Docker scenarios "user.home" is set to "?", see
* https://bugs.openjdk.java.net/browse/JDK-8193433 for some background information.
*/
fun fixupUserHomeProperty() {
val userHome = System.getProperty("user.home")
val checkedUserHome = sequenceOf(
userHome,
System.getenv(ORT_USER_HOME_ENV),
System.getenv("HOME"),
System.getenv("USERPROFILE")
).first {
it != null && it.isNotBlank() && it != "?"
}

if (checkedUserHome != userHome) System.setProperty("user.home", checkedUserHome)
}

/**
* The entry point for the application with [args] being the list of arguments.
*/
fun main(args: Array<String>) {
fixupUserHomeProperty()
return OrtMain().main(args)
}
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ versionsPluginVersion = 0.27.0
antlrVersion = 4.8-1
apachePoiSchemasVersion = 1.4
apachePoiVersion = 3.17
cliktVersion = 2.3.0
commonsCompressVersion = 1.19
config4kVersion = 0.4.1
cyclonedxCoreJavaVersion = 2.5.1
Expand Down

0 comments on commit 8efd208

Please sign in to comment.