From 9c8a1cd14ad8544e7ca8e8db2320ec152aabd676 Mon Sep 17 00:00:00 2001 From: Anthony Keenan Date: Tue, 9 Oct 2018 15:49:24 +0200 Subject: [PATCH] CORDA-2028 - Fix use of required paramers/options on command line (#4040) * Make required parameters work with --install-shell-extensions and make errors look a bit more errorey * Make blobinspector required parameter work the way it used to * Fix compilation Error --- .../net/corda/blobinspector/BlobInspector.kt | 7 +-- .../corda/blobinspector/BlobInspectorTest.kt | 2 +- .../net/corda/cliutils/CordaCliWrapper.kt | 59 +++++++++++++------ 3 files changed, 45 insertions(+), 23 deletions(-) diff --git a/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt b/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt index e65a7441be..8887247bf6 100644 --- a/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt +++ b/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt @@ -34,8 +34,8 @@ fun main(args: Array) { } class BlobInspector : CordaCliWrapper("blob-inspector", "Convert AMQP serialised binary blobs to text") { - @Parameters(index = "*..0", paramLabel = "SOURCE", description = ["URL or file path to the blob"], converter = [SourceConverter::class]) - var source: MutableList = mutableListOf() + @Parameters(index = "0", paramLabel = "SOURCE", description = ["URL or file path to the blob"], converter = [SourceConverter::class]) + var source: URL? = null @Option(names = ["--format"], paramLabel = "type", description = ["Output format. Possible values: [YAML, JSON]"]) private var formatType: OutputFormatType = OutputFormatType.YAML @@ -61,8 +61,7 @@ class BlobInspector : CordaCliWrapper("blob-inspector", "Convert AMQP serialised } fun run(out: PrintStream): Int { - require(source.count() == 1) { "You must specify URL or file path to the blob" } - val inputBytes = source.first().readBytes() + val inputBytes = source!!.readBytes() val bytes = parseToBinaryRelaxed(inputFormatType, inputBytes) ?: throw IllegalArgumentException("Error: this input does not appear to be encoded in Corda's AMQP extended format, sorry.") diff --git a/tools/blobinspector/src/test/kotlin/net/corda/blobinspector/BlobInspectorTest.kt b/tools/blobinspector/src/test/kotlin/net/corda/blobinspector/BlobInspectorTest.kt index 04cc72d621..65e1223c4f 100644 --- a/tools/blobinspector/src/test/kotlin/net/corda/blobinspector/BlobInspectorTest.kt +++ b/tools/blobinspector/src/test/kotlin/net/corda/blobinspector/BlobInspectorTest.kt @@ -53,7 +53,7 @@ class BlobInspectorTest { } private fun run(resourceName: String): String { - blobInspector.source = mutableListOf(javaClass.getResource(resourceName)) + blobInspector.source = javaClass.getResource(resourceName) val writer = StringWriter() blobInspector.run(PrintStream(WriterOutputStream(writer, UTF_8))) val output = writer.toString() diff --git a/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt b/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt index f30c3ed05d..5a35be8089 100644 --- a/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt +++ b/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt @@ -21,8 +21,6 @@ import java.util.concurrent.Callable interface Validated { companion object { val logger = contextLogger() - const val RED = "\u001B[31m" - const val RESET = "\u001B[0m" } /** @@ -36,8 +34,8 @@ interface Validated { fun validate() { val errors = validator() if (errors.isNotEmpty()) { - logger.error(RED + "Exceptions when parsing command line arguments:") - logger.error(errors.joinToString("\n") + RESET) + logger.error(ShellConstants.RED + "Exceptions when parsing command line arguments:") + logger.error(errors.joinToString("\n") + ShellConstants.RESET) CommandLine(this).usage(System.err) exitProcess(ExitCodes.FAILURE) } @@ -55,6 +53,11 @@ object CordaSystemUtils { fun getOsName(): String = System.getProperty(OS_NAME) } +object ShellConstants { + const val RED = "\u001B[31m" + const val RESET = "\u001B[0m" +} + fun CordaCliWrapper.start(args: Array) { this.args = args @@ -66,27 +69,47 @@ fun CordaCliWrapper.start(args: Array) { cmd.registerConverter(Path::class.java) { Paths.get(it).toAbsolutePath().normalize() } cmd.commandSpec.name(alias) cmd.commandSpec.usageMessage().description(description) + cmd.commandSpec.parser().collectErrors(true) try { - val defaultAnsiMode = if (CordaSystemUtils.isOsWindows()) { Help.Ansi.ON } else { Help.Ansi.AUTO } - val results = cmd.parseWithHandlers(RunLast().useOut(System.out).useAnsi(defaultAnsiMode), - DefaultExceptionHandler>().useErr(System.err).useAnsi(defaultAnsiMode), - *args) - // If an error code has been returned, use this and exit - results?.firstOrNull()?.let { - if (it is Int) { - exitProcess(it) - } else { + val defaultAnsiMode = if (CordaSystemUtils.isOsWindows()) { + Help.Ansi.ON + } else { + Help.Ansi.AUTO + } + + val results = cmd.parse(*args) + val app = cmd.getCommand() + if (cmd.isUsageHelpRequested) { + cmd.usage(System.out, defaultAnsiMode) + exitProcess(ExitCodes.SUCCESS) + } + if (cmd.isVersionHelpRequested) { + cmd.printVersionHelp(System.out, defaultAnsiMode) + exitProcess(ExitCodes.SUCCESS) + } + if (app.installShellExtensionsParser.installShellExtensions) { + System.out.println("Install shell extensions: ${app.installShellExtensionsParser.installShellExtensions}") + // ignore any parsing errors and run the program + exitProcess(app.call()) + } + val allErrors = results.flatMap { it.parseResult?.errors() ?: emptyList() } + if (allErrors.any()) { + val parameterExceptions = allErrors.asSequence().filter { it is ParameterException } + if (parameterExceptions.any()) { + System.err.println("${ShellConstants.RED}${parameterExceptions.map{ it.message }.joinToString()}${ShellConstants.RESET}") + parameterExceptions.filter { it is UnmatchedArgumentException}.forEach { (it as UnmatchedArgumentException).printSuggestions(System.out) } + usage(cmd, System.out, defaultAnsiMode) exitProcess(ExitCodes.FAILURE) } + throw allErrors.first() } - // If no results returned, picocli ran something without invoking the main program, e.g. --help or --version, so exit successfully - exitProcess(ExitCodes.SUCCESS) - } catch (e: ExecutionException) { + exitProcess(app.call()) + } catch (e: Exception) { val throwable = e.cause ?: e if (this.verbose) { throwable.printStackTrace() } else { - System.err.println("*ERROR*: ${throwable.rootMessage ?: "Use --verbose for more details"}") + System.err.println("${ShellConstants.RED}${throwable.rootMessage ?: "Use --verbose for more details"}${ShellConstants.RESET}") } exitProcess(ExitCodes.FAILURE) } @@ -126,7 +149,7 @@ abstract class CordaCliWrapper(val alias: String, val description: String) : Cal var loggingLevel: Level = Level.INFO @Mixin - private lateinit var installShellExtensionsParser: InstallShellExtensionsParser + lateinit var installShellExtensionsParser: InstallShellExtensionsParser // This needs to be called before loggers (See: NodeStartup.kt:51 logger called by lazy, initLogging happens before). // Node's logging is more rich. In corda configurations two properties, defaultLoggingLevel and consoleLogLevel, are usually used.