Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Autocompletion #22

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ gradle-app.setting

## File-based project format:
*.iws
*.iml

## Plugin-specific files:

Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 13 additions & 1 deletion src/main/kotlin/com/xenomachina/argparser/ArgParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ import kotlin.reflect.KProperty
*/
class ArgParser(args: Array<out String>,
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. */
Expand Down Expand Up @@ -364,6 +365,8 @@ class ArgParser(args: Array<out String>,

internal abstract fun toHelpFormatterValue(): HelpFormatter.Value

internal abstract fun toAutoCompletion(): List<String>

internal fun registerRoot() {
parser.checkNotParsed()
parser.delegates.add(this)
Expand Down Expand Up @@ -590,6 +593,15 @@ class ArgParser(args: Array<out String>,
throw ShowHelpException(helpFormatter, delegates.toList())
}.default(Unit).registerRoot()
}
if (autoCompletion != null) {
option<Unit>("--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!")
}
}
}

Expand Down
23 changes: 23 additions & 0 deletions src/main/kotlin/com/xenomachina/argparser/AutoCompletion.kt
Original file line number Diff line number Diff line change
@@ -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<ArgParser.Delegate<*>>): String
}
2 changes: 2 additions & 0 deletions src/main/kotlin/com/xenomachina/argparser/Default.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ fun <T> ArgParser.Delegate<T>.default(defaultValue: T): ArgParser.Delegate<T> {

override fun toHelpFormatterValue(): HelpFormatter.Value = inner.toHelpFormatterValue().copy(isRequired = false)

override fun toAutoCompletion(): List<String> = inner.toAutoCompletion()

override fun validate() {
inner.validate()
}
Expand Down
49 changes: 49 additions & 0 deletions src/main/kotlin/com/xenomachina/argparser/DefaultAutoCompletion.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright © 2016 Laurence Gonsalves
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be 2017 and your name.

//
// 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<ArgParser.Delegate<*>>): String {
val sb = StringBuilder()
sb.append("_$progName()\n")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps triple-quoted strings would make this a bit easier to read?

.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()
}
}
13 changes: 13 additions & 0 deletions src/main/kotlin/com/xenomachina/argparser/Exceptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<ArgParser.Delegate<*>>
) : SystemExitException("Help was requested", 0) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/Help/Autocompletion/

override fun printUserMessage(writer: Writer, progName: String?, columns: Int) {
writer.write(autoCompletion.format(progName, delegates))
}
}
4 changes: 4 additions & 0 deletions src/main/kotlin/com/xenomachina/argparser/OptionDelegate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,8 @@ internal class OptionDelegate<T>(
parser.registerOption(name, this)
}
}

override fun toAutoCompletion(): List<String> {
return optionNames
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,8 @@ internal class PositionalDelegate<T>(
isPositional = true,
help = help)
}

override fun toAutoCompletion(): List<String> {
return listOf()
}
}
2 changes: 2 additions & 0 deletions src/main/kotlin/com/xenomachina/argparser/WrappingDelegate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ internal class WrappingDelegate<U, W>(

override fun toHelpFormatterValue(): HelpFormatter.Value = inner.toHelpFormatterValue()

override fun toAutoCompletion(): List<String> = inner.toAutoCompletion()

override fun addValidator(validator: ArgParser.Delegate<W>.() -> Unit): ArgParser.Delegate<W> =
apply { inner.addValidator { validator(this@WrappingDelegate) } }

Expand Down
35 changes: 33 additions & 2 deletions src/test/kotlin/com/xenomachina/argparser/ArgParserTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down Expand Up @@ -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<ShowAutoCompletionException> {
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()
}
})