Bash 重定向 stdout 和 stderr 以使用时间戳分隔文件

Bash redirect stdout and stderr to seperate files with timestamps

想要记录所有 stdoutstderr 以分隔文件并为每一行添加时间戳。

尝试了以下方法,它有效但缺少时间戳。

#!/bin/bash

debug_file=./stdout.log
error_file=./stderr.log

exec > >(tee -a "$debug_file") 2> >(tee -a "$error_file")

echo "hello"
echo "hello world"

this-will-fail
and-so-will-this

添加时间戳。 (只希望时间戳作为日志输出的前缀。)

#!/bin/bash

debug_file=./stdout.log
error_file=./stderr.log

log () {
  file=; shift 
  while read -r line; do
    printf '%(%s)T %s\n' -1 "$line"
  done >> "$file"
}

exec > >(tee >(log "$debug_file")) 2> >(tee >(log "$error_file"))

echo "hello"
echo "hello world"

this-will-fail
and-so-will-this

后者会在日志中添加时间戳,但它也有可能弄乱我的终端 window。重现这种行为并不是直截了当的,它只是偶尔发生。我怀疑它与 subroutine/buffer 仍然有输出流过它有关。

弄乱我的终端的脚本示例。

# expected/desired behavior
user@system:~ ./log_test
hello
hello world
./log_test: line x: this-will-fail: command not found
./log_test: line x: and-so-will-this: command not found
user@system:~ # <-- cursor blinks here

# erroneous behavior
user@system:~ ./log_test
hello
hello world
user@system:~ ./log_test: line x: this-will-fail: command not found
./log_test: line x: and-so-will-this: command not found
# <-- cursor blinks here

# erroneous behavior
user@system:~ ./log_test
hello
hello world
./log_test: line x: this-will-fail: command not found
user@system:~
./log_test: line x: and-so-will-this: command not found
# <-- cursor blinks here

# erroneous behavior
user@system:~ ./log_test
hello
hello world
user@system:~
./log_test: line x: this-will-fail: command not found
./log_test: line x: and-so-will-this: command not found
# <-- cursor blinks here

为了好玩,我在脚本末尾放了一个 sleep 2 看看会发生什么,问题再也没有发生过。

希望有人知道答案或能指出我正确的方向。

谢谢

Edit

Judging from another question answered by Charles Duffy, what I'm trying to achieve is not really possible in bash.

诀窍是确保 tee 和进程替换 运行ning 您的 log 函数在整个脚本退出之前退出 - 这样当shell 启动 脚本打印其提示,没有任何后台进程可以在完成后写入更多输出。

作为一个工作示例(尽管它更注重明确而不是简洁):

#!/usr/bin/env bash
stdout_log=stdout.log; stderr_log=stderr.log

log () {
  file=; shift
  while read -r line; do
    printf '%(%s)T %s\n' -1 "$line"
  done >> "$file"
}

# first, make backups of your original stdout and stderr
exec {stdout_orig_fd}>&1 {stderr_orig_fd}>&2
# for stdout: start your process substitution, record its PID, start tee, record *its* PID
exec {stdout_log_fd}> >(log "$stdout_log"); stdout_log_pid=$!
exec {stdout_tee_fd}> >(tee "/dev/fd/$stdout_log_fd"); stdout_tee_pid=$!
exec {stdout_log_fd}>&- # close stdout_log_fd so the log process can exit when tee does
# for stderr: likewise
exec {stderr_log_fd}> >(log "$stderr_log"); stderr_log_pid=$!
exec {stderr_tee_fd}> >(tee "/dev/fd/$stderr_log_fd" >&2); stderr_tee_pid=$!
exec {stderr_log_fd}>&- # close stderr_log_fd so the log process can exit when tee does
# now actually swap out stdout and stderr for the processes we started
exec 1>&$stdout_tee_fd 2>&$stderr_tee_fd {stdout_tee_fd}>&- {stderr_tee_fd}>&-

# ...do the things you want to log here...
echo "this goes to stdout"; echo "this goes to stderr" >&2

# now, replace the FDs going to tee with the backups...
exec >&"$stdout_orig_fd" 2>&"$stderr_orig_fd"

# ...and wait for the associated processes to exit.
while :; do
  ready_to_exit=1
  for pid_var in stderr_tee_pid stderr_log_pid stdout_tee_pid stdout_log_pid; do
    # kill -0 just checks whether a PID exists; it doesn't actually send a signal
    kill -0 "${!pid_var}" &>/dev/null && ready_to_exit=0
  done
  (( ready_to_exit )) && break
  sleep 0.1 # avoid a busy-loop eating unnecessary CPU by sleeping before next poll
done

那么文件描述符操作是怎么回事?

确保我们清楚的几个关键概念:

  • 所有子shell 都有自己的文件描述符副本table,当它们fork()从它们的parent 中删除时创建。从那时起,每个文件描述符 table 实际上是独立的。
  • 从 FIFO(或管道)(的读取端)读取的进程将不会看到 EOF,直到 所有 程序写入该 FIFO(的写入端)已关闭描述符的副本。

...所以,如果您创建一个 FIFO 对,fork() 关闭 child 进程,并让 child 进程写入 FIFO 的写入端,无论是什么在 child 和 parent 关闭它们的副本之前,从读取端读取永远不会看到 EOF。

因此,您在这里看到的体操:

  • 当我们运行exec {stdout_log_fd}>&-时,我们正在关闭parentshell的FIFO写入副本到 stdout 的 log 函数,所以唯一剩下的副本是 tee child 进程使用的副本——这样当 tee 退出时,子 shell 运行ning log 也退出了。
  • 当我们运行exec 1>&$stdout_tee_fd {stdout_tee_fd}>&-时,我们在做两件事:首先,我们为FD 1创建一个文件描述符的副本,其编号存储在变量stdout_tee_fd中;其次,我们从文件描述符 table 中删除 stdout_tee_fd 条目,因此只保留 FD 1 上的副本。这确保稍后,当我们 运行 exec >&"$stdout_orig_fd" 时,我们将删除 stdout tee 函数的最后一个剩余写句柄,导致 tee 在 stdin 上获得 EOF(所以它退出,从而关闭它在 log 函数的 subshell 上持有的句柄,并让 subshell 也退出。

关于流程管理的一些最后说明

不幸的是,bash 处理为进程替换创建的子 shell 的方式在 still-actively-deployed 版本之间发生了重大变化;所以虽然 理论上 可以使用 wait "$pid" 让进程替换退出并收集其退出状态,但这并不总是可靠的 - 因此使用 kill -0.

但是,如果 wait "$pid" 有效,那将是非常可取的,因为 wait() 系统调用是删除 previously-exited 进程条目的原因来自进程 table:如果没有 wait()waitpid() 调用,则保证不会重用 PID(并且僵尸 process-table 条目作为占位符保留)发生了。

现代操作系统相当努力地避免 short-term PID 重用,因此在大多数情况下环绕并不是一个积极的问题。但是,如果您担心这一点,请考虑使用 中讨论的基于 flock 的机制来等待您的进程替换退出,而不是 kill -0.