Spawn in nodejs for a unix command with spaces in parameters - node.js

I would like to execute the following command using nodejs Spawn on a Debian system :
/usr/bin/apt-get upgrade -s | tail -1 | cut -f1 -d' '
I want to use spawn and not exec because of future use of root only commands and i don't want to allow a full shell access (i will update the visudo file with correct commands)
Here is my code
const apt = spawn('/usr/bin/apt-get', ['upgrade', '-s']);
const tail = spawn('tail', ['-1']);
const cut = spawn('cut', ['-f1', '-d" "']);
apt.stdout.on('data', (data) => {
tail.stdin.write(data);
});
tail.stdout.on('data', (data) => {
cut.stdin.write(data);
});
cut.stdout.on('data', (data) => {
console.log(data.toString());
});
apt.stderr.on('data', (data) => {
console.log("apt stderr: ${data}");
});
tail.stderr.on('data', (data) => {
console.log("tail stderr: ${data}");
});
cut.stderr.on('data', (data) => {
console.log("cut stderr: ${data}");
});
apt.on('close', (code) => {
if (code !== 0) {
console.log("apt process exited with code ${code}");
}
});
tail.on('close', (code) => {
if (code !== 0) {
console.log("tail process exited with code ${code}");
}
});
cut.on('close', (code) => {
if (code !== 0) {
console.log("cut process exited with code ${code}");
}
});
res.status(200).json('');
Once executed i have an error because of the '-d" "' parameter that is not recognized. I try escaping the space with a double \ or split the parameter in both but still errors

It should just be:
const cut = spawn('cut', ['-f1', '-d ']);
No double quotes or backslash escapes -- those are for the use of the shell, not cut, and there's no shell here.
This makes dealing with unknown filenames (for your future use cases) particularly easy: When your strings are passed as arguments (to software that doesn't misuse them my running eval-equivalent code later), you don't need to quote, escape, sanitize, or otherwise modify them before they can be passed as data.
(That is to say -- when you tell your shell cut -f1 -d" ", the actual syscall it invokes to start that cut process, in C syntax, looks like execve("/usr/bin/cut", {"cut", "-f1", "-d ", NULL}, environ); the quotes were syntactic, consumed by the shell when it used them to make the decision that the space after the -d should be part of the same literal argument).

Related

child_process.exec() stdout is empty string when command contains spaces

I have put the below code in a Javascript file and ran it using node.
var exec = require('child_process').exec;
var child;
child = exec("git for-each-ref --format='%(committerdate), %(authorname)' --sort=committerdate --merged develop",
function (error, stdout, stderr) {
console.log('stdout: ' + stdout);
console.log('typeof stdout: ' + typeof stdout);
console.log('stderr: ' + stderr);
if (error !== null) {
console.log('exec error: ' + error);
}
});
All I get is an empty string as stdout. There are no errors.
stdout:
stderr:
However, if I remove the space after comma in "%(committerdate), %(authorname)", I get the expected output. The below code works:-
var exec = require('child_process').exec;
var child;
child = exec("git for-each-ref --format='%(committerdate),%(authorname)' --sort=committerdate --merged develop",
function (error, stdout, stderr) {
console.log('stdout: ' + stdout);
console.log('stderr: ' + stderr);
if (error !== null) {
console.log('exec error: ' + error);
}
});
Why do I not get any output when there is a space after comma?
When I run the command directly on in PowerShell or git-bash, I get the same output with and without the space.
git for-each-ref --format='%(committerdate), %(authorname)' --sort=committerdate --merged develop
I spent quite some time to get the command working and it seemed to be that a space would cause an issue, especially when the command works fine when run directly.
Environment:
Windows 10
Node 16.7.0
It's probably worth putting into an answer to give people ideas on how to troubleshoot.
Let's look at the command first:
exec("git for-each-ref --format='%(committerdate), %(authorname)' --sort=committerdate --merged develop", ...);
If we wanted to look at the underbelly of the exec function, we would probably find a more sophisticated version of inputString.split(" ") so that node can distinguish the executable from the positional arguments/flags. So a good place to start is to try and escape the space in various ways to see if that works.
Next you need to understand how node executes the command. On different operating systems, the node child_process library is just a proxy to an underlying shell. So you need to make sure your command works in the target shell (which is likely not your favorite shell or the one you use on a day-to-day basis). For Windows, node likely uses cmd.exe under the hood.
Lastly, the child_process library also allows you to spawn a process which is a little more deliberate in specifying the executable and positional arguments/flags. I recommend using this method for most child_process operations:
spawn(<executable>, [...<args>]);
This way you are telling node exactly what binary to use while clearly identifying every positional argument. For example, the following will fail:
spawn('git', ['checkout -b my-new-branch']);
The reason this fails is because node treats the string 'checkout -b my-new-branch' as a single argument to the git command... which is not correct. Instead, you want to clearly define each positional argument separately:
spawn('git', ['checkout', '-b', 'my-new-branch']);
This is particularly useful if one of your arguments contains spaces or other special shell characters:
spawn('git', ['checkout', '--format=\'%(committerdate), %(authorname)\'', '--sort=committerdate', '--merged develop']);
Changing the underlying shell
For any of the child_process creation methods, you can pass an option for which shell to use. Be careful using this option because you can destroy the ability for your code to run on multiple operating systems. Make sure to read about shell requirements and the default windows shell:
exec("...", { shell: "powershell.exe" });
spawn("...", [...], { shell: "powershell.exe" });

Why do nodejs exec/spawn not show any output for bash 'history' command?

I am running this on Ubuntu and have tried many variations of exec/spawn functions (and their sync counterparts) and none of them can show me an output for bash 'history' command. One scenario is following:
const { spawnSync} = require('child_process');
const child = spawnSync('history', { shell: "/bin/bash" });
console.log('error: ', child.error);
console.log('stdout: ', child.stdout.toString());
console.log('stderr: ', child.stderr);
It does not show any errors and output is empty. I think this question has more to do with 'specialty' or category of the history command than nodejs's function since they work fine for normal commands like ls, pwd, whoami, etc work fine. I have looked at my .bash_history file and its filled with history so that's not the issue.
Another problem that might be similar is ll command also fails even though I have set bash as shell. But for ll, it does return an error:
/bin/bash: ll: command not found
Just to be sure, I tried running ll command in bash it worked just fine. What am I missing here?
edit: I have done some more testing it seems more like a bash thing than a node thing. When I simply write the history command, bash prints results but when I do bash -c history, it does not show any output but also no error.
.
You need to subscribe to messages from child process
child.on('error', (err) => {
});
child.stderr.on('data', (data) => {
});
child.on('exit', (code, signal) => {
});

Nodejs - best way to know a service status [linux]

Do someone know the best way (or just a good one) to know a service status from a linux (centos, here) system ?
When i run this piece of code:
{ ... }
const { spawnSync } = require('child_process'),
ts3 = spawnSync('service teamspeak status | grep active'),
{ ... },
This throw me a ENOENT error. I got the same error from my windows system when I tried a simple dir command, I had to write a stupid cmd file named "dir.cmd" with the content "dir" in my system32 (or any dir in the path env variable) and replace
dir = spawnSync('dir'),
By
dir = spawnSync('dir.cmd'), //This file is now in a dir in the PATH env var
So, i think this is related to a no-auto-resolution of the files with a sh,cmd or something else extention
But this isn't working when I replace the "service" by a "service.sh" anyway (from the first piece of code)
So, maybe someone already did this before and can help me a bit ?
Thanks,
And have a nice day !
A couple of issues, first, when using spawn, you should pass the arguments withing an array. Second, you're trying to run two processes within one spawn.
Instead, you can break down two processes and use the stdout from the first process (service), as the stdin for the second one (grep). I believe this should do it:
const { spawn } = require('child_process');
const service = spawn('service', ['teamspeak', 'status']);
const grep = spawn('grep', ['active']);
service.stdout.on('data', (data) => {
grep.stdin.write(data);
});
service.stderr.on('data', (data) => {
console.error(`service stderr: ${data}`);
});
service.on('close', (code) => {
if (code !== 0) {
console.log(`ps process exited with code ${code}`);
}
grep.stdin.end();
});
grep.stdout.on('data', (data) => {
console.log(data.toString());
});
grep.stderr.on('data', (data) => {
console.error(`grep stderr: ${data}`);
});
grep.on('close', (code) => {
if (code !== 0) {
console.log(`grep process exited with code ${code}`);
}
});
Hope this helped.

Run block of bash / shell in node's spawn

I'm writing a command line utility, and I need stdout to write to TTY or use {stdio: 'inherit'} I've been getting by with exec but it's not going to cut it. I need way for a spawn process to execute the following echo commands below. I know that spawn spins up a child process with a given command, and you pass in arguments, but I need it to just take a line-separated string of commands like this. This is what I'm currently feeding to exec. Is this possible?
const spawn = require('child_process').spawn
const child = spawn(`
echo "alpha"
echo "beta"
`)
child.stdout.on('data', (data) => {
console.log(`stdout: ${data}`)
});
child.stderr.on('data', (data) => {
console.log(`stderr: ${data}`)
});
child.on('close', (code) => {
console.log(`child process exited with code ${code}`)
});
spawn() does not involve a shell, so in order to have it execute shell commands, you must invoke the shell executable explicitly and pass the shell command(s) as an argument:
const child = spawn('/bin/sh', [ '-c', `
echo "alpha"
echo "beta"
` ])
Note I've used /bin/sh rather than /bin/bash in an effort to make your command run on a wider array of [Unix-like] platforms.
All major POSIX-like shells accept a command string via the -c option.

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

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'
}

Resources