在有限的环境中将 stdout 和 stderr 输出到文件和屏幕,并将 stderr 输出到文件
Output stdout and stderr to file and screen and stderr to file in a limited environment
使用 mkfifo
可能会解决该问题,但我的 QNAP 上不存在该问题。所以,这里是问题的描述和我到目前为止所做的尝试。
我有一个名为 activateLogs
的函数,如果将日志写入磁盘或两者(屏幕和磁盘),它会重新启动脚本。 两者选项是我想要实现的新功能。
exec 3<> "$logPath/$logFileName.log"
"[=10=]" "${mainArgs[@]}" 2>&1 1>&3 | tee -a "$logPath/$logFileName.err" 1>&3 &
这段代码是写入磁盘的版本。 mainArgs
包含传递给脚本的所有参数,并在此函数外定义。此解决方案来自 。它将 stderr 和 stdout 组合在一个文件中,并且仍然在另一个文件中输出 stderr。
所以,现在,我希望能够保留它并将打印 stderr 和 stdout 添加到屏幕。
无法应用上面链接的问题中接受的解决方案,因为脚本是 运行 使用 sh
并且 mkfifo
不存在。
尝试 #1
exec 3>&1
"[=11=]" "${mainArgs[@]}" 2>&1 1>&3 | tee -a "$logPath/$logFileName.err" 1>&3 &
--> 替换上面的代码并在 if
分支(已经存在)中我添加了 tee
命令。
local isFileDescriptor3Exist=$(command 2>/dev/null >&3 && echo "Y")
if [ "$isFileDescriptor3Exist" = "Y" ]; then
tee -a "logs/123.log" &
echo "Logs are configured"
else
### CODE ABOVE
fi
我有屏幕、错误文件,但日志文件是空的。
尝试 #2
现在,上面的 if
分支中没有发球台,但包含在重新启动命令中。
exec 3>&1
"[=13=]" "${mainArgs[@]}" 2>&1 1>&3 | tee -a "$logPath/$logFileName.err" 1>&3 3>&1 | tee -a "logs/123.log" &
同样的结果。我可能在这个中理解第一个 tee
最初没有写入文件描述符 #3,因此 3>&1
什么都不做。
尝试 #3(不再重新启动脚本)
out="${TMPDIR:-/tmp}/out.$$"
err="${TMPDIR:-/tmp}/err.$$"
busybox mkfifo "$out" "$err"
trap 'rm "$out" "$err"' EXIT
tee -a "$logPath/$logFileName.log" &
tee -a "$logPath/$logFileName.err" < "$err" >&2 &
command >"$out" 2>"$err"
我从 busybox
得到 mkfifo: applet not found
尝试 #4(不再重新启动脚本)
out="${TMPDIR:-/tmp}/out.$$"
err="${TMPDIR:-/tmp}/err.$$"
python -c "import os; os.mkfifo(\"$out\")"
python -c "import os; os.mkfifo(\"$err\")"
trap 'rm "$out" "$err"' EXIT
tee -a "$logPath/$logFileName.log" &
tee -a "$logPath/$logFileName.err" < "$err" >&2 &
command >"$out" 2>"$err"
我没有日志(既不是“真实的”日志,也不是错误)。临时文件被删除了。此外,脚本永远不会结束,这是由 trap
引起的。
尝试 #5
exec 3>&1
{ "[=16=]" "${mainArgs[@]}" | tee -a "$logPath/$logFileName.log"; } 2>&1 1>&3 | tee -a "$logPath/$logFileName.err" &
exit
该解决方案看起来很有希望,但现在重新启动脚本不起作用,因为我的代码检测到当前是否正在执行并停止它。这是正常的,因为它是在子进程中执行的,即使我在整行的末尾使用 &
,但是......(在编写时进行测试)。用 &
替换终止符 ;
修复了它。
尝试 #6
我没有马上意识到,但是屏幕上显示了 stdout 和 stderr,stderr 被写入了一个文件,但只有 stdout 被写入了一个文件,而不是两者都被写入了。
exec 3>&1
{ "[=17=]" "${mainArgs[@]}" | tee -a "$logPath/$logFileName.log" & } 2>&1 1>&3 | tee -a "$logPath/$logFileName.err" &
exit
最终版本有效
现在一切都 written/displayed 它应该在的地方。在已接受的答案中查看完整代码。
{ "[=18=]" "${mainArgs[@]}" 2>&1 1>&3 | tee -a "$logPath/$logFileName.err" 1>&3 & } 3>&1 | tee -a "$logPath/$logFileName.log" &
exit
令人难以置信的是我的第一次尝试如此接近解决方案。
也许我没有正确阅读问题,但您似乎可以这样做:
#!/bin/sh
exec 3>&1
cmd (){
echo stdout;
echo stderr >&2;
}
stderr_outfile=outfile
if test -n "$log_stdout"; then
stdout_outfile=$log_stdout
else
stdout_outfile=/dev/null
fi
{ cmd | tee "$stdout_outfile"; } 2>&1 1>&3 | tee "$stderr_outfile"
终于达到目的了。我想说我受到了@WilliamPursell 的回答的启发。
{ "[=10=]" "${mainArgs[@]}" 2>&1 1>&3 | tee -a "$logPath/$logFileName.err" 1>&3 & } 3>&1 | tee -a "$logPath/$logFileName.log" &
说明
- 重新启动脚本...
- 正在将 stderr (
2>&1
) 发送到 stdout 并...
- 正在将标准输出发送到新的文件描述符(
1>&3
)
- 将其通过管道传输到接收 stderr 的 tee 以复制文件中的错误并使用...
- 正在将标准输出发送到新的文件描述符 (
1>&3
)...
- 并且
&
确保不阻塞
- 然后使用花括号将前面的命令分组。
- 正在将分组命令新文件描述符发送到标准输出 (
3>&1
)
- 将其通过管道传输到接收标准输出的 tee,标准输出结合了写入文件并显示在屏幕上的错误和正常输出
- 并且
&
确保不阻塞
我的 activateLogs
功能的完整代码供感兴趣的人使用。我还包含了依赖项,即使它们可以插入到 activateLogs
函数中。
m=0
declare -a mainArgs
if [ ! "$#" = "0" ]; then
for arg in "$@"; do
mainArgs[$m]=$arg
m=$(($m + 1))
done
fi
function containsElement()
# string to find
# array to search in
# return 0 if there is a match, otherwise 1
{
local e match=""
shift
for e; do [[ "$e" == "$match" ]] && return 0; done
return 1
}
function hasMainArg()
# string to find
# return 0 if there is a match, otherwise 1
{
local match=""
containsElement "" "${mainArgs[@]}"
return $?
}
function activateLogs()
# = logOutput: What is the output for logs: SCREEN, DISK, BOTH. Default is DISK. Optional parameter.
{
local logOutput=
if [ "$logOutput" != "SCREEN" ] && [ "$logOutput" != "BOTH" ]; then
logOutput="DISK"
fi
if [ "$logOutput" = "SCREEN" ]; then
echo "Logs will only be output to screen"
return
fi
hasMainArg "--force-log"
local forceLog=$?
local isFileDescriptor3Exist=$(command 2>/dev/null >&3 && echo "Y")
if [ "$isFileDescriptor3Exist" = "Y" ]; then
echo "Logs are configured"
elif [ "$forceLog" = "1" ] && ([ ! -t 1 ] || [ ! -t 2 ]); then
# Use external file descriptor if they are set except if having "--force-log"
echo "Logs are configured externally"
else
echo "Relaunching with logs files"
local logPath="logs"
if [ ! -d $logPath ]; then mkdir $logPath; fi
local logFileName=$(basename "[=11=]")"."$(date +%Y-%m-%d.%k-%M-%S)
exec 4<> "$logPath/$logFileName.log" # File descriptor created only to get the underlying file in any output option
if [ "$logOutput" = "DISK" ]; then
# FROM:
exec 3<> "$logPath/$logFileName.log"
"[=11=]" "${mainArgs[@]}" 2>&1 1>&3 | tee -a "$logPath/$logFileName.err" 1>&3 &
else
# FROM:
{ "[=11=]" "${mainArgs[@]}" 2>&1 1>&3 | tee -a "$logPath/$logFileName.err" 1>&3 & } 3>&1 | tee -a "$logPath/$logFileName.log" &
fi
exit
fi
}
#activateLogs "DISK"
#activateLogs "SCREEN"
activateLogs "BOTH"
echo "FIRST"
echo "ERROR" >&2
echo "LAST"
echo "LAST2"
使用 mkfifo
可能会解决该问题,但我的 QNAP 上不存在该问题。所以,这里是问题的描述和我到目前为止所做的尝试。
我有一个名为 activateLogs
的函数,如果将日志写入磁盘或两者(屏幕和磁盘),它会重新启动脚本。 两者选项是我想要实现的新功能。
exec 3<> "$logPath/$logFileName.log"
"[=10=]" "${mainArgs[@]}" 2>&1 1>&3 | tee -a "$logPath/$logFileName.err" 1>&3 &
这段代码是写入磁盘的版本。 mainArgs
包含传递给脚本的所有参数,并在此函数外定义。此解决方案来自 。它将 stderr 和 stdout 组合在一个文件中,并且仍然在另一个文件中输出 stderr。
所以,现在,我希望能够保留它并将打印 stderr 和 stdout 添加到屏幕。
无法应用上面链接的问题中接受的解决方案,因为脚本是 运行 使用 sh
并且 mkfifo
不存在。
尝试 #1
exec 3>&1
"[=11=]" "${mainArgs[@]}" 2>&1 1>&3 | tee -a "$logPath/$logFileName.err" 1>&3 &
--> 替换上面的代码并在 if
分支(已经存在)中我添加了 tee
命令。
local isFileDescriptor3Exist=$(command 2>/dev/null >&3 && echo "Y")
if [ "$isFileDescriptor3Exist" = "Y" ]; then
tee -a "logs/123.log" &
echo "Logs are configured"
else
### CODE ABOVE
fi
我有屏幕、错误文件,但日志文件是空的。
尝试 #2
现在,上面的 if
分支中没有发球台,但包含在重新启动命令中。
exec 3>&1
"[=13=]" "${mainArgs[@]}" 2>&1 1>&3 | tee -a "$logPath/$logFileName.err" 1>&3 3>&1 | tee -a "logs/123.log" &
同样的结果。我可能在这个中理解第一个 tee
最初没有写入文件描述符 #3,因此 3>&1
什么都不做。
尝试 #3(不再重新启动脚本)
out="${TMPDIR:-/tmp}/out.$$"
err="${TMPDIR:-/tmp}/err.$$"
busybox mkfifo "$out" "$err"
trap 'rm "$out" "$err"' EXIT
tee -a "$logPath/$logFileName.log" &
tee -a "$logPath/$logFileName.err" < "$err" >&2 &
command >"$out" 2>"$err"
我从 busybox
mkfifo: applet not found
尝试 #4(不再重新启动脚本)
out="${TMPDIR:-/tmp}/out.$$"
err="${TMPDIR:-/tmp}/err.$$"
python -c "import os; os.mkfifo(\"$out\")"
python -c "import os; os.mkfifo(\"$err\")"
trap 'rm "$out" "$err"' EXIT
tee -a "$logPath/$logFileName.log" &
tee -a "$logPath/$logFileName.err" < "$err" >&2 &
command >"$out" 2>"$err"
我没有日志(既不是“真实的”日志,也不是错误)。临时文件被删除了。此外,脚本永远不会结束,这是由 trap
引起的。
尝试 #5
exec 3>&1
{ "[=16=]" "${mainArgs[@]}" | tee -a "$logPath/$logFileName.log"; } 2>&1 1>&3 | tee -a "$logPath/$logFileName.err" &
exit
该解决方案看起来很有希望,但现在重新启动脚本不起作用,因为我的代码检测到当前是否正在执行并停止它。这是正常的,因为它是在子进程中执行的,即使我在整行的末尾使用 &
,但是......(在编写时进行测试)。用 &
替换终止符 ;
修复了它。
尝试 #6
我没有马上意识到,但是屏幕上显示了 stdout 和 stderr,stderr 被写入了一个文件,但只有 stdout 被写入了一个文件,而不是两者都被写入了。
exec 3>&1
{ "[=17=]" "${mainArgs[@]}" | tee -a "$logPath/$logFileName.log" & } 2>&1 1>&3 | tee -a "$logPath/$logFileName.err" &
exit
最终版本有效
现在一切都 written/displayed 它应该在的地方。在已接受的答案中查看完整代码。
{ "[=18=]" "${mainArgs[@]}" 2>&1 1>&3 | tee -a "$logPath/$logFileName.err" 1>&3 & } 3>&1 | tee -a "$logPath/$logFileName.log" &
exit
令人难以置信的是我的第一次尝试如此接近解决方案。
也许我没有正确阅读问题,但您似乎可以这样做:
#!/bin/sh
exec 3>&1
cmd (){
echo stdout;
echo stderr >&2;
}
stderr_outfile=outfile
if test -n "$log_stdout"; then
stdout_outfile=$log_stdout
else
stdout_outfile=/dev/null
fi
{ cmd | tee "$stdout_outfile"; } 2>&1 1>&3 | tee "$stderr_outfile"
终于达到目的了。我想说我受到了@WilliamPursell 的回答的启发。
{ "[=10=]" "${mainArgs[@]}" 2>&1 1>&3 | tee -a "$logPath/$logFileName.err" 1>&3 & } 3>&1 | tee -a "$logPath/$logFileName.log" &
说明
- 重新启动脚本...
- 正在将 stderr (
2>&1
) 发送到 stdout 并... - 正在将标准输出发送到新的文件描述符(
1>&3
) - 将其通过管道传输到接收 stderr 的 tee 以复制文件中的错误并使用...
- 正在将标准输出发送到新的文件描述符 (
1>&3
)... - 并且
&
确保不阻塞 - 然后使用花括号将前面的命令分组。
- 正在将分组命令新文件描述符发送到标准输出 (
3>&1
) - 将其通过管道传输到接收标准输出的 tee,标准输出结合了写入文件并显示在屏幕上的错误和正常输出
- 并且
&
确保不阻塞
我的 activateLogs
功能的完整代码供感兴趣的人使用。我还包含了依赖项,即使它们可以插入到 activateLogs
函数中。
m=0
declare -a mainArgs
if [ ! "$#" = "0" ]; then
for arg in "$@"; do
mainArgs[$m]=$arg
m=$(($m + 1))
done
fi
function containsElement()
# string to find
# array to search in
# return 0 if there is a match, otherwise 1
{
local e match=""
shift
for e; do [[ "$e" == "$match" ]] && return 0; done
return 1
}
function hasMainArg()
# string to find
# return 0 if there is a match, otherwise 1
{
local match=""
containsElement "" "${mainArgs[@]}"
return $?
}
function activateLogs()
# = logOutput: What is the output for logs: SCREEN, DISK, BOTH. Default is DISK. Optional parameter.
{
local logOutput=
if [ "$logOutput" != "SCREEN" ] && [ "$logOutput" != "BOTH" ]; then
logOutput="DISK"
fi
if [ "$logOutput" = "SCREEN" ]; then
echo "Logs will only be output to screen"
return
fi
hasMainArg "--force-log"
local forceLog=$?
local isFileDescriptor3Exist=$(command 2>/dev/null >&3 && echo "Y")
if [ "$isFileDescriptor3Exist" = "Y" ]; then
echo "Logs are configured"
elif [ "$forceLog" = "1" ] && ([ ! -t 1 ] || [ ! -t 2 ]); then
# Use external file descriptor if they are set except if having "--force-log"
echo "Logs are configured externally"
else
echo "Relaunching with logs files"
local logPath="logs"
if [ ! -d $logPath ]; then mkdir $logPath; fi
local logFileName=$(basename "[=11=]")"."$(date +%Y-%m-%d.%k-%M-%S)
exec 4<> "$logPath/$logFileName.log" # File descriptor created only to get the underlying file in any output option
if [ "$logOutput" = "DISK" ]; then
# FROM:
exec 3<> "$logPath/$logFileName.log"
"[=11=]" "${mainArgs[@]}" 2>&1 1>&3 | tee -a "$logPath/$logFileName.err" 1>&3 &
else
# FROM:
{ "[=11=]" "${mainArgs[@]}" 2>&1 1>&3 | tee -a "$logPath/$logFileName.err" 1>&3 & } 3>&1 | tee -a "$logPath/$logFileName.log" &
fi
exit
fi
}
#activateLogs "DISK"
#activateLogs "SCREEN"
activateLogs "BOTH"
echo "FIRST"
echo "ERROR" >&2
echo "LAST"
echo "LAST2"