Làm cách nào để tôi có được cả STDOUT và STDERR để đi đến thiết bị đầu cuối và tệp nhật ký?


104

Tôi có một tập lệnh sẽ được chạy tương tác bởi người dùng không phải là kỹ thuật. Tập lệnh ghi các cập nhật trạng thái vào STDOUT để người dùng có thể chắc chắn rằng tập lệnh đang chạy OK.

Tôi muốn cả STDOUT và STDERR được chuyển hướng đến thiết bị đầu cuối (để người dùng có thể thấy rằng tập lệnh đang hoạt động cũng như xem có sự cố không). Tôi cũng muốn cả hai luồng được chuyển hướng đến một tệp nhật ký.

Tôi đã thấy một loạt các giải pháp trên mạng. Một số không hoạt động và những người khác phức tạp kinh khủng. Tôi đã phát triển một giải pháp khả thi (mà tôi sẽ nhập dưới dạng câu trả lời), nhưng nó rất khó.

Giải pháp hoàn hảo sẽ là một dòng mã duy nhất có thể được kết hợp vào phần đầu của bất kỳ tập lệnh nào gửi cả hai luồng tới cả thiết bị đầu cuối và tệp nhật ký.

CHỈNH SỬA: Chuyển hướng STDERR thành STDOUT và chuyển hướng kết quả đến tee hoạt động, nhưng nó phụ thuộc vào việc người dùng nhớ chuyển hướng và chuyển hướng đầu ra. Tôi muốn việc ghi nhật ký phải chống đánh lừa và tự động (đó là lý do tại sao tôi muốn có thể nhúng giải pháp vào chính tập lệnh.)


Đối với những người đọc khác: câu hỏi tương tự: stackoverflow.com/questions/692000/…
pevik Ngày

1
Tôi khó chịu khi tất cả mọi người (kể cả tôi!) Ngoại trừ @JasonSydes bị trật bánh và trả lời một câu hỏi khác. Và câu trả lời của Jason là không đáng tin cậy, như tôi đã nhận xét. Tôi rất muốn thấy một câu trả lời thực sự đáng tin cậy cho câu hỏi bạn đã hỏi (và được nhấn mạnh trong EDIT của bạn).
Don Hatch

Chờ đã, tôi lấy lại. Câu trả lời được chấp nhận của @PaulTromblin không trả lời nó. Tôi đã không đọc đủ xa về nó.
Don Hatch

Câu trả lời:


167

Sử dụng "tee" để chuyển hướng đến một tệp và màn hình. Tùy thuộc vào shell bạn sử dụng, trước tiên bạn phải chuyển hướng stderr sang stdout bằng cách sử dụng

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

hoặc là

./a.out |& tee output

Trong csh, có một lệnh tích hợp được gọi là "script" sẽ thu thập mọi thứ chuyển ra màn hình thành một tệp. Bạn bắt đầu nó bằng cách gõ "script", sau đó làm bất cứ điều gì bạn muốn nắm bắt, sau đó nhấn Control-D để đóng tệp script. Tôi không biết một từ tương đương cho sh / bash / ksh.

Ngoài ra, vì bạn đã chỉ ra rằng đây là những tập lệnh sh của riêng bạn mà bạn có thể sửa đổi, bạn có thể thực hiện chuyển hướng bên trong bằng cách bao quanh toàn bộ tập lệnh bằng dấu ngoặc hoặc dấu ngoặc vuông, như

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

4
Tôi không biết bạn có thể đặt dấu ngoặc lệnh trong các tập lệnh shell. Hấp dẫn.
Jamie

1
Tôi cũng đánh giá cao phím tắt Bracket! Vì một số lý do, 2>&1 | tee -a filenamekhông lưu stderr vào tệp từ tập lệnh của tôi, nhưng nó hoạt động tốt khi tôi sao chép lệnh và dán nó vào thiết bị đầu cuối! Tuy nhiên, thủ thuật dấu ngoặc hoạt động tốt.
Ed Brannin,

8
Lưu ý rằng sự phân biệt giữa stdout và stderr sẽ bị mất, vì tee in mọi thứ vào stdout.
Flimm

2
FYI: Lệnh 'script' có sẵn trong hầu hết các bản phân phối (nó là một phần của gói
use

2
@Flimm, có cách nào (bất kỳ cách nào khác) để giữ lại sự khác biệt giữa stdout và stderr không?
Gabriel

20

Khoảng nửa thập kỷ sau ...

Tôi tin rằng đây là "giải pháp hoàn hảo" được OP tìm kiếm.

Đây là một lớp lót mà bạn có thể thêm vào đầu tập lệnh Bash của mình:

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

Đây là một đoạn script nhỏ thể hiện công dụng của nó:

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

(Lưu ý: Điều này chỉ hoạt động với Bash. Nó sẽ không hoạt động với / bin / sh.)

Phỏng theo đây ; bản gốc đã không, từ những gì tôi có thể nói, bắt STDERR trong logfile. Đã sửa bằng một ghi chú từ đây .


3
Lưu ý rằng sự phân biệt giữa stdout và stderr sẽ bị mất, vì tee in mọi thứ vào stdout.
Flimm

@Flimm stderr có thể được chuyển hướng đến quy trình phát bóng khác nhau, quy trình này một lần nữa có thể được chuyển hướng đến stderr.
jarno,

@Flimm, tôi đã viết lên đề nghị Jarno ở đây: stackoverflow.com/a/53051506/1054322
MatrixManAtYrService

1
Giải pháp này, giống như hầu hết các giải pháp khác được đề xuất cho đến nay, là dễ chạy đua. Nghĩa là, khi tập lệnh hiện tại hoàn thành và trả về, đến lời nhắc của người dùng hoặc một số tập lệnh gọi cấp cao hơn, tee, đang chạy trong nền, sẽ vẫn chạy và có thể phát ra một vài dòng cuối cùng ra màn hình và logfile trễ (nghĩa là đến màn hình sau lời nhắc và đến logfile sau khi logfile dự kiến ​​hoàn thành).
Don Hatch

1
Tuy nhiên, đây là câu trả lời duy nhất được đề xuất cho đến nay thực sự giải quyết được câu hỏi!
Don Hatch

9

Hoa văn

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

Điều này chuyển hướng cả stdout và stderr một cách riêng biệt và nó gửi các bản sao riêng biệt của stdout và stderr đến người gọi (có thể là thiết bị đầu cuối của bạn).

  • Trong zsh, nó sẽ không tiếp tục câu lệnh tiếp theo cho đến khi các câu teekết thúc.

  • Trong bash, bạn có thể thấy rằng một vài dòng đầu ra cuối cùng xuất hiện sau bất kỳ câu lệnh nào tiếp theo.

Trong cả hai trường hợp, các bit phù hợp sẽ đến đúng nơi.


Giải trình

Đây là một tập lệnh (được lưu trữ trong ./example):

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

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

Đây là một phiên:

$ foo=$(./example)
    err

$ echo $foo
    out

$ cat stdout.txt
    out

$ cat stderr.txt
    err

Đây là cách nó hoạt động:

  1. Cả hai teequy trình đều được bắt đầu, các stdins của chúng được gán cho các bộ mô tả tệp. Bởi vì chúng được bao quanh trong các thay thế quy trình , các đường dẫn đến các bộ mô tả tệp đó được thay thế trong lệnh gọi, vì vậy bây giờ nó trông giống như sau:

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

  1. the_cmd chạy, ghi stdout vào bộ mô tả tệp đầu tiên và stderr vào bộ thứ hai.

  2. Trong trường hợp bash, sau khi the_cmdkết thúc, câu lệnh sau sẽ xảy ra ngay lập tức (nếu thiết bị đầu cuối của bạn là người gọi, thì bạn sẽ thấy lời nhắc của mình xuất hiện).

  3. Trong trường hợp zsh, khi the_cmdkết thúc, trình bao đợi cả hai teequá trình kết thúc trước khi tiếp tục. Thêm về điều này ở đây .

  4. teeQuá trình đầu tiên , đang đọc từ the_cmdstdout của, viết một bản sao của stdout đó trở lại người gọi bởi vì đó là những gì teecó. Đầu ra của nó không được chuyển hướng, vì vậy chúng làm cho nó trở lại người gọi không thay đổi

  5. teeQuá trình thứ hai có nó được stdoutchuyển hướng đến người gọi stderr(điều này tốt, vì stdin đang đọc từ the_cmd's stderr). Vì vậy, khi nó ghi vào stdout của nó, những bit đó sẽ được chuyển đến stderr của người gọi.

Điều này giúp stderr tách biệt khỏi stdout cả trong tệp và đầu ra của lệnh.

Nếu tee đầu tiên viết bất kỳ lỗi nào, chúng sẽ hiển thị trong cả tệp stderr và trong stderr của lệnh, nếu tee thứ hai viết bất kỳ lỗi nào, chúng sẽ chỉ hiển thị trong stderr của terminal.


Điều này trông thực sự hữu ích và những gì tôi muốn. Tuy nhiên, tôi không chắc cách sao chép việc sử dụng dấu ngoặc (như được hiển thị trong dòng đầu tiên) trong Windows Batch Script. ( teecó sẵn trên hệ thống được đề cập.) Lỗi tôi nhận được là "Quy trình không thể truy cập tệp vì nó đang được sử dụng bởi quy trình khác."
Agi Hammerthief

Giải pháp này, giống như hầu hết các giải pháp khác được đề xuất cho đến nay, là dễ chạy đua. Nghĩa là, khi tập lệnh hiện tại hoàn thành và trả về, đến lời nhắc của người dùng hoặc một số tập lệnh gọi cấp cao hơn, tee, đang chạy trong nền, sẽ vẫn chạy và có thể phát ra một vài dòng cuối cùng ra màn hình và logfile trễ (nghĩa là đến màn hình sau lời nhắc và đến logfile sau khi logfile dự kiến ​​hoàn thành).
Don Hatch

2
@DonHatch Bạn có thể đề xuất giải pháp khắc phục vấn đề này không?
pylipp

Tôi cũng quan tâm đến một trường hợp thử nghiệm giúp cuộc đua rõ ràng. Không phải tôi nghi ngờ, nhưng thật khó để cố tránh vì tôi chưa thấy nó xảy ra.
MatrixManAtYrService

@pylipp Tôi không có giải pháp. Tôi sẽ rất quan tâm đến một.
Don Hatch

4

để chuyển hướng stderr đến stdout, hãy nối thêm điều này vào lệnh của bạn: 2>&1 Để xuất ra terminal và đăng nhập vào tệp, bạn nên sử dụngtee

Cả hai cùng nhau sẽ trông như thế này:

 mycommand 2>&1 | tee mylogfile.log

CHỈNH SỬA: Để nhúng vào tập lệnh của bạn, bạn cũng sẽ làm như vậy. Vì vậy, kịch bản của bạn

#!/bin/sh
whatever1
whatever2
...
whatever3

sẽ kết thúc như

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

2
Lưu ý rằng sự phân biệt giữa stdout và stderr sẽ bị mất, vì tee in mọi thứ vào stdout.
Flimm

4

CHỈNH SỬA: Tôi thấy tôi đã bị trật bánh và cuối cùng đã trả lời một câu hỏi khác với câu hỏi được hỏi. Câu trả lời cho câu hỏi thực sự nằm ở cuối câu trả lời của Paul Tomblin. (Nếu bạn muốn nâng cao giải pháp đó để chuyển hướng stdout và stderr riêng biệt vì một lý do nào đó, bạn có thể sử dụng kỹ thuật tôi mô tả ở đây.)


Tôi đang muốn một câu trả lời giúp duy trì sự khác biệt giữa stdout và stderr. Thật không may, tất cả các câu trả lời được đưa ra cho đến nay mà bảo tồn sự khác biệt đó là dễ bị chủng tộc: chúng có nguy cơ các chương trình nhìn thấy đầu vào không đầy đủ, như tôi đã chỉ ra trong các bình luận.

Tôi nghĩ rằng cuối cùng tôi đã tìm ra một câu trả lời giúp duy trì sự khác biệt, không thiên về chủng tộc, và cũng không quá khó hiểu.

Khối xây dựng đầu tiên: để hoán đổi stdout và stderr:

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

Khối xây dựng thứ hai: nếu chúng ta chỉ muốn lọc (ví dụ: tee) chỉ stderr, chúng ta có thể thực hiện điều đó bằng cách hoán đổi stdout & stderr, lọc và sau đó hoán đổi lại:

{ my_command 3>&1 1>&2 2>&3- | stderr_filter;} 3>&1 1>&2 2>&3-

Bây giờ, phần còn lại thật dễ dàng: chúng ta có thể thêm một bộ lọc stdout, ngay từ đầu:

{ { my_command | stdout_filter;} 3>&1 1>&2 2>&3- | stderr_filter;} 3>&1 1>&2 2>&3-

hoặc ở cuối:

{ my_command 3>&1 1>&2 2>&3- | stderr_filter;} 3>&1 1>&2 2>&3- | stdout_filter

Để thuyết phục bản thân rằng cả hai lệnh trên đều hoạt động, tôi đã sử dụng như sau:

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;}'

Đầu ra là:

...(1 second pause)...
teed stdout: to stdout
...(another 1 second pause)...
teed stderr: to stderr

và lời nhắc của tôi quay lại ngay sau dấu " teed stderr: to stderr", như mong đợi.

Chú thích cuối trang về zsh :

Giải pháp trên hoạt động trong bash (và có thể một số shell khác, tôi không chắc chắn), nhưng nó không hoạt động trong zsh. Có hai lý do khiến nó không thành công trong zsh:

  1. cú pháp 2>&3-không được hiểu bởi zsh; cái đó phải được viết lại thành2>&3 3>&-
  2. trong zsh (không giống như các shell khác), nếu bạn chuyển hướng một trình mô tả tệp đã được mở, trong một số trường hợp (tôi không hoàn toàn hiểu cách nó quyết định) thay vào đó, nó thực hiện một hành vi giống như tee tích hợp. Để tránh điều này, bạn phải đóng từng fd trước khi chuyển hướng nó.

Vì vậy, ví dụ, giải pháp thứ hai của tôi phải được viết lại cho 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(cũng hoạt động trong bash, nhưng rất dài dòng).

Mặt khác, bạn có thể tận dụng khả năng phát bóng ngầm được tích hợp sẵn bí ẩn của zsh để có được một giải pháp ngắn hơn nhiều cho zsh, vốn hoàn toàn không chạy phát bóng:

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

(Tôi sẽ không thể đoán được từ các tài liệu mà tôi tìm thấy rằng >&12>&2là thứ kích hoạt phát bóng ngầm của zsh; tôi đã phát hiện ra điều đó bằng cách thử và sai.)


Tôi đã thử với cái này trong bash và nó hoạt động tốt. Chỉ cần một lời cảnh báo cho người dùng zsh với một thói quen giả định khả năng tương thích (như bản thân mình), nó thường chạy khác có: gist.github.com/MatrixManAtYrService/...
MatrixManAtYrService

@MatrixManAtYrService Tôi tin rằng mình đã xử lý được tình huống zsh và hóa ra có một giải pháp gọn gàng hơn nhiều trong zsh. Xem chỉnh sửa của tôi "Chú thích cuối trang về zsh".
Don Hatch

Cảm ơn vì đã giải thích giải pháp chi tiết như vậy. Bạn cũng biết cách truy xuất mã trả về khi sử dụng hàm ( my_function) trong bộ lọc stdout / stderr lồng nhau? Tôi đã làm { { my_function || touch failed;} 3>&1 1>&2 2>&3- | stderr_filter;} 3>&1 1>&2 2>&3- | stdout_filternhưng cảm thấy kỳ lạ khi tạo một tệp như là dấu hiệu của sự thất bại ...
pylipp

@pylipp Tôi không thuận tay. Bạn có thể hỏi điều đó như một câu hỏi riêng (có thể với một đường dẫn đơn giản hơn).
Don Hatch

2

Sử dụng scriptlệnh trong script của bạn (man 1 script)

Tạo một shellcript của trình bao bọc (2 dòng) để thiết lập script () và sau đó gọi exit.

Phần 1: wrap.sh

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

Phần 2: realscript.sh

#!/bin/sh
echo 'Output'

Kết quả:

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

Sử dụng chương trình tee và stderr lặp để stdout.

 program 2>&1 | tee > logfile

1

Tôi đã tạo một tập lệnh có tên "RunScript.sh". Nội dung của script này là:

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

Tôi gọi nó như thế này:

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

Điều này hoạt động, nhưng nó yêu cầu các tập lệnh của ứng dụng phải được chạy thông qua một tập lệnh bên ngoài. Nó hơi kludgy.


9
Bạn sẽ mất nhóm các đối số có chứa khoảng trắng với $ 1 $ 2 $ 3 ... , bạn nên sử dụng (w / ngoặc kép): "$ @"
NVRAM 28/10/09

1

Một năm sau, đây là một tập lệnh bash cũ để ghi lại mọi thứ. Ví dụ:
teelog make ...ghi nhật ký vào tên nhật ký đã tạo (và xem thủ thuật ghi nhật ký lồng nhau 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

Tôi biết rằng đã muộn để thêm một bình luận nhưng tôi chỉ phải nói cảm ơn vì kịch bản này. Rất hữu ích và được ghi lại đầy đủ!
stephenmm

Cảm ơn @stephenmm; đó là không bao giờ quá muộn để nói "hữu ích" hoặc "có thể được cải thiện".
denis
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.