Skip to content

Commit

Permalink
Simplify creating eager options (#135)
Browse files Browse the repository at this point in the history
  • Loading branch information
ajalt authored Feb 22, 2020
1 parent e6e3f95 commit 623fdf0
Show file tree
Hide file tree
Showing 33 changed files with 183 additions and 79 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Changelog

## [Unreleased]
### Added
- `eagerOption {}` function to more easily register eager options.
- Eager options can now be added to option groups in help out by passing a value for `groupName` when creating them.

### Fixed
- `file()` and `path()` conversions will now properly expand leading `~` in paths to the home directory for `mustExist`, `canBeFile`, and `canBeDir` checks. The property value is unchanged, and can still begin with a `~`. ([#131](https://github.com/ajalt/clikt/issues/79))

Expand All @@ -11,6 +15,7 @@
### Added
- Clikt is now available as a Kotlin Multiplatform Project, supporting JVM, NodeJS, and native Windows, Linux, and macOS.
- `canBeSymlink` parameter to `file()` and `path()` conversions that can be used to disallow symlinks
- `CliktCommand.eagerOption` to simplify creating custom eager options

### Changed
- The `CliktCommand.context` property has been deprecated in favor of the new name, `currentContext`, to avoid confusion with the `CliktCommand.context{}` method.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,18 @@ interface ParameterHolder {
fun registerOption(option: GroupableOption)
}

interface StaticallyGroupedOption : Option {
/** The name of the group, or null if this option should not be grouped in the help output. */
val groupName: String?
}

/**
* An option that can be added to a [ParameterGroup]
*/
interface GroupableOption : Option {
interface GroupableOption : StaticallyGroupedOption {
/** The group that this option belongs to, or null. Set by the group. */
var parameterGroup: ParameterGroup?

/** The name of the group, or null if this option should not be grouped in the help output. */
var groupName: String?
override var groupName: String?
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ open class PrintMessage(message: String) : CliktError(message)
* @param forceUnixLineEndings if true, all line endings in the message should be `\n`, regardless
* of the current operating system.
*/
class PrintCompletionMessage(message: String, val forceUnixLineEndings: Boolean): PrintMessage(message)
class PrintCompletionMessage(message: String, val forceUnixLineEndings: Boolean) : PrintMessage(message)

/**
* An internal exception that signals a usage error.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package com.github.ajalt.clikt.mpp

import com.github.ajalt.clikt.core.CliktCommand

internal val ANSI_CODE_RE = Regex("${"\u001B"}\\[[^m]*m")

internal expect val String.graphemeLengthMpp: Int
Expand All @@ -12,7 +10,7 @@ internal expect fun isWindowsMpp(): Boolean

internal expect fun exitProcessMpp(status: Int): Nothing

internal expect fun isLetterOrDigit(c: Char) : Boolean
internal expect fun isLetterOrDigit(c: Char): Boolean

internal expect fun readFileIfExists(filename: String): String?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ open class CliktHelpFormatter(
buildString {
append(firstIndent).append(col1)
// Pad the difference between this column's width and the table's first column width
repeat(firstWidth - col1.graphemeLength + colSpacing) {append(" ")}
repeat(firstWidth - col1.graphemeLength + colSpacing) { append(" ") }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ object TermUi {
val result = try {
convert.invoke(value)
} catch (err: UsageError) {
echo(err.helpMessage(), console=console)
echo(err.helpMessage(), console = console)
continue
}

Expand Down Expand Up @@ -180,7 +180,7 @@ object TermUi {
"n", "no" -> false
"" -> default
else -> {
echo("Error: invalid input", console=console)
echo("Error: invalid input", console = console)
continue@l
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ inline fun <T1 : Any, T2 : Any> ProcessedArgument<T1, T1>.wrapValue(
fail(err.message ?: "")
}
}
return copy(conv, defaultAllProcessor(), defaultValidator())
return copy(conv, defaultAllProcessor(), defaultValidator())
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.github.ajalt.clikt.parameters.groups

import com.github.ajalt.clikt.completion.CompletionCandidates.Fixed
import com.github.ajalt.clikt.core.BadParameterValue
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.Context
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,35 @@
package com.github.ajalt.clikt.parameters.options

import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.Context
import com.github.ajalt.clikt.core.PrintHelpMessage
import com.github.ajalt.clikt.core.PrintMessage
import com.github.ajalt.clikt.core.*
import com.github.ajalt.clikt.parsers.FlagOptionParser
import com.github.ajalt.clikt.parsers.OptionParser

/**
* An [Option] with no values that is [finalize]d before other types of options.
*
* @param callback This callback is called when the option is encountered on the command line. If you want to
* print a message and halt execution normally, you should throw a [PrintMessage] exception. The callback it
* passed the current execution context as a parameter.
* @param callback This callback is called when the option is encountered on the command line. If
* you want to print a message and halt execution normally, you should throw a [PrintMessage]
* exception. If you want to exit normally without printing a message, you should throw
* [`Abort(error=false)`][Abort]. The callback is passed the current execution context as a
* parameter.
*/
class EagerOption(
override val names: Set<String>,
override val nvalues: Int,
override val help: String,
override val hidden: Boolean,
override val helpTags: Map<String, String>,
private val callback: OptionTransformContext.() -> Unit) : Option {
override val groupName: String?,
private val callback: OptionTransformContext.() -> Unit
) : StaticallyGroupedOption {
constructor(vararg names: String, nvalues: Int = 0, help: String = "", hidden: Boolean = false,
helpTags: Map<String, String> = emptyMap(), callback: OptionTransformContext.() -> Unit)
: this(names.toSet(), nvalues, help, hidden, helpTags, callback)
helpTags: Map<String, String> = emptyMap(), groupName: String?=null,
callback: OptionTransformContext.() -> Unit)
: this(names.toSet(), nvalues, help, hidden, helpTags, groupName, callback)

init {
require(names.isNotEmpty()) { "options must have at least one name" }
}

override val secondaryNames: Set<String> get() = emptySet()
override val parser: OptionParser = FlagOptionParser
Expand All @@ -34,14 +40,61 @@ class EagerOption(
}
}

internal fun helpOption(names: Set<String>, message: String) = EagerOption(names, 0, message, false, emptyMap(),
callback = { throw PrintHelpMessage(context.command) })
internal fun helpOption(names: Set<String>, message: String): EagerOption {
return EagerOption(names, 0, message, false, emptyMap(), null) { throw PrintHelpMessage(context.command) }
}

/**
* Add an eager option to this command that, when invoked, runs [action].
*
* @param name The names that can be used to invoke this option. They must start with a punctuation character.
* @param help The description of this option, usually a single line.
* @param hidden Hide this option from help outputs.
* @param helpTags Extra information about this option to pass to the help formatter
* @param groupName All options with that share a group name will be grouped together in help output.
* @param action This callback is called when the option is encountered on the command line. If
* you want to print a message and halt execution normally, you should throw a [PrintMessage]
* exception. If you want to exit normally without printing a message, you should throw
* [`Abort(error=false)`][Abort]. The callback is passed the current execution context as a
* parameter.
*/
fun <T : CliktCommand> T.eagerOption(
name: String,
vararg additionalNames: String,
help: String = "",
hidden: Boolean = false,
helpTags: Map<String, String> = emptyMap(),
groupName: String? = null,
action: OptionTransformContext.() -> Unit
): T = eagerOption(listOf(name) + additionalNames, help, hidden, helpTags, groupName, action)

/**
* Add an eager option to this command that, when invoked, runs [action].
*
* @param names The names that can be used to invoke this option. They must start with a punctuation character.
* @param help The description of this option, usually a single line.
* @param hidden Hide this option from help outputs.
* @param helpTags Extra information about this option to pass to the help formatter
* @param groupName All options with that share a group name will be grouped together in help output.
* @param action This callback is called when the option is encountered on the command line. If
* you want to print a message and halt execution normally, you should throw a [PrintMessage]
* exception. If you want to exit normally without printing a message, you should throw
* [`Abort(error=false)`][Abort]. The callback is passed the current execution context as a
* parameter.
*/
fun <T : CliktCommand> T.eagerOption(
names: Collection<String>,
help: String = "",
hidden: Boolean = false,
helpTags: Map<String, String> = emptyMap(),
groupName: String? = null,
action: OptionTransformContext.() -> Unit
): T = apply { registerOption(EagerOption(names.toSet(), 0, help, hidden, helpTags, groupName, action)) }

/** Add an eager option to this command that, when invoked, prints a version message and exits. */
inline fun <T : CliktCommand> T.versionOption(
version: String,
help: String = "Show the version and exit",
names: Set<String> = setOf("--version"),
crossinline message: (String) -> String = { "$commandName version $it" }): T = apply {
registerOption(EagerOption(names, 0, help, false, emptyMap()) { throw PrintMessage(message(version)) })
}
crossinline message: (String) -> String = { "$commandName version $it" }
): T = eagerOption(names, help) { throw PrintMessage(message(version)) }
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
package com.github.ajalt.clikt.parameters.options

import com.github.ajalt.clikt.completion.CompletionCandidates
import com.github.ajalt.clikt.core.CliktError
import com.github.ajalt.clikt.core.Context
import com.github.ajalt.clikt.core.GroupableOption
import com.github.ajalt.clikt.core.ParameterHolder
import com.github.ajalt.clikt.core.*
import com.github.ajalt.clikt.mpp.isLetterOrDigit
import com.github.ajalt.clikt.output.HelpFormatter
import com.github.ajalt.clikt.parsers.OptionParser
Expand Down Expand Up @@ -49,7 +46,9 @@ interface Option {
get() = when {
hidden -> null
else -> HelpFormatter.ParameterHelp.Option(names, secondaryNames, metavar, help, nvalues, helpTags,
groupName = if (this is GroupableOption) groupName ?: parameterGroup?.groupName else null)
groupName = (this as? StaticallyGroupedOption)?.groupName
?: (this as? GroupableOption)?.parameterGroup?.groupName
)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.required
import com.github.ajalt.clikt.parameters.types.int
import com.github.ajalt.clikt.testing.TestCommand
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.data.forall
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.tables.row
import kotlin.js.JsName
import kotlin.test.Test
import io.kotest.tables.*

@Suppress("unused")
class CliktCommandTest {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.github.ajalt.clikt.core

import com.github.ajalt.clikt.testing.TestCommand
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.beInstanceOf
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import io.kotest.matchers.types.shouldBeSameInstanceAs
import kotlin.js.JsName
import kotlin.test.Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,7 @@ import com.github.ajalt.clikt.core.subcommands
import com.github.ajalt.clikt.output.HelpFormatter.ParameterHelp
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.arguments.multiple
import com.github.ajalt.clikt.parameters.groups.OptionGroup
import com.github.ajalt.clikt.parameters.groups.cooccurring
import com.github.ajalt.clikt.parameters.groups.groupChoice
import com.github.ajalt.clikt.parameters.groups.groupSwitch
import com.github.ajalt.clikt.parameters.groups.mutuallyExclusiveOptions
import com.github.ajalt.clikt.parameters.groups.*
import com.github.ajalt.clikt.parameters.options.*
import com.github.ajalt.clikt.parameters.types.int
import com.github.ajalt.clikt.testing.TestCommand
Expand Down Expand Up @@ -429,11 +425,13 @@ class CliktHelpFormatterTest {
class G1 : OptionGroup("G1") {
val opt1 by option()
}

class G2 : OptionGroup("G2") {
val opt2 by option()
}

class C : TestCommand() {
val opt by option(help="select group").groupChoice("g1" to G1(), "g2" to G2())
val opt by option(help = "select group").groupChoice("g1" to G1(), "g2" to G2())
}

val c = C()
Expand All @@ -460,11 +458,13 @@ class CliktHelpFormatterTest {
class G1 : OptionGroup("G1") {
val opt1 by option()
}

class G2 : OptionGroup("G2") {
val opt2 by option()
}

class C : TestCommand() {
val opt by option(help="select group").groupSwitch("--g1" to G1(), "--g2" to G2())
val opt by option(help = "select group").groupSwitch("--g1" to G1(), "--g2" to G2())
}

val c = C()
Expand Down Expand Up @@ -533,6 +533,9 @@ class CliktHelpFormatterTest {
requiredOptionMarker = "*"
)
}

eagerOption("--eager", "-e", help = "this is an eager option with a group", groupName = "My Group") {}
eagerOption("--eager2", "-E", help = "this is an eager option") {}
}
}

Expand Down Expand Up @@ -562,6 +565,7 @@ class CliktHelpFormatterTest {
|
|* --group-foo TEXT foo for group (required)
| -g, --group-bar TEXT bar for group
| -e, --eager this is an eager option with a group
|
|Another group:
|* --group-baz TEXT this group doesn't have help (required)
Expand All @@ -582,6 +586,7 @@ class CliktHelpFormatterTest {
|* --foo INT foo option help (required)
| -b, --bar META bar option help (default: optdef)
| --baz / --no-baz baz option help
| -E, --eager2 this is an eager option
| --version Show the version and exit
| -h, --help Show this message and exit
|
Expand Down
Loading

0 comments on commit 623fdf0

Please sign in to comment.