เปลี่ยนเส้นทาง COPY ของ stdout ไปยังล็อกไฟล์จากภายในสคริปต์ทุบตี


235

ฉันรู้วิธีเปลี่ยนเส้นทาง stdoutเป็นไฟล์:

exec > foo.log
echo test

สิ่งนี้จะนำ 'ทดสอบ' ไปไว้ในไฟล์ foo.log

ตอนนี้ฉันต้องการเปลี่ยนเส้นทางผลลัพธ์ไปยังล็อกไฟล์และเก็บไว้ที่ stdout

นั่นคือสามารถทำได้เล็กน้อยจากนอกสคริปต์:

script | tee foo.log

แต่ฉันต้องการประกาศในสคริปต์เอง

ฉันเหนื่อย

exec | tee foo.log

แต่มันไม่ทำงาน


3
คำถามของคุณเป็นประโยคที่ไม่ดี เมื่อคุณเรียกใช้ 'exec> foo.log' stdout ของสคริปต์คือไฟล์ foo.log ฉันคิดว่าคุณหมายความว่าคุณต้องการให้ผลลัพธ์ไปที่ foo.log และไปที่ tty เนื่องจากการไปที่ foo.log จะเป็น stdout
วิลเลียม Pursell

สิ่งที่ฉันต้องการจะทำคือการใช้ | บน 'exec' ที่จะสมบูรณ์แบบสำหรับฉันคือ "exec | tee foo.log" โชคไม่ดีที่คุณไม่สามารถใช้การเปลี่ยนเส้นทางไปป์บนการเรียก exec
Vitaly Kushner

คำตอบ:


297
#!/usr/bin/env bash

# Redirect stdout ( > ) into a named pipe ( >() ) running "tee"
exec > >(tee -i logfile.txt)

# Without this, only stdout would be captured - i.e. your
# log file would not contain any error messages.
# SEE (and upvote) the answer by Adam Spiers, which keeps STDERR
# as a separate stream - I did not want to steal from him by simply
# adding his answer to mine.
exec 2>&1

echo "foo"
echo "bar" >&2

โปรดทราบว่านี้ไม่ได้bash shถ้าคุณเรียกใช้สคริปต์ด้วยคุณจะได้รับข้อผิดพลาดตามสายของsh myscript.shsyntax error near unexpected token '>'

หากคุณทำงานกับดักสัญญาณคุณอาจต้องการใช้tee -iตัวเลือกเพื่อหลีกเลี่ยงการหยุดทำงานของสัญญาณถ้ามีสัญญาณเกิดขึ้น (ขอบคุณ JamesThomasMoon1979 สำหรับความคิดเห็น)


เครื่องมือที่เปลี่ยนเอาต์พุตขึ้นอยู่กับว่าพวกเขาเขียนไปยังlsไพพ์หรือเทอร์มินัล ( ตัวอย่างเช่นการใช้สีและเอาต์พุตแบบคอลัมน์) จะตรวจจับโครงสร้างดังกล่าวข้างต้นตามความหมายที่ส่งออกไปยังไพพ์

มีตัวเลือกในการบังคับใช้ colorizing / columnizing (เช่นls -C --color=always) โปรดทราบว่าสิ่งนี้จะส่งผลให้รหัสสีถูกเขียนไปยังล็อกไฟล์เช่นกันทำให้อ่านได้น้อยลง


5
Tee บนระบบส่วนใหญ่จะถูกบัฟเฟอร์ดังนั้นเอาต์พุตอาจมาไม่ถึงจนกว่าสคริปต์จะเสร็จสิ้น นอกจากนี้เนื่องจากทีออฟนี้ทำงานใน subshell ไม่ใช่โพรเซสลูกจึงไม่สามารถใช้การรอเพื่อซิงโครไนซ์เอาต์พุตกับกระบวนการเรียกใช้ สิ่งที่คุณต้องการคือ tee ที่ไม่มีข้อผูกมัด

14
@Barry: POSIXระบุว่าteeไม่ควรบัฟเฟอร์เอาต์พุต หากบัฟเฟอร์ในระบบส่วนใหญ่ก็จะแตกในระบบส่วนใหญ่ นั่นเป็นปัญหาของการteeใช้งานไม่ใช่วิธีแก้ปัญหาของฉัน
DevSolar

3
@ เซบาสเตียน: execทรงพลังมาก แต่ก็มีส่วนเกี่ยวข้องมาก คุณสามารถ "สำรอง" stdout ปัจจุบันไปยัง filescriptor อื่นแล้วกู้คืนได้ในภายหลัง Google "bash exec tutorial" มีเนื้อหาขั้นสูงมากมายอยู่ที่นั่น
DevSolar

2
@ AdamSpiers: ฉันไม่แน่ใจว่า Barry พูดเกี่ยวกับอะไร ทุบตีของexecอยู่ในเอกสารไม่ได้ที่จะเริ่มต้นกระบวนการใหม่>(tee ...)เป็นมาตรฐานท่อชื่อ / กระบวนการทดแทนและ&ในการเปลี่ยนเส้นทางของหลักสูตรมีอะไรจะทำอย่างไรกับ backgrounding ... :-)?
DevSolar

11
ผมขอแนะนำให้ผ่านไป-i teeมิฉะนั้นสัญญาณขัดจังหวะ (กับดัก) จะขัดขวาง stdout ในสคริปต์หลัก ตัวอย่างเช่นถ้าคุณมีtrap 'echo foo' EXITแล้วกดctrl+cคุณจะไม่เห็น " foo " exec &> >(tee -ia file)ดังนั้นฉันจะแก้ไขคำตอบ
JamesThomasMoon1979

173

คำตอบที่ยอมรับไม่ได้เก็บรักษา STDERR เป็นตัวอธิบายไฟล์แยกต่างหาก นั่นหมายความว่า

./script.sh >/dev/null

จะไม่ส่งออกbarไปยังเทอร์มินัลเฉพาะกับไฟล์บันทึกการทำงานและ

./script.sh 2>/dev/null

จะส่งออกทั้งfooและbarไปยังสถานี เห็นได้ชัดว่านั่นไม่ใช่พฤติกรรมที่ผู้ใช้ปกติน่าจะคาดหวัง สิ่งนี้สามารถแก้ไขได้โดยใช้กระบวนการ tee แยกสองกระบวนการทั้งสองต่อท้ายไฟล์บันทึกเดียวกัน:

#!/bin/bash

# See (and upvote) the comment by JamesThomasMoon1979 
# explaining the use of the -i option to tee.
exec >  >(tee -ia foo.log)
exec 2> >(tee -ia foo.log >&2)

echo "foo"
echo "bar" >&2

(โปรดทราบว่าข้างต้นไม่ได้ตัดทอนไฟล์บันทึกในขั้นต้น - หากคุณต้องการพฤติกรรมที่คุณควรเพิ่ม

>foo.log

ด้านบนของสคริปต์)

POSIX.1-2008 คุณสมบัติของtee(1)ต้องผลลัพธ์ที่ unbuffered คือไม่ได้สายบัฟเฟอร์ดังนั้นในกรณีนี้มันเป็นไปได้ว่า STDOUT และ STDERR จะจบลงในบรรทัดเดียวกันของfoo.log; อย่างไรก็ตามสิ่งนี้อาจเกิดขึ้นที่เทอร์มินัลดังนั้นไฟล์บันทึกจะเป็นภาพสะท้อนที่ซื่อสัตย์ของสิ่งที่สามารถเห็นได้บนเทอร์มินัลหากไม่ใช่กระจกที่แน่นอนของมัน หากคุณต้องการให้บรรทัด STDOUT แยกออกจากกันอย่างชัดเจนจากบรรทัด STDERR ให้พิจารณาใช้ไฟล์บันทึกสองไฟล์ซึ่งอาจมีส่วนนำหน้าการประทับวันที่ในแต่ละบรรทัดเพื่ออนุญาตให้ประกอบใหม่ตามลำดับเวลาได้ในภายหลัง


ด้วยเหตุผลบางอย่างในกรณีของฉันเมื่อสคริปต์ถูกเรียกใช้จากการเรียกระบบ c-program () กระบวนการย่อย tee ทั้งสองยังคงมีอยู่แม้ว่าสคริปต์หลักจะออก ดังนั้นฉันจึงต้องเพิ่มกับดักเช่นนี้:exec > >(tee -a $LOG) trap "kill -9 $! 2>/dev/null" EXIT exec 2> >(tee -a $LOG >&2) trap "kill -9 $! 2>/dev/null" EXIT
alveko

15
ผมขอแนะนำให้ผ่านไป-i teeมิฉะนั้นสัญญาณขัดจังหวะ (กับดัก) จะขัดขวาง stdout ในสคริปต์ ตัวอย่างเช่นหากคุณtrap 'echo foo' EXITแล้วกดctrl+cคุณจะไม่เห็น " foo " exec > >(tee -ia foo.log)ดังนั้นฉันจะแก้ไขคำตอบ
JamesThomasMoon1979

ฉันสร้างสคริปต์ "sourceable" เล็กน้อยตามนี้ สามารถใช้พวกเขาในสคริปต์เช่น. logหรือ. log foo.log: sam.nipl.net/sh/log sam.nipl.net/sh/log-a
Sam Watkins

1
ปัญหาของวิธีนี้คือข้อความจะSTDOUTปรากฏเป็นแบทช์ก่อนจากนั้นข้อความจะSTDERRปรากฏขึ้น พวกเขาจะไม่ interleaved ตามที่คาดไว้
CMCDragonkai

28

โซลูชันสำหรับ busybox, macOS bash, และ non-bash shells

คำตอบที่ยอมรับนั้นเป็นตัวเลือกที่ดีที่สุดสำหรับการทุบตี ฉันทำงานในสภาพแวดล้อม Busybox โดยไม่ต้องเข้าถึง bash และไม่เข้าใจexec > >(tee log.txt)ไวยากรณ์ นอกจากนี้ยังทำไม่exec >$PIPEถูกต้องพยายามสร้างไฟล์ธรรมดาที่มีชื่อเดียวกันกับไพพ์ที่มีชื่อซึ่งล้มเหลวและหยุดทำงาน

หวังว่านี่จะเป็นประโยชน์กับคนที่ไม่มีทุบตี

นอกจากนี้สำหรับทุกคนที่ใช้ไพพ์ที่มีชื่อก็ปลอดภัยที่จะ rm $PIPEไพพ์ที่เพราะนั่นจะยกเลิกการเชื่อมโยงไพพ์จาก VFS แต่กระบวนการที่ใช้ยังคงรักษาจำนวนการอ้างอิงไว้จนกว่าจะเสร็จสิ้น

โปรดทราบว่าการใช้ $ * อาจไม่ปลอดภัย

#!/bin/sh

if [ "$SELF_LOGGING" != "1" ]
then
    # The parent process will enter this branch and set up logging

    # Create a named piped for logging the child's output
    PIPE=tmp.fifo
    mkfifo $PIPE

    # Launch the child process with stdout redirected to the named pipe
    SELF_LOGGING=1 sh $0 $* >$PIPE &

    # Save PID of child process
    PID=$!

    # Launch tee in a separate process
    tee logfile <$PIPE &

    # Unlink $PIPE because the parent process no longer needs it
    rm $PIPE    

    # Wait for child process, which is running the rest of this script
    wait $PID

    # Return the error code from the child process
    exit $?
fi

# The rest of the script goes here

นี่เป็นทางออกเดียวที่ฉันได้เห็นจนถึงขณะนี้ที่ทำงานบน mac
Mike Baglio Jr.

19

ในไฟล์สคริปต์ของคุณให้ใส่คำสั่งทั้งหมดไว้ในวงเล็บดังนี้:

(
echo start
ls -l
echo end
) | tee foo.log


ใช่ฉันคิดว่า แต่นี่ไม่ใช่การเปลี่ยนเส้นทางของ stdout ปัจจุบันชนิดของการโกงคุณใช้ subshell และทำการเปลี่ยนเส้นทางไพเพอร์ปกติ งานคิด ฉันแยกกับส่วนนี้และโซลูชัน "tail -f foo.log &" จะรอสักครู่เพื่อดูว่าพื้นผิวอาจดีกว่านี้หรือไม่ ถ้าไม่น่าจะไปตั้งถิ่นฐาน)
Vitaly Kushner

8
{} เรียกใช้รายการในสภาพแวดล้อมเชลล์ปัจจุบัน () เรียกใช้งานรายการในสภาพแวดล้อม subshell

ประณาม. ขอบคุณ. คำตอบที่ยอมรับนั้นไม่ได้ผลสำหรับฉันพยายามกำหนดเวลาให้สคริปต์ทำงานภายใต้ MingW บนระบบ Windows ฉันเชื่อว่าฉันบ่นเกี่ยวกับการทดแทนกระบวนการที่ยังไม่ได้ดำเนินการ คำตอบนี้ใช้ได้ดีหลังจากการเปลี่ยนแปลงต่อไปนี้เพื่อจับทั้ง stderr และ stdout: `` `-) | tee foo.log +) 2> & 1 | tee foo.log
Jon Carter

14

วิธีง่ายๆในการสร้างบันทึกสคริปต์ทุบตีไปยัง syslog เอาต์พุตสคริปต์พร้อมใช้งานทั้งผ่าน/var/log/syslogและผ่าน stderr syslog จะเพิ่มข้อมูลเมตาที่เป็นประโยชน์รวมถึงการประทับเวลา

เพิ่มบรรทัดนี้ที่ด้านบน:

exec &> >(logger -t myscript -s)

หรือส่งบันทึกไปยังไฟล์แยกต่างหาก:

exec &> >(ts |tee -a /tmp/myscript.output >&2 )

สิ่งนี้ต้องการmoreutils(สำหรับtsคำสั่งซึ่งเพิ่มการประทับเวลา)


10

การใช้คำตอบที่ยอมรับแล้วสคริปต์ของฉันยังคงกลับมาเร็วกว่าปกติ (หลังจาก 'exec>> (ที๋ ... )') ทำให้สคริปต์ที่เหลือของฉันทำงานในพื้นหลัง ในขณะที่ฉันไม่สามารถหาวิธีแก้ปัญหาในแบบของฉันได้ฉันพบวิธีแก้ไขปัญหาอื่น / แก้ไขปัญหา:

# Logging setup
logfile=mylogfile
mkfifo ${logfile}.pipe
tee < ${logfile}.pipe $logfile &
exec &> ${logfile}.pipe
rm ${logfile}.pipe

# Rest of my script

สิ่งนี้ทำให้เอาต์พุตจากสคริปต์ไปจากกระบวนการผ่านไพพ์ไปสู่กระบวนการย่อยของ 'tee' ที่บันทึกทุกอย่างลงในดิสก์และไปยัง stdout ดั้งเดิมของสคริปต์

โปรดทราบว่า 'exec &>' เปลี่ยนเส้นทางทั้ง stdout และ stderr เราสามารถเปลี่ยนเส้นทางแยกต่างหากหากเราต้องการหรือเปลี่ยนเป็น 'exec>' หากเราต้องการ stdout

แม้ท่านจะลบไพพ์ออกจากระบบไฟล์ในตอนต้นของสคริปต์มันจะยังคงทำงานต่อไปจนกว่ากระบวนการจะเสร็จสิ้น เราไม่สามารถอ้างอิงได้โดยใช้ชื่อไฟล์หลังจาก rm-line


คำตอบที่คล้ายกันเป็นความคิดที่สองจากเดวิด Z ดูความคิดเห็นของมัน +1 ;-)
olibre

ทำได้ดี. ฉันไม่ได้ทำความเข้าใจส่วนหนึ่งของ$logfile tee < ${logfile}.pipe $logfile &โดยเฉพาะผมพยายามที่จะปรับเปลี่ยนนี้ในการจับภาพเต็มขยายสายบันทึกคำสั่ง (จากset -x) ไปยังแฟ้มในขณะที่เพียงแสดงให้เห็นเส้นโดยไม่ต้องนำ '+' ใน stdout โดยการเปลี่ยนไปแต่ได้รับข้อผิดพลาดเกี่ยวกับ(tee | grep -v '^+.*$') < ${logfile}.pipe $logfile & $logfileคุณช่วยอธิบายteeรายละเอียดเพิ่มเติมได้ไหม?
Chris Johnson

ฉันทดสอบแล้วและดูเหมือนว่าคำตอบนี้จะไม่รักษา STDERR (รวมกับ STDOUT) ดังนั้นหากคุณพึ่งพาสตรีมที่แยกต่างหากสำหรับการตรวจจับข้อผิดพลาดหรือการเปลี่ยนเส้นทางอื่นคุณควรดูคำตอบของอดัม
HeroCC


1

ไม่สามารถพูดได้ว่าฉันพอใจกับวิธีแก้ปัญหาใด ๆ ฉันชอบที่จะใช้ทีออฟโดยตรงดังนั้นฉันจึงเรียกสคริปท์ด้วยทีออฟเมื่อมีการร้องขอ:

# my script: 

check_tee_output()
{
    # copy (append) stdout and stderr to log file if TEE is unset or true
    if [[ -z $TEE || "$TEE" == true ]]; then 
        echo '-------------------------------------------' >> log.txt
        echo '***' $(date) $0 $@ >> log.txt
        TEE=false $0 $@ 2>&1 | tee --append log.txt
        exit $?
    fi 
}

check_tee_output $@

rest of my script

สิ่งนี้อนุญาตให้คุณทำสิ่งนี้:

your_script.sh args           # tee 
TEE=true your_script.sh args  # tee 
TEE=false your_script.sh args # don't tee
export TEE=false
your_script.sh args           # tee

คุณสามารถปรับแต่งสิ่งนี้ได้เช่นทำให้ tee = false เป็นค่าเริ่มต้นแทนทำให้ TEE เก็บไฟล์บันทึกไว้แทน ฯลฯ ฉันคิดว่าโซลูชันนี้คล้ายกับของ jbarlow แต่ง่ายกว่าบางทีฉันอาจมีข้อ จำกัด ที่ฉันยังไม่เจอ


-1

ทั้งสองอย่างนี้เป็นโซลูชั่นที่สมบูรณ์แบบ แต่ต่อไปนี้เป็นสองสิ่งที่คุณควรลอง:

exec >foo.log
tail -f foo.log &
# rest of your script

หรือ

PIPE=tmp.fifo
mkfifo $PIPE
exec >$PIPE
tee foo.log <$PIPE &
# rest of your script
rm $PIPE

ไฟล์ที่สองจะทิ้งไฟล์ไพพ์ไว้รอบ ๆ หากมีบางอย่างผิดปกติกับสคริปต์ของคุณซึ่งอาจจะใช่หรือไม่ใช่ปัญหาrmก็ได้


1
tail จะปล่อยให้กระบวนการทำงานอยู่เบื้องหลังในทีสคริปต์ 2 จะปิดกั้นหรือคุณจะต้องเรียกใช้ด้วย & ในกรณีนี้กระบวนการจะออกจากกระบวนการเช่นเดียวกับที่ 1
Vitaly Kushner

@Vitaly: อ๊ะลืมพื้นหลังtee- ฉันแก้ไขแล้ว อย่างที่ฉันได้กล่าวไปแล้วว่าไม่ใช่วิธีการแก้ปัญหาที่สมบูรณ์แบบ แต่กระบวนการเบื้องหลังจะถูกฆ่าเมื่อเชลล์พาเรนต์สิ้นสุดลงดังนั้นคุณไม่ต้องกังวลเกี่ยวกับทรัพยากรเหล่านั้นที่ใช้ทรัพยากรตลอดไป
David Z

1
Yikes: รูปลักษณ์เหล่านี้น่าสนใจ แต่เอาท์พุทของ tail -f ก็จะเป็น foo.log ด้วย คุณสามารถแก้ไขได้โดยการเรียกใช้ tail -f ก่อนที่ exec แต่หางจะยังคงทำงานหลังจากที่ผู้ปกครองยุติ คุณต้องฆ่ามันอย่างชัดเจนซึ่งอาจเป็นกับดัก 0
William Pursell

Yeap หากสคริปต์ถูกแบ็คกราวน์สคริปต์จะออกจากกระบวนการทั้งหมด
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.