วิธีการอ่านจากไฟล์หรือ STDIN ใน Bash?


244

สคริปต์ Perl ต่อไปนี้ ( my.pl) สามารถอ่านได้จากไฟล์ในบรรทัดคำสั่ง args หรือจาก STDIN:

while (<>) {
   print($_);
}

perl my.plจะอ่านจาก STDIN ในขณะที่จะอ่านจากperl my.pl a.txt a.txtมันสะดวกมาก

สงสัยว่ามี Bash เทียบเท่าใน?

คำตอบ:


409

โซลูชันต่อไปนี้อ่านจากไฟล์หากสคริปต์ถูกเรียกด้วยชื่อไฟล์เป็นพารามิเตอร์แรก$1มิฉะนั้นจากอินพุตมาตรฐาน

while read line
do
  echo "$line"
done < "${1:-/dev/stdin}"

การทดแทน${1:-...}ใช้$1ถ้ากำหนดไว้มิฉะนั้นจะใช้ชื่อไฟล์ของอินพุตมาตรฐานของกระบวนการของตัวเอง


1
นีซมันใช้งานได้ คำถามอื่นคือทำไมคุณเพิ่มใบเสนอราคาสำหรับมัน "$ {1: - / proc / $ {$} / fd / 0}"
Dagang

15
ชื่อไฟล์ที่คุณระบุในบรรทัดคำสั่งอาจมีช่องว่าง
Fritz G. Mehner

3
มีความแตกต่างระหว่างการใช้งาน/proc/$$/fd/0และ/dev/stdin? ฉันสังเกตว่าหลังดูจะธรรมดากว่าและดูตรงไปกว่า
knowah

19
ดีกว่าที่จะเพิ่มคำสั่ง-rของคุณreadเพื่อที่จะไม่กิน\ ตัวอักษรโดยไม่ตั้งใจ; ใช้while IFS= read -r lineเพื่อรักษาพื้นที่ว่างชั้นนำและต่อท้าย
mklement0

1
@Narkark: อยากรู้อยากเห็น; ฉันเพิ่งตรวจสอบว่ามันใช้งานได้บนแพลตฟอร์มนั้นแม้เมื่อใช้/bin/sh- คุณใช้เปลือกนอกbashหรือshไม่
mklement0

119

บางทีทางออกที่ง่ายที่สุดคือการเปลี่ยนเส้นทาง stdin ด้วยการรวมตัวดำเนินการเปลี่ยนเส้นทาง:

#!/bin/bash
less <&0

Stdin คือ file descriptor zero ด้านบนจะส่งอินพุตที่ส่งไปยังสคริปต์ทุบตีของคุณลงใน stdin ที่น้อยลง

อ่านข้อมูลเพิ่มเติมเกี่ยวกับแฟ้มเปลี่ยนเส้นทางให้คำอธิบาย


1
ฉันหวังว่าฉันจะมี upvotes มากขึ้นเพื่อให้คุณฉันกำลังมองหาสิ่งนี้มานานหลายปี
Marcus Downing

13
ไม่มีประโยชน์ที่จะใช้<&0ในสถานการณ์นี้ - ตัวอย่างของคุณจะทำงานแบบเดียวกันกับที่มีหรือไม่มี - ดูเหมือนว่าเครื่องมือที่คุณเรียกใช้จากภายในสคริปต์ทุบตีโดยค่าเริ่มต้นจะดู stdin เช่นเดียวกับสคริปต์ (เว้นแต่สคริปต์จะใช้ก่อน)
mklement0

@ mkelement0 ดังนั้นถ้าเครื่องมืออ่านครึ่งหนึ่งของบัฟเฟอร์อินพุตเครื่องมือถัดไปที่ฉันเรียกใช้จะได้รับส่วนที่เหลือหรือไม่
ซาด Saeeduddin

"ชื่อไฟล์หายไป (" น้อยกว่า - ช่วย "เพื่อขอความช่วยเหลือ)" เมื่อฉันทำสิ่งนี้ ... Ubuntu 16.04
OmarOthman

5
ส่วน "หรือจากไฟล์" อยู่ที่ไหนในคำตอบนี้
เซบาสเตียน

84

นี่คือวิธีที่ง่ายที่สุด:

#!/bin/sh
cat -

การใช้งาน:

$ echo test | sh my_script.sh
test

ในการกำหนดstdinให้กับตัวแปรคุณอาจใช้: STDIN=$(cat -)หรือเพียงแค่STDIN=$(cat)ไม่จำเป็นต้องใช้โอเปอเรเตอร์ (ตามความคิดเห็น @ mklement0 )


ในการแยกแต่ละบรรทัดจากอินพุตมาตรฐานลองสคริปต์ต่อไปนี้:

#!/bin/bash
while IFS= read -r line; do
  printf '%s\n' "$line"
done

หากต้องการอ่านจากไฟล์หรือstdin (หากไม่มีอาร์กิวเมนต์) คุณสามารถขยายเป็น:

#!/bin/bash
file=${1--} # POSIX-compliant; ${1:--} can be used either.
while IFS= read -r line; do
  printf '%s\n' "$line" # Or: env POSIXLY_CORRECT=1 echo "$line"
done < <(cat -- "$file")

หมายเหตุ:

- read -r- อย่าใช้อักขระแบ็กสแลชในลักษณะพิเศษใด ๆ พิจารณาแบ็กสแลชแต่ละรายการเพื่อเป็นส่วนหนึ่งของบรรทัดอินพุต

- โดยไม่ต้องตั้งค่าIFSเริ่มต้นลำดับของSpaceและTabที่จุดเริ่มต้นและจุดสิ้นสุดของบรรทัดจะถูกละเว้น (ตัด)

- ใช้printfแทนechoเพื่อหลีกเลี่ยงการพิมพ์บรรทัดว่างเมื่อสายประกอบด้วยเดียว-e, หรือ-n -Eอย่างไรก็ตามมีวิธีแก้ปัญหาโดยการใช้env POSIXLY_CORRECT=1 echo "$line"ซึ่งเรียกใช้ GNU ภายนอกของคุณechoซึ่งรองรับ ดู: ฉันจะสะท้อน "-e" ได้อย่างไร?

โปรดดู: วิธีการอ่าน stdin เมื่อไม่มีการส่งผ่านอาร์กิวเมนต์? ที่ stackoverflow SE


คุณสามารถลดความซับซ้อนของการ[ "$1" ] && FILE=$1 || FILE="-" FILE=${1:--}(Quibble: ดีกว่าเพื่อหลีกเลี่ยงตัวแปรเชลล์ตัวพิมพ์ใหญ่ทั้งหมดเพื่อหลีกเลี่ยงการชนชื่อกับตัวแปรสภาพแวดล้อม )
mklement0

ด้วยความยินดี; ที่จริงแล้ว${1:--} เป็นไปตาม POSIX ดังนั้นจึงควรทำงานกับเชลล์ที่มีลักษณะคล้าย POSIX ทั้งหมด สิ่งที่ใช้ไม่ได้ในเชลล์เหล่านี้คือการทดแทนโปรเซส ( <(...)); มันจะทำงานในทุบตี, ksh, zsh แต่ไม่ได้อยู่ในเส้นประตัวอย่างเช่น นอกจากนี้ควรเพิ่มคำสั่ง-rของคุณreadเพื่อที่จะไม่กิน\ ตัวอักษรโดยไม่ตั้งใจ เตรียมที่IFS= จะรักษาช่องว่างชั้นนำและต่อท้าย
mklement0

4
ในความเป็นจริงรหัสของคุณยังคงแบ่งเพราะechoถ้าบรรทัดประกอบด้วย-e, -nหรือ-Eมันจะไม่ปรากฏ เพื่อแก้ไขปัญหานี้คุณต้องใช้:printf printf '%s\n' "$line"ฉันไม่ได้รวมไว้ในการแก้ไขก่อนหน้า ... บ่อยเกินไปการแก้ไขของฉันจะย้อนกลับเมื่อฉันแก้ไขข้อผิดพลาด:(นี้
gniourf_gniourf

1
ไม่มันไม่ได้ล้มเหลว และ--ไม่มีประโยชน์อะไรถ้าอาร์กิวเมนต์แรกคือ'%s\n'
gniourf_gniourf

1
คำตอบของคุณก็ดีโดยฉัน (ฉันหมายถึงไม่มีข้อบกพร่องหรือคุณสมบัติที่ไม่พึงประสงค์ฉันรู้อีกต่อไป) - แม้ว่ามันจะไม่จัดการกับข้อโต้แย้งหลาย ๆ อย่างที่ Perl ทำ ในความเป็นจริงถ้าคุณต้องการที่จะจัดการกับข้อโต้แย้งหลายคุณจะจบลงเขียนโจนาธาน Leffler ของคำตอบในความเป็นจริงคุณที่ดีจะดีกว่าเนื่องจากคุณต้องการใช้IFS=กับreadและแทนprintf . echo:)
gniourf_gniourf

19

ฉันคิดว่านี่เป็นวิธีที่ตรงไปตรงมา:

$ cat reader.sh
#!/bin/bash
while read line; do
  echo "reading: ${line}"
done < /dev/stdin

-

$ cat writer.sh
#!/bin/bash
for i in {0..5}; do
  echo "line ${i}"
done

-

$ ./writer.sh | ./reader.sh
reading: line 0
reading: line 1
reading: line 2
reading: line 3
reading: line 4
reading: line 5

4
สิ่งนี้ไม่พอดีกับความต้องการของผู้โพสต์สำหรับการอ่านจาก stdin หรืออาร์กิวเมนต์ไฟล์เพียงแค่อ่านจาก stdin
แนช

2
ออกจาก @ คัดค้านที่ถูกต้องของแนชกัน: readอ่านจาก stdin โดยค่าเริ่มต้นจึงมีความจำเป็นต้อง< /dev/stdinสำหรับ
mklement0

13

การechoแก้ปัญหาจะเพิ่มบรรทัดใหม่เมื่อใดก็ตามที่IFSแบ่งกระแสอินพุต คำตอบของ @fgmสามารถแก้ไขได้เล็กน้อย:

cat "${1:-/dev/stdin}" > "${2:-/dev/stdout}"

คุณช่วยอธิบายสิ่งที่คุณหมายถึงโดย "โซลูชั่น echo เพิ่มบรรทัดใหม่เมื่อใดก็ตามที่ IFS หยุดพักสตรีม" ในกรณีที่คุณหมายถึงreadพฤติกรรมในขณะที่read ไม่อาจแยกออกเป็นราชสกุลหลายคนโดยตัวอักษร มีอยู่ใน$IFSมันจะส่งกลับโทเค็นเดียวถ้าคุณระบุชื่อตัวแปรเดียวเท่านั้น (แต่จดจ้องและนำหน้าและช่องว่างตามค่าเริ่มต้น)
mklement0

@ mklement0 ฉันเห็นด้วย 100% กับคุณเกี่ยวกับพฤติกรรมของreadและ$IFS- echoตัวเองเพิ่มบรรทัดใหม่โดยไม่มีการ-nตั้งค่าสถานะ "ยูทิลิตี echo เขียนตัวถูกดำเนินการที่ระบุใด ๆ คั่นด้วยอักขระว่างเปล่า (` ') เดี่ยวและตามด้วยอักขระขึ้นบรรทัดใหม่ (`\ n') ไปยังเอาต์พุตมาตรฐาน"
David Souther

เข้าใจแล้ว อย่างไรก็ตามเพื่อเลียนแบบห่วง Perl คุณต้องมีการต่อท้าย\nเพิ่มโดยecho: Perl's $_ รวมถึงบรรทัดที่ลงท้ายด้วย\nบรรทัดที่อ่านในขณะที่ทุบตีreadไม่ได้ (อย่างไรก็ตามเนื่องจาก @gniourf_gniourf ชี้ให้เห็นที่อื่น ๆ วิธีการที่แข็งแกร่งกว่าคือการใช้printf '%s\n'แทนecho)
mklement0

8

Perl ห่วงในคำถามอ่านจากอาร์กิวเมนต์ชื่อไฟล์ทั้งหมดบนบรรทัดคำสั่งหรือจากอินพุตมาตรฐานหากไม่มีการระบุไฟล์ คำตอบที่ฉันเห็นทั้งหมดดูเหมือนว่าจะประมวลผลไฟล์เดียวหรืออินพุตมาตรฐานหากไม่มีการระบุไฟล์

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

cat "$@" |
while read -r line
do
    echo "$line"
done

ข้อเสียเพียงอย่างเดียวคือสร้างไปป์ไลน์ที่ทำงานใน sub-shell ดังนั้นสิ่งต่าง ๆ เช่นการกำหนดตัวแปรในwhileลูปจะไม่สามารถเข้าถึงได้นอกไพพ์ไลน์ bashทางรอบที่เป็นกระบวนการชดเชย :

while read -r line
do
    echo "$line"
done < <(cat "$@")

สิ่งนี้จะทำให้การwhileวนซ้ำทำงานในเชลล์หลักดังนั้นตัวแปรที่ตั้งในลูปจะสามารถเข้าถึงได้นอกลูป


1
จุดที่ยอดเยี่ยมเกี่ยวกับหลายไฟล์ ฉันไม่ทราบว่าทรัพยากรและประสิทธิภาพที่เกี่ยวข้องจะเป็นอย่างไร แต่ถ้าคุณไม่ได้อยู่ใน bash, ksh หรือ zsh ดังนั้นจึงไม่สามารถใช้การทดแทนกระบวนการคุณสามารถลอง here-doc พร้อมกับการทดแทนคำสั่ง (กระจายข้าม 3) >>EOF\n$(cat "$@")\nEOFบรรทัด) ในที่สุดการเล่นลิ้น: while IFS= read -r lineเป็นการประมาณที่ดีขึ้นของสิ่งที่while (<>)เกิดขึ้นใน Perl (เก็บรักษาช่องว่างชั้นนำและต่อท้าย - แม้ว่า Perl ยังรักษาเส้นทาง\n)
mklement0

4

พฤติกรรมของ Perl ด้วยรหัสที่ให้ใน OP ไม่สามารถมีอาร์กิวเมนต์ได้หลายข้อและถ้าอาร์กิวเมนต์เป็นยัติภังค์เดียว-ก็เข้าใจได้ว่าเป็น stdin $ARGVนอกจากนี้ก็มักจะเป็นไปได้ที่จะมีชื่อไฟล์ที่มี ไม่มีคำตอบใด ๆ ที่เลียนแบบพฤติกรรมของ Perl ในแง่เหล่านี้ นี่คือความเป็นไปได้ของ Bash ที่บริสุทธิ์ เคล็ดลับคือการใช้execอย่างเหมาะสม

#!/bin/bash

(($#)) || set -- -
while (($#)); do
   { [[ $1 = - ]] || exec < "$1"; } &&
   while read -r; do
      printf '%s\n' "$REPLY"
   done
   shift
done

$1ในชื่อไฟล์ที่มีอยู่ของ

หากไม่มีข้อโต้แย้งใด ๆ เราจะตั้งค่า-เป็นพารามิเตอร์ตำแหน่งแรกโดยไม่ตั้งใจ จากนั้นเราวนลูปกับพารามิเตอร์ ถ้าพารามิเตอร์ไม่ได้เป็นเราเปลี่ยนเส้นทางเข้ามาตรฐานจากชื่อไฟล์ด้วย- execหากการเปลี่ยนเส้นทางนี้สำเร็จเราจะวนwhileซ้ำ ฉันใช้มาตรฐานตัวแปรและในกรณีนี้คุณไม่จำเป็นต้องตั้งค่าREPLY IFSหากคุณต้องการชื่ออื่นคุณต้องรีเซ็ตIFSเช่นนั้น (เว้นแต่คุณไม่ต้องการและรู้ว่าคุณกำลังทำอะไร):

while IFS= read -r line; do
    printf '%s\n' "$line"
done

2

แม่นยำยิ่งขึ้น ...

while IFS= read -r line ; do
    printf "%s\n" "$line"
done < file

2
ฉันคิดว่านี่เป็นหลักความคิดเห็นในstackoverflow.com/a/6980232/45375ไม่ใช่คำตอบ เพื่อให้การแสดงความคิดเห็นอย่างชัดเจน: การเพิ่ม IFS=และ-r ไปยังreadมั่นใจว่าคำสั่งที่แต่ละบรรทัดจะอ่านไม่แปร (รวมชั้นนำและต่อท้ายช่องว่าง)
mklement0

2

โปรดลองรหัสต่อไปนี้:

while IFS= read -r line; do
    echo "$line"
done < file

1
โปรดทราบว่าแม้จะแก้ไขแล้วสิ่งนี้จะไม่อ่านจากอินพุตมาตรฐานหรือจากหลายไฟล์ดังนั้นจึงไม่ใช่คำตอบที่สมบูรณ์ของคำถาม (เป็นเรื่องที่น่าแปลกใจที่เห็นการแก้ไขสองครั้งในเวลาไม่เกิน 3 นาทีหลังจากส่งคำตอบเป็นครั้งแรก)
Jonathan Leffler

@JonathanLeffler ขอโทษสำหรับการแก้ไขดังกล่าวเก่า (และไม่ดีจริงๆ) คำตอบ ... แต่ฉันไม่สามารถยืนได้เห็นน่าสงสารนี้readโดยไม่ต้องIFS=และ-rและคนยากจน$lineไม่ทราบราคาที่มีสุขภาพดี
gniourf_gniourf

1
@gniourf_gniourf: ฉันไม่ชอบread -rสัญกรณ์ IMO, POSIX ผิดพลาด; ตัวเลือกควรเปิดใช้งานความหมายพิเศษสำหรับเครื่องหมายแบ็กสแลชต่อท้ายไม่ใช่ปิดใช้งาน - เพื่อให้สคริปต์ที่มีอยู่ (จากก่อน POSIX มีอยู่) จะไม่แตกเนื่องจาก-rถูกละเว้น อย่างไรก็ตามฉันสังเกตว่ามันเป็นส่วนหนึ่งของ IEEE 1003.2 1992 ซึ่งเป็นรุ่นแรกสุดของ POSIX เชลล์และมาตรฐานยูทิลิตี้ ฉันไม่เคยเจอปัญหาเพราะรหัสของฉันไม่ได้ใช้-r; ฉันจะต้องโชคดี ไม่สนใจฉันในเรื่องนี้
Jonathan Leffler

1
@ JonathanLeffler ฉันเห็นด้วยอย่างยิ่งว่า-rควรจะเป็นมาตรฐาน ฉันยอมรับว่าไม่น่าเป็นไปได้ในกรณีที่ไม่ได้ใช้งานจะทำให้เกิดปัญหา แม้ว่ารหัสที่ใช้งานไม่ได้จะเป็นรหัสที่ใช้งานไม่ได้ การแก้ไขของฉันถูกเรียกครั้งแรกโดย$lineตัวแปรที่ไม่ดีซึ่งไม่ได้รับเครื่องหมายคำพูด ฉันแก้ไขในreadขณะที่ฉันอยู่ที่มัน ฉันไม่ได้แก้ไขechoเพราะเป็นประเภทการแก้ไขที่ย้อนกลับได้ :(.
gniourf_gniourf

1

รหัส${1:-/dev/stdin}จะเข้าใจอาร์กิวเมนต์แรกดังนั้นเรื่องนี้

ARGS='$*'
if [ -z "$*" ]; then
  ARGS='-'
fi
eval "cat -- $ARGS" | while read line
do
   echo "$line"
done

1

ฉันไม่พบคำตอบใด ๆ ที่ยอมรับได้ โดยเฉพาะคำตอบที่ตอบรับจะจัดการกับพารามิเตอร์บรรทัดคำสั่งแรกเท่านั้นและไม่สนใจส่วนที่เหลือ โปรแกรม Perl ที่พยายามเลียนแบบจัดการพารามิเตอร์บรรทัดคำสั่งทั้งหมด ดังนั้นคำตอบที่ยอมรับไม่ได้ตอบคำถาม คำตอบอื่น ๆ ใช้ส่วนขยาย bash เพิ่มคำสั่ง 'cat' ที่ไม่จำเป็นทำงานเฉพาะกับกรณีอย่างง่ายของการสะท้อนอินพุตไปยังเอาต์พุตหรือเพียงซับซ้อนโดยไม่จำเป็น

อย่างไรก็ตามฉันต้องให้เครดิตพวกเขาเพราะพวกเขาให้ความคิดกับฉัน นี่คือคำตอบที่สมบูรณ์:

#!/bin/sh

if [ $# = 0 ]
then
        DEFAULT_INPUT_FILE=/dev/stdin
else
        DEFAULT_INPUT_FILE=
fi

# Iterates over all parameters or /dev/stdin
for FILE in "$@" $DEFAULT_INPUT_FILE
do
        while IFS= read -r LINE
        do
                # Do whatever you want with LINE here.
                echo $LINE
        done < "$FILE"
done

1

ฉันรวมคำตอบข้างต้นทั้งหมดและสร้างฟังก์ชันเชลล์ที่เหมาะกับความต้องการของฉัน นี่คือจากเทอร์มินัล cygwin ของเครื่อง Windows10 2 เครื่องของฉันซึ่งฉันมีโฟลเดอร์แชร์อยู่ระหว่างเครื่อง ฉันต้องสามารถจัดการกับสิ่งต่อไปนี้:

  • cat file.cpp | tx
  • tx < file.cpp
  • tx file.cpp

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

ดูเถิดสคริปต์ที่ดีที่สุดสำหรับความต้องการของฉัน:

tx ()
{
  if [ $# -eq 0 ]; then
    local TMP=/tmp/tx.$(date +'%H%M%S')
    while IFS= read -r line; do
        echo "$line"
    done < /dev/stdin > $TMP
    cp $TMP //$OTHER/stargate/$(date +'%a')/
    rm -f $TMP
  else
    [ -r $1 ] && cp $1 //$OTHER/stargate/$(date +'%a')/ || echo "cannot read file"
  fi
}

หากมีวิธีใดที่คุณสามารถเห็นการเพิ่มประสิทธิภาพนี้ฉันอยากจะรู้


0

ผลงานต่อไปนี้มีมาตรฐานsh(ทดสอบกับdashเดเบียน) และอ่านได้ง่าย แต่นั่นเป็นเรื่องของรสนิยม:

if [ -n "$1" ]; then
    cat "$1"
else
    cat
fi | commands_and_transformations

รายละเอียด: หากพารามิเตอร์แรกไม่ว่างเปล่าcatไฟล์นั้นจะเป็นcatอินพุตมาตรฐานอื่น จากนั้นการส่งออกของทั้งงบการประมวลผลโดยifcommands_and_transformations


IMHO เป็นคำตอบที่ดีที่สุดเพราะชี้ไปที่ทางออกที่แท้จริง: cat "${1:--}" | any_command. การอ่านตัวแปร shell และการสะท้อนอาจใช้กับไฟล์ขนาดเล็กได้
Andreas Spindler

ได้ง่ายไป[ -n "$1" ] [ "$1" ]
agc


-1

เกี่ยวกับ

for line in `cat`; do
    something($line);
done

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