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

Consider supporting async option/argument parsers #1900

Closed
aweebit opened this issue Jul 2, 2023 · 7 comments
Closed

Consider supporting async option/argument parsers #1900

aweebit opened this issue Jul 2, 2023 · 7 comments

Comments

@aweebit
Copy link
Contributor

aweebit commented Jul 2, 2023

I would like to parse an argument that is expected to equal an index to an array that can only be read asynchronously. Currently, I see no better way than to use an async argument parser and handle the promise it returns in the action handler. Here is a simple example with lowdb:

import { Command, InvalidArgumentError } from 'commander';

import { Low } from 'lowdb';
import { JSONFile } from 'lowdb/node';

const adapter = new JSONFile('test.json');
const db = new Low(adapter, ['foo', 'bar']);
await db.read();
await db.write();

const indexParser = async (val) => {
  if (val) {
    val = Number(val);
    if (Number.isInteger(val) && val >= 0) {
      await db.read();
      if (val < db.data.length) return val;
    }
  }
  throw new InvalidArgumentError('Index out of bounds');
};

const program = new Command()
  .argument('index', 'Array element to access', indexParser)
  .action(function(indexPromise) {
    indexPromise.then(async function(index) {
      await db.read();
      console.log(db.data[index]);
    });
  });

// go modify test.json manually or whatever...

setTimeout(() => program.parse(), 10000);

Obviously, it would be better and more intuitive if the action handler execution were deferred until after the promise is resolved.

Possible implementations:

  • explicit, something along these lines:

    program.addArgument(new Argument('index').argParser(indexParser).async())
  • defer action handler execution whenever parser returns a thenable object

    • if a certain new method was called on the command, e.g. program.respectAsyncArgParsers(true)
    • or if the parseAsync() method was called (breaking)
@shadowspawn
Copy link
Collaborator

It is quite tricky supporting mixed sync and async, but it is something I am interested in if if can be done in a way that does not require changing too much code.

A suggestion about your example work-around. You can have an async action handler, in which case you call program.parseAsync() instead of program.parse(). (I don't think the setTimeout is helping much and you can use await in the action handler.)

The two callbacks that currently support async are the action handler, and the event hook.

@aweebit
Copy link
Contributor Author

aweebit commented Jul 4, 2023

I only added setTimeout() to better illustrate the problem, namely that db.data might have changed without us knowing, and that the only way to find out is by calling db.read() at the place where we need it.

@aweebit
Copy link
Contributor Author

aweebit commented Jul 5, 2023

I have come up with a very simple solution thanks to your mention of event hooks.

async function awaitHook(thisCommand, actionCommand) {
  actionCommand.processedArgs = await Promise.all(
    actionCommand.processedArgs
  );
}

await program.hook('preAction', awaitHook).parseAsync();

This achieves exactly what I wanted.

@shadowspawn
Copy link
Collaborator

shadowspawn commented Jul 5, 2023

Nice partial solution! I had not thought of action hook or looking at processedArgs.

@aweebit
Copy link
Contributor Author

aweebit commented Jul 6, 2023

Does not work for options though unless I access actionCommand._optionValues which I assume I am not supposed to.

@aweebit
Copy link
Contributor Author

aweebit commented Jul 6, 2023

Oh nevermind, I have just discovered there is setOptionValue() for such cases. The hook now looks like this:

async function awaitHook(thisCommand, actionCommand) {
  actionCommand.processedArgs = await Promise.all(
    actionCommand.processedArgs
  );
  const entries = Object.entries(actionCommand.opts());
  for (const [key, value] of entries) {
    actionCommand.setOptionValue(key, await value);
  }
}

A good idea would be to add a shortcut for program.hook('preAction', awaitHook), e.g. program.awaitHook(). This would give the user the freedom to decide when exactly to transform the arguments and option values.

@shadowspawn
Copy link
Collaborator

Building in support for async option/argument parsers is fairly messy to support usage without an action handler, and there are workarounds which are reasonably simple for the author to implement by returning promise from parser and await the results later in parsing: #1900 (comment)

See also PRs adding built-in support for the await-promise-later approach: #1902 #1913 #1915

Feel free to open a new issue if it comes up again, with new information and renewed interest.

Thank you for your contributions.

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

No branches or pull requests

2 participants