How can I parse a string into appropriate arguments for child_process.spawn? - node.js

I want to be able to take a command string, for example:
some/script --option="Quoted Option" -d --another-option 'Quoted Argument'
And parse it into something that I can send to child_process.spawn:
spawn("some/script", ["--option=\"Quoted Option\"", "-d", "--another-option", "Quoted Argument"])
All of the parsing libraries I've found (e.g. minimist, etc.) do too much here by parsing it into some kind of options object, etc. I basically want the equivalent of whatever Node does to create process.argv in the first place.
This seems like a frustrating hole in the native APIs since exec takes a string, but doesn't execute as safely as spawn. Right now I'm hacking around this by using:
spawn("/bin/sh", ["-c", commandString])
However, I don't want this to be tied to UNIX so strongly (ideally it'd work on Windows too). Halp?

Standard Method (no library)
You don't have to parse the command string into arguments, there's an option on child_process.spawn named shell.
options.shell
If true, runs command inside of a shell.
Uses /bin/sh on UNIX, and cmd.exe on Windows.
Example:
let command = `some_script --option="Quoted Option" -d --another-option 'Quoted Argument'`
let process = child_process.spawn(command, [], { shell: true }) // use `shell` option
process.stdout.on('data', (data) => {
console.log(data)
})
process.stderr.on('data', (data) => {
console.log(data)
})
process.on('close', (code) => {
console.log(code)
})

The minimist-string package might be just what you're looking for.
Here's some sample code that parses your sample string -
const ms = require('minimist-string')
const sampleString = 'some/script --option="Quoted Option" -d --another-option \'Quoted Argument\'';
const args = ms(sampleString);
console.dir(args)
This piece of code outputs this -
{
_: [ 'some/script' ],
option: 'Quoted Option',
d: true,
'another-option': 'Quoted Argument'
}

Related

How to present node spawn arguments

Before people start crying "duplicate", I've already examined
Spawning process with arguments in node.js
Use NodeJS spawn to call node script with arguments
How do I pass command line arguments to a Node.js program?
The first of these is basically the same question in a different use case and as a result the answers do not address my use case.
So... how do you encode a command line like the following with named parameters separated from their values by a space?
arduino-cli compile --fqbn arduino:avr:nano
Should it look like this (1)?
let cp = child.process(
"/path/to/arduino-cli.exe",
[
"compile",
"--fqbn arduino:avr:nano"
]
);
or this (2)?
let cp = child.process(
"/path/to/arduino-cli.exe",
[
"compile",
"--fqbn",
"arduino:avr:nano"
]
);
or this (3)?
let cp = child.process(
"/path/to/arduino-cli.exe",
[
"compile",
"fqbn",
"arduino:avr:nano"
]
);
or this (4)?
let cp = child.process(
"/path/to/arduino-cli.exe",
{
_: ["compile"],
fqbn: "arduino:avr:nano"
}
);
TypeScript won't allow the last option even though I suspect it is the right answer, so I submit the problem for wider consideration.
After setting up for repeatable testing
let args: any[] = [];
args.push(["compile", `--fqbn ${selectedBoard.board.fqbn}`]);
args.push(["compile", "--fqbn", selectedBoard.board.fqbn]);
args.push(["compile", "fqbn", selectedBoard.board.fqbn]);
args.push({ _: ["compile"], fqbn: selectedBoard.board.fqbn });
let cp = child_process.spawn(cliPath, args[1], { cwd: getInoPath() });
cp.stdout.on("data", (data: any) => outputChannel.append(data.toString()));
cp.stderr.on("data", (data: any) => outputChannel.append(data.toString()));
cp.on("error", (err: any) => {
outputChannel.append(err);
});
I found that #jfriend00 was right, it is indeed the second arguments version
["compile", "--fqbn", selectedBoard.board.fqbn]
but there was another problem causing it to fail – the CWD needed to be set in the options.
let cp = child_process.spawn(cliPath, args[1], { cwd: getInoPath() });
The key insight here is to capture both error events and stderr. The failure was reported on stderr and no error event was raised. After exposing stderr the problem was quickly resolved.

Jest cannot test commander help function

With jest I'm not able to test commander module functions that result in process exit.
For example, if I pass the --help option or an invalid parameter like -x (see below) process.exit or process.stdout.write are not called as they should looking at the commander sources.
import {Command} from "commander";
let mockExit: jest.SpyInstance;
let mockStdout: jest.SpyInstance;
beforeAll(() => {
mockExit = jest.spyOn(process, "exit").mockImplementation();
mockStdout = jest.spyOn(process.stdout, "write").mockImplementation();
});
afterAll(() => {
mockExit.mockRestore();
mockStdout.mockRestore();
});
test("Ask for help", () => {
// Setup
const save = JSON.parse(JSON.stringify(process.argv));
process.argv = ["--help"]; // Same setting it to "-x"
const program = new Command();
program
.option("-v, --verbose [level]", "verbose level")
.parse(process.argv);
expect(mockExit).toBeCalled();
// expect(mockStdout).toBeCalled();
// Cleanup
process.argv = save;
});
What is strange is that, from the behavior of other tests, process.argv is not restored after this one.
Tests are in typescript and passed through ts-jest.
Any ideas?
Thanks!
I suggest you use .exitOverride(), which is the approach Commander uses in its own tests. This means early "termination" is via a throw rather than exit.
https://github.com/tj/commander.js#override-exit-handling
The first problem though (from comments) is the arguments. Commander expects the parse arguments follow the conventions of node with argv[0] is the application and argv[1] is the script being run, with user parameters after that.
So instead of:
argsToParse = ["--help"];
something like:
argsToParse = ['node", "dummy.js", "--help"];
(No need to modify process.argv as such.)

How to start an interactive terminal window on mac using child_process library of node?

I want to execute a bash command - dotnet dev-certs https --trust in an interactive terminal through child_process library.
Since it asks for the user password, it has to be interactive terminal.
I've already tried using AppleScript for it, but the user experience is subpar since it tends to leave half-closed terminal windows around.
Edit - Added the code snippet that I am using to create a child_process.
import * as cp from 'child_process'
cp.spawn('dotnet dev-certs https --trust', {})
I've tried many many combinations of cp.spawn and cp.exec.
e.g. cp.spawn('...', { shell: true, stdio: 'ignore', detached: true }) etc.
cp.spawn actually creates a process, but it's not interactive and immediately terminates.
You have to filter the incoming data from stdout with stdout.on('data', data => {}) to find the shell request for user input. After you found that specific line you can simply send data to the shell via the stdin.write('any input \n')
import * as cp from 'child_process'
const ls = cp.spawn('dotnet dev-certs https --trust', {})
ls.stdout.on('data', function (data) {
const currentData = data.toString();
//check for password input notification:
if(currentData === "input credentials: ")
{
//send the password via stdin. \n does the trick over here.
ls.stdin.write('Password\n');
}
});

Node.js pass text as stdin of `spawnSync`

I'd think this would be simple, but the following does not work as expected.
I want to pipe data to a process, say (just an arbitrary command for illustration) wc, from Node.
The docs and other SO questions seem to indicate that passing a Stream should work:
const {spawnSync} = require('child_process')
const {Readable} = require('stream')
const textStream = new Readable()
textStream.push("one two three")
textStream.push(null)
const stdio = [textStream, process.stdout, process.stderr]
spawnSync('wc', ["-c"], { stdio })
Unfortunately this throws an error:
The value "Readable { ... } is invalid for option "stdio"
The relevant bit of code from internal/child_process.js does not immediately reveal what the anticipated valid options are.
To present particular data as stdin data for the child process, you can use the input option:
spawnSync('wc', ['-c'], { input : 'one two three' })

Use child_process#spawn with a generic string

I have a script in the form of a string that I would like to execute in a Node.js child process.
The data looks like this:
const script = {
str: 'cd bar && fee fi fo fum',
interpreter: 'zsh'
};
Normally, I could use
const exec = [script.str,'|',script.interpreter].join(' ');
const cp = require('child_process');
cp.exec(exec, function(err,stdout,sterr){});
however, cp.exec buffers the stdout/stderr, and I would like to be able to be able to stream stdout/stderr to wherever.
does anyone know if there is a way to use cp.spawn in some way with a generic string, in the same way you can use cp.exec? I would like to avoid writing the string to a temporary file and then executing the file with cp.spawn.
cp.spawn will work with a string but only if it has a predictable format - this is for a library so it needs to be extremely generic.
...I just thought of something, I am guessing the best way to do this is:
const n = cp.spawn(script.interpreter);
n.stdin.write(script.str); // <<< key part
n.stdout.setEncoding('utf8');
n.stdout.pipe(fs.createWriteStream('./wherever'));
I will try that out, but maybe someone has a better idea.
downvoter: you are useless
Ok figured this out.
I used the answer from this question:
Nodejs Child Process: write to stdin from an already initialised process
The following allows you to feed a generic string to a child process, with different shell interpreters, the following uses zsh, but you could use bash or sh or whatever executable really.
const cp = require('child_process');
const n = cp.spawn('zsh');
n.stdin.setEncoding('utf8');
n.stdin.write('echo "bar"\n'); // <<< key part, you must use newline char
n.stdout.setEncoding('utf8');
n.stdout.on('data', function(d){
console.log('data => ', d);
});
Using Node.js, it's about the same, but seems like I need to use one extra call, that is, n.stdin.end(), like so:
const cp = require('child_process');
const n = cp.spawn('node').on('error', function(e){
console.error(e.stack || e);
});
n.stdin.setEncoding('utf-8');
n.stdin.write("\n console.log(require('util').inspect({zim:'zam'}));\n\n"); // <<< key part
n.stdin.end(); /// seems necessary to call .end()
n.stdout.setEncoding('utf8');
n.stdout.on('data', function(d){
console.log('data => ', d);
});

Resources