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

Rewrite parser to support custom run signatures #508

Merged
merged 43 commits into from
Apr 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
88d4dcb
Add NoSuchArgument
ajalt Mar 30, 2024
50b8cf1
Separate parsing from execution
ajalt Mar 30, 2024
c53d570
Remove deprecated functions from CliktCommand
ajalt Mar 30, 2024
821fe04
Add BaseCliktCommand
ajalt Mar 30, 2024
1e8ac8b
Update everything to use new main extensions
ajalt Mar 31, 2024
b3f4759
Finalize and execute in parse
ajalt Mar 31, 2024
cd3a24c
Move command parser into a class
ajalt Mar 31, 2024
942d7ca
Fix finalization context
ajalt Mar 31, 2024
ff02f3e
Fix excess arguments inside a multiple subcommand
ajalt Mar 31, 2024
8af13e9
Add reusable main implementation
ajalt Apr 1, 2024
5b33996
Set context eagerly
ajalt Apr 1, 2024
2d1b262
Remove Function bound on generic
ajalt Apr 1, 2024
9706b8f
Change multi arg parsing with allowMultipleSubcommands
ajalt Apr 2, 2024
5768269
Move errors list to individual invocations
ajalt Apr 2, 2024
e1237ed
Re-add support for invokeWithoutSubcommand
ajalt Apr 2, 2024
2eb96cc
Handle eager options in groups
ajalt Apr 2, 2024
45c0d31
Add CommandLineParser.parseAndRun
ajalt Apr 3, 2024
d5d0a2f
Fix double validation of grouped options
ajalt Apr 3, 2024
66dd09c
Report value errors during finalization
ajalt Apr 5, 2024
065b708
Reenable completion generation
ajalt Apr 6, 2024
45db143
Deprecate Context.originalArgv
ajalt Apr 6, 2024
012078a
Move completion check to finalize
ajalt Apr 7, 2024
0ca8c89
Implement Context.invokedSubcommands and close
ajalt Apr 7, 2024
b49b8c0
make parse results recursive
ajalt Apr 9, 2024
76b57cf
Fix final tests
ajalt Apr 10, 2024
a5d7b33
Switch to self types instead of runner
ajalt Apr 10, 2024
2d58250
Add test for expandedArgv
ajalt Apr 10, 2024
434e552
Add SuspendingCliktCommand
ajalt Apr 10, 2024
e7230c7
Add ChainedCliktCommand
ajalt Apr 11, 2024
0f397df
Add CliktUtil.exitProcess
ajalt Apr 11, 2024
28989a4
Simplify throwErrors
ajalt Apr 11, 2024
68db911
Remove most CliktCommand constructor parameters
ajalt Apr 12, 2024
cda4a34
Rename commandHelp -> help
ajalt Apr 12, 2024
8dafb16
Add docs for tokenize
ajalt Apr 13, 2024
1c919ff
Add docs for new command types
ajalt Apr 13, 2024
c4ed82f
Add .test() extenstion for new command types
ajalt Apr 14, 2024
cf4edb8
Add docs for custom command types
ajalt Apr 14, 2024
1417057
Add migration guide
ajalt Apr 14, 2024
e9d9cf1
Optimize imports
ajalt Apr 14, 2024
0b0d7d9
Remove deprecated annotations
ajalt Apr 14, 2024
057dcc0
Dump api
ajalt Apr 14, 2024
f2c5de4
Update plugins sample
ajalt Apr 14, 2024
8a86b0d
Remove Invocation -> OptionInvocation
ajalt Apr 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,47 @@
# Changelog

## Unreleased
### Added
- Added `NoSuchArgument` exception that is thrown when too many arguments were given on the command line. Previously, a less specific `UsageError` was thrown instead.
- Added `CliktUtil.exitProcess`, which is a cross-platform way to exit the process with a status code.
- Added `CommandLineParser.tokenize` that splits a string into argv tokens.
- Added `CommandLineParser` that provides functions for parsing and finalizing commands manually for more control.
- Added `Context.invokedSubcommands` that contains all subcommands of the current command that are going to be invoked when `allowMultipleSubcommands` is `true`.
- Added `SuspendingCliktCommand` that has a `suspend fun run` method, allowing you to use coroutines in your commands.
- Added `ChainedCliktCommand` that allows you to return a value from your `run` method and pass it to the next command in the chain.

## 4.4.0
### Added
- Publish `linuxArm64` and `wasmJs` targets.

### Changed
- In a subcommand with `argument().multiple()`, the behavior is now the same regardless of the value of `allowMultipleSubcommands`: if a token matches a subcommand name, it's now treated as a subcommand rather than a positional argument.
- Due to changes to the internal parsing algorithm, the exact details of error messages when multiple usage errors occur have changed in some cases.
- **Breaking Change:** Moved the following parameters from `CliktCommand`'s constructor; override the corresponding properties instead:

| removed parameter | replacement property |
|-----------------------------|---------------------------------|
| `help` | `fun help` |
| `epilog` | `fun helpEpilog` |
| `invokeWithoutSubcommand` | `val invokeWithoutSubcommand` |
| `printHelpOnEmptyArgs` | `val printHelpOnEmptyArgs` |
| `helpTags` | `val helpTags` |
| `autoCompleteEnvvar` | `val autoCompleteEnvvar` |
| `allowMultipleSubcommands` | `val allowMultipleSubcommands` |
| `treatUnknownOptionsAsArgs` | `val treatUnknownOptionsAsArgs` |
| `hidden` | `val hiddenFromHelp` |
- The following methods on `CliktCommand` have been renamed: `commandHelp` -> `help`, `commandHelpEpilog` -> `epilog`. The old names are deprecated.
- **Breaking Change:** `CliktCommand.main` and `CliktCommand.parse` are now extension functions rather than methods.

### Fixed
- Fixed excess arguments not being reported when `allowMultipleSubcommands=true` and a subcommand has excess arguments followed by another subcommand.

### Deprecated
- Deprecated `Context.originalArgv`. It will now always return an empty list. If your commands need an argv, you can pass it to them before you run them.

### Removed
- Removed previously deprecated experimental annotations.

## 4.3.0
### Added
- Added `limit` parameter to `option().counted()` to limit the number of times the option can be used. You can either clamp the value to the limit, or throw an error if the limit is exceeded. ([#483](https://github.com/ajalt/clikt/issues/483))
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ dependencies {

#### Multiplatform

Clikt supports the following targets: `jvm`, `mingwX64`, `linuxX64`, `linuxArm64`, `macosX64`, `macosArm64'`,
Clikt supports the following targets: `jvm`, `mingwX64`, `linuxX64`, `linuxArm64`, `macosX64`, `macosArm64`,
and `js` and `wasmJs` (for both Node.js and Browsers). [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.
Expand Down
243 changes: 171 additions & 72 deletions clikt/api/clikt.api

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions clikt/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ kotlin {
dependencies {
api(kotlin("test"))
api(libs.kotest)
api(libs.coroutines.core)
api(libs.coroutines.test)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package com.github.ajalt.clikt.command

import com.github.ajalt.clikt.core.*
import com.github.ajalt.clikt.parsers.CommandLineParser
import com.github.ajalt.clikt.testing.CliktCommandTestResult
import com.github.ajalt.clikt.testing.test
import com.github.ajalt.mordant.rendering.AnsiLevel

/**
* A version of [CliktCommand] that returns a value from the [run] function, which is then passed to
* subcommands.
*
* This command works best if you set [allowMultipleSubcommands] to `true`.
*/
abstract class ChainedCliktCommand<T>(
/**
* The name of the program to use in the help output. If not given, it is inferred from the
* class name.
*/
name: String? = null,
) : BaseCliktCommand<ChainedCliktCommand<T>>(name) {
/**
* Perform actions after parsing is complete and this command is invoked.
*
* This takes the value returned by the previously invoked command and returns a new value.
*
* This is called after command line parsing is complete. If this command is a subcommand, this
* will only be called if the subcommand is invoked.
*
* If one of this command's subcommands is invoked, this is called before the subcommand's
* arguments are parsed.
*/
abstract fun run(value: T): T
}


/**
* Parse the command line and print helpful output if any errors occur.
*
* This function calls [parse] and catches any [CliktError]s that are thrown, exiting the process
* with the specified [status code][CliktError.statusCode]. Other errors are allowed to pass
* through.
*
* If you don't want Clikt to exit your process, call [parse] instead.
*/
fun <T> ChainedCliktCommand<T>.main(argv: List<String>, initial: T): T {
return CommandLineParser.mainReturningValue(this) { parse(argv, initial) }
}

/**
* Parse the command line and print helpful output if any errors occur.
*
* This function calls [parse] and catches any [CliktError]s that are thrown, exiting the process
* with the specified [status code][CliktError.statusCode]. Other errors are allowed to pass
* through.
*
* If you don't want Clikt to exit your process, call [parse] instead.
*/
fun <T> ChainedCliktCommand<T>.main(argv: Array<out String>, initial: T): T {
return main(argv.asList(), initial)
}

/**
* Parse the command line and throw an exception if parsing fails.
*
* You should use [main] instead unless you want to handle output yourself.
*/
fun <T> ChainedCliktCommand<T>.parse(argv: Array<String>, initial: T): T {
return parse(argv.asList(), initial)
}

/**
* Parse the command line and throw an exception if parsing fails.
*
* You should use [main] instead unless you want to handle output yourself.
*/
fun <T> ChainedCliktCommand<T>.parse(argv: List<String>, initial: T): T {
var value = initial
CommandLineParser.parseAndRun(this, argv) { value = it.run(value) }
return value
}


/**
* 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 <T> ChainedCliktCommand<T>.test(
argv: String,
initial: T,
stdin: String = "",
envvars: Map<String, String> = emptyMap(),
includeSystemEnvvars: Boolean = false,
ansiLevel: AnsiLevel = AnsiLevel.NONE,
width: Int = 79,
height: Int = 24,
): CliktCommandTestResult {
val argvArray = CommandLineParser.tokenize(argv)
return test(argvArray, initial, 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 <T> ChainedCliktCommand<T>.test(
argv: Array<String>,
initial: T,
stdin: String = "",
envvars: Map<String, String> = emptyMap(),
includeSystemEnvvars: Boolean = false,
ansiLevel: AnsiLevel = AnsiLevel.NONE,
width: Int = 79,
height: Int = 24,
): CliktCommandTestResult {
return test(
argv.asList(), initial, 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 <T> ChainedCliktCommand<T>.test(
argv: List<String>,
initial: T,
stdin: String = "",
envvars: Map<String, String> = emptyMap(),
includeSystemEnvvars: Boolean = false,
ansiLevel: AnsiLevel = AnsiLevel.NONE,
width: Int = 79,
height: Int = 24,
): CliktCommandTestResult {
return test(argv, stdin, envvars, includeSystemEnvvars, ansiLevel, width, height) {
parse(it, initial)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package com.github.ajalt.clikt.command

import com.github.ajalt.clikt.core.*
import com.github.ajalt.clikt.parsers.CommandLineParser
import com.github.ajalt.clikt.testing.CliktCommandTestResult
import com.github.ajalt.clikt.testing.test
import com.github.ajalt.mordant.rendering.AnsiLevel

/**
* A version of [CliktCommand] that supports a suspending [run] function.
*/
abstract class SuspendingCliktCommand(
/**
* The name of the program to use in the help output. If not given, it is inferred from the
* class name.
*/
name: String? = null,
) : BaseCliktCommand<SuspendingCliktCommand>(name) {
/**
* Perform actions after parsing is complete and this command is invoked.
*
* This is called after command line parsing is complete. If this command is a subcommand, this
* will only be called if the subcommand is invoked.
*
* If one of this command's subcommands is invoked, this is called before the subcommand's
* arguments are parsed.
*/
abstract suspend fun run()
}


/**
* Parse the command line and print helpful output if any errors occur.
*
* This function calls [parse] and catches any [CliktError]s that are thrown, exiting the process
* with the specified [status code][CliktError.statusCode]. Other errors are allowed to pass
* through.
*
* If you don't want Clikt to exit your process, call [parse] instead.
*/
suspend fun SuspendingCliktCommand.main(argv: List<String>) {
CommandLineParser.main(this) { parse(argv) }
}

/**
* Parse the command line and print helpful output if any errors occur.
*
* This function calls [parse] and catches any [CliktError]s that are thrown, exiting the process
* with the specified [status code][CliktError.statusCode]. Other errors are allowed to pass
* through.
*
* If you don't want Clikt to exit your process, call [parse] instead.
*/
suspend fun SuspendingCliktCommand.main(argv: Array<out String>) = main(argv.asList())

/**
* Parse the command line and throw an exception if parsing fails.
*
* You should use [main] instead unless you want to handle output yourself.
*/
suspend fun SuspendingCliktCommand.parse(argv: Array<String>) {
parse(argv.asList())
}

/**
* Parse the command line and throw an exception if parsing fails.
*
* You should use [main] instead unless you want to handle output yourself.
*/
suspend fun SuspendingCliktCommand.parse(argv: List<String>) {
CommandLineParser.parseAndRun(this, argv) { it.run() }
}

/**
* 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
*/
suspend fun SuspendingCliktCommand.test(
argv: String,
stdin: String = "",
envvars: Map<String, String> = emptyMap(),
includeSystemEnvvars: Boolean = false,
ansiLevel: AnsiLevel = AnsiLevel.NONE,
width: Int = 79,
height: Int = 24,
): CliktCommandTestResult {
val argvArray = CommandLineParser.tokenize(argv)
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
*/
suspend fun SuspendingCliktCommand.test(
argv: Array<String>,
stdin: String = "",
envvars: Map<String, String> = emptyMap(),
includeSystemEnvvars: Boolean = false,
ansiLevel: AnsiLevel = AnsiLevel.NONE,
width: Int = 79,
height: Int = 24,
): CliktCommandTestResult {
return test(argv.asList(), 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
*/
suspend fun SuspendingCliktCommand.test(
argv: List<String>,
stdin: String = "",
envvars: Map<String, String> = emptyMap(),
includeSystemEnvvars: Boolean = false,
ansiLevel: AnsiLevel = AnsiLevel.NONE,
width: Int = 79,
height: Int = 24,
): CliktCommandTestResult {
return test(argv, stdin, envvars, includeSystemEnvvars, ansiLevel, width, height) {
parse(it)
}
}