Allow nested subcommands a chance at modifying the Context
aw185176 opened this issue · comments
I have created a CLI using ffcli
that has multiple tiers of subcommands. Some subcommand trees have different requirements than others. For a contrived example, one subcommand tree needs to produce one type of client, and one subcommand tree needs to produce another type of client.
I would like to populate the context.Context
passed from the root command via Run(context.Content)
with computed information that is dependent on the subcommand tree root, such that if the command is foo setup bar <....>
where bar
is a subcommand of setup
, setup
gets a chance to modify the context being passed through.
I am thinking along the lines of how things like logr.FromContext()
works, as a means of loosely coupling my layers that have distinct needs.
In situations like this, the solution is usually to put a sort of configuration object into the context, with reference semantics. This allows "deeper" layers of code to write information that can be subsequently read by "higher" layers of code, without needing to play games with wrapping or run multiple passes. ctxdata is an example of this pattern. Would that solve your use case?
But, I would probably caution against putting stuff like the client (in your example) into a context. If some code depends on a client in order to do its job, providing it through a context has the effect of hiding it, and makes it easy to forget to provide it. A client should probably be expressed as an explicit dependency, so it becomes a clear part of the API contract, visible, testable, etc.
For that particular example, I'd suggest taking a look at the two-phase approach used by the objectctl example. It has a client that's instantiated after the command tree is parsed in the parse phase, and can then be consumed by any subcommand during the run phase.
Thanks for the thoughtful response, I've been using ff/ffcli for a few years now very happily.
But, I would probably caution against putting stuff like the client (in your example) into a context. If some code depends on a client in order to do its job, providing it through a context has the effect of hiding it, and makes it easy to forget to provide it. A client should probably be expressed as an explicit dependency, so it becomes a clear part of the API contract, visible, testable, etc.
Definitely agree, my contrived example wasn't great. Something a little more realistic is foo build <subcommands>
and foo apply <subcommands>
requiring orthogonal configuration contexts. Rather than only foo
getting a crack at mutating the context, if the selected subcommand root also got a chance, I could add only the needed configuration contexts.
Your suggestion of ctxdata
is right on, but I don't think it is possible today with ffcli to let any actors write to that context other than the root command and the leaf command that was selected (no levels in between). I may be (probably am) missing an angle here.
but I don't think it is possible today with ffcli to let any actors write to that context other than the root command and the leaf command that was selected (no levels in between). I may be (probably am) missing an angle here.
Then I'm not sure I understand what you're after. If someone does objectctl foo bar baz
what should happen differently than today? I don't think it makes sense for the Exec functions of the root, foo, and bar subcommands to be invoked, right?
edit: This maybe seems like some kind of hierarchical set-up or initialization code? That is, pretend each subcommand had a Setup
function, separate from Exec
, which was run after the parse phase, but before the run phase. Then, given objectctl foo bar baz
, the run phase would first execute root.Setup
, then foo.Setup
, then bar.Setup
, then baz.Setup
, and then finally baz.Exec
?
If that's right, then you can solve this in a similar way to shared global flags in the objectctl
example. Define each command's setup code as a function alongside the command. Then, write your exec functions so that the first thing they do is call a "composite" setup function, which you define, during construction, as the composition of all parent setup functions.
rootcmd = ...
rootsetup = ...
rootcmd.Exec = func() { rootsetup(); ... }
foocmd = ...
foosetup = ...
foocmd.Exec = func() { rootsetup(); foosetup(); ... }
barcmd = ...
barsetup = ...
barcmd.Exec = func() { rootsetup(); foosetup(); barsetup(); ... }
It's easy to imagine a helper utility to compose Exec functions with ancestral setup functions.
I don't think it makes sense for the Exec functions of the root, foo, and bar subcommands to be invoked, right?
I agree.
This maybe seems like some kind of hierarchical set-up or initialization code?
Right on. What you describe is exactly what I am after.
Define each command's setup code as a function alongside the command. Then, write your exec functions so that the first thing they do is call a "composite" setup function, which you define, during construction, as the composition of all parent setup functions.
This is what I do today. I do think I will pursue the helper utility you're describing. Thanks again for your time, I'll share the end result if I come up with anything worthwhile. I think we can close this now