У меня есть сценарий, который будут запускать в интерактивном режиме нетехнические пользователи. Сценарий записывает обновления статуса в STDOUT, чтобы пользователь мог быть уверен, что сценарий работает нормально.
Я хочу, чтобы и STDOUT, и STDERR были перенаправлены на терминал (чтобы пользователь мог видеть, что скрипт работает, а также видеть, возникла ли проблема). Я также хочу, чтобы оба потока были перенаправлены в файл журнала.
Я видел кучу решений в сети. Некоторые не работают, а другие ужасно сложны. Я разработал работоспособное решение (которое я введу в качестве ответа), но оно неуклюже.
Идеальным решением была бы одна строка кода, которую можно было бы включить в начало любого сценария, который отправляет оба потока как на терминал, так и в файл журнала.
Обновлено: Перенаправление STDERR в STDOUT и передача результата в tee работает, но это зависит от того, не забывают ли пользователи перенаправить и передать вывод по конвейеру. Я хочу, чтобы ведение журнала было надежным и автоматическим (вот почему я хотел бы иметь возможность встроить решение в сам скрипт).





Используйте «тройник» для перенаправления в файл и на экран. В зависимости от используемой оболочки вам сначала нужно перенаправить stderr на stdout, используя
./a.out 2>&1 | tee output
или же
./a.out |& tee output
В csh есть встроенная команда под названием «скрипт», которая фиксирует все, что попадает на экран, в файл. Вы запускаете его, набирая «скрипт», затем выполняя все, что вы хотите захватить, затем нажимаете Ctrl-D, чтобы закрыть файл скрипта. Я не знаю эквивалента для sh / bash / ksh.
Кроме того, поскольку вы указали, что это ваши собственные сценарии sh, которые вы можете изменять, вы можете выполнить внутреннее перенаправление, заключив весь сценарий в фигурные скобки или скобки, например
#!/bin/sh
{
... whatever you had in your script before
} 2>&1 | tee output.file
Я не знал, что вы можете заключать команды в скобки в сценариях оболочки. Интересно.
Я также ценю ярлык Bracket! По какой-то причине 2>&1 | tee -a filename не сохранял stderr в файл из моего скрипта, но он работал нормально, когда я скопировал команду и вставил ее в терминал! Однако трюк с скобками работает нормально.
Обратите внимание, что различие между stdout и stderr будет потеряно, поскольку tee выводит все на stdout.
К вашему сведению: команда 'script' доступна в большинстве дистрибутивов (это часть пакета util-linux)
@Flimm, есть ли способ (любым другим способом) сохранить различие между stdout и stderr?
Также работает в Powershell. Это *>&1 | tee filename для перенаправления всех.
@Gabriel, я считаю, что это резервирует различие, о котором вы говорите: stackoverflow.com/a/53051506/1054322
@MatrixManAtYrService делает это за счет нестабильных проблем с синхронизацией; см. добавленный мною комментарий. Я не знаю, можно ли надежно разделить stdout и stderr, сохраняя при этом правильную синхронизацию; Я подозреваю, что это невозможно.
@PaulTomlin Я просмотрел историю изменений вопроса и обнаружил, что с самого начала действительно ясно, что то, что было нужно, было способом перенаправить весь вывод скрипта. (Редакция подчеркивает это, но не меняет его.) Вы даете отличный ответ в конце, но я почти пропустил его, потому что вы начали с ответа на другой вопрос. Я бы хотел проголосовать за это, но не могу из-за этого.
@DonHatch Учитывая, что я ответил на этот вопрос, и он был принят в качестве ответа 11 лет назад, я не собираюсь терять из-за этого сон. Кроме того, вы неправильно написали мое имя.
@PaulTomblin Надеюсь, вы не возражаете против того, что я удалил слово «сейчас» из вашего ответа, так что ваш ответ больше не подразумевает, что OP изменил значение их вопроса, что (в моем восприятии) неправильно помещало их в отрицательный свет. Я не ищу здесь войны редактирования, но я, являюсь, заинтересован в улучшении вашего ответа, сделав его более уважительным по отношению к OP.
Лично я считаю, что единственное проявленное здесь неуважение - это «улучшить» ответ и в то же время отрицать его.
@PaulTomblin Я снял свой голос против. Я не буду давать вашему ответу положительную оценку, поскольку проблема, которую я описал ранее, не была устранена. Я не уверен, что делать с тем фактом, что вы воспринимаете от меня неуважение; ничто не было предназначено, во всем, что я сказал или сделал здесь.
Кто-нибудь смог заставить это работать в GNU Make? Я использую GNU Make 4.3 в Windows 10, и пока я делаю тройник, показанный Полом Томблином, прямо в CMD, он отлично работает, когда я пытаюсь передать его в функцию оболочки GNU Make (gnu.org/software/make/manual/html_node/…), он вылетает. Я добавил некоторые отладочные данные в Make и обнаружил, что GNU Make каким-то образом интерпретирует вывод команды перед конвейером как другую команду, которую он должен выполнить, что, конечно же, терпит неудачу.
@antred, который будет полностью зависеть от того, какую оболочку вызывает GNU Make. Это баш?
@PaulTomblin Оказывается, проблема заключалась в неправильном использовании мной функции оболочки GNU Make. В том месте, где я ее использовал, функция оболочки была ненужной (и, видимо, даже вызывала проблемы). Запуск рецепта напрямую, без функции оболочки, работал нормально. P.S. И оболочка, которую я использовал, была стандартной оболочкой cmd в Windows.
Если вам нужно сохранить статус выхода скрипта, вы можете объединить это с ответом Джейсона Сидса / MatrixManAtYrService, чтобы стать: { ... } 1> >(tee -a output.file) 2>&1. Порядок вывода содержащихся скриптов будет сохранен, но запись в output.file будет немного задерживаться.
Используйте программу tee и дублируйте stderr в stdout.
program 2>&1 | tee > logfile
Я создал сценарий под названием «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 ...
Это работает, но требует, чтобы скрипты приложения запускались через внешний скрипт. Это немного неуклюже.
Вы потеряете группировку аргументов, содержащих пробелы с $ 1 $ 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
Обратите внимание, что различие между stdout и stderr будет потеряно, поскольку tee выводит все на stdout.
Используйте команду 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
~:
Год спустя вот старый 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
Приближается полдесятилетия спустя ...
Я считаю, что это «идеальное решение», к которому стремится ОП.
Вот один лайнер, который вы можете добавить в начало вашего сценария 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 в файле журнала. Исправлено примечанием от здесь.
Обратите внимание, что различие между stdout и stderr будет потеряно, поскольку tee выводит все на stdout.
@Flimm stderr может быть перенаправлен на другой процесс tee, который снова может быть перенаправлен на stderr.
@Flimm, я написал здесь предложение Ярно: stackoverflow.com/a/53051506/1054322
Это решение, как и большинство других решений, предложенных до сих пор, подвержено гонкам. То есть, когда текущий сценарий завершается и возвращается либо к приглашению пользователя, либо к некоторому вызывающему сценарию более высокого уровня, тройник, который работает в фоновом режиме, все еще будет работать и может выдавать последние несколько строк на экран и файл журнала поздно (то есть на экран после приглашения и в файл журнала после того, как ожидается, что файл журнала будет завершен).
Мне нравится это решение. Но это влияет на сценарий с помощью apt-get (он не может подключиться к серверам или apt-get early не завершает обновление до второго apt-get). Я не знаю почему. (Похоже, что происходит состояние гонки.) Все, что я знаю, это то, что когда я закомментировал строку, скрипт работает стабильно. Это не только это решение, но и другие, с тройником или без него. Итак, все сводится к следующему: некоторые команды чувствительны к этому. Не знаю почему.
the_cmd 1> >(tee stdout.txt ) 2> >(tee stderr.txt >&2 )
Это перенаправляет и stdout, и stderr отдельно, и отправляет отдельный копии stdout и stderr вызывающей стороне (которая может быть вашим терминалом).
В zsh он не перейдет к следующему оператору, пока не закончат работу tee.
В 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
Вот как это работает:
tee запускаются, их stdins назначаются файловым дескрипторам. Поскольку они заключены в замены процесса, пути к этим файловым дескрипторам подставляются в вызывающую команду, поэтому теперь это выглядит примерно так:the_cmd 1> /proc/self/fd/13 2> /proc/self/fd/14
the_cmd запускается, записывая stdout в первый файловый дескриптор и stderr во второй.
В случае bash, как только the_cmd завершает работу, немедленно выполняется следующий оператор (если ваш терминал является вызывающим, вы увидите, что появляется ваше приглашение).
В случае zsh после завершения the_cmd оболочка ожидает завершения обоих процессов tee, прежде чем продолжить. Подробнее об этом здесь.
Первый процесс tee, который читает из стандартного вывода the_cmd, записывает копию этого стандартного вывода обратно вызывающей стороне, потому что это то, что делает tee. Его выходы не перенаправляются, поэтому они возвращаются к вызывающему без изменений.
Второй процесс tee перенаправляет свой stdout на stderr вызывающего абонента (что хорошо, потому что стандартный ввод читается из стандартного потока the_cmd). Поэтому, когда он записывает в свой стандартный вывод, эти биты переходят в стандартный поток вызывающего объекта.
Это сохраняет stderr отдельно от stdout как в файлах, так и в выводе команды.
Если первый tee записывает какие-либо ошибки, они будут отображаться как в файле stderr, так и в stderr команды, если второй tee записывает какие-либо ошибки, они будут отображаться только в stderr терминала.
Это выглядит действительно полезным, и я хочу этого. Однако я не уверен, как воспроизвести использование скобок (как показано в первой строке) в пакетном скрипте Windows. (tee доступен в рассматриваемой системе.) Я получаю сообщение об ошибке: «Процесс не может получить доступ к файлу, потому что он используется другим процессом».
Это решение, как и большинство других решений, предложенных до сих пор, подвержено гонкам. То есть, когда текущий сценарий завершается и возвращается либо к приглашению пользователя, либо к некоторому вызывающему сценарию более высокого уровня, тройник, который работает в фоновом режиме, все еще будет работать и может выдавать последние несколько строк на экран и файл журнала поздно (то есть на экран после приглашения и в файл журнала после того, как ожидается, что файл журнала будет завершен).
@DonHatch Можете ли вы предложить решение этой проблемы?
Я также был бы заинтересован в тестовом примере, который делает гонку очевидной. Не то чтобы я сомневался, но трудно попытаться избежать этого, потому что я не видел, чтобы это произошло.
@pylipp У меня нет решения. Я был бы заинтересован в очень.
@MatrixManAtYrService Для демонстрации в интерактивной оболочке bash: { ( echo "to stdout"; echo "to stderr" >&2 ) > >(sleep 1; tee stdout.txt); } 2> >(sleep 2; tee stderr.txt >&2 ). Приглашение оболочки возвращается немедленно; "to stdout" через 1 секунду, "to stderr" через 2 секунды. В правильном решении подсказка не появится до тех пор, пока не появится «to stderr».
@DonHatch Спасибо за тест. Мне интересно, что он проходит с этим ответом в zsh, но не работает в bash. Как бы то ни было, я предпринял еще одну попытку: gist.github.com/mattrixman/e796abd41a086f2990926b27bea5c4ce Как вы думаете, это решит проблему гонки? Если это так, я могу опубликовать его как отдельный ответ (поскольку он работает в случае zsh).
Добрая душа в обмене стеками unix / linux помогла мне понять разницу в zsh: unix.stackexchange.com/a/558315/146169
@MatrixManAtYrService Я посмотрел на вашу суть, со стадами, которые сообщают от процессов тройника, что они выполнены. Это умно. Вроде работает, и я не вижу дыр в логике. Хотя это действительно кажется очень неудобным. Возможно, это можно рассматривать как доказательство существования, пока мы продолжаем думать о том, есть ли рецепт лучше.
@pylipp, думаю, я наконец нашел хорошее решение. См. Ответ, который я только что опубликовал.
@DonHatch Спасибо за отзыв. Я согласен, что это неудобно. Я могу повозиться, сколько из этой возни можно спрятать под ковриком в моем .bashrc. Обычно я защитник bash и ему подобных, но это не должно быть так сложно ...
Обновлено: Я вижу, что сошел с рельсов и в итоге ответил на вопрос, отличный от заданного. Ответ на настоящий вопрос находится в конце ответа Пола Томблина. (Если вы по какой-то причине хотите улучшить это решение для перенаправления 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:
2>&3- не распознается zsh; это должно быть переписано
как 2>&3 3>&-Так, например, мое второе решение нужно переписать для 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, но ужасно многословно).
С другой стороны, вы можете воспользоваться таинственным встроенным неявным teeing в zsh, чтобы получить гораздо более короткое решение для zsh, которое вообще не запускает tee:
my_command >&1 >stdout.txt 2>&2 2>stderr.txt
(Я бы не догадался из документов, которые я обнаружил, что >&1 и 2>&2 - это то, что запускает неявную игру zsh; я обнаружил это методом проб и ошибок.)
Я поигрался с этим в bash, и он хорошо работает. Просто предупреждение для пользователей zsh с привычкой предполагать совместимость (как и я), там он ведет себя по-другому: gist.github.com/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 Я не нахожу. Вы можете задать это как отдельный вопрос (возможно, с более простым конвейером).
Вот решение, работающее для bash путем перенаправления, путем объединения решения «kvantour, MatrixManAtYrService» и «Jason Sydes»:
#!/bin/bash
exec 1> >(tee x.log) 2> >(tee x.err >&2)
echo "test for log"
echo "test for err" 1>&2
Сохраните приведенный выше сценарий как x.sh. После запуска ./x.sh x.log включает только stdout, а x.err включает только stderr.
Для других читателей: аналогичный вопрос: stackoverflow.com/questions/692000/…