在有限的环境中将 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"