dthree / vorpal

Node's framework for interactive CLIs

Home Page:http://vorpal.js.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

allow Vorpal to run arbitrary bash commands and pipe back to shell

ORESoftware opened this issue · comments

My Vorpal options are like so:

  Commands:

    help [command...]  Provides help for a given command.
    exit               Exits application.
    pwd                echo present working directory
    run [file]         run a single test script
    find [options]     find test files to run

I'd like to be able to type a command - if it's not a recognized option - to send it to bash and have bash interpret it.

One thing I could do is create a new option

bash [command...]

and then I could send all the arguments to bash that way..

however that requires users to type in something like:

> bash "ls -a"

or

> bash "ps aux | grep node"

like I said, it would be cool to be able to give Vorpal the option of sending unrecognized commands to bash and then piping the results back to vorpal terminal.

ok so this works:

  vorpal
  .mode('bash')
  .delimiter('bash:')
  .init(function(args: Array<string>, callback: Function){
    this.log('Welcome to bash mode.\nYou can now directly enter arbitrary bash commands. To exit, type `exit`.');
    callback();
  })
  .action(function(command: string, cb: Function) {
    
    const k = cp.spawn('bash');
    k.stdout.pipe(process.stdout);
    k.stderr.pipe(process.stderr);
  
    k.stdin.write(command);
    k.stdin.end('\n');
  
    k.once('exit', cb as any);
    
  });

the problem with the above, is that the user has to type in 'bash' to then drop into a bash session... it would be really cool if I could configure vorpal to send any unrecognized command to bash, that way the user wouldn't have to type "bash" before executing a bash command.

Doesn't Vorpal allow for the parsing of the option to be turned off for specific commands? I thought I read that but I don't know where to look to confirm or deny that.

Using "mode" allows me to turn off options parsing and just get a raw string - it's described in the readme - however that forces users to drop into yet another shell.

I am definitely looking for a way to get a raw string from the command line, when the first prompt pops up.

Solution

I was looking into the same behaviour - and got it working with the following:

  1. Use vorpal.catch(...) to handle any unrecognized commands (these will go into bash)
  2. In our handler, use child_process.exec to run the unrecognized command in the shell
  3. In the callback to exec, log any results or error, and give control back to vorpal:
const vorpal = require('vorpal')();
const exec   = require('child_process').exec;

// unrecognized commands will be executed in the default shell
vorpal.catch('[bashcmds...]').action(function(args, next){
    exec(args.bashcmds.join(' '), (err, result) => {
        if (err){
            console.log('Error!');
        }
        else {
            console.log(result);
        }

        next();
    });
});

vorpal.delimiter('vorpal$ ').show();

Todo

  • Add support for using pipe |

@ujc nice, that's a step in the right direction - but you don't want to use cp.exec, you ideally want to use cp.spawn. If you can get the plain string from command line, then you can do this:

// unrecognized commands will be executed in the default shell
vorpal.catch('[bashcmds...]').action(function(args, str, next){
      const k = cp.spawn('bash');
      k.stdin.write(str);
      k.stdin.end('\n');
      k.once('exit',next);
    });
});

that way you don't have to parse the commands, you just send the string from the command line directly to bash to be interpreted, it's one the best ways to handle a generic string. But the problem is that I don't know how to get a plain string representation of what the user typed at the command line - it's always pre-parsed.

@ujc the above post is very important to understand. I think your solution would be great (using catch) the problem is that I can't find a way to get vorpal to give the user the plain string representation of what's at the command line.

Here is the best solution I found so far:

  vorpal
  .mode('bash')
  .delimiter('bash:')
  .init(function (args: Array<string>, callback: Function) {
    this.log('Welcome to bash mode.\nYou can now directly enter arbitrary bash commands. To exit, type `exit`.');
    callback();
  })
  .action(
    makeExecuteCommand('bash', projectRoot)
  );

the only problem with this, is that the user had to drop into yet another shell by typing 'bash' at the vorpal command line. I like your solution much better, I just can't figure out how to get the plain string from the vorpal command line.

@dthree @danielhickman @milesj

Instead of dropping into yet another shell using vorpal.mode(), is there a way to get the unadulterated string from the vorpal command line so we can feed it directly into bash if the command is not recognized by vorpal?

Right now, I can do that, but only using vorpal.mode(). @ujc made a breakthrough by discovering that vorpal.catch can be used, but in regular vorpal mode, I cannot figure out how to access the plain string from the vorpal command line - it's always pre-parsed.

This is a really important feature for me - I assume it would not be that hard to create a PR to pass the plain unparsed string from the command line, something like this:

 vorpal.command('find')
  .action(function (args: Array<string>, cb: Function) {
     const v = args.plainString; // worst case scenario - we could just make plainString a property on the array
  });

@ORESoftware
Ok, I managed to get the raw input, and it works ok with ls.
Got some errors with cat. I probably need better handling of \r and \n etc - but the gist of it seems to work.

Hope this helps (;

const vorpal = require('vorpal')();
const exec   = require('child_process').exec;

let rawInput = '';
let shouldCapture = true;

process.stdin.on('data', captureRawInput);


// pass unrecognized commands into our bash instance
vorpal.catch('[bashcmds...]').action(function(args, next){
    shouldCapture = false;
    console.log('RAW: ' + rawInput);
    exec(rawInput, (err, result) => {
        if (err){
            console.log('Error!', err);
        }
        else {
            console.log(result);
        }

        shouldCapture = true;
        rawInput = '';
        next();
    });

});

vorpal.delimiter('vorpal$ ').show();

function captureRawInput(chunk){
    const str = chunk.toString();
    if (shouldCapture && !str.includes('\r')){
        rawInput += str;
    }
}

P.S. - What are the advantages of using spawn in this use case?

the advantages of using spawn are nearly infinite in this case - if you use exec, you have to pre-parse the command only for bash to interpret it again.

To see why that's problematic, try including arguments with spaces in them:

ls -a "dog bear cat" | grep "sally road"

you will see the problem. alternatively, with spawn, you simply pass the unadulterated string straight to bash, with the way I showed you.

your stdin trick is a good hack, although I am hoping for something cleaner, because backspace key! the user can type stuff then delete it, so hard to keep track of what's actually at the command line, no?

Using catch, I have this:

vorpal
  .catch('[foo]', 'Catches incorrect commands')
  .action(function (args: Array<string>, cb: Function) {
    console.log('caught => my args:', util.inspect(args));
    cb();
  });

I can't seem to access the raw string, it's always pre-parsed, that causes problems that I cannot get around. Is there some option I can use to prevent parsing of the command?

@ujc this PR should solve the problem
#297

with that PR, we can access the raw string from the command line and pass it directly to bash

@ORESoftware
Hmm... maybe I'm missing something - but I think exec is the one to use if we don't want to parse the string, since the signature for spawn requires the first param to be the command-to-run, and then an array of arguments to pass into that command.

For example:
Command: ls -a "dog bear cat" | grep "sally road"

Using spawn: spawn(ls, ['-a', '"dog bear cat"', '|', 'grep', '"sally road"'])

Using exec: exec('ls -a "dog bear cat" | grep "sally road"')

Plus, spawn requires us to read from stdout and\or stderr to get the result, where as exec will provide the results to the callback - so maybe it's a better fit in this specific use case?

Back to the raw-string issue
Yes, a full solution will need to keep track of control-characters - such as line-feed, carriage-return, backspace, cursor-moves etc.. - BUT - it's not that hard to implement - definitely doable (perhaps I'll have time for that next weekend).

Also, we could publish it as a vorpal extension \ plugin - something like vorpal-shell-fallback.
This will be a cleaner way to use, and will allow anyone to add it just by doing vorpal.use('vorpal-shell-fallback')

spawn is a better option most of the time because it can stream the output, exec will buffer the output: https://www.hacksparrow.com/difference-between-spawn-and-exec-of-node-js-child_process.html

but yes, you're right, exec can take a plain/raw string. to pass a raw string to spawn, you have to write to the child process' stdin.

Adding to what we currently have - here's another way to get the raw input (this is probably better than manually processing different keystrokes from stdin):

const readline = require('readline');
const rl = readline.createInterface( { input:process.stdin } );

let rawInput = null;
rl.on('line', line => rawInput = line);

This gives us the final, raw, input - just as the user typed it - no need to manually deal with carriage-return \ arrow-keys \ spaces etc..

Here is an updated version that works well. It gets the raw input from the user and passes it to the shell.

const {spawn} = require('child_process');

const passthru = function(args, done){
  let vorpal = this;
  // get the unparsed raw value typed in to the prompt
  let raw = vorpal.commandWrapper.command; // not an official API method but works until a significant change to the lib

  // shell:true option https://stackoverflow.com/questions/23487363/how-can-i-parse-a-string-into-appropriate-arguments-for-child-process-spawn
  let passthru_process = spawn(raw, [], {shell:true});
  // using vorpal.log instead of process.stdout because I want to be able to pipe output to other vorpal commands
  passthru_process.stdout.on('data', buffer => {
    vorpal.log(buffer.toString('utf8'))
  })
  // perhaps this could go to vorpal.log as well, your use may vary
  passthru_process.stderr.pipe(process.stderr);

  passthru_process.on('close', done)
}



  vorpal
    .catch('[cmd...]')
    .action(passthru)
    .allowUnknownOptions(true)

Seems to handle opts and args gracefully:

tool$ ls -laR ./logs
total 16
drwxr-xr-x   4 me  staff   128 Feb 13 14:35 .
drwxr-xr-x@ 40 me  staff  1280 Feb 12 13:30 ..
-rw-r--r--   1 me  staff  2027 Feb 13 14:35 AppRegistration.mocha.log
-rw-r--r--   1 me  staff  2145 Feb 13 14:35 AppRegistration.mocha.server.log