Skip to content

Commit

Permalink
Add requireConfirmation to prompt
Browse files Browse the repository at this point in the history
  • Loading branch information
ajalt committed Jul 29, 2023
1 parent 41a6acc commit 671c250
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 146 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
## 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` instead.
- 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
Original file line number Diff line number Diff line change
Expand Up @@ -427,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 @@ -451,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
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 @@ -24,20 +24,60 @@ data class CliktCommandTestResult(
val statusCode: Int,
)

/**
* Test this command, returning a result that captures the output and result status code.
*
* Note that only output printed with [echo][CliktCommand.echo] will be captured. Anything printed with [print] or
* [println] is not.
*
* @param argv The command line to send to the command
* @param stdin Content of stdin that will be read by prompt options. Multiple inputs should be separated by `\n`.
* @param envvars A map of environment variable name to value for envvars that can be read by the command
* @param includeSystemEnvvars Set to true to include the environment variables from the system in addition to those
* defined in [envvars]
* @param ansiLevel Defaults to no colored output; set to [AnsiLevel.TRUECOLOR] to include ANSI codes in the output.
* @param width The width of the terminal, used to wrap text
* @param height The height of the terminal
*/
fun CliktCommand.test(
argv: String,
stdin: String = "",
envvars: Map<String, String> = emptyMap(),
includeSystemEnvvars: Boolean = false,
ansiLevel: AnsiLevel = AnsiLevel.NONE,
width: Int = 79,
height: Int = 24,
): CliktCommandTestResult {
return test(shlex("test", argv, null), stdin, envvars)
val argvArray = shlex("test", argv, null)
return test(argvArray, stdin, envvars, includeSystemEnvvars, ansiLevel, width, height)
}


/**
* Test this command, returning a result that captures the output and result status code.
*
* Note that only output printed with [echo][CliktCommand.echo] will be captured. Anything printed with [print] or
* [println] is not.
*
* @param argv The command line to send to the command
* @param stdin Content of stdin that will be read by prompt options. Multiple inputs should be separated by `\n`.
* @param envvars A map of environment variable name to value for envvars that can be read by the command
* @param includeSystemEnvvars Set to true to include the environment variables from the system in addition to those
* defined in [envvars]
* @param ansiLevel Defaults to no colored output; set to [AnsiLevel.TRUECOLOR] to include ANSI codes in the output.
* @param width The width of the terminal, used to wrap text
* @param height The height of the terminal
*/
fun CliktCommand.test(
argv: List<String>,
stdin: String = "",
envvars: Map<String, String> = emptyMap(),
): CliktCommandTestResult = test(argv.toTypedArray(), stdin, envvars)
includeSystemEnvvars: Boolean = false,
ansiLevel: AnsiLevel = AnsiLevel.NONE,
width: Int = 79,
height: Int = 24,
): CliktCommandTestResult =
test(argv.toTypedArray(), stdin, envvars, includeSystemEnvvars, ansiLevel, width, height)

/**
* Test this command, returning a result that captures the output and result status code.
Expand All @@ -64,11 +104,11 @@ fun CliktCommand.test(
height: Int = 24,
): CliktCommandTestResult {
var exitCode = 0
val iface = TerminalRecorder(ansiLevel, width, height)
iface.inputLines = stdin.split("\n").toMutableList()
val recorder = TerminalRecorder(ansiLevel, width, height)
recorder.inputLines = stdin.split("\n").toMutableList()
context {
envvarReader = { envvars[it] ?: (if (includeSystemEnvvars) readEnvvar(it) else null) }
terminal = Terminal(terminal.theme, terminal.tabWidth, iface)
terminal = Terminal(terminal.theme, terminal.tabWidth, recorder)
}

try {
Expand All @@ -77,5 +117,5 @@ fun CliktCommand.test(
echoFormattedHelp(e)
exitCode = e.statusCode
}
return CliktCommandTestResult(iface.stdout(), iface.stderr(), iface.output(), exitCode)
return CliktCommandTestResult(recorder.stdout(), recorder.stderr(), recorder.output(), exitCode)
}
Loading

0 comments on commit 671c250

Please sign in to comment.