Skip to content

Commit

Permalink
Attach docs to properties rather than using @Property
Browse files Browse the repository at this point in the history
  • Loading branch information
ajalt committed Jun 24, 2023
1 parent fec34c3 commit 6570552
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 121 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,43 +21,61 @@ import com.github.ajalt.mordant.terminal.YesNoPrompt
* [option] and [argument]. You can then parse `argv` by calling [main], which will take care of printing
* errors and help to the user. If you want to handle output yourself, you can use [parse] instead.
*
* Once the command line has been parsed and all of the parameters are populated, [run] is called.
*
* @param help The help for this command. The first line is used in the usage string, and the entire string is
* used in the help output. Paragraphs are automatically re-wrapped to the terminal width.
* @param epilog Text to display at the end of the full help output. It is automatically re-wrapped to the
* terminal width.
* @param name The name of the program to use in the help output. If not given, it is inferred from the class
* name.
* @param invokeWithoutSubcommand Used when this command has subcommands, and this command is called
* without a subcommand. If true, [run] will be called. By default, a [PrintHelpMessage] is thrown instead.
* @param printHelpOnEmptyArgs If this command is called with no values on the command line, print a
* help message (by throwing [PrintHelpMessage]) if this is true, otherwise run normally.
* @param helpTags Extra information about this option to pass to the help formatter.
* @param autoCompleteEnvvar The envvar to use to enable shell autocomplete script generation. Set
* to null to disable generation.
* @param allowMultipleSubcommands If true, allow multiple of this command's subcommands to be
* called sequentially. This will disable `allowInterspersedArgs` on the context of this command an
* its descendants. This functionality is experimental, and may change in a future release.
* @param treatUnknownOptionsAsArgs If true, any options on the command line whose names aren't
* valid will be parsed as an argument rather than reporting an error. You'll need to define an
* `argument().multiple()` to collect these options, or an error will still be reported. Unknown
* short option flags grouped with other flags on the command line will always be reported as
* errors.
* @param hidden If true, don't display this command in help output when used as a subcommand.
* Once the command line has been parsed and all the parameters are populated, [run] is called.
*/
@Suppress("PropertyName")
@ParameterHolderDsl
abstract class CliktCommand(
/**
* The help for this command. The first line is used in the usage string, and the entire string
* is used in the help output. Paragraphs are automatically re-wrapped to the terminal width.
*/
help: String = "",
/**
* Text to display at the end of the full help output. It is automatically re-wrapped to the
* terminal width.
*/
epilog: String = "",
/**
* The name of the program to use in the help output. If not given, it is inferred from the
* class name.
*/
name: String? = null,
/**
* Used when this command has subcommands, and this command is called without a subcommand. If
* true, [run] will be called. By default, a [PrintHelpMessage] is thrown instead.
*/
val invokeWithoutSubcommand: Boolean = false,
/**
* If this command is called with no values on the command line, print a help message (by
* throwing [PrintHelpMessage]) if this is true, otherwise run normally.
*/
val printHelpOnEmptyArgs: Boolean = false,
/**
* Extra information about this option to pass to the help formatter.
*/
val helpTags: Map<String, String> = emptyMap(),
/**
* The envvar to use to enable shell autocomplete script generation. Set to null to disable
* generation.
*/
private val autoCompleteEnvvar: String? = "",
/**
* If true, allow multiple of this command's subcommands to be called sequentially. This will
* disable `allowInterspersedArgs` on the context of this command and its descendants. This
* functionality is experimental, and may change in a future release.
*/
internal val allowMultipleSubcommands: Boolean = false,
/**
* If true, any options on the command line whose names aren't valid will be parsed as an
* argument rather than reporting an error. You'll need to define an `argument().multiple()` to
* collect these options, or an error will still be reported. Unknown short option flags grouped
* with other flags on the command line will always be reported as errors.
*/
internal val treatUnknownOptionsAsArgs: Boolean = false,
/**
* If true, don't display this command in help output when used as a subcommand.
*/
private val hidden: Boolean = false,
) : ParameterHolder {
/**
Expand Down Expand Up @@ -93,7 +111,11 @@ abstract class CliktCommand(

private fun registeredOptionNames() = _options.flatMapTo(mutableSetOf()) { it.names }

private fun createContext(argv: List<String>, parent: Context?, ancestors: List<CliktCommand>): Context {
private fun createContext(
argv: List<String>,
parent: Context?,
ancestors: List<CliktCommand>,
): Context {
_context = Context.build(this, parent, argv, _contextConfig)

if (allowMultipleSubcommands) {
Expand Down Expand Up @@ -175,7 +197,8 @@ abstract class CliktCommand(
}

/** The help displayed in the commands list when this command is used as a subcommand. */
protected fun shortHelp(): String = Regex("""\s*(?:```)?\s*(.+)""").find(commandHelp)?.groups?.get(1)?.value ?: ""
protected fun shortHelp(): String =
Regex("""\s*(?:```)?\s*(.+)""").find(commandHelp)?.groups?.get(1)?.value ?: ""

/** The names of all direct children of this command */
fun registeredSubcommandNames(): List<String> = _subcommands.map { it.commandName }
Expand Down Expand Up @@ -543,7 +566,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 Down
104 changes: 77 additions & 27 deletions clikt/src/commonMain/kotlin/com/github/ajalt/clikt/core/Context.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,51 +11,101 @@ import kotlin.properties.ReadOnlyProperty
typealias TypoSuggestor = (enteredValue: String, possibleValues: List<String>) -> List<String>

/**
* A object used to control command line parsing and pass data between commands.
* An object used to control command line parsing and pass data between commands.
*
* A new Context instance is created for each command each time the command line is parsed.
*
* @property parent If this context is the child of another command, [parent] is the parent command's context.
* @property command The command that this context associated with.
* @property allowInterspersedArgs If false, options and arguments cannot be mixed; the first time an argument is
* encountered, all remaining tokens are parsed as arguments.
* @property autoEnvvarPrefix The prefix to add to inferred envvar names. If null, the prefix is based on the
* parent's prefix, if there is one. If no command specifies, a prefix, envvar lookup is disabled.
* @property printExtraMessages Set this to false to prevent extra messages from being printed automatically.
* You can still access them at [CliktCommand.messages] inside of [CliktCommand.run].
* @property helpOptionNames The names to use for the help option. If any names in the set conflict with other
* options, the conflicting name will not be used for the help option. If the set is empty, or contains no
* unique names, no help option will be added.
* @property helpFormatter The help formatter for this command.
* @property tokenTransformer An optional transformation function that is called to transform command line
* tokens (options and commands) before parsing. This can be used to implement e.g. case insensitive
* behavior.
* @property terminal The terminal to used to read and write messages.
* @property argumentFileReader A block that returns the content of an argument file for a given filename.
* @property correctionSuggestor A callback called when the command line contains an invalid option or
* subcommand name. It takes the entered name and a list of all registered names option/subcommand
* names and filters the list down to values to suggest to the user.
* @property allowGroupedShortOptions If true, short options can be grouped after a single `-` prefix.
*/

class Context private constructor(
/**
* If this context is the child of another command, [parent] is the parent command's context.
*/
val parent: Context?,
/**
* The command that this context associated with.
*/
val command: CliktCommand,
/**
* If false, options and arguments cannot be mixed; the first time an argument is
* encountered, all remaining tokens are parsed as arguments.
*/
val allowInterspersedArgs: Boolean,
/**
* If true, short options can be grouped after a single `-` prefix.
*/
val allowGroupedShortOptions: Boolean,
/**
* The prefix to add to inferred envvar names. If null, the prefix is based on the
* parent's prefix, if there is one. If no command specifies, a prefix, envvar lookup is disabled.
*/
val autoEnvvarPrefix: String?,
/**
* Set this to false to prevent extra messages from being printed automatically.
* You can still access them at [CliktCommand.messages] inside of [CliktCommand.run].
*/
val printExtraMessages: Boolean,
/**
* The names to use for the help option. If any names in the set conflict with other options,
* the conflicting name will not be used for the help option. If the set is empty, or contains
* no unique names, no help option will be added.
*/
val helpOptionNames: Set<String>,
/**
* The help formatter for this command.
*/
val helpFormatter: (Context) -> HelpFormatter,
/**
* An optional transformation function that is called to transform command line tokens (options
* and commands) before parsing. This can be used to implement e.g. case-insensitive behavior.
*/
val tokenTransformer: Context.(String) -> String,
/**
* The terminal to used to read and write messages.
*/
val terminal: Terminal,
/**
* A block that returns the content of an argument file for a given filename.
*/
var argumentFileReader: ((filename: String) -> String)?,
/**
* If `false`,the [valueSource] is searched before environment variables.
*
* By default, environment variables will be searched for option values before the
* [valueSource].
*/
val readEnvvarBeforeValueSource: Boolean,
/**
* The source that will attempt to read values for options that aren't present on the
* command line.
*/
val valueSource: ValueSource?,
/**
* A callback called when the command line contains an invalid option or subcommand name. It
* takes the entered name and a list of all registered names option/subcommand names and filters
* the list down to values to suggest to the user.
*/
val correctionSuggestor: TypoSuggestor,
/**
* Localized strings to use for help output and error reporting.
*/
val localization: Localization,
/**
* A function called by Clikt to get a parameter value from a given environment variable
*
* The function returns `null` if the envvar is not defined.
*
* You can set this to read from a map or other source during tests.
*/
val readEnvvar: (String) -> String?,
/**
* An arbitrary object on the context.
*
* This object can be retrieved with functions [findOrSetObject] and [requireObject]. You
* can also set the object on the context itself after it's been constructed.
*/
var obj: Any?,
/**
* The original command line arguments.
*/
val originalArgv: List<String>,
) {
var invokedSubcommand: CliktCommand? = null
Expand Down Expand Up @@ -275,13 +325,13 @@ class Context private constructor(
}

/** Find the closest object of type [T], or throw a [NullPointerException] */
@Suppress("unused") // these extensions don't use their receiver, but we want to limit where they can be called
@Suppress("UnusedReceiverParameter") // these extensions don't use their receiver, but we want to limit where they can be called
inline fun <reified T : Any> CliktCommand.requireObject(): ReadOnlyProperty<CliktCommand, T> {
return ReadOnlyProperty { thisRef, _ -> thisRef.currentContext.findObject()!! }
}

/** Find the closest object of type [T], or null */
@Suppress("unused")
@Suppress("UnusedReceiverParameter")
inline fun <reified T : Any> CliktCommand.findObject(): ReadOnlyProperty<CliktCommand, T?> {
return ReadOnlyProperty { thisRef, _ -> thisRef.currentContext.findObject() }
}
Expand All @@ -294,7 +344,7 @@ inline fun <reified T : Any> CliktCommand.findObject(): ReadOnlyProperty<CliktCo
* without accessing the property, call [Context.findOrSetObject] in your [run][CliktCommand.run]
* function instead.
*/
@Suppress("unused")
@Suppress("UnusedReceiverParameter")
inline fun <reified T : Any> CliktCommand.findOrSetObject(crossinline default: () -> T): ReadOnlyProperty<CliktCommand, T> {
return ReadOnlyProperty { thisRef, _ -> thisRef.currentContext.findOrSetObject(default) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,29 @@ import com.github.ajalt.clikt.parameters.options.longestName
* An exception during command line processing that should be shown to the user.
*
* If calling [CliktCommand.main], these exceptions will be caught and the appropriate info will be printed.
*
* @property statusCode The value to use as the exit code for the process. If you use
* [CliktCommand.main], it will pass this value to `exitProcess` after printing [message]. Defaults
* to 1.
*/
open class CliktError(
message: String? = null,
cause: Exception? = null,
/**
* The value to use as the exit code for the process.
*
* If you use [CliktCommand.main], it will pass this value to `exitProcess` after printing
* [message]. Defaults to 1.
*/
val statusCode: Int = 1,
/**
* If true, the error message should be printed to stderr.
*/
val printError: Boolean = true,
) : RuntimeException(message, cause)

/** An interface for CliktErrors that have a context attached */
interface ContextCliktError {
/**
* The context of the command that raised this error.
* The context of the command that raised this error.
*
* Will be set automatically if thrown during command line processing.
* Will be set automatically if thrown during command line processing.
*/
var context: Context?
}
Expand All @@ -36,11 +41,14 @@ interface ContextCliktError {
* An exception that indicates that the command's help should be printed.
*
* Execution should be immediately halted.
*
* @property error If true, execution should halt with an error. Otherwise, execution halt with no error code.
*/
class PrintHelpMessage(override var context: Context?, val error: Boolean = false) :
CliktError(printError = false), ContextCliktError
class PrintHelpMessage(
override var context: Context?,
/**
* If true, the error message should be printed to stderr.
*/
val error: Boolean = false,
) : CliktError(printError = false), ContextCliktError

/**
* An exception that indicates that a message should be printed.
Expand Down Expand Up @@ -70,17 +78,23 @@ class Abort : ProgramResult(statusCode = 1)
*/
class PrintCompletionMessage(message: String) : PrintMessage(message)

/**
* An internal exception that signals a usage error.
*
* @property message The error message. Subclasses can leave this null and use [formatMessage] instead.
* @property paramName The name of the parameter that caused the error. If possible, this should be
* set to the actual name used. Will be set automatically if thrown from a `convert` lambda.
* @property statusCode The process status code to use if exiting the process as a result of this error.
*/
/** An exception that signals a usage error. */
open class UsageError(
/** The error message. Subclasses can leave this null and use [formatMessage] instead. */
message: String?,
/**
* The name of the parameter that caused the error.
*
* If possible, this should be set to the actual name used. Will be set automatically if thrown
* from a `convert` lambda.
*/
var paramName: String? = null,
/**
* The value to use as the exit code for the process.
*
* If you use [CliktCommand.main], it will pass this value to `exitProcess` after printing
* [message]. Defaults to 1.
*/
statusCode: Int = 1,
) : CliktError(message, statusCode = statusCode), ContextCliktError {
constructor(message: String, argument: Argument, statusCode: Int = 1)
Expand Down

0 comments on commit 6570552

Please sign in to comment.