dthree / vorpal

Node's framework for interactive CLIs
http://vorpal.js.org
MIT License
5.64k stars 280 forks source link

provide a direct cli mode #171

Closed sramam closed 3 years ago

sramam commented 8 years ago

Vorpal is great when humans are using the cli. However, most cli's have a mixed mode usage - humans and scripts. What would be great is for vorpal to recognize which mode should be invoked and do the needful by default.

Currently, this is the work that needs to be done to make this happen:

var vorpal = require('vorpal')();

//--> 1. Need to install minimist as a package dependency
var argv = require('minimist')(process.argv.slice(2));

var interactive = false; // we'll see the use for this in just a minute

vorpal
    .command('some-cmd', 'just a mock command')
    .action(function(args, cb) {
        // do the command stuff
        // ...

        //--> 2. conditionally invoke the callback
        return interactive ? cb(): null; 
  })

//--> 3. Do the mode detection and invoke appropriate call.
if(argv._.length) {
    // command line option provided. direct mode - execute command and exit
    vorpal
        .delimiter('') //--> 4. Replaces default prompt with an empty string
        .parse(process.argv)
} else {
   interactive = true;
    vorpal
        .delimiter('myprompt $')
        .show()
}

In usage, this is what it would look so:

$ ./issue33.js some-cmd

some-cmd invoked
$
$ ./issue33.js
myprompt $ help

  Commands:

    help [command...]  Provides help for a given command.
    exit               Exits application.
    some-cmd           just a mock command

myprompt $

It would be a great addition to vorpal to implement these semantics directly.

Thoughts?

dthree commented 8 years ago

What about vorpal.parse?

This will execute a command and then exit. If you also want to do interactive, you can add in a quick check:

if (process.argv.length > 2) {
  vorpal.show();
}

There's more on this in the Wiki. BTW, Vorpal also exposes the minimist parser for you as a convenience.

https://github.com/dthree/vorpal/wiki/API-%7C-vorpal#vorpalparseargv-options

sramam commented 8 years ago

After some experimenting, this is the best I could get working.

#! /usr/bin/env node
var vorpal = require('vorpal')();
var interactive = vorpal.parse(process.argv, {use: 'minimist'})._ === undefined;

vorpal
    .command('some-cmd', 'just a mock command')
    .action(function(args, cb) {
        // do the command stuff
        // ...
        console.log('some-cmd invoked')
        setTimeout(function() {
          console.log('some-cmd completed after 1000 ms')
          return interactive ? cb(): null; // <<----- cb() will drop one back to the vorpal prompt, parse() or show().
        }, 1000)
  })

if (interactive) {
    vorpal
        .delimiter('myprompt $')
        .show()
} else {
    // argv is mutated by the first call to parse.
    process.argv.unshift('')
    process.argv.unshift('')
    vorpal
        .delimiter('')
        .parse(process.argv)
}

Sure it's only one check and a few lines, but it take a bit to grok all the nuance.

The painful part is the interactive detection, which is littered through all the command implementations. I can see how that would be a recipe for time-consuming bugs.

IMO, this dual mode use of CLIs is pretty common and should be the default implementation within vorpal. Hopefully I can get you to agree! :)

chrisui commented 8 years ago

I am also having some troubles getting this sort of behaviour to work at all. My code looks like the following:

vorpal
  .command('stats <files>')
  .description('Prints a bunch of stats about your application')
  .action(args => {
    vorpal.log('stats', args);
    return spawnEngine({files: args.files}).then(pullStats).then(dumpStats);
  });

// final setup - either user wants interactive mode or is just running a command
const parsedArgs = vorpal.parse(process.argv, {use: 'minimist'});
if (parsedArgs._[0] === 'interactive') {
  vorpal.delimiter('recon$').show();
} else {
  vorpal.parse(parsedArgs._);
}

Running the following works fine:

$ node index.js interactive
recon$ stats "**/*.js"
# It runs the command and I see all the output I would expect

However the following just doesn't run anything and has no error. Note when I added a .catch() in to test it was hit, but why am I not seeing any error? The args look sound.

$ node index.js stats "**/*.js"
# executes the program with no errors but no commands run :(
sramam commented 8 years ago

I never replied to the use of vorpal.parse(). My issue with it was that it drops me into the app prompt. There seems to be no way to change this behaviour without fiddling with the command implementation.

praxiq commented 7 years ago

I'm struggling with this, too. I found a few simplifications to the above:

  1. To check whether the app was called with command-line arguments, I just check if the last element of argv ends with the expected name of the app. This isn't bulletproof, but at the very least it should handle simple cases correctly without resorting to minimist.
  2. Immediately after vorpal.parse(), I call process.exit(0). This seems to work for simple commands, but I don't know if it'll work if your command does anything async.
if(process.argv[process.argv.length -1 ].endsWith('cli.js')) {
    vorpal.show();
} else {
    console.log(process.argv);
    vorpal.delimiter('').parse(process.argv);  // hide delimiter and run task from command line
    process.exit(0); 
}

I would love to see better support for building an app that decides based on the command-line arguments whether to go interactive or not.

chasset commented 7 years ago

I agree with these comments. Actually, I plan to develop one part with vorpal.js for its excellent interactive shell and another part with args.js with its traditional one line command. The solution of @praxiq works only for one command.

soletan commented 7 years ago

@dthree "What about vorpal.parse? This will execute a command and then exit."

Well, curiously, this simply isn't true. Using vorpal.parse() UI gets attached and obviously is kept attached afterwards. IMHO another API design flaw arises from invocation of vorpal.exec() contained in vorpal.parse() doesn't pass returned promise out of vorpal.parse() so any caller might wait for vorpal.parse() to finish processing detected command.

Finally, I couldn't even manage to exit script using code like this:

Vorpal
    .delimiter( "" );

Vorpal.ui
    .attach( Vorpal );

Vorpal
    .exec( Options.command )
    .then( function() {
        console.log( "found" );
        Vorpal.ui.detach( Vorpal ).cancel();
    }, function() {
        console.log( "failed" );
        Vorpal.ui.detach( Vorpal ).cancel();
    } );

Options.command is properly selecting some action involving some use of vorpal.ui.prompt. Logging appears after command has prompted and finished. But detaching (and incancelling) UI didn't work out here, either.

Actually, after having checked the code myself I'm somewhat curious about it being quite noisy and containing unneccessary stuff in several places. IMHO it might help explain its behaviour mismatching expectations as documented in API.

UPDATE: Here comes the hackish way I succeeded finally:

Vorpal.ui.parent = Vorpal;
Vorpal.ui.refresh();

Vorpal
    .exec( Options.command )
    .then( function() {
        process.exit( 0 );
    }, function() {
        process.exit( process.exitCode || 1 );
    } );
soletan commented 7 years ago

@praxiq Your approach might have worked out if vorpal.parse() would properly return the result of internally invoked vorpal.exec() so your code might wait for the whole process to finish. Currently the approach fails to support any asynchronous processing which is a total no-go in the world of javascript.

https://nodejs.org/api/process.html#process_process_exit_code is commenting:

It is important to note that calling process.exit() will force the process to exit as quickly as possible even if there are still asynchronous operations pending that have not yet completed fully, including I/O operations to process.stdout and process.stderr.

aegyed91 commented 7 years ago

why not simply register an evt listener?

vorpal
  .on('client_command_executed', evt => process.exit(0))
  .parse(process.argv)
ghost commented 7 years ago

@tsm91 , it seems that when an invalid command is passed the suggested 'client_command_executed' event is not fired. Hooking into 'client_command_error' seems to do nothing for us as well.

toadkicker commented 7 years ago

I took all the comments under consideration and came up with this solution:

var program = require('vorpal')();
var colors = require('colors');
var path = require('path');

this.noargs = program.parse(process.argv, {use: 'minimist'})._ === undefined;

program
  .command('some-cmd', 'just a mock command')
  .autocomplete(['new', 'demo', 'build'])
  .action(function (args, cb) {
    // do the command stuff
    // ...
    console.log('some-cmd invoked');
    // setTimeout(function () {
    //   console.log('some-cmd completed after 1000 ms');
      return this.noargs ? cb() : null; // <<----- cb() will drop one back to the vorpal prompt, parse() or show().
    // }, 1000)
  });

if (this.noargs) {
  program
    .delimiter('$')
    .exec('help')
} else {
  // argv is mutated by the first call to parse.
  process.argv.unshift('');
  process.argv.unshift('');
  program
    .on('client_command_executed', function (evt) {
      process.exit(0)
    })
    .delimiter('$')
    .parse(process.argv)
}

What this does is:

I personally like this behavior and it seems to work well as a mix of invoking interactive and direct input. The key bit is @tsm91's suggestion of using

.on('client_command_executed', function (evt) {
      process.exit(0)
    })

or you can do some fancier pattern matching on parsing args/noargs and invoke process.exit(0) there. AFAIK I think Vorpal supports this, but it just isn't documented well.

soletan commented 7 years ago

@toadkicker Tried your concluded approach today but it didn't properly work as desired, thus had to revise it in some aspects resulting in slightly different semantics others like me might favour over the ones your approach is providing. Here is my counter proposal:

const Path = require( "path" );

const Minimist = require( "minimist" );
const Vorpal = require( "vorpal" )();

let argv = process.argv.slice( 0 );

let args = Minimist( argv.slice( 2 ) );
let repl = !( args._ && args._.length ) && !( args.h || args.help );

if ( args.h || args.help ) {
    argv = [].concat.apply( argv.slice( 0, 2 ).concat( "help" ), argv.slice( 2 ).filter( i => i[0] !== "-" ) );
}

Vorpal.catch( "[words...]", "Catches incorrect commands" )
    .action( function( args, cb ) {
        this.log( ( args.words ? args.words.join( " " ) : "<unknown>" ) + " is not a valid command." );
        cb();
    } );

Vorpal
    .command( "some-cmd", "some command" )
    .action( function( args, cb ) {
        // invokes command code in module providing vorpal and arguments, supporting promise as result
        Promise.resolve( require( "./actions/some-cmd" )( this, args ) ).then( repl ? cb : null );
    } );

if ( repl ) {
    Vorpal
        .delimiter( "$" )
        .show();
} else {
    Vorpal
        .on( "client_command_executed", function() {
            process.exit( 0 )
        } )
        .delimiter( "$" )
        .parse( argv.slice( 0 ) );
}
djdmbrwsk commented 7 years ago

I execute single/multiple commands like this:

# single command
node myApp commandOne

# multiple commands
node myApp commandOne commandTwo

# commands with arguments
node myApp "commandOne argOne argTwo" commandTwo

The solution:

const package = require('./package.json');
const vorpal = require('vorpal')();

// Command declarations here...

const startupCommands = process.argv.slice(2);
startupCommands
  .reduce((prev, cur) => prev.then(() => vorpal.exec(cur)), Promise.resolve()) // Sequential promises
  .then(() => {
    // This will start the CLI prompt AFTER executing the commands, but you could easily
    // adapt this to make it conditional.
    vorpal
      .delimiter(`${package.name}$`)
      .show();
  })
  .catch((err) => console.error(err));
ronbravo commented 5 years ago

Not sure if this helps anyone but this is what I did as a Hackaround. Just added a run method to Vorpal instance that is pretty much the parse method but with the process.exit() commented out and instead runs a callback.


// cli.js
// NOTE: This is a modification to an instance of Vorpal.
const vorpal = require('vorpal')();

...

vorpal.run = function (argv, options, done) {
  // Modification of code found here: https://github.com/dthree/vorpal/blob/master/lib/vorpal.js#L155-L184
  // Get reference to loadsh attached to vorpal.
  var _ = this.lodash;

  options = options || {};
  var args = argv;
  var result = this;
  var catchExists = !(_.find(this.commands, {_catch: true}) === undefined);
  args.shift();
  args.shift();
  if (args.length > 0 || catchExists) {
    if (options.use === 'minimist') {
      result = minimist(args);
    } else {
      // Wrap the spaced args back in quotes.
      for (let i = 0; i < args.length; ++i) {
        if (i === 0) {
          continue;
        }
        if (args[i].indexOf(' ') > -1) {
          args[i] = `"${args[i]}"`;
        }
      }
      this.exec(args.join(' '), function (err) {
        if (err !== undefined && err !== null) {
          throw new Error(err);
        }

        // NOTE: Here is where I got rid of the process exit.
        // and replaced with a promise.
        if (done) { done(); }

        // process.exit(0);
      });
    }
  }
  return result;
};
smaudet commented 3 years ago

The this.prompt functionality is broken when executing like this... if you get rid of:

       if (!this.parent) {
         return prompt;
       }

In ui.js, this somewhat disappears...

Vorpal should have better support for this way of executing, literally every other prompt utility kit has first class support for ... just running, its nice that there's this whole interactive system, but I kinda want to write the author a sternly worded letter about not breaking common expectations...

sramam commented 3 years ago

Vorpal should have better support for this way of executing, literally every other prompt utility kit has first class support for ... just running, its nice that there's this whole interactive system, but I kinda want to write the author a sternly worded letter about not breaking common expectations...

I applaud your enthusiasm, but your expectations are misplaced. Your strongly worded comment incited me to respond:

  1. This is an open source library, the author is not obligated to meet your expectations (I am not the author).
  2. This is a project that has been abandoned for ~3 years and the author says so on the readme.
  3. Please feel free to fork and patch as you see fit. I'm sure other users will be grateful.
dthree commented 3 years ago

@smaudet i've been sternly notified :) Unfortunately this project is several years old and hasn't been touched.