Как заставить оба STDOUT и STDERR перейти к терминалу и файлу журнала?

105

У меня есть сценарий, который будут запускать в интерактивном режиме нетехнические пользователи. Сценарий записывает обновления статуса в STDOUT, чтобы пользователь мог быть уверен, что сценарий работает нормально.

Я хочу, чтобы и STDOUT, и STDERR были перенаправлены на терминал (чтобы пользователь мог видеть, что сценарий работает, а также видеть, возникла ли проблема). Я также хочу, чтобы оба потока были перенаправлены в файл журнала.

Я видел кучу решений в сети. Некоторые не работают, а другие ужасно сложны. Я разработал работоспособное решение (которое я введу в качестве ответа), но оно неуклюже.

Идеальным решением была бы одна строка кода, которую можно было бы включить в начало любого скрипта, который отправляет оба потока как на терминал, так и в файл журнала.

РЕДАКТИРОВАТЬ: перенаправление STDERR в STDOUT и передача результата в tee работает, но это зависит от пользователей, которые не забывают перенаправить и передать вывод. Я хочу, чтобы ведение журнала было надежным и автоматическим (поэтому я хотел бы иметь возможность встроить решение в сам скрипт).

JPLemme
источник
Для других читателей: аналогичный вопрос: stackoverflow.com/questions/692000/…
pevik 09
1
Меня раздражает, что все (включая меня!), Кроме @JasonSydes, сошли с рельсов и ответили на другой вопрос. И ответ Джейсона ненадежен, как я прокомментировал. Я хотел бы увидеть настоящий надежный ответ на вопрос, который вы задали (и подчеркнули в своем РЕДАКТИРОВАНИИ).
Дон Хэтч
Ой, подождите, я заберу это обратно. Принятый ответ @PaulTromblin отвечает на него. Я не вникал в это достаточно глубоко.
Дон Хэтч

Ответы:

169

Используйте «тройник» для перенаправления в файл и на экран. В зависимости от используемой оболочки вам сначала нужно перенаправить stderr на stdout, используя

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

или

./a.out |& tee output

В csh есть встроенная команда под названием «скрипт», которая фиксирует все, что попадает на экран, в файл. Вы запускаете его, набирая «script», затем выполняя все, что хотите захватить, затем нажимаете Ctrl-D, чтобы закрыть файл сценария. Я не знаю эквивалента для sh / bash / ksh.

Кроме того, поскольку вы указали, что это ваши собственные сценарии sh, которые вы можете изменять, вы можете выполнить внутреннее перенаправление, заключив весь сценарий в фигурные скобки или скобки, например

  #!/bin/sh
  {
    ... whatever you had in your script before
  } 2>&1 | tee output.file
Пол Томблин
источник
4
Я не знал, что вы можете заключать команды в скобки в сценариях оболочки. Интересный.
Джейми
1
Я также ценю ярлык Bracket! По какой-то причине я 2>&1 | tee -a filenameне сохранял stderr в файл из моего скрипта, но он работал нормально, когда я скопировал команду и вставил ее в терминал! Однако трюк с скобками работает нормально.
Эд Браннин,
8
Обратите внимание, что различие между stdout и stderr будет потеряно, поскольку tee печатает все в stdout.
Flimm
2
К вашему сведению: команда 'script' доступна в большинстве дистрибутивов (она часть пакета util-linux)
SamWN
2
@Flimm, есть ли способ (любым другим способом) сохранить различие между stdout и stderr?
Габриэль
20

Приближается полдесятилетия спустя ...

Я считаю, что это «идеальное решение», к которому стремится ОП.

Вот один лайнер, который вы можете добавить в начало вашего сценария 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.)

Адаптировано отсюда ; оригинал, насколько я могу судить, не улавливал STDERR в файле журнала. Исправлено примечанием отсюда .

Джейсон Сидс
источник
3
Обратите внимание, что различие между stdout и stderr будет потеряно, поскольку tee печатает все в stdout.
Flimm
@Flimm stderr может быть перенаправлен на другой процесс tee, который снова может быть перенаправлен на stderr.
Ярно
@Flimm, я написал здесь предложение Ярно: stackoverflow.com/a/53051506/1054322
MatrixManAtYrService
1
Это решение, как и большинство других предложенных на данный момент решений, подвержено гонкам. То есть, когда текущий сценарий завершается и возвращается либо к приглашению пользователя, либо к некоторому вызывающему сценарию более высокого уровня, тройник, который работает в фоновом режиме, все еще будет работать и может выдавать последние несколько строк на экран и файл журнала задерживается (то есть на экран после приглашения и в файл журнала после того, как ожидается, что файл журнала будет завершен).
Дон Хэтч
1
Однако это единственный предложенный на данный момент ответ, который действительно решает вопрос!
Дон Хэтч
9

Шаблон

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

Это перенаправляет как stdout, так и stderr отдельно, и отправляет отдельные копии stdout и stderr вызывающей стороне (которая может быть вашим терминалом).

  • В zsh он не перейдет к следующему оператору, пока tees не завершится.

  • В bash вы можете обнаружить, что последние несколько строк вывода появляются после любого следующего оператора.

В любом случае правильные биты попадают в нужные места.


Объяснение

Вот сценарий (хранится в ./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процесса запускаются, их stdins назначаются файловым дескрипторам. Поскольку они заключены в подстановки процессов , пути к этим файловым дескрипторам подставляются в вызывающую команду, поэтому теперь это выглядит примерно так:

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. Первый teeпроцесс, который читает из the_cmdstdout, записывает копию этого stdout обратно вызывающей стороне, потому что это то, что teeделает. Его выходы не перенаправляются, поэтому они возвращаются к вызывающему без изменений.

  5. Второй teeпроцесс stdoutперенаправляет его вызывающему stderr(что хорошо, потому что stdin читает из the_cmdstderr). Поэтому, когда он записывает в свой стандартный вывод, эти биты переходят в стандартный поток вызывающего.

Это сохраняет stderr отдельно от stdout как в файлах, так и в выводе команды.

Если первый tee записывает какие-либо ошибки, они будут отображаться как в файле stderr, так и в stderr команды, если второй tee записывает какие-либо ошибки, они будут отображаться только в stderr терминала.

MatrixManAtYrService
источник
Это выглядит действительно полезным, и я хочу этого. Однако я не уверен, как воспроизвести использование скобок (как показано в первой строке) в пакетном скрипте Windows. ( teeдоступен в рассматриваемой системе.) Я получаю сообщение об ошибке: «Процесс не может получить доступ к файлу, потому что он используется другим процессом».
Agi
Это решение, как и большинство других предлагаемых до сих пор решений, подвержено гонкам. То есть, когда текущий сценарий завершается и возвращается либо к приглашению пользователя, либо к некоторому вызывающему сценарию более высокого уровня, тройник, который работает в фоновом режиме, все еще будет работать и может выводить последние несколько строк на экран и файл журнала задерживается (то есть, на экран после приглашения и в файл журнала после того, как ожидается, что файл журнала будет завершен).
Дон Хэтч
2
@DonHatch Можете ли вы предложить решение этой проблемы?
pylipp
Я также был бы заинтересован в тестовом примере, который делает гонку очевидной. Не то чтобы я сомневался, но трудно попытаться избежать этого, потому что я не видел, чтобы это произошло.
MatrixManAtYrService
@pylipp У меня нет решения. Я был бы очень заинтересован в одном.
Дон Хэтч
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
flolo
источник
2
Обратите внимание, что различие между stdout и stderr будет потеряно, поскольку tee печатает все в stdout.
Flimm
4

РЕДАКТИРОВАТЬ: Я вижу, что я сошел с рельсов и в конечном итоге ответил на вопрос, отличный от заданного. Ответ на настоящий вопрос находится в конце ответа Пола Томблина. (Если вы по какой-то причине хотите улучшить это решение для перенаправления stdout и stderr отдельно, вы можете использовать метод, который я описываю здесь.)


Я давно хотел получить ответ, который сохранит различие между 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 (и, возможно, в некоторых других оболочках, я не уверен), но не в zsh. Есть две причины, по которым он не работает в zsh:

  1. синтаксис 2>&3-не понимается zsh; это должно быть переписано как2>&3 3>&-
  2. в zsh (в отличие от других оболочек), если вы перенаправляете дескриптор файла, который уже открыт, в некоторых случаях (я не совсем понимаю, как он решает) вместо этого он выполняет встроенное поведение, подобное тройнику. Чтобы избежать этого, вы должны закрыть каждый fd перед его перенаправлением.

Так, например, мое второе решение нужно переписать для zsh as {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, но ужасно многословно).

С другой стороны, вы можете воспользоваться таинственным встроенным неявным teeing в zsh, чтобы получить гораздо более короткое решение для zsh, которое вообще не запускает tee:

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

(Я бы не догадался из документов, которые я обнаружил, что >&1и 2>&2- это то, что запускает неявную игру zsh; я обнаружил это методом проб и ошибок.)

Дон Хэтч
источник
Я поигрался с этим в bash, и он хорошо работает. Просто предупреждение для пользователей ЗШ с привычкой при условии совместимости (как я), он ведет себя по- другому там: gist.github.com/MatrixManAtYrService/...
MatrixManAtYrService
@MatrixManAtYrService Я полагаю, что разобрался в ситуации с zsh, и оказалось, что в zsh есть гораздо более аккуратное решение. См. Мою правку «Сноска о zsh».
Дон Хэтч
Спасибо, что объяснили решение так подробно. Вы также знаете, как получить код возврата при использовании функции ( 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 Я не нахожу. Вы можете задать это как отдельный вопрос (возможно, с более простым конвейером).
Дон Хэтч
2

Используйте scriptкоманду в своем скрипте (скрипт man 1)

Создайте оболочку оболочки (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
~:
гнуд
источник
1

Используйте программу tee и дублируйте stderr в stdout.

 program 2>&1 | tee > logfile
tvanfosson
источник
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 ...

Это работает, но требует, чтобы скрипты приложения запускались через внешний скрипт. Это немного неуклюже.

JPLemme
источник
9
Вы потеряете группировку аргументов, содержащих пробелы, с $ 1 $ 2 $ 3 ... , вы должны использовать (без кавычек): «$ @»
NVRAM
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; это не никогда не слишком поздно , чтобы сказать «полезной» или «может быть улучшена».
денис