c4spar / deno-cliffy

Command line framework for deno 🦕 Including Commandline-Interfaces, Prompts, CLI-Table, Arguments Parser and more...

Home Page:https://cliffy.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Support for setup actions?

chmac opened this issue · comments

Firstly, thanks a lot for cliffy, it's awesome. 🧡

I'm trying to do some setup actions which should occur before all commands are invoked, but which rely on the global options being parsed. Is there currently a way to do that in cliffy? I naively tried command.getGlobalOptions() before calling .parse(), but that doesn't seem to do what I'm looking for.

The specific use case is to call log.setup() based on the --debug and --verbose flags. I'm hoping to invoke that once so that it doesn't need to be repeated in each of my action handlers.

Hi @chmac, you can take a look at option action handler. They are executed after all options are parsed but before the command action handler is executed.

@c4spar Apologies, my original issue wasn't strictly accurate. I said after the global options are parsed, but I actually mean after all the options are parsed. We have 2 commands, one which starts a daemon, and it's configured via env vars, and the other which takes arguments like --foo. I was hoping to roll up all the parsed options and env vars for whatever the current command is, and then do some setup stuff.

I've written this as a custom function, and I can invoke it from both of my actions. But if such an option was available, I'd prefer to have 1 hook that I can be sure will be called before every command, even if I add more in the future, etc.

Perhaps our use case is a bit of an edge case, so if you think this is out of scope, no worries.

You mean you basically want to have a global action handler like this?

new Command()
  .globalOption("--debug", "...")
  .globalOption("--verbose", "...")
  .globalAction(({debug, verbose}) => {
    console.log("global action");
    // log.setup({...})
  })
  .command("foo")
  .action(() => console.log("foo action"))
  .command("bar")
  .action(() => console.log("bar action"))
$ deno run example.ts foo --debug --verbose
global action
foo action

@c4spar Yes, exactly!

Even more awesome, would be if .globalAction() could pass a return value, maybe called context which is passed to all .action() calls.

For example, to setup a logger based on global options (env vars, etc) and then pass it into the action handler. Or to manage database setup and then pass the database connection to the action handler.

I think this would make the generics more complicated and I'm not sure it's worth it. this can also be acheaved with a shared module. But adding a global action handler is fine more me 👍

Yes, I can see your point. The challenge I had with the shared module approach was that some of my commands accept arguments while they all accept environment variables. So figuring out the typing for my generic "setup" function was a bit of a headache. I figured a single pre-action hook would be easier. But it might not be enough of an advantage to be worth incorporating into cliffy. It might also be such an unusual need that again, not worth it.

If you feel like the idea is out of scope for cliffy, happy to close the issue.

Reading your last message again, maybe I misunderstood what a global action handler would do. How would you see that working?

I think it should be very easy to implement the globalAction handler.
In the parseCommand method we can check if a global action handler is registered and add it to the ParseContext and then execute all global action handlers after the option action handler but before the main action handler.

@c4spar So the globalAction handler would run every time, but would be "isolated" from the specific action handlers, did I get that right? By isolated I mean, there's no way for a globalAction handler to pass values to an action handler.

Would it be possible to add something to this in the globalAction handler? Not sure how that would work as far as TypeScript is concerned though. Presumably there could also be multiple globalAction handlers, which might make it even more confusing from a type perspective.

Yep, i think thats how i would implement it in the first run. I think for a shared ctx there are more breaking changes required. I already thought about merging all generic type params from the command class into a single one and add an interface for it, than we could use the second type param as a ctx. This way it's also easier to define some specific type params wihtout defining all the previous ones.

Currently the command class has 8 generics type params, which is already a lot:

Command<ParantOpts, ParentTypes, Opts, Args, GlobalOpts, Types, GlobalTypes, Parent>

I was thinking about to change the generics to something like this:

Command<{
  args?: Array<string>,
  opts?: Record<string, unknown>,
  globalOpts?: Record<string, unknown>,
  types?: Record<string, unknown>,
  globalTypes?: Record<string, unknown>,
  parent?: Command,
  parentOpts?: Record<string, unknown>,
  parentTypes?: Record<string, unknown>
}, {
  log: MyLogger,
  db: MyDatabase
}>

Than we could use the second type param for the ctx. But to avoid breaking changes to the action handler, we would need to add the ctx to the options object or to the this type.

But I think that would be something I would maybe do in v2.

@chmac I opened a PR with the global action handler #555. If you want you can try it out to see if this would work for you.

@c4spar Looks good to me. It would allow me to set a global variable in the globalAction() method and then reference it inside my action() methods. Thanks a lot.