swift-argument-parser
swift-argument-parser copied to clipboard
Passing state from root to subcommands
Working on a tool with a handful of subcommands, I find myself wanting to handle common work in the root command, and then pass some state to the subcommand. I’m not sure what might be possible, but something like this is what I’d like to be able to do:
% raise3d --addr 192.168.1.23 monitor --notify
@main
struct RootCommand : AsyncParsableCommand<Raise3DAPI> {
static
var
configuration = CommandConfiguration(commandName: "raise3d",
abstract: "A utility to interact witih Raise3D printers.",
subcommands: [Monitor.self])
@Option(help: "The printer’s local address.")
var addr: String
@Option(help: "The printer’s password.")
var password: String?
mutating
func run() async throws -> Raise3DAPI {
let password = self.password ?? { /* prompt user for password */ }()
let api = Raise3DAPI(host: self.options.addr, password: self.options.password)
try await api.login()
return api
}
}
struct Monitor : AsyncParsableCommand<Raise3DAPI> {
static
var configuration = CommandConfiguration(commandName: "monitor",
abstract: "Monitor the printer and optionally notify of errors.")
@Option(help: "Notify if error.")
var notify: Bool = false
mutating
func run(state inAPI: Raise3DAPI) async throws
{
while (true) {
let jobInfo = try await inAPI.getJobInfo()
if self.notify && jobInfo.status == .error {
// Send notification
}
Task.sleep(for: .seconds(1))
}
}
}
class Raise3DAPI
{
// …
}
This lets subcommands be a little cleaner, because they don't have to redundantly declare the global options, and all the common work is done in the parent command. The desired state is generic.
If you nest Monitor
inside RootCommand
, then you can add a property annotated with @OptionGroup
in the former to access the options of the latter:
@main
struct RootCommand : AsyncParsableCommand<Raise3DAPI> {
...
struct Monitor : AsyncParsableCommand<Raise3DAPI> {
...
@OptionGroup
var root: RootCommand
mutating func run() async throws
// Access `root.addr` and `root.password`...
}
}
But you can't just make run
in RootCommand
return a value that you inject into the subcommand: Only one run
command is invoked and that's the one of the subcommand.
Maybe you can make Raise3DAPI
conform to ParsableArguments
such that you can inject that value directly as an @OptionGroup
(docs).
A similar problem I'm having, although not related to passing state, is that if my Root command is a container for subcommands, I don't have a chance to execute any code at startup. For example this doesn't let me setup a logging system since the only code that runs is from the subcommand. I can revert to using main.swift and manually running the main method on the command (although you need to dance a bit with async context in order to call the proper overload), but it would be nice if there was a way to run setup code and then let the specific subcommand run. There maybe some way but I haven't found it 🤔
I've been able to do this via the validate
method on the Root command, as SAP runs that method that for every command in the tree, even if you don't have a run
method implemented