Як отримати як STDOUT, так і STDERR, щоб перейти до терміналу та файлу журналу?


104

У мене є сценарій, який інтерактивно запускатимуть нетехнічні користувачі. Сценарій пише оновлення стану в STDOUT, щоб користувач міг бути впевненим, що сценарій працює нормально.

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

Я бачив купу рішень у мережі. Деякі не працюють, а інші страшенно складні. Я розробив працездатне рішення (яке я вкажу як відповідь), але воно є важким.

Ідеальним рішенням буде один рядок коду, який може бути включений на початок будь-якого сценарію, який надсилає обидва потоки як до терміналу, так і до журналу.

EDIT: перенаправлення STDERR на STDOUT і передача результату на трійник працює, але це залежить від того, як користувачі пам’ятають про перенаправлення та передачу виводу. Я хочу, щоб журналювання було надійним та автоматичним (саме тому я хотів би мати можливість вбудувати рішення в сам сценарій).


Для інших читачів: подібне питання: stackoverflow.com/questions/692000 / ...
pevik

1
Мене дратує, що всіх (включаючи мене!), Крім @JasonSydes, зігнали з рейок і відповіли на інше запитання. І відповідь Джейсона є ненадійною, як я вже коментував. Я хотів би побачити справжню надійну відповідь на запитання, яке ви задали (і наголосили у вашому EDIT).
Дон Хетч

О, почекай, я беру це назад. Прийнята відповідь @PaulTromblin відповідає на неї. Я не читав цього досить далеко.
Дон Хетч,

Відповіді:


167

Використовуйте "трійник" для переспрямування на файл та екран. Залежно від оболонки, яку ви використовуєте, спочатку потрібно перенаправити stderr на stdout за допомогою

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

або

./a.out |& tee output

У csh є вбудована команда під назвою "скрипт", яка буде фіксувати все, що надходить на екран, у файл. Ви починаєте його, набираючи "script", потім робите все, що хочете, і натискаєте control-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 буде втрачена, оскільки трійник друкує все на stdout.
Flimm

2
FYI: Команда '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 буде втрачена, оскільки трійник друкує все на stdout.
Флім

@Flimm stderr може бути перенаправлений на інший процес трійника, який знову може бути перенаправлений на stderr.
jarno

@Flimm, я написав тут пропозицію jarno: 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). Отже, коли він пише на свій stdout, ці біти надходять на stderr абонента.

Це утримує stderr окремо від stdout як у файлах, так і в результатах команди.

Якщо перший трійник пише будь-які помилки, вони відображатимуться як у файлі stderr, так і в команді stderr, якщо другий трійник пише помилки, вони відображатимуться лише в терміналі stderr.


Це виглядає дійсно корисно і те, що я хочу. Однак я не впевнений, як повторити використання дужок (як показано в першому рядку) у пакетному сценарії Windows. ( teeдоступний у відповідній системі.) Я отримую помилку: "Процес не може отримати доступ до файлу, оскільки він використовується іншим процесом."
Agi Hammerthief

Це рішення, як і більшість інших запропонованих дотепер рішень, схильне до перегонів. Тобто, коли поточний скрипт завершується і повертається, або до запиту користувача, або до якогось сценарію виклику вищого рівня, трійник, який працює у фоновому режимі, все одно буде працювати, і може видавати кілька останніх рядків на екран і до файл журналу із запізненням (тобто на екран після підказки та файл журналу після завершення журналу).
Дон Хетч

2
@DonHatch Чи можете ви запропонувати рішення, яке доповнює це питання?
pylipp

Мене також зацікавив тест, який робить гонку очевидною. Це не те, що я сумніваюся, але важко спробувати уникнути цього, бо я не бачив, щоб це сталося.
MatrixManAtYrService

@pylipp У мене немає рішення. Мене б дуже зацікавив один.
Дон Хетч,

4

to redirect stderr to 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
Зверніть увагу, що різниця між stdout та stderr буде втрачена, оскільки трійник друкує все на stdout.
Флім

4

РЕДАГУВАТИ: Я бачу, що мене зіпсували з рейок, і в підсумку я відповів на запитання, відмінне від поставленого. Відповідь на справжнє питання знаходиться внизу відповіді Пола Томбліна. (Якщо ви хочете вдосконалити це рішення для перенаправлення 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 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, але жахливо багатослівне).

З іншого боку, ви можете скористатися таємничим вбудованим неявним трійником zsh, щоб отримати набагато коротше рішення для zsh, яке взагалі не запускає трійник:

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

(Я б не здогадався з документів, які я виявив, що >&1і 2>&2є тим, що викликає неявний взаємозв'язок zsh; я виявив це методом спроб і помилок.)


Я погрався з цим у bash, і це працює добре. Просто попередження для користувачів zsh, які мають звичку припускати сумісність (як і я), там він поводиться інакше: 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 script)

Створіть оболонку оболонки (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

Я створив сценарій під назвою "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

Через рік, ось старий скрипт 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.