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

Add requireConfirmation to option().prompt() #433

Merged
merged 4 commits into from
Jul 29, 2023
Merged
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
12 changes: 10 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# Changelog

## Unreleased
### Added
- Added `CliktCommand.terminal` extension for accessing the terminal from a command.
- Added `includeSystemEnvvars`, `ansiLevel`, `width`, and `height` parameters to all `CliktCommand.test` overloads.

### Deprecated
- Deprecated `CliktCommand.prompt`, use `CliktCommand.terminal.prompt` or `Prompt` instead.
- Deprecated `CliktCommand.confirm`, use `YesNoPrompt` instead.

### Fixed
- Fixed incorrect error message when a `defaultLazy` option referenced a `required` option. ([#430](https://github.com/ajalt/clikt/issues/430))

Expand Down Expand Up @@ -280,7 +288,7 @@

### Changed
- `option()` and `argument()` now take optional `completionCandidates` parameters to override how completion is generated. The constructor and `copy` functions of `OptionsWithValues` and `ProcessedArgument` have changed to support default values.
- The overloads of `findObject` ([1](https://ajalt.github.io/clikt/api/clikt/com.github.ajalt.clikt.core/-context/find-object/) [2](https://ajalt.github.io/clikt/api/clikt/com.github.ajalt.clikt.core/find-object/)) that take a default value have been renamed `findOrSetObject`. The existing names are marked with `@Deprecated`, and IntelliJ can convert your callsites automatically. ([#110](https://github.com/ajalt/clikt/issues/110))
- The overloads of `findObject` ([1](https://ajalt.github.io/clikt/api/clikt/com.github.ajalt.clikt.core/-context/find-object/) [2](https://ajalt.github.io/clikt/api/clikt/com.github.ajalt.clikt.core/find-object/)) that take a default value have been renamed `findOrSetObject`. The existing names are marked with `@Deprecated`, and IntelliJ can convert your call sites automatically. ([#110](https://github.com/ajalt/clikt/issues/110))
- `enum()` parameters now accept case-insensitive values by default. You change this behavior by passing `ignoreCase = false` to `enum()` ([#115](https://github.com/ajalt/clikt/issues/115))

### Fixed
Expand Down Expand Up @@ -326,7 +334,7 @@
- Marking options as deprecated with [`option().deprecated()`](https://ajalt.github.io/clikt/api/clikt/com.github.ajalt.clikt.parameters.options/deprecated/)
- You can manually set the pattern to split envvars on by passing a pattern to the `envvarSplit` parameter of [`option()`](https://ajalt.github.io/clikt/api/clikt/com.github.ajalt.clikt.parameters.options/option/)
- [Option groups](https://ajalt.github.io/clikt/documenting/#grouping-options-in-help), [mutually exclusive groups](https://ajalt.github.io/clikt/options/#prompting-for-input), [co-occurring groups](https://ajalt.github.io/clikt/options/#co-occurring-option-groups), and [choice options with groups](https://ajalt.github.io/clikt/options/#choice-options-with-groups)
- Support for [Command line argument files](https://ajalt.github.io/clikt/advanced/#command-line-argument-files-files) a.k.a "@-files"
- Support for [Command line argument files](https://ajalt.github.io/clikt/advanced/#command-line-argument-files-files) a.k.a. "@-files"

### Changed
- If multiple `--` tokens are present on the command line, all subsequent occurrences after the first are now parsed as positional arguments. Previously, subsequent `--` tokens were skipped.
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Clikt has:
* arbitrary nesting of commands
* composable, type safe parameter values
* generation of help output and shell autocomplete scripts
* multiplatform packages for JVM, NodeJS, and native Linux, Windows and MacOS
* multiplatform packages for JVM, Node.js, and native Linux, Windows and macOS

What does it look like? Here's a complete example of a simple Clikt program:

Expand Down Expand Up @@ -69,10 +69,10 @@ dependencies {

#### Multiplatform

Clikt supports the following targets: `jvm`, `mingwX64`, `linuxX64`, `macosX64`, and `js` (for both NodeJS and
Browsers). Artifacts for macosArm64 are also published, but not tested with CI.
[See the docs](https://ajalt.github.io/clikt/advanced/#multiplatform-support) for more information about functionality
supported on each target. You'll need to use Gradle 6 or newer.
Clikt supports the following targets: `jvm`, `mingwX64`, `linuxX64`, `macosX64`, and `js` (for both
Node.js and Browsers). Artifacts for macosArm64 are also published, but not tested with CI. [See the
docs](https://ajalt.github.io/clikt/advanced/#multiplatform-support) for more information about
functionality supported on each target. You'll need to use Gradle 6 or newer.

#### Snapshots

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ annotation class ExperimentalCompletionCandidates
*/
sealed class CompletionCandidates {
/** Do not autocomplete this parameter */
object None : CompletionCandidates()
data object None : CompletionCandidates()

/** Complete with filesystem paths */
object Path : CompletionCandidates()
data object Path : CompletionCandidates()

/** Complete with entries in the system's hostfile */
object Hostname : CompletionCandidates()
data object Hostname : CompletionCandidates()

/** Complete with usernames from the current system */
object Username : CompletionCandidates()
data object Username : CompletionCandidates()

/** Complete the parameter with a fixed set of strings */
data class Fixed(val candidates: Set<String>) : CompletionCandidates() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ abstract class CliktCommand(
/**
* Register an option with this command.
*
* This is called automatically for the built in options, but you need to call this if you want to add a
* This is called automatically for the built-in options, but you need to call this if you want to add a
* custom option.
*/
fun registerOption(option: Option) {
Expand All @@ -270,7 +270,7 @@ abstract class CliktCommand(
/**
* Register an argument with this command.
*
* This is called automatically for the built in arguments, but you need to call this if you want to add a
* This is called automatically for the built-in arguments, but you need to call this if you want to add a
* custom argument.
*/
fun registerArgument(argument: Argument) {
Expand Down Expand Up @@ -366,22 +366,15 @@ abstract class CliktCommand(
}

/**
* Print [text] to the user and return the value they enter.
*
* @param text The message asking for input to show the user
* @param default The value to return if the user enters an empty line, or `null` to require a value
* @param showDefault If true and a [default] is specified, add the default value to the prompt
* @param hideInput If true, the user's input will be treated like a password and hidden from
* the screen. This value will be ignored on platforms where it is not supported.
* @param choices The set of values that the user must choose from.
* @param promptSuffix A string to append after [text] when showing the user the prompt
* @param invalidChoiceMessage The message to show the user if [choices] is specified,
* and they enter a value that isn't one of the choices.
*
* @return The user input, or `null` if EOF was reached before this function was called.
*
* @see Terminal.prompt
* A deprecated alias for `terminal.prompt`.
*/
@Deprecated(
"Use terminal.prompt instead",
ReplaceWith(
"terminal.prompt(text, default, showDefault, showChoices, hideInput, choices, promptSuffix, invalidChoiceMessage)",
"com.github.ajalt.clikt.core.terminal"
)
)
fun prompt(
text: String,
default: String? = null,
Expand All @@ -403,23 +396,15 @@ abstract class CliktCommand(
)

/**
* Print [text] to the user and return the value they enter.
*
* @param text The message asking for input to show the user
* @param default The value to return if the user enters an empty line, or `null` to require a value
* @param showDefault If true and a [default] is specified, add the default value to the prompt
* @param hideInput If true, the user's input will be treated like a password and hidden from
* the screen. This value will be ignored on platforms where it is not supported.
* @param choices The set of values that the user must choose from.
* @param promptSuffix A string to append after [text] when showing the user the prompt
* @param invalidChoiceMessage The message to show the user if [choices] is specified,
* and they enter a value that isn't one of the choices.
* @param convert A function that converts the user input to the final value
*
* @return The converted user input, or `null` if EOF was reached before this function was called.
*
* @see Terminal.prompt
* A deprecated alias for `terminal.prompt`.
*/
@Deprecated(
"Use terminal.prompt instead",
ReplaceWith(
"terminal.prompt(text, default, showDefault, showChoices, hideInput, choices, promptSuffix, invalidChoiceMessage, convert)",
"com.github.ajalt.clikt.core.terminal"
)
)
fun <T> prompt(
text: String,
default: T? = null,
Expand All @@ -442,21 +427,14 @@ abstract class CliktCommand(
convert
)

/**
* Prompt for user confirmation.
*
* @param text The message asking for input to show the user
* @param default The value to return if the user enters an empty line, or `null` to require a value
* @param uppercaseDefault If true and [default] is not `null`, the default choice will be shown in uppercase.
* @param showChoices If true, the choices will be added to the [prompt]
* @param choiceStrings The strings to accept for `true` and `false` inputs
* @param promptSuffix A string to append after [prompt] when showing the user the prompt
* @param invalidChoiceMessage The message to show the user if they enter a value that isn't one of the [choiceStrings].
*
* @return The converted user input, or `null` if EOF was reached before this function was called.
*
* @see Terminal.prompt
*/
@Deprecated(
"Use YesNoPrompt instead",
ReplaceWith(
"YesNoPrompt(text, terminal, default, uppercaseDefault, showChoices, choiceStrings, promptSuffix, invalidChoiceMessage).ask()",
"com.github.ajalt.clikt.core.terminal",
"com.github.ajalt.mordant.terminal.YesNoPrompt"
)
)
fun confirm(
text: String,
default: Boolean? = null,
Expand All @@ -466,16 +444,7 @@ abstract class CliktCommand(
promptSuffix: String = ": ",
invalidChoiceMessage: String = "Invalid value, choose from ",
): Boolean? {
return YesNoPrompt(
text,
currentContext.terminal,
default,
uppercaseDefault,
showChoices,
choiceStrings,
promptSuffix,
invalidChoiceMessage,
).ask()
return YesNoPrompt(text, terminal, default, uppercaseDefault, showChoices, choiceStrings, promptSuffix, invalidChoiceMessage).ask()
}

/**
Expand Down Expand Up @@ -588,7 +557,7 @@ fun <T : CliktCommand> T.subcommands(vararg commands: CliktCommand): T = apply {
* here.
*/
fun <T : CliktCommand> T.context(block: Context.Builder.() -> Unit): T = apply {
// save the old config to allow multiple context calls
// save the old config to allow multiple context calls
val c = _contextConfig
_contextConfig = {
c()
Expand All @@ -605,3 +574,7 @@ private fun CliktCommand.inferCommandName(): String {
"${it.groupValues[1]}-${it.groupValues[2]}"
}.lowercase()
}

/** A short for accessing the terminal from the [currentContext][CliktCommand.currentContext] */
val CliktCommand.terminal: Terminal
get() = currentContext.terminal
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface ParameterHolder {
/**
* Register an option with this command or group.
*
* This is called automatically for the built in options, but you need to call this if you want to add a
* This is called automatically for the built-in options, but you need to call this if you want to add a
* custom option.
*/
fun registerOption(option: GroupableOption)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ internal expect fun readEnvvar(key: String): String?

internal expect fun isWindowsMpp(): Boolean

/** Doesn't return Nothing, since it's a no-op on the browser */
/** Doesn't return [Nothing], since it's a no-op on the browser */
internal expect fun exitProcessMpp(status: Int)

internal expect fun isLetterOrDigit(c: Char): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ object TermUi {
*
* @see editText for usage and parameter descriptions.
*/
@Suppress("unused")
fun editFile(
filename: String,
editor: String? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ internal fun <T> defaultValidator(): ArgValidator<T> = {}
* @param help The description of this argument for help output.
* @param helpTags Extra information about this option to pass to the help formatter
*/
@Suppress("unused")
@Suppress("UnusedReceiverParameter")
fun CliktCommand.argument(
name: String = "",
help: String = "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ typealias ValueConverter<InT, ValueT> = OptionCallTransformContext.(InT) -> Valu
typealias ValuesTransformer<ValueT, EachT> = OptionCallTransformContext.(List<ValueT>) -> EachT

/**
* A callback that transforms all of the calls to the final option type.
* A callback that transforms all the calls to the final option type.
*
* The input list will have a size equal to the number of times the option appears on the command line.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import com.github.ajalt.clikt.core.MissingOption
import com.github.ajalt.clikt.core.UsageError
import com.github.ajalt.clikt.output.HelpFormatter
import com.github.ajalt.clikt.output.ParameterFormatter
import com.github.ajalt.mordant.terminal.ConfirmationPrompt
import com.github.ajalt.mordant.terminal.ConversionResult
import com.github.ajalt.mordant.terminal.Prompt
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName

Expand Down Expand Up @@ -191,35 +193,37 @@ fun <T : Any> NullableOption<T, T>.prompt(
hideInput: Boolean = false,
promptSuffix: String = ": ",
showDefault: Boolean = true,
requireConfirmation: Boolean = false,
confirmationPrompt: String = "Repeat for confirmation",
confirmationMismatchMessage: String = "Values do not match, try again",
): OptionWithValues<T, T, T> = transformAll { invocations ->
val promptText = text ?: longestName()?.let { splitOptionPrefix(it).second }
?.replace(Regex("\\W"), " ")?.capitalize2() ?: "Value"

when (val provided = invocations.lastOrNull()) {
null -> {
if (context.errorEncountered) throw Abort()
context.terminal.prompt(
prompt = promptText,
default = default,
showDefault = showDefault,
hideInput = hideInput,
promptSuffix = promptSuffix,
) { input ->
val provided = invocations.lastOrNull()
if (provided != null) return@transformAll provided
if (context.errorEncountered) throw Abort()

val builder: (String) -> Prompt<T> = {
object : Prompt<T>(
prompt = it,
terminal = terminal,
default = default,
showDefault = showDefault,
hideInput = hideInput,
promptSuffix = promptSuffix,
) {
override fun convert(input: String): ConversionResult<T> {
val ctx = OptionCallTransformContext("", this@transformAll, context)
try {
val v =
transformAll(listOf(transformEach(ctx, listOf(transformValue(ctx, input)))))
if (v != null) {
@Suppress("UNCHECKED_CAST")
(option as? OptionWithValues<T, T, T>)?.transformValidator?.invoke(
this@transformAll,
v
)
}
ConversionResult.Valid(v)
val v = transformEach(ctx, listOf(transformValue(ctx, input)))

@Suppress("UNCHECKED_CAST")
val validator = (option as? OptionWithValues<T, T, T>)?.transformValidator
validator?.invoke(this@transformAll, v)
return ConversionResult.Valid(v)
} catch (e: UsageError) {
e.context = e.context ?: context
ConversionResult.Invalid(
return ConversionResult.Invalid(
e.formatMessage(
context.localization,
ParameterFormatter.Plain
Expand All @@ -228,9 +232,18 @@ fun <T : Any> NullableOption<T, T>.prompt(
}
}
}

else -> provided
} ?: throw Abort()
}
val result = if (requireConfirmation) {
ConfirmationPrompt.create(
promptText,
confirmationPrompt,
confirmationMismatchMessage,
builder
).ask()
} else {
builder(promptText).ask()
}
return@transformAll result ?: throw Abort()
}

// the stdlib capitalize was deprecated without a replacement
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ internal object Parser {
command.currentContext.invokedSubcommand = subcommand
if (command.currentContext.printExtraMessages) {
for (warning in command.messages) {
command.currentContext.terminal.warning(warning, stderr = true)
command.terminal.warning(warning, stderr = true)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ interface ValueSource {
* @param joinSubcommands If null, keys will not include names of subcommands. If given,
* this string be used will join subcommand names to the beginning of keys. For options
* that are in a root command, this has no effect. For option in subcommands, the
* subcommand name will joined. The root command name is never included.
* subcommand name will be joined. The root command name is never included.
* @param uppercase If true, returned keys will be entirely uppercase.
* @param replaceDashes `-` characters in option names will be replaced with this character.
*/
Expand Down
Loading