peterbourgon / ff

Flags-first package for configuration
Apache License 2.0
1.34k stars 59 forks source link

Allow nested subcommands a chance at modifying the Context #101

Closed aw185176 closed 1 year ago

aw185176 commented 1 year ago

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.

peterbourgon commented 1 year ago

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?

peterbourgon commented 1 year ago

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.

aw185176 commented 1 year ago

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.

peterbourgon commented 1 year ago

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.

aw185176 commented 1 year ago

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