Improve ergonomics of early-exit flags on commands that require subcommands #10

Open
opened 2023-11-12 16:47:17 -07:00 by torque · 0 comments
Owner

This is a use case that deserves further consideration rather than an obvious technical issue. Let's discuss the use case.

Consider a reasonably complex command tree, whose root is responsible for loading or otherwise initializing program-global configuration. However, the program also provides an explicit flag to write its default configuration to the disk, which is a simplistic approach to help with non-backwards-compatible updates to the configuration schema.

So for example, normal use would be:

my-program do-something-useful -p hello

And to do a config refresh,

my-program --write-default-config

Which would write the config to disk and then exit immediately.

Normally, my-program should be specified as requiring a subcommand, since it does nothing when invoked on its own. However, the --write-default-config command does not execute a subcommand, so this imposes some restriction on the usage. There are currently a few ways to solve or fail to solve this:

  1. make write-default-config a subcommand rather than a command. This doesn't work in this use case because the write-default-config behavior has to be known to the my-program root callback so that it doesn't try to load the existing config as it would normally do (since the existing config may have the wrong schema and fail to parse, which should not cause the new config to fail to be emitted but should fail in all other use scenarios).

  2. mark --write-default-config as an eager parameter. eager parameters are converted before the subcommand_required flag is checked, so the converter could call std.process.exit to exit before the flag is checked. This would work in theory, but the ergonomics suck because it requires partially reimplementing the builtin flag converter (which takes a string because flags can be specified by env vars, if that is configured. Calling the default flag converter is nontrivial because it's generated by the generics, which isn't actually necessary for the flag converter specifically, though it does need to know the user context type).

  3. set subcommand_required to false and handle the flag in the root command group callback. However, this callback has no way to know if a subcommand is scheduled to run after it, so there's no way to inform the user that they need to provide a subcommand to do actual work.

Looking at this list, two obvious routes for improvement stick out: allow eager options to take a callback that is passed the post-conversion value, so common cases like flags don't require the extra boilerplate, and change the command callback signature to take an optional string (or possibly an optional ParserInterface) of the name of the subcommand that will be run. The semantics of the eager callback perhaps have some design space in them, as

Either one of those on its own would solve the problem, but I think it potentially makes sense to implement both. For this particular use case, handling the logic in a callback would be a bit more ergonomic as it would allow leaving subcommand_required true and benefitting from the default behavior there. If we allow multiple possible callback signatures, this could be made opt-in, which would be nice.

This is a use case that deserves further consideration rather than an obvious technical issue. Let's discuss the use case. Consider a reasonably complex command tree, whose root is responsible for loading or otherwise initializing program-global configuration. However, the program also provides an explicit flag to write its default configuration to the disk, which is a simplistic approach to help with non-backwards-compatible updates to the configuration schema. So for example, normal use would be: ```fish my-program do-something-useful -p hello ``` And to do a config refresh, ```fish my-program --write-default-config ``` Which would write the config to disk and then exit immediately. Normally, `my-program` should be specified as requiring a subcommand, since it does nothing when invoked on its own. However, the `--write-default-config` command does not execute a subcommand, so this imposes some restriction on the usage. There are currently a few ways to solve or fail to solve this: 1. make `write-default-config` a subcommand rather than a command. This doesn't work in this use case because the `write-default-config` behavior has to be known to the `my-program` root callback so that it doesn't try to load the existing config as it would normally do (since the existing config may have the wrong schema and fail to parse, which should not cause the new config to fail to be emitted but should fail in all other use scenarios). 2. mark `--write-default-config` as an eager parameter. eager parameters are converted before the `subcommand_required` flag is checked, so the converter could call `std.process.exit` to exit before the flag is checked. This would work in theory, but the ergonomics suck because it requires partially reimplementing the builtin flag converter (which takes a string because flags can be specified by env vars, if that is configured. Calling the default flag converter is nontrivial because it's generated by the generics, which isn't actually necessary for the flag converter specifically, though it does need to know the user context type). 3. set `subcommand_required` to false and handle the flag in the root command group callback. However, this callback has no way to know if a subcommand is scheduled to run after it, so there's no way to inform the user that they need to provide a subcommand to do actual work. Looking at this list, two obvious routes for improvement stick out: allow eager options to take a callback that is passed the post-conversion value, so common cases like flags don't require the extra boilerplate, and change the command callback signature to take an optional string (or possibly an optional ParserInterface) of the name of the subcommand that will be run. The semantics of the eager callback perhaps have some design space in them, as Either one of those on its own would solve the problem, but I think it potentially makes sense to implement both. For this particular use case, handling the logic in a callback would be a bit more ergonomic as it would allow leaving `subcommand_required` true and benefitting from the default behavior there. If we allow multiple possible callback signatures, this could be made opt-in, which would be nice.
torque added this to the 0.1.0 milestone 2023-11-12 16:47:17 -07:00
torque added the
Kind/Enhancement
label 2023-11-12 16:47:17 -07:00
torque self-assigned this 2023-11-12 16:47:17 -07:00
Sign in to join this conversation.
No description provided.