Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Passing state from root to subcommands #632

Open
JetForMe opened this issue Apr 17, 2024 · 3 comments
Open

Passing state from root to subcommands #632

JetForMe opened this issue Apr 17, 2024 · 3 comments

Comments

@JetForMe
Copy link

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.

@bisgardo
Copy link

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).

@alexito4
Copy link

alexito4 commented May 6, 2024

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 🤔

@Mordil
Copy link

Mordil commented Jul 12, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants