脚本输出被缓冲到一条消息中,尽管有单独的 echo 语句?

Script output is buffered into one message, despite separate echo statements?

我有一个包含三个 echo 语句的 shell 脚本:

echo 'first message'

echo 'second message'

echo 'third message'

然后我在节点中 运行 这个脚本并通过这个代码收集输出:

var child = process.spawn('./test.sh');
child.stdout.on('data', data => {
   data = JSON.stringify(data.toString('utf8'));
   console.log(data);
});

但是单数输出是"first message\nsecond message\nthird message\n",这是个问题。我期望三个输出,而不是一个由于某种形式的缓冲而混在一起的输出。而且我不能只按换行符拆分,因为各个输出可能包含换行符。

有什么方法可以区分各个echo语句的消息吗? (或其他输出命令,即 printf,或任何导致数据写入 stdout 或 stderror 的命令)

编辑:我已经尝试了 unbufferstdbuf,两者都不起作用,因为简单的测试可以证明这一点。这是 stdbuf 尝试的示例,我尝试了各种不同的参数值,基本上是所有可能的选项。

 var child = process.spawn('stdbuf', ['-i0', '-o0', '-e0', './test.sh']);

需要说明的是,当我 运行 来自 node 的 python 脚本也只有三个简单的 print 语句时,就会发生此问题。所以它与语言无关,特别是与 bash 脚本无关。它是关于在基于 unix 的系统上成功检测任何语言的脚本的各个输出。如果这是 C/C++ 可以做的事情并且我必须从节点连接到它,我愿意去那里。欢迎任何可行的解决方案。


编辑: 我最初通过将脚本的输出管道传输到 sed 并使用 s/$/uniqueString 在末尾插入标识符来自己解决了这个问题每个单独的输出,然后将接收到的数据拆分到该标识符上。

我悬赏的答案将适用于单行输出,但会中断多行输出。我测试中的一个错误让我认为情况并非如此,但事实确实如此。公认的答案是更好的解决方案,适用于任何大小的输出。但是,如果您无法控制脚本并且必须处理用户创建的脚本,那么我的 sed 解决方案是我发现的唯一可行的方法。它 确实 有效,非常好。

您可以使用作为节点 API 的一部分提供的 readline 接口。更多信息请点击此处 https://nodejs.org/api/readline.html#readline_event_line。您将按原样使用 spawn 但是将 stdout 传递给 readline 以便它可以解析这些行。不确定这是否是您打算做的。这是一些示例代码:

var process = require('child_process');
const readline = require('readline');

var child = process.spawn('./test.sh');

// Use readline interface
const readlinebyline = readline.createInterface({ input: child.stdout });

// Called when a line is received
readlinebyline.on('line', (line) => {
    line = JSON.stringify(line.toString('utf8'));
    console.log(line);
});

输出:

"first message"
"second message"
"third message"

如果您遇到类似 TypeError: input.on is not a function 的错误,请确保您通过 chmod +x test.sh.

test.sh 脚本具有执行权限

不使用console.log:

const  process_module  = require('child_process');

var child = process_module.spawn('./test.sh');
child.stdout.on('data', data => {
   process.stdout.write(data);
});

UPDATE(只是为了显示 process 模块和 process 全局对象之间的区别):

const process = require('child_process');

var child = process.spawn(`./test.sh`);
child.stdout.on('data', data => {
   global.process.stdout.write(data); // notice global object
});

我用来测试这个脚本的文件是:

Python:

#!/usr/bin/env python

print("first message")
print("second message")
print("third message")

Bash:

#!/usr/bin/env bash

echo 'first message'
echo 'second message'
echo 'third message'

输出:

first message
second message
third message

确保它们是可执行脚本:

chmod a+x test.sh
chmod a+x test.py

bash 和 python 的基础 C 库是 per-line stdout 缓冲的库。 stdbufunbuffer 会处理这个问题,但不会处理操作系统的缓冲。

例如,

Linux 为您的 node.js 进程和 bash 进程之间的管道分配 4096 字节作为缓冲区。

事实是,管道一端 (node.js) 的进程无法在另一端看到单独的写入(echo 调用)。这不是正确的设计(您可以通过单独的文件而不是标准输出进行通信)。

如果你坚持,你可以尝试欺骗 OS 调度程序:如果没有任何东西甚至远程接近写入管道,那么它将 schedule-in reader 进程( node.js) 将读取 OS 缓冲区中当前的内容。

我在 Linux 上测试了这个:

$ cat test.sh 
echo 'first message'
sleep 0.1
echo 'second message'
sleep 0.1
echo 'third message'
$ cat test.js 
const  child_process  = require('child_process');
var child = child_process.spawn(`./test.sh`);
child.stdout.on('data', data => {
   data = JSON.stringify(data.toString('utf8'));
   global.process.stdout.write(data); // notice global object
});
$ node test.js
"first message\n""second message\n""third message\n"

我 运行 在之前的项目中遇到了同样的问题。我在 echo 语句上使用了解释开关,然后将字符串拆分为 non-printable 字符。

示例:

echo -e 'one\u0016'

echo -e "two\u0016"

echo -e 'three\u0016'

结果:

"one\u0016\ntwo\u0016\nthree\u0016\n"

和对应的Javascript:

var child = process.spawn('./test.sh');
child.stdout.on('data', data => {
   var value = data.toString('utf8');
   var values = value.split("\u0016\n").filter(item => item);
   console.log(values);
});

如果您希望 test.sh 的输出始终按行发送,那么恕我直言,您最好的选择是使用 readline

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

const child = spawn('./test.sh');
const rl = readline.createInterface({
    input: child.stdout
});

rl.on('line', (input) => {
    console.log(`Received: ${input}`);
});

有一个非常简单的解决方案。只需将 sleep 1 添加到 bash 脚本,.on('data') 处理程序将不会合并输出。

所以脚本是这样的:

#/bin/bash
echo 'first message'
sleep 1
echo 'second message'
sleep 1
echo 'third message'

以及您的确切脚本(修复了缺少的 require('child_process');

var process = require('child_process');
var child = process.spawn('./test.sh');
child.stdout.on('data', data => {
   data = JSON.stringify(data.toString('utf8'));
   console.log(data);
});

如果您尝试 split-interpret 每封邮件,这可能会有所帮助: (本人对node经验不多,如有不妥请见谅)

test.sh:

#!/bin/bash
echo -n 'first message'
echo -ne '[=10=]'
echo -n 'second message'
echo -ne '[=10=]'
echo -n 'third message'
echo -ne '[=10=]'

节点:

var child = process.spawn('./test.sh');
var data_buffer  = Buffer.from([]);
var data_array   = [];
child.stdout.on('data', data => {
  data_buffer   += data;
  while (data_buffer.includes("[=11=]")) {
    let i        = data_buffer.indexOf("[=11=]");
    let s        = data_buffer.slice(0,i);
    data_array.push(s);
    data_buffer  = data_buffer.slice(i+1);
    let json     = JSON.stringify(s.toString('utf8'));
    console.log('--8<-------- split ------------');
    console.log('index: '+i);
    console.log('received: '+s);
    console.log('json: '+json);
    console.log(data_array);
  }
});

这实际上会使用 NULL-delimited 字符串而不是 newline-delimited。另一种选择是利用 IFS,但我未能实现这一点。此方法将使您免于使用 readline.

需要注意的一件事是你必须将所有接收到的数据存储在一个全局变量中,因为你无法控制数据块如何到达(我不知道是否有办法控制它) .话虽如此,您可以通过剪切已经解释的部分来减小它的大小,因此是第二个切片。

要使其正常工作,您当然必须确保数据中没有任何空字符。但是如果你这样做的话,你可以改变定界符。

这种做法,我觉得恕我直言比较彻底。

如果你需要python3:

#!/usr/bin/python3
print("first message", end = '\x00')
print("second message", end = '\x00')
print("third message", end = '\x00')