关于bash:如何让STDOUT和STDERR同时转到终端和日志文件?

How do I get both STDOUT and STDERR to go to the terminal and a log file?

我有一个脚本,将由非技术用户交互运行。脚本将状态更新写入stdout,这样用户就可以确保脚本运行正常。

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

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

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

编辑:将stderr重定向到stdout,并通过管道将结果发送到tee,这是可行的,但这取决于用户是否记住重定向和传输输出。我希望日志记录是防错和自动的(这就是为什么我希望能够将解决方案嵌入到脚本本身中)。


使用"tee"重定向到文件和屏幕。根据使用的shell,首先必须使用将stderr重定向到stdout

1
./a.out 2>&1 | tee output

1
./a.out |& tee output

在CSH中,有一个名为"script"的内置命令,它将捕获所有进入屏幕的文件。首先输入"script",然后执行想要捕获的操作,然后点击control-d关闭脚本文件。我不知道sh/bash/ksh的等价物。

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

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


半个十年后…

我相信这是行动计划寻求的"完美解决方案"。

下面是一个可以添加到bash脚本顶部的一行程序:

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

下面是一个小脚本演示它的用法:

1
2
3
4
5
6
7
8
9
#!/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一起使用。)

从这里改编;据我所知,原作没有在日志文件中捕获stderr。用这里的注释修复。


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

这两个看起来都是这样的:

1
 mycommand 2>&1 | tee mylogfile.log

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

1
2
3
4
5
#!/bin/sh
whatever1
whatever2
...
whatever3

将最终成为

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


一年后,这里有一个用于记录任何东西的老bash脚本。例如,teelog make ...将日志记录到生成的日志名称(也可以看到记录嵌套make的技巧)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#!/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:]_
' - `
}

#-------------------------------------------------------------------------------
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


我创建了一个名为"runscript.sh"的脚本。此脚本的内容是:

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

我这样称呼它:

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

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


使用tee程序和dup stderr to stdout。

1
 program 2>&1 | tee > logfile


这是一个技巧,也保留了stdout和stderr之间的区别:

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

下面是一个脚本:

1
2
3
4
5
6
7
the_cmd()
{
    echo out;
    1>&2 echo err;
}

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

下面是一个会话:

1
2
3
4
5
6
7
8
9
10
11
$ foo=$(./example.sh)
    err

$ echo $foo
    out

$ cat stdout.txt
    out

$ cat stderr.txt
    err

工作原理如下:

  • 支架中的部件将作为一个单元运行。其stdout将直接写入stdout,但其stderr将被发送到正确的部分。

  • 首先,the_cmd中的stdout被发送到tee中,后者将其转发到stdout,但也将其保存到文件中。

  • 然后,步骤2中的stderr被发送到tee,后者将其中继到stdout,因此我们必须显式地将stdout发送给tee second命令到stderr。


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

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

    第1部分:包装

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

    第2部分:realscript.sh

    1
    2
    #!/bin/sh
    echo 'Output'

    结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    ~: 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
    ~: