diff --git a/.gitignore b/.gitignore index 4f3e15b..c485bb5 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ gradle-app.setting ## File-based project format: *.iws +*.iml ## Plugin-specific files: diff --git a/README.md b/README.md index 277cde5..5646479 100644 --- a/README.md +++ b/README.md @@ -411,6 +411,11 @@ The creation of the `--help` option can be disabled by passing `null` as the constructing a `HelpFormatter` instance. In the above example a `DefaultHelpFormatter` was created with the prologue and epilogue. +## Auto Completion + +To enable auto completion generation for bash & zsh you have to pass the parameter `autoCompletion` an instance of `DefaultAutoCompletion`. +If the argument `autoCompletion` is not null the arg parser will add the `--auto-completion` option. +If this option is present the program will halt and generate the script for bash/zsh. ## Caveats diff --git a/src/main/kotlin/com/xenomachina/argparser/ArgParser.kt b/src/main/kotlin/com/xenomachina/argparser/ArgParser.kt index ab7e36f..a202a62 100644 --- a/src/main/kotlin/com/xenomachina/argparser/ArgParser.kt +++ b/src/main/kotlin/com/xenomachina/argparser/ArgParser.kt @@ -37,7 +37,8 @@ import kotlin.reflect.KProperty */ class ArgParser(args: Array, mode: Mode = Mode.GNU, - helpFormatter: HelpFormatter? = DefaultHelpFormatter()) { + helpFormatter: HelpFormatter? = DefaultHelpFormatter(), + autoCompletion: AutoCompletion? = null) { enum class Mode { /** For GNU-style option parsing, where options may appear after positional arguments. */ @@ -364,6 +365,8 @@ class ArgParser(args: Array, internal abstract fun toHelpFormatterValue(): HelpFormatter.Value + internal abstract fun toAutoCompletion(): List + internal fun registerRoot() { parser.checkNotParsed() parser.delegates.add(this) @@ -590,6 +593,15 @@ class ArgParser(args: Array, throw ShowHelpException(helpFormatter, delegates.toList()) }.default(Unit).registerRoot() } + if (autoCompletion != null) { + option("--auto-completion", + errorName = "AUTOCOMPLETION", // This should never be used, but we need to say something + help = "generates the autocompletion script for bash/zsh") { + throw ShowAutoCompletionException(autoCompletion, delegates.toList()) + }.default(Unit).registerRoot() + } else { + println("No auto completion!") + } } } diff --git a/src/main/kotlin/com/xenomachina/argparser/AutoCompletion.kt b/src/main/kotlin/com/xenomachina/argparser/AutoCompletion.kt new file mode 100644 index 0000000..62b0b22 --- /dev/null +++ b/src/main/kotlin/com/xenomachina/argparser/AutoCompletion.kt @@ -0,0 +1,23 @@ +// Copyright © 2016 Laurence Gonsalves +// +// This file is part of kotlin-argparser, a library which can be found at +// http://github.com/xenomachina/kotlin-argparser +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published by the +// Free Software Foundation; either version 2.1 of the License, or (at your +// option) any later version. +// +// This library is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License +// for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this library; if not, see http://www.gnu.org/licenses/ + +package com.xenomachina.argparser + +interface AutoCompletion { + fun format(progName: String?, delegates: List>): String +} \ No newline at end of file diff --git a/src/main/kotlin/com/xenomachina/argparser/Default.kt b/src/main/kotlin/com/xenomachina/argparser/Default.kt index 487a6e3..c85e3ce 100644 --- a/src/main/kotlin/com/xenomachina/argparser/Default.kt +++ b/src/main/kotlin/com/xenomachina/argparser/Default.kt @@ -47,6 +47,8 @@ fun ArgParser.Delegate.default(defaultValue: T): ArgParser.Delegate { override fun toHelpFormatterValue(): HelpFormatter.Value = inner.toHelpFormatterValue().copy(isRequired = false) + override fun toAutoCompletion(): List = inner.toAutoCompletion() + override fun validate() { inner.validate() } diff --git a/src/main/kotlin/com/xenomachina/argparser/DefaultAutoCompletion.kt b/src/main/kotlin/com/xenomachina/argparser/DefaultAutoCompletion.kt new file mode 100644 index 0000000..25ad039 --- /dev/null +++ b/src/main/kotlin/com/xenomachina/argparser/DefaultAutoCompletion.kt @@ -0,0 +1,49 @@ +// Copyright © 2016 Laurence Gonsalves +// +// This file is part of kotlin-argparser, a library which can be found at +// http://github.com/xenomachina/kotlin-argparser +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published by the +// Free Software Foundation; either version 2.1 of the License, or (at your +// option) any later version. +// +// This library is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License +// for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this library; if not, see http://www.gnu.org/licenses/ + +package com.xenomachina.argparser + +class DefaultAutoCompletion : AutoCompletion { + override fun format(progName: String?, delegates: List>): String { + val sb = StringBuilder() + sb.append("_$progName()\n") + .append("{\n") + .append("\tlocal cur prev opts\n") + .append("\tCOMPREPLY=()\n") + .append("\tcur=\"\${COMP_WORDS[COMP_CWORD]}\"\n") + .append("\tprev=\"\${COMP_WORDS[COMP_CWORD - 1]}\"\n") + .append("\topts=\"") + + delegates.forEach { + it.toAutoCompletion().forEach { + sb.append("$it ") + } + } + + sb.append("\"\n") + .append("\n") + .append("\tif [[ \${cur} == -* ]] ; then\n") + .append("\t\tCOMPREPLY=( \$(compgen -W \"\${opts}\" -- \${cur}) )\n") + .append("\t\treturn 0\n") + .append("\tfi\n") + .append("}\n") + .append("complete -F _$progName $progName") + + return sb.toString() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/xenomachina/argparser/Exceptions.kt b/src/main/kotlin/com/xenomachina/argparser/Exceptions.kt index 59c7885..acd97fc 100644 --- a/src/main/kotlin/com/xenomachina/argparser/Exceptions.kt +++ b/src/main/kotlin/com/xenomachina/argparser/Exceptions.kt @@ -90,3 +90,16 @@ open class UnexpectedOptionArgumentException(val optName: String) : */ open class UnexpectedPositionalArgumentException(val valueName: String?) : SystemExitException("unexpected argument${if (valueName == null) "" else " after $valueName"}", 2) + +/** + * Indicates that the user requested that the bash/zsh autocompletion + * script should be generated + */ +class ShowAutoCompletionException internal constructor( + private val autoCompletion: AutoCompletion, + private val delegates: List> +) : SystemExitException("Help was requested", 0) { + override fun printUserMessage(writer: Writer, progName: String?, columns: Int) { + writer.write(autoCompletion.format(progName, delegates)) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/xenomachina/argparser/OptionDelegate.kt b/src/main/kotlin/com/xenomachina/argparser/OptionDelegate.kt index c275dc7..11497ab 100644 --- a/src/main/kotlin/com/xenomachina/argparser/OptionDelegate.kt +++ b/src/main/kotlin/com/xenomachina/argparser/OptionDelegate.kt @@ -75,4 +75,8 @@ internal class OptionDelegate( parser.registerOption(name, this) } } + + override fun toAutoCompletion(): List { + return optionNames + } } diff --git a/src/main/kotlin/com/xenomachina/argparser/PositionalDelegate.kt b/src/main/kotlin/com/xenomachina/argparser/PositionalDelegate.kt index 6178a1c..53ce638 100644 --- a/src/main/kotlin/com/xenomachina/argparser/PositionalDelegate.kt +++ b/src/main/kotlin/com/xenomachina/argparser/PositionalDelegate.kt @@ -56,4 +56,8 @@ internal class PositionalDelegate( isPositional = true, help = help) } + + override fun toAutoCompletion(): List { + return listOf() + } } diff --git a/src/main/kotlin/com/xenomachina/argparser/WrappingDelegate.kt b/src/main/kotlin/com/xenomachina/argparser/WrappingDelegate.kt index 0b28ff8..22cc6f0 100644 --- a/src/main/kotlin/com/xenomachina/argparser/WrappingDelegate.kt +++ b/src/main/kotlin/com/xenomachina/argparser/WrappingDelegate.kt @@ -44,6 +44,8 @@ internal class WrappingDelegate( override fun toHelpFormatterValue(): HelpFormatter.Value = inner.toHelpFormatterValue() + override fun toAutoCompletion(): List = inner.toAutoCompletion() + override fun addValidator(validator: ArgParser.Delegate.() -> Unit): ArgParser.Delegate = apply { inner.addValidator { validator(this@WrappingDelegate) } } diff --git a/src/test/kotlin/com/xenomachina/argparser/ArgParserTest.kt b/src/test/kotlin/com/xenomachina/argparser/ArgParserTest.kt index 2384266..65f1978 100644 --- a/src/test/kotlin/com/xenomachina/argparser/ArgParserTest.kt +++ b/src/test/kotlin/com/xenomachina/argparser/ArgParserTest.kt @@ -34,8 +34,9 @@ val TEST_HELP = "test help message" fun parserOf( vararg args: String, mode: ArgParser.Mode = ArgParser.Mode.GNU, - helpFormatter: HelpFormatter? = DefaultHelpFormatter() -) = ArgParser(args, mode, helpFormatter) + helpFormatter: HelpFormatter? = DefaultHelpFormatter(), + autoCompletion: AutoCompletion? = null +) = ArgParser(args, mode, helpFormatter, autoCompletion) enum class Color { RED, GREEN, BLUE } @@ -1498,3 +1499,33 @@ class Issue18Test_DefaultThenValidator : Test({ val x = Args(parserOf()).x x shouldEqual 0 }) + +class AutoCompletionTest : Test({ + class Args(parser: ArgParser) { + val manual by parser.storing("--named-by-hand", help = TEST_HELP, argName = "HANDYS-ARG") + val auto by parser.storing(TEST_HELP, argName = "OTTOS-ARG") + val foo by parser.adding(help = TEST_HELP, argName = "BAR") { toInt() } + val bar by parser.adding("--baz", help = TEST_HELP, argName = "QUUX") + } + + shouldThrow { + Args(parserOf("--auto-completion", autoCompletion = DefaultAutoCompletion())).manual + }.run { + // TODO: find a way to make this less brittle (ie: don't use help text) + StringWriter().apply { printUserMessage(this, "testcase", 10000) }.toString().trim() shouldBe """ +_testcase() +{ + local cur prev opts + COMPREPLY=() + cur="${'$'}{COMP_WORDS[COMP_CWORD]}" + prev="${'$'}{COMP_WORDS[COMP_CWORD - 1]}" + opts="-h --help --auto-completion --named-by-hand --auto --foo --baz " + + if [[ ${'$'}{cur} == -* ]] ; then + COMPREPLY=( ${'$'}(compgen -W "${'$'}{opts}" -- ${'$'}{cur}) ) + return 0 + fi +} +complete -F _testcase testcase""".trim() + } +}) \ No newline at end of file