-
Notifications
You must be signed in to change notification settings - Fork 121
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
Suspending command support #503
Comments
Thanks for the links, Jake. To summarize the previous discussions: Making Correct me if I'm wrong, but there's isn't any functionality gained my providing a If Clikt made I guess we could provide |
If you propagate the I think the big advantage to making such a change is that it becomes easier to use in Kotlin/JS and with a In general, you're not supposed to put |
Those are good points. And running outside of the class only really works well if you don't have any subcommands. It's a bummer that changing I don't know, do you think that it's worth making that backwards incompatible change? |
While I'm open to this idea of a separate SuspendingCliktCommand, I am strongly opposed to forcing all CliktCommand classes to be suspending and opposed to adding a dependency on kotlinx.coroutines for the core clikt module. There are many cases where people will not use any coroutines and it would be a significant amount of bloating, both cognitive and physical, to be forced to have these functions be suspending. The main reason I got into making command line programs is to be small and minimalistic. I think its important that this library is a bit more strict with regard to keeping things minimal on the core module. I think a separate SuspendingCliktCommand could be nice as long as it is not forced on anyone. In my opinion, it can just have a |
I am not opposed to having a separate module that provides an Main module:
"async" module:
|
I think starting with a I'm also interested in how blocking commands and suspending commands compose in various subcommands setups. An "async" module as proposed doesn't seem useful enough to me, and generally you don't want to be in the business of creating scopes unless your execution naturally maps to a lifecycle.
Yeah function coloring really bites here because really all we want to do is propagate the function color through the library. There are probably a few ways to accomplish this with a significant design change in the library, though. For example, one way to do this would be to split parsing from executing. Instead if calling Want synchronous? class MyCommand(..) {
override val runner = {
println("Hello")
}
}
fun main(vararg args: String) {
MyCommand().parse(args).invoke()
} Want class MyCommand(..) {
override val runner = suspend {
delay(1.seconds)
println("Hello")
}
}
suspend fun main(vararg args: String) {
MyCommand().parse(args).invoke()
} Want... Compose?!? class MyCommand(..) {
override val runner = @Composable {
Text("Hello")
}
}
suspend fun main(vararg args: String) = runMosaic {
val runner = MyCommand().parse(args)
setContent(runner)
} Granted this is very off-the-cuff and shouldn't be taken too seriously verbatim. |
Splitting parsing and executing in this way sounds like an excellent idea! |
So I thought I'd share the design I'm working on in case anyone had feedback. I'm introducing a new base class with a generic runner abstract class BaseCliktCommand<RunnerT : Function<*>> {
abstract val runner: RunnerT
// everything currently in CliktCommand moves here
}
abstract class CliktCommand: BaseCliktCommand<() -> Unit>() {
final override val runner: () -> Unit get() = ::run
abstract fun run()
} The I'll probably also remove all of the constructor parameters to The generics on fun <RunnerT : Function<*>, CommandT : BaseCliktCommand<RunnerT>> CommandT.subcommands(
vararg commands: BaseCliktCommand<RunnerT>,
) I'll provide a way to manually control parse and finalize: object CommandLineParser {
// does not throw, returns info on which commands to run and a list of any errors encountered
fun <RunnerT : Function<*>> parse(
command: BaseCliktCommand<RunnerT>, originalArgv: List<String>,
): CommandLineParseResult<RunnerT>
// throws exceptions encountered during finalization
fun finalize(invocation: CommandInvocation<*>)
} Then the current fun BaseCliktCommand<() -> Unit>.parse(argv: List<String>) {
val result = CommandLineParser.parse(this, argv)
result.throwErrors()
for (invocation in result.invocations) {
CommandLineParser.finalize(invocation)
invocation.command.runner()
}
} I'll provide base commands for a couple of different runner types: abstract class SuspendingCliktCommand: BaseCliktCommand<suspend () -> Unit>() {
override val runner: suspend () -> Unit get() = ::run
abstract suspend fun run()
} and maybe /** Passes the output of one subcommand to the next one */
abstract class ChainedCliktCommand<T>: BaseCliktCommand<(T) -> T>() {
override val runner: (T) -> T get() = ::run
abstract fun run(t: T): T
}
fun <T> BaseCliktCommand<(T) -> T>.parse(argv: List<String>, initial: T): T {
var value = initial
val result = CommandLineParser.parse(this, argv)
result.throwErrors()
for (invocation in result.invocations) {
CommandLineParser.finalize(invocation)
value = invocation.command.runner(value)
}
return value
} Anyway, that's what I'm working on. It's a fair amount of work since I have to rewrite most of the internals and handle all of the parsing edge cases without being able to interleave parsing and finalization. Fortunately, the work done in #474 makes it possible. |
Thank you for sharing. Great ideas.
I vote for not having the One small naming question that arises if we don't include an upper bound of Overall this update seems like it will be really useful. |
The I'm definitely open to any naming suggestions, though. Now's the time to bikeshed that sort of thing. |
Let me see if I understand this correctly.
I do not know what And then the I think this is just making me wonder why exactly In most circumstances subclasses such as But the only time Let me try to explain with an example. Say that this is // note it no longer needs a generic parameter
abstract class BaseCliktCommand {
// everything currently CliktCommand, minus `run` and `runner`
} And now say I define for myself: abstract class FlowCliktCommand<out T>: BaseCliktCommand {
val flow: Flow<T> = // create a special flow based on the parameters
} Now I will need to also write my fun FlowCliktCommand<T>.parse(argv: List<String>): Flow<T> {
val result = CommandLineParser.parse(this, argv)
result.throwErrors()
return flow {
for (invocation in result.invocations) {
CommandLineParser.finalize(invocation) // maybe this belongs outside the flow?
emitAll(invocation.command.flow)
}
}
} The only point I am making here is that I created my perfect custom |
Sometimes a naming issue hints at a design issue... when it isn't procrastination or bikeshedding |
The generic parameter allows us to enforce that all subcommands have the same runner type as their parent command. The parse result class looks like this: data class CommandInvocation<RunnerT>(
val command: BaseCliktCommand<RunnerT>,
val optionInvocations: Map<Option, List<Invocation>>,
val argumentInvocations: List<ArgumentInvocation>,
)
class CommandLineParseResult<RunnerT>(
val invocations: List<CommandInvocation<RunnerT>>,
val errors: List<CliktError>,
) Without the generic parameter, you'd have to cast the It also means that I'm seeing some unnecessary repetition, so I'll change |
Thank you for explaining about the type checking here. I agree that we don't want to require downcasting. Could we have a self-referencing type parameter? I am curious if this could give us the same type checking benefits you described in your last comment, but would still be able to remove the unnecessary I'm sharing the messy snippet below just to show that the type checking seems to work all around as expected in various scenarios. abstract class BaseCliktCommand<C: BaseCliktCommand<C>> {
// everything currently CliktCommand, minus `run` and `runner`
private val mutableSubCommands = mutableListOf<C>()
val subCommands: List<C> = mutableSubCommands
fun addSubCommand(subCommand: C) = mutableSubCommands.add(subCommand)
}
abstract class NormalCliktCommand: BaseCliktCommand<NormalCliktCommand>() {
abstract fun run()
}
class MyNormalCliktCommand1: NormalCliktCommand() {
init {
addSubCommand(MyNormalCliktCommand2())
}
override fun run() {
println("1")
}
}
class MyNormalCliktCommand2: NormalCliktCommand() {
override fun run() {
println("2")
}
}
abstract class FlowCliktCommand<T>: BaseCliktCommand<FlowCliktCommand<T>>() {
val flow: Flow<T> = flow {
emitOutput()
}
protected abstract suspend fun FlowCollector<T>.emitOutput()
}
object CommandLineParser {
fun <C: BaseCliktCommand<C>> parse(command: BaseCliktCommand<C>,arv: List<String>): CommandLineParseResult<C> {
}
fun <C: BaseCliktCommand<C>> finalize(invokation: CommandInvocation<C>) {
}
}
data class CommandInvocation<C: BaseCliktCommand<C>>(
val command: C,
val optionInvocations: Map<String,String>,
val argumentInvocations: List<String>,
)
class CommandLineParseResult<C: BaseCliktCommand<C>>(
val invocations: List<CommandInvocation<C>>,
val errors: List<Exception>,
) {
fun throwErrors() {
errors.forEach {
throw it // in real implementation combine them or whatever
}
}
}
fun <T> FlowCliktCommand<T>.parse(argv: List<String>): Flow<T> {
val result = CommandLineParser.parse(this, argv)
result.throwErrors()
return flow {
for (invocation in result.invocations) {
CommandLineParser.finalize(invocation) // maybe this belongs outside the flow?
emitAll(invocation.command.flow)
}
}
}
abstract class NumberFlowCommand<N: Number>: FlowCliktCommand<N>()
class AnyNumberFlowCommand: NumberFlowCommand<Number>() {
override suspend fun FlowCollector<Number>.emitOutput() {
emit(1)
emit(2.0)
}
}
class IntFlowCommand: NumberFlowCommand<Int>() {
override suspend fun FlowCollector<Int>.emitOutput() {
emit(1)
emit(2)
}
}
fun main() {
IntFlowCommand().addSubCommand(IntFlowCommand())
AnyNumberFlowCommand().addSubCommand(AnyNumberFlowCommand())
IntFlowCommand().addSubCommand(AnyNumberFlowCommand()) // compilation error here, as expected
} |
Is there any built-in support for commands that can suspend? I wrote this basic wrapper around
CliktCommand
that adds a suspending command for my application:The text was updated successfully, but these errors were encountered: