有没有办法读取命令的控制台输出,而无需重定向或管道传输其 STDOUT/STDERR?
重定向或管道的问题在于,当某些命令的 STDOUT 和/或 STDERR 被重定向时,它们的行为会有所不同,例如颜色或某些格式被删除,或者更相关的差异。旧tput
版本需要常规控制台上的 STDOUT 或 STDERR 来读取其尺寸。此外,在管道的情况下,原始命令失去了控制原始 shell 的能力,例如,exit
无法从函数内部执行脚本,因为该函数的输出已通过管道传输。
我想要实现的是执行一个命令,以便它直接将其输出打印到控制台,能够终止/退出 shell,但解析/处理并记录其输出。这tee
是一个显而易见的解决方案,但它存在上述问题。
我尝试了几次read
,运行一个循环,尝试从命令的文件描述符或/dev/stdout
或中读取/dev/tty
,但在所有情况下,显示的命令输出中没有一行实际上是read
。
例如
#!/bin/bash
apt update 2>&1 & pid=$!
while [[ -f /proc/$pid/fd/1 ]] && read -r line
do
echo "$line" >> ./testfile
done < /proc/$pid/fd/1
而且还在后台运行 while 循环并在前台运行命令(在我看来这是更好的选择),例如:
#!/bin/bash
while read -r line
do
echo "$line" >> ./testfile
done < /dev/tty & pid=$!
apt update
kill $pid
但在两种情况下./testfile
仍然是空的。
/dev/stdout
是每个进程自己的 STDOUT 文件描述符,当然这不能工作。
也许有人知道如何实现这一目标或类似的替代方案?
答案1
初步说明
在这个答案中,我使用术语“tty”来表示任何终端设备,通常用于點數。
简单的方法
您可以使用一些工具来捕获命令的输出,同时仍为其提供 tty :unbuffer
,,,,,,,script
可能还有其他。expect
screen
tmux
ssh
对于单个外部命令来说,这相对容易。以 为例ls --color=auto
。尝试一下:
ls --color=auto | tee ./log
正如您所注意到的,“存在上述问题”。但是这个:
unbuffer ls --color=auto | tee ./log # or tee -a
很好地打印了彩色和列化的输出,并将完整副本存储在中./log
。下面这个也是这样:
script -qc 'ls --color=auto' ./log # or script -a
尽管在这种情况下文件中会有页眉和页脚,但您可能会或可能不会喜欢它。
我不会详细说明expect
、screen
或tmux
。作为最后的手段(当没有其他可用工具时),可以ssh
在设置从本地主机到其自身的无密码 SSH 访问后使用。如下所示:
ssh -tt localhost "cd ${PWD@Q} && ls --color=auto" | tee ./log
(${var@Q}
扩展为可以重用为输入的格式的引用值var
;这里很完美。运行的 shellssh
必须是 Bash,语法不可移植。)
unbuffer
似乎是最简单的解决方案。顾名思义,它的主要目的是禁用缓冲,然而它确实创建了一个伪终端。
并发症
您希望能够捕获 shell 函数的输出,而不会失去与解释脚本的主 shell 的连接。为此,必须在主 shell 中运行该函数,不能使用上述使用运行某些外部命令的工具的简单方法,除非外部命令是整个脚本:
unbuffer ./the-script | tee ./log
显然,这个解决方案不是脚本所固有的。我猜你只是想运行./the-script
并捕获进入终端的输出。因此,脚本需要为本身无论如何。这是最棘手的部分。
可能的解决方案
一个可能的解决方案是运行
unbuffer something | tee ./log & # or tee -a
并将文件描述符1
和(可选)2
主 shell 重定向到为 创建的 tty something
。something
应该默默地坐在那里,几乎不做任何事情。
优点:
- 您可以将原始文件描述符保存为不同的数字,然后可以通过将 stdin 和 stdout 重定向回原来的状态来随时停止记录。
- 您可以运行多个
unbuffer … | tee … &
并混合文件描述符以将脚本不同部分的输出记录到不同的文件中。 - 您可以有选择地重定向任何单个命令的 stdout 和/或 stderr。
缺点:
脚本应该
kill
unbuffer
在something
日志不再需要时执行。它应该在正常退出或由于信号而退出时执行此操作。如果它被强制终止,那么它将无法执行此操作。也许something
应该定期检查主进程是否仍然存在并最终退出。有一个巧妙的解决方案flock
(见下文)。something
需要以某种方式将其 tty 报告给主 shell。仅打印 的输出tty
是一种可能性,然后主 shell 将./log
独立打开并检索信息。此后,它只是文件中的垃圾(以及原始终端屏幕上的垃圾)。脚本可以截断文件,这仅适用于tee -a
(因为tee -a
vstee
就像>>
vs>
中的我的这个答案)。最好something
通过单独的通道传递信息:一个临时文件或专门为此创建的命名 fifo。
概念验证
以下代码需要unbuffer
与expect
(在 Debian 中:expect
包)和flock
(在 Debian 中:util-linux
包)关联。
#!/bin/bash
save-stdout-stderr() {
exec 7>&1 8>&2
}
restore-stdout-stderr() {
exec 1>&7 2>&8 7>&- 8>&-
}
create-logging-tty() {
# usage: create-logging-tty descriptor log
local tmpdir tmpfifo tmpdesc tty descriptor log
descriptor="$1"
log="$2"
tmpdir="$(mktemp -d)"
tmpfifo="$tmpdir/fifo"
mkfifo "$tmpfifo"
eval 'exec '"$descriptor"'>/dev/null'
exec {tmpdesc}<>"$tmpfifo"
flock "$tmpdesc"
unbuffer sh -c '
exec 3<>"$1"
tty >&3
flock 3
flock 2
' sh "$tmpfifo" | tee "$log" &
if ! IFS= read -ru "$tmpdesc" -t 5 tty; then
rm -r "$tmpdir"
exec {descriptor}>&-
flock -u "$tmpdesc"
return 1
fi
rm -r "$tmpdir"
eval 'exec '"$descriptor"'> "$tty"'
flock "$descriptor"
flock -u "$tmpdesc"
}
destroy-logging-tty() {
# usage: destroy-logging-tty descriptor
local descriptor
descriptor="$1"
flock -u "$descriptor"
exec {descriptor}>&-
}
# here the actual script begins
save-stdout-stderr
echo "This won't be logged."
create-logging-tty 21 ./log
exec 1>&21 2>&21
echo "This will be logged."
# proof of concept
ls --color=auto /dev
restore-stdout-stderr
destroy-logging-tty 21
echo "This won't be logged."
笔记:
save-stdout-stderr
并restore-stdout-stderr
使用硬编码值7
和8
。您不应将这些描述符用于其他任何用途。如果需要,请重建此功能。create-logging-tty 21 ./log
是创建文件描述符21
(任意数字)的请求,该文件描述符将记录到 tty./log
(任意路径名)。必须从主 shell(而不是从子 shell)调用该函数,因为它应该为主 shell 创建文件描述符。create-logging-tty
用于eval
创建具有请求号码的文件描述符。eval
可能是邪恶的但在这儿它是安全的,除非您传递一些不幸的(或恶意的)shell 代码而不是数字。该函数不会验证其参数是否为数字。您的工作是确保它是数字(或添加适当的测试)。总的来说,示例中没有错误处理,所以也许你想添加一些。
return 1
当函数无法通过 fifo 获取到新创建的 tty 的路径时;函数的退出状态仍然未在主代码中处理。自行修复此问题及其他问题。特别是,在将任何内容重定向到所需描述符之前,你可能希望测试所需描述符是否真的指向 tty ([ -t 21 ]
)。create-logging-tty
使用{variable}<>…
语法创建临时文件描述符,其中 shell 为其选择一个未使用的数字(10 或更大)并将该数字分配给variable
。为了确保这不会纯粹偶然地获取请求的数字,该函数首先使用请求的数字创建一个文件描述符,然后才知道描述符最终应指向的 tty。实际上,您可以请求任何合理的数字,并且函数的内部不会与任何东西发生冲突。如果您的整个脚本使用
{variable}<>…
或类似的语法,那么您可能不喜欢 21 这样的硬编码数字。这可以轻松解决:exec {foo}>/dev/null create-logging-tty "$foo" ./log exec 1>&"$foo" 2>&"$foo" … destroy-logging-tty "$foo"
内部
unbuffer
tty
(command) 用于获取 提供的 tty 路径unbuffer
。正式tty
报告其 stdin,但我们更想知道其 stdout。这并不重要,因为它们都指向同一个 tty。由于
flock
无需杀死它unbuffer
或它生成的 shell。它的工作原理如下:save-stdout-stderr
锁定它所创建的 fifo,它为此使用 fifo 的打开描述符。注意:- 该函数在主 shell 中运行,因此实际上描述符是在主 shell 中打开的,因此
bash
解释整个脚本的进程 () 持有锁。 - 锁定不会阻止其他进程写入 fifo。只有当它们想为自己锁定 fifo 时,它才会阻止它们。这就是下面运行的 shell 代码
unbuffer
要做的事情。
- 该函数在主 shell 中运行,因此实际上描述符是在主 shell 中打开的,因此
- 在下面运行的 shell 代码
unbuffer
通过 fifo 报告它的 tty,然后它尝试使用它自己的文件描述符 3 来锁定 fifo。要点是flock
阻塞,直到它获得锁。 - 该函数读取有关 tty 的信息,创建请求的描述符,并使用该描述符锁定 tty。然后它才会解锁 fifo。
- 第一个不再被阻塞。执行转到第二个
flock
,它尝试锁定 tty 并阻塞。unbuffer
flock
- 主脚本继续。当不再需要 tty 时,主 shell 通过 将其解锁
destroy-logging-tty
。 - 只有此时第二个
flock
下级unbuffer
才会解除阻塞。该 shell 退出(自动释放其锁定),unbuffer
销毁 tty 并退出,tee
退出。无需维护。
如果我们不锁定 fifo,而是让 shell
unbuffer
立即锁定 tty,则可能会发生 shell 在主 shell 之前获得锁定,因此它会立即终止。主 shell 在了解 tty 是什么之前无法锁定 tty。通过使用另一个锁以及正确的锁定和解锁顺序,我们可以确保unbuffer
仅在主 shell 完成 tty 后退出。最大的优点是:如果主 shell
SIGKILL
在运行前因任何原因(包括)退出destroy-logging-tty
,则内核无论如何都会释放该进程持有的所有锁。这意味着unbuffer
最终将终止,不会有陈旧的进程。您可能想知道
tty
写入 fifo 是否会阻塞,直到函数从中读取为止。好吧,打开 fifo 进行读取就足够了。即使从未读取过 fifo,像我们这样的写入过程tty
也将被允许在阻塞之前向其中写入几 (千) 个字节。fifo 在主 shell 中打开以进行读取,但即使它过早退出,里面的 shell 也unubffer
刚刚打开了 fifo 进行写入和阅读。这不应该被阻止。如果主 shell 在不幸的时刻终止,唯一剩下的可能就是 fifo 及其目录。您可以抑制或捕获某些信号,直到不幸的时刻过去;或者您可以
trap … EXIT
从陷阱内部清除。仍然有一些场景(例如SIGKILL
)您的脚本无法执行任何操作。我使用交互测试了该解决方案
mc
,它基本上有效。预计此类应用程序的输出 ( ) 包含许多控制序列。例如,可以在不太小的终端中./log
重播日志。pv -qL 400 log
在我的测试中,它对其控制终端(即主终端,而不是来自的终端)
mc
做出反应并重新绘制了它的窗口,但它使用来自终端的大小作为其输出(来自的终端),并且它从未改变。SIGWINCH
unbuffer
unbuffer
即使
unbuffer
做出反应SIGWINCH
或被迫更新尺寸,也可能为时已晚,mc
可能已经读取了旧尺寸。似乎无论如何都不会发生这样的更新。一个简单的解决方法是限制自己调整终端的大小。
更广泛的问题
调整大小的问题mc
是由于更广泛的问题造成的。你写道:
我想要实现的是执行一个命令,以便它直接将其输出打印到控制台 [...]
当有另一个 tty 的输出被记录并打印到原始控制台时,上述或类似的解决方案肯定不是“直接”的。mc
如果它直接打印到原始终端,则会正确更新其大小。
通常,除非终端本身支持日志记录,否则您无法直接打印到终端并记录终端收到的内容。由screen
或创建的伪终端tmux
可以执行此操作,并且您可以通过脚本以编程方式设置它们。某些带有 GUI 的终端仿真器可能允许您转储它们收到的内容,您需要通过 GUI 配置它们。关键是您需要一个具有此功能的终端。在“错误”的终端中运行脚本,您无法以这种方式记录(您可以使用reptyr
将其“移动”到另一个终端)。该脚本可以像我们的脚本一样重新路由其输出,但这不是“直接”的。或者……
有多种方式可以监听 tty (例子)。也许你会找到适合你需求的东西。通常,这种窥探需要提升访问权限,即使你想窥探一个你可以读取和写入的 tty。