Bash 重定向 stdout 和 stderr 以使用时间戳分隔文件
Bash redirect stdout and stderr to seperate files with timestamps
想要记录所有 stdout
和 stderr
以分隔文件并为每一行添加时间戳。
尝试了以下方法,它有效但缺少时间戳。
#!/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
.
想要记录所有 stdout
和 stderr
以分隔文件并为每一行添加时间戳。
尝试了以下方法,它有效但缺少时间戳。
#!/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 运行ninglog
也退出了。 - 当我们运行
exec 1>&$stdout_tee_fd {stdout_tee_fd}>&-
时,我们在做两件事:首先,我们为FD 1创建一个文件描述符的副本,其编号存储在变量stdout_tee_fd
中;其次,我们从文件描述符 table 中删除stdout_tee_fd
条目,因此只保留 FD 1 上的副本。这确保稍后,当我们 运行exec >&"$stdout_orig_fd"
时,我们将删除 stdouttee
函数的最后一个剩余写句柄,导致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
.