Commander.js: Async typescript functions as actions

Created on 21 May 2018  路  14Comments  路  Source: tj/commander.js

When I pass an async typescript function into a command's action() method, the async function is called correctly, but is not run to the end, as the program does not wait when awaiting another function. It simply ends execution. I've seen a similar question here, but it was not answered, as far as I understand.

Consider this example:

#!usr/bin/env node

import * as commander from 'commander';
import axios from 'axios';

async function run() : Promise<void>  {  
    var response = await axios.get("https://dummyimage.com/600x400/000/fff", { "responseType": "arraybuffer" });
    if (response.status === 200) {
        console.log(`Downloaded file: ${response.data.byteLength} bytes.`)
    }
    else {
        console.error('Error downloading');        
    } 
}

async function main() {
  commander
  .command("do", "Test")
  .action(run)
  .parse(process.argv);
//  await run();
  console.log('finished');
}

try {
  main();
}
catch (error) {
  console.error(error);
}

If you run the example above, you can see that finished is called but the download is never finished. Calling await run() without the help of commander works, though.

So, is it possible to use commander with typescript's async functions?

enhancement

Most helpful comment

For future readers: I've moved to yargs, as it supports async actions.

All 14 comments

For future readers: I've moved to yargs, as it supports async actions.

I investigated yargs as an option but frankly I think commander.js offer the best design. However that doesn't mean that commander.js does not have an issue in this regard.

I am very experienced but quite new to JavaScript. It leaves a lot to be desired when compared to most other languages.

With that said, I find myself in a position where I have to build an enterprise-quality command-line app that must run under node.js. The app will also offer a REST API interface to some functions. Obviously neither it, nor the CLI can be allowed to block the message/UI thread.

commander.js seems to be a little schizophrenic in this regard. Is it a parser or a command dispatcher? It's a really nice parser. Dispatcher? No so much. Command actions definitely do not block the thread however there is no reasonable way to return anything from the handling function, including a Promise.

I have evaluated a few options but none of them will allow me to sleep well at night after this thing goes live. So I have a suggested change:

  1. Offer a version of command.parse that doesn't actually dispatch an action but rather, returns the objects that would have been supplied when calling the action. This would allow the consuming app to dispatch it's own method using either Promise or TypeScript async/await.

  2. Offer awaitable and sync versions of command.parse that returns whatever value is returned by the action function.

I would suggest option 1 as it provides a clean separation between parsing and functional execution.

I thought about forking this project and making the changes myself but I have to say my learning curve would make it too costly and that same learning curve would probably produce less than optimal source code. If you were not able to accept our pull request then we would be in the situation where we would have to merge your changes into our version, forever.

This is just food for thought. For our project we may write a parser from the ground up using TypeScript and the semantics of commander.js in the spec. Another possibility will be to just write a parser specific to the apps requirements.

By the way, if there is something I have wrong, please feel free to constructively enlighten me.

I think this behavior is correct. Exactly, promise-like return is not provided through commander api though.

Why not wrap commander with promise api manually? By the way, I also don't think that it is needed to do jobs simultaneously around commander initiation. Generally, commander initiation should be down at the top level of a problem or a system at the entry file, which, I think, is a best practice.

I had the same issue, turned out it was a version problem.
Upgrading from version 2.15.0 to 2.18.0 fixed the issue.

Can this pattern be used?

https://stackoverflow.com/questions/48376479/executing-multiple-sequelize-js-model-query-methods-with-promises-node?answertab=oldest#tab-top

const axiosPromise = axios.get(...);
const sequelizePromise = sequelize.query(...);
const anotherPromise = anotherFunctionThatReturnsAPromise();
Promise
    .all([axiosPromise, sequelizePromise, anotherPromise])
    .then(responses => {
        console.log('**********COMPLETE RESULTS****************');
        console.log(responses[0]); // axios result
        console.log(responses[1]); // sequelize result
        console.log(responses[2]); // another result
    })
    .catch(err => {
        console.log('**********ERROR RESULT****************');
        console.log(err);
    });

@chrisgo I'm sure it can, but the point of my question was if it is possible to use async await.

I had the same issue, turned out it was a version problem.
Upgrading from version 2.15.0 to 2.18.0 fixed the issue.
I have 2.19.0 The async actions are not managed well.

I looked in the code. I see it is not prepared for async chaining at all. EventEmmiter is used to execute a command. Is a strategy pattern enough? Do contributors have plans to adjust it to be chainable?

I came across this issue on Yargs to improve their async support. Some interesting detail on possible code patterns: https://github.com/yargs/yargs/issues/1420

Same problem.

Oddly, some async tasks run to completion. Others do not. In particular, waiting on a promise that never resolves exits with status 0, which is a big no-no. version 3.0.2

Cheese it:

    let lifeline = setInterval(() => {}, 500);
    try {
        ...
    } finally {
        clearInterval(lifeline);
    }

I am wondering about option 2 from https://github.com/tj/commander.js/issues/806#issuecomment-412360848

So, we would add a .parseAsync() which you should use instead of .parse() if you have any async action handlers, and it returns whatever the action handler returns. Up to calling code to call the correct parse and do the right thing with what it returns.

Might that work for people wanting to use async?

I have added a draft Pull Request to add .parseAsync(). Feedback from interested parties welcome.

Added .parseAsync() in Commander v4.1.

See https://github.com/tj/commander.js#action-handler-subcommands

Was this page helpful?
0 / 5 - 0 ratings

Related issues

mathiasbynens picture mathiasbynens  路  3Comments

mtrabelsi picture mtrabelsi  路  3Comments

DeoLeung picture DeoLeung  路  4Comments

youurayy picture youurayy  路  5Comments

cmoulliard picture cmoulliard  路  4Comments