From e5c7e6995b4307dce8eaafabc1a9d9dc89aa1e90 Mon Sep 17 00:00:00 2001 From: James Ayvaz Date: Sat, 19 May 2018 07:51:04 -0700 Subject: [PATCH] type-safe builders for defining command line interfaces --- README.md | 43 +++++++++++++++++++ .../kotlin/kotlinx/cli/examples/Example2.kt | 35 +++++++++++++++ .../kotlinx/cli/CommandLineApplication.kt | 27 ++++++++++++ .../kotlin/kotlinx/cli/CommandLineContext.kt | 13 ++++++ .../kotlin/kotlinx/cli/CommandLineError.kt | 3 ++ .../kotlinx/cli/CommandLineInterface.kt | 2 +- 6 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 kotlinx.cli.jvm/src/main/kotlin/kotlinx/cli/examples/Example2.kt create mode 100644 src/main/kotlin/kotlinx/cli/CommandLineApplication.kt create mode 100644 src/main/kotlin/kotlinx/cli/CommandLineContext.kt create mode 100644 src/main/kotlin/kotlinx/cli/CommandLineError.kt diff --git a/README.md b/README.md index 2364383..8344faa 100644 --- a/README.md +++ b/README.md @@ -69,3 +69,46 @@ Args: [-r, 16, CAFE, BABE, DEAD, BEEF, --sum] Integers: [51966, 47806, 57005, 48879] Sum: 205656 ``` + +## Type-Safe Builders (DSL) syntax + +kotlinx.cli also provides type-safe builders to make defining commands easy. +Useful if you have multiple commands you want to implement. + +```kotlin +package kotlinx.cli.examples + +import kotlinx.cli.* +import kotlin.system.exitProcess + +fun main(args: Array) { + + val cli = command { + commandName = "Example2" + + // Define commandName-line interface + val integers by positionalArgumentsList("N+", "Integers", minArgs = 1) + val radix by flagValueArgument("-r", "radix", "Input numbers radix", 10) { it.toInt() } + val sum by flagArgument("--sum", "Print sum") + val max by flagArgument("--max", "Print max") + val min by flagArgument("--min", "Print min") + + // main block is where you do something useful + main { + if (!sum || !max || !min) { + // CommandLineContext::exitProcess() looks like standard kotlin + exitProcess(1) + } + + val ints = integers.map { it.toInt(radix) } + println("Args: ${args.asList()}") + println("Integers: $ints") + if (sum) println("Sum: ${ints.sum()}") + if (max) println("Max: ${ints.max()}") + if (min) println("Min: ${ints.min()}") + } + } + + val exitCode = cli.run(args) + exitProcess(exitCode) +} diff --git a/kotlinx.cli.jvm/src/main/kotlin/kotlinx/cli/examples/Example2.kt b/kotlinx.cli.jvm/src/main/kotlin/kotlinx/cli/examples/Example2.kt new file mode 100644 index 0000000..86ef6b8 --- /dev/null +++ b/kotlinx.cli.jvm/src/main/kotlin/kotlinx/cli/examples/Example2.kt @@ -0,0 +1,35 @@ +package kotlinx.cli.examples + +import kotlinx.cli.* +import kotlin.system.exitProcess + +fun main(args: Array) { + val cli = command { + commandName = "Example2" + + // Define commandName-line interface + val integers by positionalArgumentsList("N+", "Integers", minArgs = 1) + val radix by flagValueArgument("-r", "radix", "Input numbers radix", 10) { it.toInt() } + val sum by flagArgument("--sum", "Print sum") + val max by flagArgument("--max", "Print max") + val min by flagArgument("--min", "Print min") + + // main block is where you do something useful + main { + // Do something useful + if (!sum || !max || !min) { + exitProcess(1) // CommandLineContext::exitProcess() looks like standard kotlin + } + + val ints = integers.map { it.toInt(radix) } + println("Args: ${args.asList()}") + println("Integers: $ints") + if (sum) println("Sum: ${ints.sum()}") + if (max) println("Max: ${ints.max()}") + if (min) println("Min: ${ints.min()}") + } + } + + val exitCode = cli.run(args) + exitProcess(exitCode) +} \ No newline at end of file diff --git a/src/main/kotlin/kotlinx/cli/CommandLineApplication.kt b/src/main/kotlin/kotlinx/cli/CommandLineApplication.kt new file mode 100644 index 0000000..982f8d6 --- /dev/null +++ b/src/main/kotlin/kotlinx/cli/CommandLineApplication.kt @@ -0,0 +1,27 @@ +package kotlinx.cli + +class CommandLineApplication: CommandLineInterface("") { + private lateinit var exec: CommandLineContext.(args: Array) -> Unit + + /** + * The main fun signature is intended to look like standard main fun so that it is easy to adopt + */ + infix fun main(func: CommandLineContext.(args: Array) -> Unit) { + exec = func + } + + fun run(args: Array): Int { + return try { + // parse the command line arguments + parse(args) + CommandLineContext().apply { exec(args) }.statusCode + } catch (exitProcess: CommandLineError) { + exitProcess.status + } catch (throwable: Throwable) { + 1 + } + } +} + +fun command(func: CommandLineApplication.() -> Unit): CommandLineApplication = + CommandLineApplication().apply(func) \ No newline at end of file diff --git a/src/main/kotlin/kotlinx/cli/CommandLineContext.kt b/src/main/kotlin/kotlinx/cli/CommandLineContext.kt new file mode 100644 index 0000000..1f792a1 --- /dev/null +++ b/src/main/kotlin/kotlinx/cli/CommandLineContext.kt @@ -0,0 +1,13 @@ +package kotlinx.cli + +/** + * This class executes the main{} block and defines exitProcess so that Process::exitProcess isn't called + * directly. + */ +class CommandLineContext { + var statusCode: Int = 0 + + fun exitProcess(status: Int): Int { + throw CommandLineError(status) + } +} \ No newline at end of file diff --git a/src/main/kotlin/kotlinx/cli/CommandLineError.kt b/src/main/kotlin/kotlinx/cli/CommandLineError.kt new file mode 100644 index 0000000..dbc4c21 --- /dev/null +++ b/src/main/kotlin/kotlinx/cli/CommandLineError.kt @@ -0,0 +1,3 @@ +package kotlinx.cli + +class CommandLineError(val status: Int) : Throwable() diff --git a/src/main/kotlin/kotlinx/cli/CommandLineInterface.kt b/src/main/kotlin/kotlinx/cli/CommandLineInterface.kt index f2efb13..3e438a9 100644 --- a/src/main/kotlin/kotlinx/cli/CommandLineInterface.kt +++ b/src/main/kotlin/kotlinx/cli/CommandLineInterface.kt @@ -1,7 +1,7 @@ package kotlinx.cli open class CommandLineInterface( - val commandName: String, + var commandName: String, private val usage: String? = null, private val description: String? = null, private val epilogue: String? = null,