无需重定向或管道即可读取控制台输出

无需重定向或管道即可读取控制台输出

有没有办法读取命令的控制台输出,而无需重定向或管道传输其 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可能还有其他。expectscreentmuxssh

对于单个外部命令来说,这相对容易。以 为例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

尽管在这种情况下文件中会有页眉和页脚,但您可能会或可能不会喜欢它。

我不会详细说明expectscreentmux。作为最后的手段(当没有其他可用工具时),可以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 somethingsomething应该默默地坐在那里,几乎不做任何事情。

优点:

  • 您可以将原始文件描述符保存为不同的数字,然后可以通过将 stdin 和 stdout 重定向回原来的状态来随时停止记录。
  • 您可以运行多个unbuffer … | tee … &并混合文件描述符以将脚本不同部分的输出记录到不同的文件中。
  • 您可以有选择地重定向任何单个命令的 stdout 和/或 stderr。

缺点:

  • 脚本应该kill unbuffersomething日志不再需要时执行。它应该在正常退出或由于信号而退出时执行此操作。如果它被强制终止,那么它将无法执行此操作。也许something应该定期检查主进程是否仍然存在并最终退出。有一个巧妙的解决方案flock(见下文)。

  • something需要以某种方式将其 tty 报告给主 shell。仅打印 的输出tty是一种可能性,然后主 shell 将./log独立打开并检索信息。此后,它只是文件中的垃圾(以及原始终端屏幕上的垃圾)。脚本可以截断文件,这仅适用于tee -a(因为tee -avstee就像>>vs>中的我的这个答案)。最好something通过单独的通道传递信息:一个临时文件或专门为此创建的命名 fifo。


概念验证

以下代码需要unbufferexpect(在 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-stderrrestore-stdout-stderr使用硬编码值78。您不应将这些描述符用于其他任何用途。如果需要,请重建此功能。

  • 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。它的工作原理如下:

    1. save-stdout-stderr锁定它所创建的 fifo,它为此使用 fifo 的打开描述符。注意:
      • 该函数在主 shell 中运行,因此实际上描述符是在主 shell 中打开的,因此bash解释整个脚本的进程 () 持有锁。
      • 锁定不会阻止其他进程写入 fifo。只有当它们想为自己锁定 fifo 时,它才会阻止它们。这就是下面运行的 shell 代码unbuffer要做的事情。
    2. 在下面运行的 shell 代码unbuffer通过 fifo 报告它的 tty,然后它尝试使用它自己的文件描述符 3 来锁定 fifo。要点是flock阻塞,直到它获得锁。
    3. 该函数读取有关 tty 的信息,创建请求的描述符,并使用该描述符锁定 tty。然后它才会解锁 fifo。
    4. 第一个不再被阻塞。执行转到第二个flock,它尝试锁定 tty 并阻塞。unbufferflock
    5. 主脚本继续。当不再需要 tty 时,主 shell 通过 将其解锁destroy-logging-tty
    6. 只有此时第二个flock下级unbuffer才会解除阻塞。该 shell 退出(自动释放其锁定),unbuffer销毁 tty 并退出,tee退出。无需维护。

    如果我们不锁定 fifo,而是让 shellunbuffer立即锁定 tty,则可能会发生 shell 在主 shell 之前获得锁定,因此它会立即终止。主 shell 在了解 tty 是什么之前无法锁定 tty。通过使用另一个锁以及正确的锁定和解锁顺序,我们可以确保unbuffer仅在主 shell 完成 tty 后退出。

    最大的优点是:如果主 shellSIGKILL在运行前因任何原因(包括)退出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做出反应并重新绘制了它的窗口,但它使用来自终端的大小作为其输出(来自的终端),并且它从未改变。SIGWINCHunbufferunbuffer

    即使unbuffer做出反应SIGWINCH或被迫更新尺寸,也可能为时已晚,mc可能已经读取了旧尺寸。似乎无论如何都不会发生这样的更新。一个简单的解决方法是限制自己调整终端的大小。


更广泛的问题

调整大小的问题mc是由于更广泛的问题造成的。你写道:

我想要实现的是执行一个命令,以便它直接将其输出打印到控制台 [...]

当有另一个 tty 的输出被记录并打印到原始控制台时,上述或类似的解决方案肯定不是“直接”的。mc如果它直接打印到原始终端,则会正确更新其大小。

通常,除非终端本身支持日志记录,否则您无法直接打印到终端并记录终端收到的内容。由screen或创建的伪终端tmux可以执行此操作,并且您可以通过脚本以编程方式设置它们。某些带有 GUI 的终端仿真器可能允许您转储它们收到的内容,您需要通过 GUI 配置它们。关键是您需要一个具有此功能的终端。在“错误”的终端中运行脚本,您无法以这种方式记录(您可以使用reptyr将其“移动”到另一个终端)。该脚本可以像我们的脚本一样重新路由其输出,但这不是“直接”的。或者……

有多种方式可以监听 tty (例子)。也许你会找到适合你需求的东西。通常,这种窥探需要提升访问权限,即使你想窥探一个你可以读取和写入的 tty。

相关内容