Open abesto opened 3 weeks ago
Pipe-dream: #[command(flatten, from_global)] would magically, transparently transform the global args into from_global variants. For extra strength pipe-dream: also magically share the impl. I suspect this would require hooking into the globals passing machinery of clap such that we don't generate a new struct, but instead use GlobalOptions in both sites (instead of doing this at an external-to-clap-machinery macro level).
The challenge with #[command(flatten, from_global)]
is that proc macros can't talk to each other at compile time. We'd have to implement this at runtime through the code we generate. This requires expanding the Args
trait with "just one more parameter" to change how the code is generated. Unfortunately, we have a lot of these "just one more"s and are concerned about what principle is it ok to expand this and that its either a breaking change or a messy transition because people can hand implement these traits and the new functionality wouldn't work with them.
Maybe one way of doing this is if #[command(flatten, from_global)]
only invoked FromArgMatches
and not Args
. This would require everything in the struct to be from_global
or else they get ignored.
This might be asking for too much magic, maybe? If so, a less magical but still-nice solution might be (possibly not part of clap?) a macro that generates both the global and the from_global variants (with doc comments and the impl copied). Maybe something like:
Not thrilled with having macros for such specialized cases. Also, personally I'm bothered when macros do anything besides generate a trait impl because its harder to understand what they do.
If instead the preferred way is an explicitly propagated GlobalOptions instance, and from_global is just kinda... there, then knowing that would also be good!
For myself, I find probably 75% (made up number) of times people discuss using globals, they are reaching for the wrong tool. They want to "DRY" their code when there isn't an inherent requirement that all subcommands have a shared argument. Instead, its only happenstance. We have this problem with cargo itself today, e.g. --offline
was made global but it is meaningless in some commands.
Of the remaining uses for globals, I somewhat question the value of from_global
. It was added in #2026 but there was no discussion on use cases. Without a good understanding of why the use case is important enough for built-in support, it makes it harder to smooth out the path in even more directions.
Thank you for the quick and detailed response!
I pinky-promise I do actually have a real actual use-case for global args, but that doesn't change the maths on ecosystem-wide cost/benefit.
Of the remaining uses for globals, I somewhat question the value of from_global. It was added in https://github.com/clap-rs/clap/pull/2026 but there was no discussion on use cases. Without a good understanding of why the use case is important enough for built-in support, it makes it harder to smooth out the path in even more directions.
That's good to know. As for the why: for me, it's mainly ergonomics. Slightly stronger: if all global propagation logic is handled by clap
, then building some kinds of generic abstractions over clap
becomes simpler. This rapidly devolves into "how do you do subcommand dispatch with zero boilerplate", which is not a can of worms I wanna open right now (and possibly not a matter clap
itself should take a stand on?)
Given all of ^, it sounds like explicitly propagated GlobalOptions
structs remain the way to go (until such time as someone comes around and provides a real compelling case for from_global
and also solves OP).
This is an outcome I can work with!
Please complete the following tasks
Clap Version
4.5.4
Describe your use case
Consider some global options, and some logic that reads multiple fields. Implementing this with arg-level
global
andfrom_global
attributes quickly becomes clumsy. To avoid a lot of duplication, it's useful to group these args into astruct
and place the shared logic in itsimpl
(andflatten
it into each subcommand).Currently (AFAICT) this requires one struct with
global
, and another withfrom_global
, with the fields matched up manually, including documentation (both for--help
and IDEs on thefrom_global
side). In the worst case, theimpl
also needs to be duplicated. This might look like this:The
impl
can probably be deduplicated somehow, but even so: this is clumsy and prone to drift.Describe the solution you'd like
Pipe-dream:
#[command(flatten, from_global)]
would magically, transparently transform theglobal
args intofrom_global
variants. For extra strength pipe-dream: also magically share theimpl
. I suspect this would require hooking into the globals passing machinery ofclap
such that we don't generate a newstruct
, but instead useGlobalOptions
in both sites (instead of doing this at an external-to-clap
-machinery macro level).So the above example would become:
(And then
some_command.global.some_logic()
would be valid.)This is slightly preferable to manual arg-level
from_global
s even in the absence of animpl
block, because of the automatically-synced doc comment between theglobal
andfrom_global
variant.Alternatives, if applicable
This might be asking for too much magic, maybe? If so, a less magical but still-nice solution might be (possibly not part of
clap
?) a macro that generates both theglobal
and thefrom_global
variants (with doc comments and theimpl
copied). Maybe something like:Or maybe:
This feels less neat, but hey, maybe it's the best we can get.
Additional Context
If instead the preferred way is an explicitly propagated
GlobalOptions
instance, andfrom_global
is just kinda... there, then knowing that would also be good!