如何让STDOUT和STDERR转到终端和日志文件?

时间:2008-12-12 16:18:31

标签: bash logging shell

我有一个非技术用户以交互方式运行的脚本。该脚本将状态更新写入STDOUT,以便用户可以确保脚本运行正常。

我希望STDOUT和STDERR都重定向到终端(这样用户就可以看到脚本正常工作,看看是否有问题)。我还希望将两个流重定向到日志文件。

我在网上看到了很多解决方案。有些不起作用,有些则非常复杂。我已经开发了一个可行的解决方案(我将作为答案输入),但它很蹩脚。

完美的解决方案是一行代码,可以合并到任何脚本的开头,将脚本发送到终端和日志文件。

编辑:将STDERR重定向到STDOUT并将结果传递给tee工作,但这取决于用户记住重定向和管道输出。我希望日志记录是万无一失的(这就是为什么我希望能够将解决方案嵌入到脚本本身中。)

9 个答案:

答案 0 :(得分:117)

使用“tee”重定向到文件和屏幕。根据您使用的shell,首先必须使用

将stderr重定向到stdout
./a.out 2>&1 | tee output

./a.out |& tee output

在csh中,有一个名为“script”的内置命令可以捕获进入文件的所有内容。您可以通过键入“script”来启动它,然后执行您要捕获的任何操作,然后按control-D关闭脚本文件。我不知道sh / bash / ksh的等价物。

此外,由于您现在已经指出这些是您自己可以修改的sh脚本,您可以通过用大括号或括号括起整个脚本来内部进行重定向,例如

  #!/bin/sh
  {
    ... whatever you had in your script before
  } 2>&1 | tee output.file

答案 1 :(得分:13)

接近五年后......

我相信这是OP寻求的“完美解决方案”。

以下是您可以添加到Bash脚本顶部的一行内容:

exec > >(tee -a $HOME/logfile) 2>&1

这是一个展示其用途的小脚本:

#!/usr/bin/env bash

exec > >(tee -a $HOME/logfile) 2>&1

# Test redirection of STDOUT
echo test_stdout

# Test redirection of STDERR
ls test_stderr___this_file_does_not_exist

(注意:这仅适用于Bash。使用/ bin / sh。)

改编自here;根据我的判断,原始文件没有在日志文件中捕获STDERR。修复了here的注释。

答案 2 :(得分:3)

将stderr重定向到stdout会在您的命令中附加:2>&1 要输出到终端并登录到文件,您应该使用tee

两者一起看起来像这样:

 mycommand 2>&1 | tee mylogfile.log

编辑:要嵌入到您的脚本中,您也可以这样做。所以你的脚本

#!/bin/sh
whatever1
whatever2
...
whatever3

最终会成为

#!/bin/sh
( whatever1
whatever2
...
whatever3 ) 2>&1 | tee mylogfile.log

答案 3 :(得分:1)

一年后,这是一个用于记录任何内容的旧bash脚本。例如,
teelog make ...记录到生成的日志名称(并查看用于记录嵌套make的技巧。)

#!/bin/bash
me=teelog
Version="2008-10-9 oct denis-bz"

Help() {
cat <<!

    $me anycommand args ...

logs the output of "anycommand ..." as well as displaying it on the screen,
by running
    anycommand args ... 2>&1 | tee `day`-command-args.log

That is, stdout and stderr go to both the screen, and to a log file.
(The Unix "tee" command is named after "T" pipe fittings, 1 in -> 2 out;
see http://en.wikipedia.org/wiki/Tee_(command) ).

The default log file name is made up from "command" and all the "args":
    $me cmd -opt dir/file  logs to `day`-cmd--opt-file.log .
To log to xx.log instead, either export log=xx.log or
    $me log=xx.log cmd ...
If "logdir" is set, logs are put in that directory, which must exist.
An old xx.log is moved to /tmp/\$USER-xx.log .

The log file has a header like
    # from: command args ...
    # run: date pwd etc.
to show what was run; see "From" in this file.

Called as "Log" (ln -s $me Log), Log anycommand ... logs to a file:
    command args ... > `day`-command-args.log
and tees stderr to both the log file and the terminal -- bash only.

Some commands that prompt for input from the console, such as a password,
don't prompt if they "| tee"; you can only type ahead, carefully.

To log all "make" s, including nested ones like
    cd dir1; \$(MAKE)
    cd dir2; \$(MAKE)
    ...
export MAKE="$me make"

!
  # See also: output logging in screen(1).
    exit 1
}


#-------------------------------------------------------------------------------
# bzutil.sh  denisbz may2008 --

day() {  # 30mar, 3mar
    /bin/date +%e%h  |  tr '[A-Z]' '[a-z]'  |  tr -d ' '
}

edate() {  # 19 May 2008 15:56
    echo `/bin/date "+%e %h %Y %H:%M"`
}

From() {  # header  # from: $*  # run: date pwd ...
    case `uname` in Darwin )
        mac=" mac `sw_vers -productVersion`"
    esac
    cut -c -200 <<!
${comment-#} from: $@
${comment-#} run: `edate`  in $PWD `uname -n` $mac `arch` 

!
    # mac $PWD is pwd -L not -P real
}

    # log name: day-args*.log, change this if you like --
logfilename() {
    log=`day`
    [[ $1 == "sudo" ]]  &&  shift
    for arg
    do
        log="$log-${arg##*/}"  # basename
        (( ${#log} >= 100 ))  &&  break  # max len 100
    done
            # no blanks etc in logfilename please, tr them to "-"
    echo $logdir/` echo "$log".log  |  tr -C '.:+=[:alnum:]_\n' - `
}

#-------------------------------------------------------------------------------
case "$1" in
-v* | --v* )
    echo "$0 version: $Version"
    exit 1 ;;
"" | -* )
    Help
esac

    # scan log= etc --
while [[ $1 == [a-zA-Z_]*=* ]]; do
    export "$1"
    shift
done

: ${logdir=.}
[[ -w $logdir ]] || {
    echo >&2 "error: $me: can't write in logdir $logdir"
    exit 1
    }
: ${log=` logfilename "$@" `}
[[ -f $log ]]  &&
    /bin/mv "$log" "/tmp/$USER-${log##*/}"


case ${0##*/} in  # basename
log | Log )  # both to log, stderr to caller's stderr too --
{
    From "$@"
    "$@"
} > $log  2> >(tee /dev/stderr)  # bash only
    # see http://wooledge.org:8000/BashFAQ 47, stderr to a pipe
;;

* )
#-------------------------------------------------------------------------------
{
    From "$@"  # header: from ... date pwd etc.

    "$@"  2>&1  # run the cmd with stderr and stdout both to the log

} | tee $log
    # mac tee buffers stdout ?

esac

答案 4 :(得分:1)

这可以解决问题,并保留stdout和stderr之间的区别:

{ the_cmd > >(tee stdout.txt ); } 2> >(tee stderr.txt >&2 )

这是一个脚本:

the_cmd()
{
    echo out;
    1>&2 echo err;
}

{ the_cmd > >(tee stdout.txt ); } 2> >(tee stderr.txt >&2 )

这是一个会话:

$ foo=$(./example.sh)
    err

$ echo $foo
    out

$ cat stdout.txt
    out

$ cat stderr.txt
    err

这是它的工作方式:

    the_cmd
  1. stdout发送到tee,后者将其保存到文件,但同时将副本传递到其块的stdout({大括号中的部分)。 the_cmd的stderr默认情况下进入块的stderr。

  2. 我们不会将stdout从块中重定向(因为我们已经将其捕获到文件中),因此默认情况下它会将其输出到终端,但是我们确实将块的stderr重定向到另一个tee命令(因为我们仍然需要捕获它。)

  3. 最后的tee命令将其stdin(即the_cmd的stderr)写入文件,但还将一个副本传递到stdout,它将其与块的stdout混合(我们不需要),因此我们使用>&2将其显式发送回stderr。

这在文件和命令输出中都使stderr与stdout分开。

如果第一个tee记录了任何错误,则它们将同时显示在stderr文件和命令的stderr中;如果第二个tee记录了任何错误,则它们将仅显示在终端的stderr中。

答案 5 :(得分:1)

编辑: 我发现我出轨了,最终回答了一个与被问到的问题不同的问题。真正问题的答案在保罗·汤姆林的答案的底部。 (如果出于某种原因要增强该解决方案以分别重定向stdout和stderr,则可以使用我在此处描述的技术。)


我一直想要一个保留stdout和stderr之间区别的答案。 不幸的是,到目前为止给出的所有答案都保留了这种区别。 我容易在种族中使用:正如我在评论中指出的那样,它们有可能使程序看到输入不完整的风险。

我想我终于找到了保留该区别的答案, 也不容易比赛,也不是很烦躁。

第一个构建块:交换标准输出和标准错误:

my_command 3>&1 1>&2 2>&3-

第二个构建块:如果我们只想过滤(例如tee)stderr, 我们可以通过交换stdout&stderr,过滤,然后交换回去来实现:

{ my_command 3>&1 1>&2 2>&3- | stderr_filter;} 3>&1 1>&2 2>&3-

现在剩下的事情很简单:我们可以在开头添加一个stdout过滤器:

{ { my_command | stdout_filter;} 3>&1 1>&2 2>&3- | stderr_filter;} 3>&1 1>&2 2>&3-

或末尾:

{ my_command 3>&1 1>&2 2>&3- | stderr_filter;} 3>&1 1>&2 2>&3- | stdout_filter

为使自己确信以上两个命令均有效,我使用了以下命令:

alias my_command='{ echo "to stdout"; echo "to stderr" >&2;}'
alias stdout_filter='{ sleep 1; sed -u "s/^/teed stdout: /" | tee stdout.txt;}'
alias stderr_filter='{ sleep 2; sed -u "s/^/teed stderr: /" | tee stderr.txt;}'

输出为:

...(1 second pause)...
teed stdout: to stdout
...(another 1 second pause)...
teed stderr: to stderr

和我的提示一样,在“ teed stderr: to stderr”之后立即返回。

关于zsh的脚注

以上解决方案可在bash中运行(我不确定,也许还有其他一些shell),但在zsh中不起作用。它在zsh中失败的原因有两个:

  1. zsh无法理解语法2>&3-;必须重写 为2>&3 3>&-
  2. 如果重定向文件描述符,则使用zsh中的
  3. (与其他Shell不同) 它已经开放,在某些情况下(我不完全了解它是如何决定的),它会执行内置的类似tee的行为。为避免这种情况,您必须在关闭每个fd之前 重定向它。

因此,例如,我的第二种解决方案必须针对zsh重写为{my_command 3>&1 1>&- 1>&2 2>&- 2>&3 3>&- | stderr_filter;} 3>&1 1>&- 1>&2 2>&- 2>&3 3>&- | stdout_filter(它也可以在bash中使用,但是非常冗长)。

另一方面,您可以利用zsh神秘的内置隐式teeing来获得zsh的更短解决方案,而后者根本不会运行tee:

my_command >&1 >stdout.txt 2>&2 2>stderr.txt

(我不会从文档中猜出我发现>&12>&2是触发zsh隐式发球的事物;我发现这是通过反复试验得出的。)< / p>

答案 6 :(得分:1)

使用tee程序和dup stderr to stdout。

 program 2>&1 | tee > logfile

答案 7 :(得分:1)

我创建了一个名为“RunScript.sh”的脚本。该脚本的内容是:

${APP_HOME}/${1}.sh ${2} ${3} ${4} ${5} ${6} 2>&1 | tee -a ${APP_HOME}/${1}.log

我称之为:

./RunScript.sh ScriptToRun Param1 Param2 Param3 ...

这有效,但它需要通过外部脚本运行应用程序的脚本。这有点笨拙。

答案 8 :(得分:0)

在脚本中使用script命令(man 1脚本)

创建一个包装器shellcript(2行),用于设置script(),然后调用exit。

第1部分:wrap.sh

#!/bin/sh
script -c './realscript.sh'
exit

第2部分:realscript.sh

#!/bin/sh
echo 'Output'

结果:

~: sh wrap.sh 
Script started, file is typescript
Output
Script done, file is typescript
~: cat typescript 
Script started on fr. 12. des. 2008 kl. 18.07 +0100
Output

Script done on fr. 12. des. 2008 kl. 18.07 +0100
~:
免费获取贴纸的机会↓↓↓
豫ICP备18024241号-1