diff --git a/.travis.yml b/.travis.yml index 0e8615b71..28aa0c661 100755 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: java jdk: - - oraclejdk8 + - openjdk8 script: ./gradlew :clikt:check --info --stacktrace --console=plain --max-workers=1 --no-daemon -Dkotlin.compiler.execution.strategy="in-process" -Dkotlin.colors.enabled=false diff --git a/CHANGELOG.md b/CHANGELOG.md index 37a0e90df..1fa07bf80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [Unreleased] ### Changed - There are now several ways of [preventing @-file expansion](https://ajalt.github.io/clikt/arguments/#preventing-file-expansion) +- added `enum()` type for parameter values to simplify enum handling ## [2.1.0] - 2019-05-23 ### Added diff --git a/clikt/src/main/kotlin/com/github/ajalt/clikt/parameters/types/enum.kt b/clikt/src/main/kotlin/com/github/ajalt/clikt/parameters/types/enum.kt new file mode 100644 index 000000000..a575b0dfc --- /dev/null +++ b/clikt/src/main/kotlin/com/github/ajalt/clikt/parameters/types/enum.kt @@ -0,0 +1,33 @@ +package com.github.ajalt.clikt.parameters.types + +import com.github.ajalt.clikt.completion.CompletionCandidates +import com.github.ajalt.clikt.core.BadParameterValue +import com.github.ajalt.clikt.parameters.arguments.RawArgument +import com.github.ajalt.clikt.parameters.arguments.convert +import com.github.ajalt.clikt.parameters.options.RawOption +import com.github.ajalt.clikt.parameters.options.convert + +// this function needs to be accessible, because `enum()` needs to be inlined for the reified type. +// see also https://stackoverflow.com/a/41905907 +@PublishedApi +internal fun completions(values: Array) = + CompletionCandidates.Fixed(values.map { it.toString() }.toSet()) + +// this function needs to be accessible, because `enum()` needs to be inlined for the reified type. +// see also https://stackoverflow.com/a/41905907 +@PublishedApi +internal inline fun > valueToEnum(value: String): T { + return try { + enumValueOf(value.toUpperCase()) + } catch (ex: IllegalArgumentException) { + // workaround to avoid T::class.simpleName which would require kotlin-reflections.jar + throw BadParameterValue("Unknown enum constant ${T::class.java.simpleName}.$value") + } +} + +inline fun > RawArgument.enum() = + convert(completionCandidates = completions(enumValues())) { valueToEnum(it) } + +inline fun > RawOption.enum() = + convert(completionCandidates = completions(enumValues())) { valueToEnum(it) } + diff --git a/clikt/src/test/kotlin/com/github/ajalt/clikt/parameters/types/EnumTest.kt b/clikt/src/test/kotlin/com/github/ajalt/clikt/parameters/types/EnumTest.kt new file mode 100644 index 000000000..944564585 --- /dev/null +++ b/clikt/src/test/kotlin/com/github/ajalt/clikt/parameters/types/EnumTest.kt @@ -0,0 +1,77 @@ +package com.github.ajalt.clikt.parameters.types + +import com.github.ajalt.clikt.core.BadParameterValue +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.arguments.multiple +import com.github.ajalt.clikt.parameters.arguments.optional +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.testing.TestCommand +import io.kotlintest.data.forall +import io.kotlintest.shouldBe +import io.kotlintest.shouldThrow +import io.kotlintest.tables.row +import org.junit.Test + +@Suppress("unused") +class EnumTest { + enum class TestEnum { A, B } + + @Test + fun `enum option`() = forall( + row("", null), + row("--xx A", TestEnum.A), + row("--xx=A", TestEnum.A), + row("-xB", TestEnum.B)) { argv, expected -> + class C : TestCommand() { + val x by option("-x", "--xx").enum() + override fun run_() { + x shouldBe expected + } + } + + C().parse(argv) + } + + @Test + fun `enum option error`() { + class C : TestCommand() { + val foo by option().enum() + } + + shouldThrow { C().parse("--foo bar") } + .message shouldBe "Invalid value for \"--foo\": Unknown enum constant TestEnum.bar" + } + + @Test + fun `enum option with default`() = forall( + row("", TestEnum.B), + row("--xx A", TestEnum.A), + row("--xx=A", TestEnum.A), + row("-xA", TestEnum.A)) { argv, expected -> + class C : TestCommand() { + val x by option("-x", "--xx").enum().default(TestEnum.B) + override fun run_() { + x shouldBe expected + } + } + C().parse(argv) + } + + @Test + fun `enum argument`() = forall( + row("", null, emptyList()), + row("A", TestEnum.A, emptyList()), + row("A A B", TestEnum.A, listOf(TestEnum.A, TestEnum.B))) { argv, ex, ey -> + class C : TestCommand() { + val x by argument().enum().optional() + val y by argument().enum().multiple() + override fun run_() { + x shouldBe ex + y shouldBe ey + } + } + + C().parse(argv) + } +} diff --git a/docs/parameters.md b/docs/parameters.md index 709db9302..b27617e96 100644 --- a/docs/parameters.md +++ b/docs/parameters.md @@ -95,16 +95,15 @@ arguments. val opt: String? by option().choice("A", "B") ``` - To create an argument that requires the user to choose from the values - of an enum: + You can convert the values on the fly by using a map: ```kotlin - enum class Color { RED, GREEN } - val color: Color by argument().choice("RED" to Color.RED, "GREEN" to Color.GREEN) + val color: Int by argument().choice("RED" to 1, "GREEN" to 2) ``` * `File`: [`option().file()` and `argument().file()`][file] * `Path`: [`option().path()` and `argument().path()`][path] +* `Enum`: [`option().enum()` and `argument().enum()`][enum] These conversion functions take extra parameters that allow you to require that values are file paths that have certain attributes, such @@ -226,5 +225,6 @@ Error: --bigger-number must be bigger than --number [choice]: api/clikt/com.github.ajalt.clikt.parameters.types/choice.md [file]: api/clikt/com.github.ajalt.clikt.parameters.types/file.md [path]: api/clikt/com.github.ajalt.clikt.parameters.types/path.md +[enum]: api/clikt/com.github.ajalt.clikt.parameters.types/enum.md [convert]: api/clikt/com.github.ajalt.clikt.parameters.options/convert.md [validate]: api/clikt/com.github.ajalt.clikt.parameters.options/validate.md