我可以将 shell 配置为以不同颜色打印 STDERR 和 STDOUT 吗?

我可以将 shell 配置为以不同颜色打印 STDERR 和 STDOUT 吗?

我想设置我的终端,以便以与;stderr不同的颜色打印stdout也许是红色的。这样可以更容易地区分两者。

有没有办法在 中配置它.bashrc?如果不是,这可能吗?


笔记:这个问题已合并其他要求stderr,stdout 和用户输入回显要输出在3种不同颜色。答案可能针对任一问题。

答案1

查看stderred。它用于LD_PRELOAD挂钩libcto 的write()调用,stderr对发送到终端的所有输出进行着色。 (默认为红色。)

答案2

这是一个更难的版本在屏幕上仅显示 stderr,但将 stdout 和 stderr 写入文件

终端中运行的应用程序使用单一通道与其进行通信;应用程序有两个输出端口:stdout 和 stderr,但它们都连接到同一通道。

您可以将其中一个通道连接到不同的通道,向该通道添加颜色,然后合并这两个通道,但这会导致两个问题:

  • 合并的输出可能与没有重定向时的顺序不完全相同。这是因为对其中一个通道添加的处理需要(一点)时间,因此彩色通道可能会延迟。如果进行任何缓冲,则紊乱会更严重。
  • 终端使用颜色变化转义序列来确定显示颜色,例如␛[31m表示“切换到红色前景”。这意味着,如果某些发往 stdout 的输出恰好在显示 stderr 的某些输出时到达,则输出将出现颜色错误。 (更糟糕的是,如果转义序列中间有通道切换,您会看到垃圾。)

原则上,可以编写一个程序,同步侦听两个 ptys1(即在处理另一个通道上的输出时不会接受一个通道上的输入),并立即使用适当的颜色更改指令输出到终端。您将失去运行与终端交互的程序的能力。我不知道这个方法有任何实现。

write另一种可能的方法是通过挂钩所有在加载的库中调用系统调用的 libc 函数,使程序输出正确的颜色变化序列LD_PRELOAD。看西吉尔的回答对于现有的实施,或斯特凡·查泽拉斯的回答的混合方法利用strace.

在实践中,如果适用,我建议将 stderr 重定向到 stdout 并通过管道传输到基于模式的着色器,例如彩尾或者多尾,或特殊用途的着色器,例如颜色gcc或者色彩制作

1伪终端。由于缓冲,管道无法工作:源可以写入缓冲区,这会破坏与着色器的同步性。

答案3

对用户输入进行着色很困难,因为在一半的情况下,它是由终端驱动程序输出的(带有本地回显),因此在这种情况下,该终端中运行的应用程序可能不知道用户何时要键入文​​本并相应地更改输出颜色。只有伪终端驱动程序(在内核中)知道(终端仿真器(如 xterm)在某些按键时向其发送一些字符,并且终端驱动程序可能会发回一些字符进行回显,但 xterm 无法知道这些字符是否来自本地回显或应用程序输出到伪终端从属端的内容)。

然后,还有另一种模式,终端驱动程序被告知不要回显,但这次应用程序会输出一些内容。应用程序(例如使用 gdb、bash 等 readline 的应用程序)可能会在其 stdout 或 stderr 上发送该内容,这将很难与它为其他内容输出的内容区分开来,而不是回显用户输入。

然后,为了区分应用程序的标准输出和标准错误,有多种方法。

其中许多涉及将命令 stdout 和 stderr 重定向到管道以及应用程序读取的这些管道以对其进行着色。这样做有两个问题:

  • 一旦 stdout 不再是终端(如管道),许多应用程序就会倾向于调整其行为以开始缓冲其输出,这意味着输出将以大块的形式显示。
  • 即使是处理两个管道的同一进程,也不能保证应用程序在 stdout 和 stderr 上写入的文本的顺序将被保留,因为读取进程无法知道(如果有东西要从两者读取)是否从“stdout”管道或“stderr”管道开始读取。

另一种方法是修改应用程序,使其对标准输出和标准输入进行着色。这通常是不可能或不现实的。

然后一个技巧(对于动态链接的应用程序)可以是劫持(使用$LD_PRELOAD西吉尔的回答)应用程序调用的输出函数来输出某些内容,并在其中包含代码,根据它们是否要在 stderr 或 stdout 上输出某些内容来设置前景色。然而,这意味着劫持 C 库和任何其他执行write(2)由应用程序直接调用的系统调用的库中的每个可能的函数,最终可能会在 stdout 或 stderr 上写入一些内容(printf、puts、perror...),甚至这样,这可能会改变其行为。

另一种方法可能是在每次调用系统调用时使用 PTRACE 技巧 asstrace或do 来挂钩我们自己,并根据文件描述符是在文件描述符 1 还是 2 上设置输出颜色。gdbwrite(2)write(2)

然而,这是一件相当大的事情。

我刚刚玩过的一个技巧是strace使用 LD_PRELOAD 劫持自身(在每次系统调用之前进行挂钩自身的肮脏工作),告诉它根据是否检测到write(2)fd 1 或2.

从源代码来看strace,我们可以看到它的所有输出都是通过vfprintf函数完成的。我们需要做的就是劫持该功能。

LD_PRELOAD 包装器如下所示:

#define _GNU_SOURCE
#include <dlfcn.h>
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
#include <unistd.h>

int vfprintf(FILE *outf, const char *fmt, va_list ap)
{
  static int (*orig_vfprintf) (FILE*, const char *, va_list) = 0;
  static int c = 0;
  va_list ap_orig;
  va_copy(ap_orig, ap);
  if (!orig_vfprintf) {
    orig_vfprintf = (int (*) (FILE*, const char *, va_list))
      dlsym (RTLD_NEXT, "vfprintf");
  }

  if (strcmp(fmt, "%ld, ") == 0) {
    int fd = va_arg(ap, long);
    switch (fd) {
    case 2:
      write(2, "\e[31m", 5);
      c = 1;
      break;
    case 1:
      write(2, "\e[32m", 5);
      c = 1;
      break;
    }
  } else if (strcmp(fmt, ") ") == 0) {
    if (c) write(2, "\e[m", 3);
    c = 0;
  }
  return orig_vfprintf(outf, fmt, ap_orig);
}

然后,我们用以下命令编译它:

cc -Wall -fpic -shared -o wrap.so wrap.c -ldl

并将其用作:

LD_PRELOAD=/path/to/wrap.so strace -qqf -a0 -s0 -o /dev/null \
  -e write -e status=successful -P "$(tty)" \
  env -u LD_PRELOAD some-cmd

您会注意到,如果替换some-cmdbash,bash 提示符和您键入的内容将显示为红色(stderr),而zsh替换为黑色(因为 zsh 将 stderr 复制到新的 fd 上以显示其提示符和回显)。

即使对于您不期望的应用程序(例如那些使用颜色的应用程序),它似乎也确实表现得出奇地好。

strace着色模式在假定为终端的 stderr上输出。使用-P "$(tty)",我们可以避免对不发送到终端的写入执行此操作,例如 stdout/stderr 已重定向时。

该解决方案有其局限性:

  • 那些固有的strace:性能问题,您无法运行其他 PTRACE 命令,例如stracegdb其中的命令,或者 setuid/setgid 问题
  • 它的着色基于write每个单独进程的 stdout/stderr 上的 s。例如, in sh -c 'echo error >&2'error将是绿色的,因为echo将其输出到它是stdout(sh 重定向到 sh 的 stderr,但所有 strace 看到的都是 a write(1, "error\n", 6))。

2021年10月编辑。该包装器不再在带有 strace 5.10、glibc 2.32、gcc 10.30.0 的 Debian 不稳定版上工作,因为现在需要包装的函数__vfprintf_chk以及要查找的格式已更改。包装器需要更改为:

#define _GNU_SOURCE
#include <dlfcn.h>
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
#include <unistd.h>

int __vfprintf_chk(FILE *outf, int x, const char *fmt, va_list ap)
{
  static int (*orig_vfprintf) (FILE*, int, const char *, va_list) = 0;
  static int c = 0;
  va_list ap_orig;
  va_copy(ap_orig, ap);
  if (!orig_vfprintf) {
    orig_vfprintf = (int (*) (FILE*, int, const char *, va_list))
      dlsym (RTLD_NEXT, "__vfprintf_chk");
  }

  if (strcmp(fmt, "%d") == 0) {
    int fd = va_arg(ap, long);
    switch (fd) {
    case 2:
      write(2, "\e[31m", 5);
      c = 1;
      break;
    case 1:
      write(2, "\e[32m", 5);
      c = 1;
      break;
    }
  } else if (strcmp(fmt, "= %lu") == 0) {
    if (c) write(2, "\e[m", 3);
    c = 0;
  }
  return orig_vfprintf(outf, x, fmt, ap_orig);
}

它确实表明这种方法有点脆弱,因为它使用未记录的、不稳定的 API(不是真正的 API)。

答案4

这是我不久前所做的概念验证。

它仅适用于 zsh。

# make standard error red
rederr()
{
    while read -r line
    do
        setcolor $errorcolor
        echo "$line"
        setcolor normal
    done
}

errorcolor=red

errfifo=${TMPDIR:-/tmp}/errfifo.$$
mkfifo $errfifo
# to silence the line telling us what job number the background job is
exec 2>/dev/null
rederr <$errfifo&
errpid=$!
disown %+
exec 2>$errfifo

它还假设您有一个名为 setcolor 的函数。

简化版本:

setcolor()
{
    case "$1" in
    red)
        tput setaf 1
        ;;
    normal)
        tput sgr0
        ;;
    esac
}

相关内容