preserve color when executing child_process.spawn
Asked Answered
M

10

93

I'm trying to execute a windows command through cmd.exe in node.js using child_process.spawn. It executes correctly, but only displays in default text color. How do I preserver the color. Is it possible?

var spawn = require('child_process').spawn,
    cmd    = spawn('cmd', ['/s', '/c', 'C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319\\MSBuild c:\\test.sln']);

cmd.stdout.on('data', function(data){
    process.stdout.write(data);
});

cmd.stderr.on('data', function(data){
    process.stderr.write(data);
});

cmd.on('exit', function(code){
    console.log(code);
});

When executing via node, the color is not preserved. Executing via node.js

When executing via cmd.exe directly, the color is present. (This is the expected behavior). How do I get this behvior when executing via node. When executing through cmd.exe

Millda answered 11/10, 2011 at 12:8 Comment(1)
+1 for the simple, executable example, and the nice screenshots explaining the now and later. Nice!Thomson
T
12

Try this instead:

var spawn = require('child_process').spawn
  , command = 'C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319\\MSBuild c:\\test.sln'
  , cmd    = spawn('cmd', ['/s', '/c', command], { customFds: [0,1,2] });

cmd.on('exit', function(code){
    console.log(code);
});

Note that I'm not positive whether or not customFds works on Windows. I know that it's old deprecated functionality doesn't work, but when only passing [0,1,2] as the fd's, I think there is a special case for that.

I've been doing something similar here, but I've only ran that command on Unix machines. So let me know if that works on Windows.

Thomson answered 29/12, 2011 at 18:45 Comment(5)
Since customFds is deprecated, is there a way to do this without customFds on Unix machines as well?Danette
@JPRichardson Unfortunately there is no other way to do it without the deprecated customFds. I don't believe it is going anywhere anytime soon, especially for this use-case.Thomson
As a side note @prabir, I'm assuming you are attempting to compile a native addon for node. You should definitely checkout node-gyp and use that, it will make life easier :)Thomson
As a side note, anybody wanting this functionality for real should upvote this: github.com/joyent/node/issues/2754Thomson
This doesn't work for me using just ls under ubuntu bashAccentor
H
174

There are new 'stdio' option for child_process.spawn(). Try following:

spawn("path to executable", ["params"], {stdio: "inherit"});

"Inherit" means [0, 1, 2] or [process.stdin, process.stdout, process.stderr].

Helmet answered 9/1, 2013 at 9:3 Comment(6)
+1 Great solution, without using deprecated functionality. Definitely the winning answer in my opinion.Romanist
Works great! Note: event funcs like spawnedThing.stdout.on and spawnedThing.stderr.on will no longer exist since it's inheriting the write streams from its parent. Took me a little while to realize this while I was getting this error message: Cannot call method 'on' of nullAzrael
I have a problem related to what Chris mentions - I want to add some custom text to each line of output, so I need the 'on' functions, but since they are now gone, how can I preserve the original output color and prepend something to each line of the output?Elfont
Works for me under Windows too.Devilment
For those with the Cannot call method 'on' of null errors... when you set stdio to inherit you can then use process.stdout.on as in the parent process the child process is inheriting stdio from.Bonze
This works great for me running appium tests with node, creating mocha as a child process. Working on a mac with ES6. Thanks for posting!Sanitary
G
31

If you are getting error:

Cannot call method 'on' of null

Try this:

spawn("command", ["args"], { env: { ...process.env, FORCE_COLOR: true } });

works with mocha

Garey answered 12/4, 2017 at 16:47 Comment(5)
If you receive the error "spawn node ENOENT", this variation worked for me: process.env.FORCE_COLOR = true; spawn("command", ["args"], { env: process.env });Unhand
You are a legendNurmi
This is the only answer that fixed the problem for me, thanks!Mun
Riffing off @KevinReilly, I did { env: { ...process.env, FORCE_COLOR: true } } to make it a one-liner.Carrefour
This one liner works in both ubuntu and windows. I tested it in both. @Prabir please make this as the accepted answer.Playback
H
25

crossplatform solution that worked for me was to use both shell: true and stdio: 'inherit':

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

const childProcess = spawn('node', ['./child.js'], { shell: true, stdio: 'inherit' });

shell: true does not seem to still be relevant with Node 21. this answer was originally written for node 0.4.7 or something 😅

stdio: 'inherit' also takes an array [Stdin, Stdout, Stderr] where you can specify 'inherit', or 'pipe', and others. see the documentation: https://nodejs.org/api/child_process.html#optionsstdio

⚠️ There is a catch. If you need to listen to childProcess.stdout.on('data', ...). It won't work when stdio is set to 'inherit' because node then set stdout to null. To go around this limitation you need to pipe manually the outputs

let output = '';
const childProcess = spawn('node', ['./child.js']);

childProcess.stdout.pipe(process.stdout);
childProcess.stdout.on('data',  (data) => {
  output += data.toString();
});
childProcess.stderr.pipe(process.stderr);
childProcess.on('exit', () => {
  console.log(output);
});
Helman answered 13/5, 2016 at 15:44 Comment(4)
This would work but how would you tell which log is from which child process? Using inherit will make child process log behaves like parent process log.Jessy
my goal was to display logs as they arrived in the console. Like we could see in a jenkins build console. In my case I didn't care about differentiating which log come from where. Using the solution I used was the only that fitted my need. By the way, a solution to do log properly I recommand is bunyan. winston is great too but bunyan is lighterHelman
awesome, just what I needed!Bothersome
shell: true was unnecessary in my case to preserve colored output, just stdio: 'inherit' is enough.Atronna
T
12

Try this instead:

var spawn = require('child_process').spawn
  , command = 'C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319\\MSBuild c:\\test.sln'
  , cmd    = spawn('cmd', ['/s', '/c', command], { customFds: [0,1,2] });

cmd.on('exit', function(code){
    console.log(code);
});

Note that I'm not positive whether or not customFds works on Windows. I know that it's old deprecated functionality doesn't work, but when only passing [0,1,2] as the fd's, I think there is a special case for that.

I've been doing something similar here, but I've only ran that command on Unix machines. So let me know if that works on Windows.

Thomson answered 29/12, 2011 at 18:45 Comment(5)
Since customFds is deprecated, is there a way to do this without customFds on Unix machines as well?Danette
@JPRichardson Unfortunately there is no other way to do it without the deprecated customFds. I don't believe it is going anywhere anytime soon, especially for this use-case.Thomson
As a side note @prabir, I'm assuming you are attempting to compile a native addon for node. You should definitely checkout node-gyp and use that, it will make life easier :)Thomson
As a side note, anybody wanting this functionality for real should upvote this: github.com/joyent/node/issues/2754Thomson
This doesn't work for me using just ls under ubuntu bashAccentor
M
8

This doesn't fix the underlying issue (lack of a proper TTY stream) but it should help get around it.

If the sub-process you're running uses supports-color (https://www.npmjs.com/package/supports-color) like chalk, then you can set an environmental variable FORCE_COLOR to any value and it will skip the rest of the checks. This will allow you to continue to use pipes (and capture/modify the returned data) unlike the inherit fix.

There is also a node-pty (https://www.npmjs.com/package/node-pty) module that provides a .spawn with the ability to pass a pty (pseudo tty) that may be a more holistic answer. I haven't played with it yet, and I'm not sure if it's cross platform or not.

Metrorrhagia answered 16/3, 2017 at 16:28 Comment(2)
I can confirm this works and enables retaining the colors while being able to capture and modify stdout. Btw, I set the environmental variable simply by passing it to spawn option env, easy peasy!Augend
Somehow I am missing something. I tried this, but still when I do: process.env.FORCE_COLOR = 'true'; const proc = execa('npm', ['outdated'], { cwd: dir, stdout: 'pipe', reject: false, env: process.env }); proc.stdout?.pipe(process.stdout); colors do not work, but if I put stdout: process.stdout (and remove the .pipe), colors shows. I must be missing something.Nipissing
N
2

I have tried many options above, and they did not work. For npm (and probably node) commands, this works:

const ps = _spawn('npm', ['outdated', '--color=always'], { stdio: 'pipe'});
ps.stdout.pipe(process.stdout);
// Then, you can also ps.stdout.on('data',...) for further processing

The trick is the --color=always (using Mac OSX with zsh). I did not need to do the FORCE_COLOR.

However, that does not work for all commands, for example ls does not seem to accept this argument.

Nipissing answered 22/2, 2021 at 23:46 Comment(1)
This worked for me when using chalk and passing the output through a spawnExcurvate
A
1

If you want to either keep the color or add some notation for the output, you can try the code below:

const {spawn} = require('node:child_process')
const {Writable} = require('node:stream')
const cmd = spawn(
    'cmd',
    [
        '/s',
        '/c',
        'C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319\\MSBuild c:\\test.sln',
    ],
    {stdio: [process.stdin, process.stdout, 'pipe']}
)
const error = new Writable()
error._write = function (data, ...argv) {
    console.log('your notation')
    process.stderr._write(data, ...argv)
}
error.stderr.pipe(customStream)
Ahem answered 4/1, 2017 at 4:32 Comment(1)
Doesn't fix the problem for me.Sarpedon
D
1

I was there too. Like said in the thread, there's no colored output because the command you're executing doesn't detect a TTY environment.
If you:

  • Don't want to inherit the child stdout
  • Don't know which command is going to be executed (then you don't know which of e.g --ansi or --colors can work)

Then you should spawn a PTY from node. You may use node-pty, but it is a hassle to install, and it doesn't support latest node versions. Instead, I suggest using this node package that I wrote.

Dextrality answered 31/12, 2021 at 21:47 Comment(2)
When linking to your own site or content (or content that you are affiliated with), you must disclose your affiliation in the answer in order for it not to be considered spam. Having the same text in your username as the URL or mentioning it in your profile is not considered sufficient disclosure under Stack Exchange policy. Also, this is your 3rd recent answer linking to the package, and that's bordering on excessive self-promotion even with disclosure.Medley
Got it. I was trying to give as much exposure as possible to let others know a solution exists, so that they do not despair like I did. But I will be careful from now on.Dextrality
T
0

To preserve colours in stdout/stderr stream don't spawn the application directly, rather using unbuffer tool (on linux you might need to run apt install expect)

let app = "command to run";
let args = ["--array", "--of", "--arguments"];
var child = cp.spawn("unbuffer", [app, ...args], {cwd:tempPath()});

child.stdout.on('data', function (data) {
  process.stdout.write('stdout: ' + data);
});

child.stderr.on('data', function (data) {
  process.stdout.write('stderr: ' + data);
});

child.on('close', (code) => {
  console.log(`child process exited with code ${code}`);
});
Touzle answered 5/1, 2023 at 8:2 Comment(0)
E
0

Child process`s stdout is not a TTY

I'm facing a similar issue

// main
const child = spawn('node', ['./child.js'], { stdio: 0, 'pipe', 2 });
child.stdout.pipe(process.stdout);

// child
console.log(process.stdout.isTTY); // undefined
console.log(123); // no color
console.log('\u001b[1;34mhello\u001b[0m'); // color preserved

Only the third console will display color, and isTTY is undefined. I tried node-pty, but that will lost IPC support. So I tried to replace the global console instance:

// main
const child = spawn('node', ['./child.js'], { stdio: 0, 'pipe', 2 });
child.stdout.pipe(process.stdout);

// child
const { Console } = require('console'); // node>=8
global.console = new Console({
  stdout: process.stdout,
  stderr: process.stderr,
  colorMode: true,
});
console.log(process.stdout.isTTY); // undefined
console.log(123); // color preserved
console.log('\u001b[1;34mhello\u001b[0m'); // color preserved
Escrow answered 30/1, 2023 at 9:24 Comment(5)
So in the end, did your code work? It’s not clear from your wording.Blastoff
@Blastoff Sorry for my bad English. I wrote a demo here: codesandbox.io/s/jolly-darkness-u9xnhtEscrow
Texts need to be wrapped with ANSI escape codes before writing to process.stdout. In nodejs, the global console instance will do this job automatically by detecting process.stdout.isTTY property, if true, console.log(true) means process.stdout.write("\u001b[1;33mtrue\u001b[0m\n"), otherwise it equals process.stdout.write("true\n").Escrow
Whether the child process runs within a TTY context or not depends on the stdio option. I'm using stdio: 'pipe', and the process.stdout.isTTY is false in the child process, so I replace it global console instance to force it to wrap the text with color escape codes before writing to its process.stdout.Escrow
Not all tools detect TTY environment, some rely on environment variables or something else.Escrow

© 2022 - 2024 — McMap. All rights reserved.