STDOUT과 STDERR 모두 터미널과 로그 파일로 이동하려면 어떻게해야합니까?


105

비 기술적 인 사용자가 대화식으로 실행할 스크립트가 있습니다. 스크립트는 사용자가 스크립트가 정상적으로 실행되고 있는지 확인할 수 있도록 상태 업데이트를 STDOUT에 기록합니다.

STDOUT과 STDERR 모두 터미널로 리디렉션되기를 원합니다 (사용자가 스크립트가 작동하고 문제가 있는지 확인할 수 있도록). 또한 두 스트림 모두 로그 파일로 리디렉션되기를 원합니다.

인터넷에서 많은 솔루션을 보았습니다. 일부는 작동하지 않고 다른 일부는 끔찍하게 복잡합니다. 나는 실행 가능한 솔루션을 개발했지만 (답으로 입력 할 것입니다), 어리 석습니다.

완벽한 솔루션은 터미널과 로그 파일 모두에 두 스트림을 모두 보내는 스크립트의 시작 부분에 통합 될 수있는 코드 한 줄입니다.

편집 : STDERR을 STDOUT으로 리디렉션하고 결과를 티에 파이프 작업하지만 출력을 리디렉션하고 파이프하는 것을 기억하는 사용자에 따라 다릅니다. 로깅이 안전하고 자동으로 이루어지기를 원합니다 (그래서 스크립트 자체에 솔루션을 포함 할 수 있기를 원합니다.)


: 비슷한 질문 : 다른 독자들에 대한 stackoverflow.com/questions/692000/...
— pevik

1
@JasonSydes를 제외한 모든 사람 (나를 포함하여!)이 탈선하여 다른 질문에 답한 것에 짜증이납니다. 그리고 Jason의 대답은 제가 언급했듯이 신뢰할 수 없습니다. 나는 당신이 물어 본 (그리고 당신의 EDIT에서 강조한) 질문에 대한 진정한 대답을보고 싶습니다.
— Don Hatch

아, 잠시만 요. @PaulTromblin의 수락 된 답변이 대답합니다. 나는 그것에 대해 충분히 읽지 않았습니다.
— Don Hatch

답변:


169

"tee"를 사용하여 파일과 화면으로 리디렉션합니다. 사용하는 셸에 따라 먼저 다음을 사용하여 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

4
쉘 스크립트에서 명령을 괄호로 묶을 수 있다는 것을 몰랐습니다. 흥미 롭군.
— Jamie

1
브래킷 바로 가기도 감사합니다! 어떤 이유로 2>&1 | tee -a filenamestderr를 내 스크립트의 파일에 저장하지 않았지만 명령을 복사하여 터미널에 붙여 넣으면 제대로 작동했습니다! 그래도 브래킷 트릭은 잘 작동합니다.
— Ed Brannin

8
tee가 모든 것을 stdout에 인쇄하므로 stdout과 stderr의 구별이 사라집니다.
— Flimm 2015 년

2
참고 : 'script'명령은 대부분의 배포판에서 사용할 수 있습니다 (util-linux 패키지의 일부)
— SamWN

2
@Flimm, stdout과 stderr의 구별을 유지하는 방법 (다른 방법)이 있습니까?
— Gabriel

20

50 년이 지난 후 ...

나는 이것이 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

(참고 : 이것은 단지 강타와 함께 작동 그것은 것이다. 하지 / bin / sh 작동 .)

여기 에서 적응 ; 원본은 내가 알 수 있듯이 로그 파일에서 STDERR을 포착하지 못했습니다. 여기 에서 메모로 수정 되었습니다 .


3
tee가 모든 것을 stdout에 인쇄하므로 stdout과 stderr의 구별이 사라집니다.
— Flimm 2015 년

@Flimm stderr은 다시 stderr로 리디렉션 될 수있는 다른 tee 프로세스로 리디렉션 될 수 있습니다.
— jarno

@Flimm, 여기에 jarno의 제안을 썼습니다. stackoverflow.com/a/53051506/1054322
— MatrixManAtYrService

1
이 솔루션은 지금까지 제안 된 대부분의 다른 솔루션과 마찬가지로 경쟁에 취약합니다. 즉, 현재 스크립트가 완료되고 사용자의 프롬프트 또는 일부 상위 수준의 호출 스크립트로 돌아 오면 백그라운드에서 실행중인 티가 계속 실행되고 화면에 마지막 몇 줄을 방출 할 수 있습니다. 로그 파일 늦음 (즉, 프롬프트 후 화면 및 로그 파일이 완료 될 것으로 예상 된 후 로그 파일).
— Don Hatch

1
그러나 이것은 실제로 질문을 해결하는 유일한 답변입니다!
— Don Hatch

9

패턴

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

이 표준 에러 별도로 표준 출력 모두를 리디렉션, 그리고 전송 분리 (터미널이 될 수 있음) 호출자에게 표준 출력 및 표준 오류의 사본을.

  • zsh에서는 tees가 완료 될 때까지 다음 문으로 진행되지 않습니다 .

  • 배쉬에서는 출력의 마지막 몇 줄이 나타나지 않을 수 있습니다 후 다음에 오는 어떤 문.

두 경우 모두 올바른 비트가 올바른 위치로 이동합니다.


설명

다음은 스크립트 (./example에 저장 됨)입니다.

#! /usr/bin/env bash
the_cmd()
{
    echo out;
    1>&2 echo err;
}

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

세션은 다음과 같습니다.

$ foo=$(./example)
    err

$ echo $foo
    out

$ cat stdout.txt
    out

$ cat stderr.txt
    err

작동 방식은 다음과 같습니다.

  1. 두 tee프로세스가 모두 시작되고 해당 stdin이 파일 설명자에 할당됩니다. 프로세스 대체로 묶여 있기 때문에 해당 파일 설명자에 대한 경로는 호출 명령에서 대체되므로 이제 다음과 같이 보입니다.

the_cmd 1> /proc/self/fd/13 2> /proc/self/fd/14

  1. the_cmd 실행되고 stdout을 첫 번째 파일 설명자에 쓰고 stderr을 두 번째 파일 설명자에 씁니다.

  2. bash의 경우 the_cmd완료되면 다음 명령문이 즉시 발생합니다 (터미널이 호출자 인 경우 프롬프트가 표시됨).

  3. zsh의 경우 the_cmd완료되면 셸은 tee계속 진행하기 전에 두 프로세스가 모두 완료 될 때까지 기다립니다 . 여기 에 대한 자세한 내용 .

  4. 의 stdout tee에서 읽는 첫 번째 프로세스 the_cmd는 해당 stdout의 복사본을 호출자에게 다시 씁니다 tee. 출력은 리디렉션되지 않으므로 변경되지 않은 상태로 호출자에게 다시 보냅니다.

  5. 두 번째 tee프로세스는 stdout호출자에게 리디렉션됩니다 stderr(stdin이 the_cmd's stderr 에서 읽고 있기 때문에 좋습니다 ). 따라서 stdout에 쓸 때 해당 비트는 호출자의 stderr로 이동합니다.

이렇게하면 파일과 명령 출력 모두에서 stderr을 stdout과 별도로 유지합니다.

첫 번째 티가 오류를 작성하면 stderr 파일과 명령의 stderr에 모두 표시되고 두 번째 티가 오류를 작성하면 터미널의 stderr에만 표시됩니다.


이것은 정말 유용하고 내가 원하는 것 같습니다. 그래도 Windows 배치 스크립트에서 대괄호 사용 (첫 번째 줄 참조)을 복제하는 방법을 모르겠습니다. ( tee문제의 시스템에서 사용할 수 있습니다.) "다른 프로세스에서 파일을 사용하고 있기 때문에 프로세스가 파일에 액세스 할 수 없습니다."라는 오류가 표시됩니다.
— Agi Hammerthief

이 솔루션은 지금까지 제안 된 대부분의 다른 솔루션과 마찬가지로 경쟁에 취약합니다. 즉, 현재 스크립트가 완료되고 사용자의 프롬프트 또는 일부 상위 수준의 호출 스크립트로 돌아 오면 백그라운드에서 실행중인 티가 계속 실행되고 화면에 마지막 몇 줄을 방출 할 수 있습니다. 로그 파일 늦음 (즉, 프롬프트 후 화면 및 로그 파일이 완료 될 것으로 예상 된 후 로그 파일).
— Don Hatch

2
@DonHatch이 문제를 수정하는 솔루션을 제안 할 수 있습니까?
— pylipp

나는 또한 인종을 분명하게 만드는 테스트 케이스에 관심이 있습니다. 의심스러워서가 아니라 그런 일이 일어나지 않았기 때문에 피하려고 시도하는 것은 어렵습니다.
— MatrixManAtYrService

@pylipp 해결책이 없습니다. 나는 하나에 매우 관심 이 있습니다 .
— Don Hatch

4

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

2
tee가 모든 것을 stdout에 인쇄하므로 stdout과 stderr의 구별이 사라집니다.
— Flimm 2015 년

4

편집 : 탈선하여 요청한 질문과 다른 질문에 답하게 된 것을 봅니다. 실제 질문에 대한 답변은 Paul Tomblin의 답변 하단에 있습니다. (어떤 이유로 stdout과 stderr를 별도로 리디렉션하도록 해당 솔루션을 향상 시키려면 여기에서 설명하는 기술을 사용할 수 있습니다.)


나는 stdout과 stderr의 구별을 유지하는 답변을 원했습니다. 불행히도 지금까지 그 구별을 유지하는 모든 답변은 인종에 취약합니다. 제가 코멘트에서 지적했듯이 프로그램이 불완전한 입력을 보는 위험이 있습니다.

나는 마침내 구별을 유지하고 인종에 취약하지 않고 끔찍하게 어리석지 않은 답을 찾았다 고 생각합니다.

첫 번째 빌딩 블록 : stdout과 stderr를 바꾸려면 :

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

두 번째 빌딩 블록 : 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에서 작동하지만 (확실하지 않은 다른 셸도있을 수 있음) zsh에서는 작동하지 않습니다. zsh에서 실패하는 이유는 두 가지입니다.

  1. 구문 2>&3-은 zsh에서 이해되지 않습니다. 다시 작성해야합니다.2>&3 3>&-
  2. zsh에서 (다른 셸과 달리) 이미 열려있는 파일 설명자를 리디렉션하면 경우에 따라 (결정 방법을 완전히 이해하지 못함) 대신 내장 티와 같은 동작을 수행합니다. 이를 방지하려면 리디렉션하기 전에 각 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의 신비한 내장 암시 적 티잉을 활용하여 티를 전혀 실행하지 않는 zsh에 대한 훨씬 더 짧은 솔루션을 얻을 수 있습니다.

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

(필자는 문서에서 짐작하지 않았을 나는 것을 발견 >&1하고 2>&2트리거 zsh을의 암시 티잉하는 것입니다,. 내가 시행 착오에 의한 것을 발견)


나는 bash에서 이것을 가지고 놀았고 잘 작동합니다. (나와 같이) 호환성을 가정하는 습관을 가진 zsh 사용자에 대한 경고 일뿐입니다
— MatrixManAtYrService

@MatrixManAtYrService 나는 zsh 상황에 대한 핸들을 얻었으며 zsh에 훨씬 더 깔끔한 솔루션이 있음이 밝혀졌습니다. 내 편집 "zsh에 대한 각주"를 참조하십시오.
— Don Hatch

솔루션을 자세히 설명해 주셔서 감사합니다. my_function중첩 된 stdout / stderr 필터링에서 함수 ( )를 사용할 때 반환 코드를 검색하는 방법도 알고 있습니까? 내가 { { my_function || touch failed;} 3>&1 1>&2 2>&3- | stderr_filter;} 3>&1 1>&2 2>&3- | stdout_filter했지만 실패의 표시기로 파일을 만드는 것이 이상하게 느껴집니다 ...
— pylipp

@pylipp 나는 손을 대지 않는다. 별도의 질문으로 질문 할 수 있습니다 (아마도 간단한 파이프 라인 사용).
— Don Hatch

2

사용 script스크립트 명령을 (man 1 스크립트).

script ()를 설정하고 exit를 호출하는 래퍼 셸 스크립트 (2 줄)를 만듭니다.

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
~:

1

tee 프로그램을 사용하고 stderr을 stdout으로 복제하십시오.

 program 2>&1 | tee > logfile

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

이것은 작동하지만 외부 스크립트를 통해 실행되는 응용 프로그램의 스크립트가 필요합니다. 약간 어색합니다.


9
$ 1 $ 2 $ 3 ...으로 공백을 포함하는 인수 그룹을 잃게됩니다. (따옴표 포함) : "$ @"
— NVRAM

1

1 년 후, 여기에 무엇이든 로깅하기위한 오래된 bash 스크립트가 있습니다. 예를 들어,
teelog make ...생성 된 로그 이름에 로그를 기록합니다 (중첩 된 makes 를 기록하는 방법도 참조 ).

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

댓글을 달기 엔 너무 늦었지만이 대본에 감사 드려야 했어요. 매우 유용하고 잘 문서화되었습니다!
— stephenmm

감사합니다 @stephenmm; 그것은 없다 결코 "유용"또는 "개선 될 수있다"말을 너무 늦게.
— denis
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.