จะอ่านไฟล์ในตัวแปรในเชลล์ได้อย่างไร?


489

ฉันต้องการอ่านไฟล์และบันทึกเป็นตัวแปร แต่ฉันต้องเก็บตัวแปรไว้และไม่เพียงพิมพ์ไฟล์ ฉันจะทำสิ่งนี้ได้อย่างไร ฉันได้เขียนสคริปต์นี้ แต่มันก็ไม่ใช่สิ่งที่ฉันต้องการ:

#!/bin/sh
while read LINE  
do  
  echo $LINE  
done <$1  
echo 11111-----------  
echo $LINE  

ในสคริปต์ของฉันฉันสามารถให้ชื่อไฟล์เป็นพารามิเตอร์ดังนั้นหากไฟล์มี "aaaa" ตัวอย่างเช่นมันจะพิมพ์ออกมานี้:

aaaa
11111-----

แต่นี่แค่พิมพ์ไฟล์ลงบนหน้าจอและฉันต้องการบันทึกลงในตัวแปร! มีวิธีง่าย ๆ ในการทำเช่นนี้?


1
ดูเหมือนว่าจะเป็นข้อความธรรมดา หากเป็นไฟล์ไบนารีคุณจะต้องใช้สิ่งนี้เนื่องจากผลลัพธ์catหรือ$(<someFile)จะทำให้เกิดผลลัพธ์ที่ไม่สมบูรณ์ (ขนาดน้อยกว่าไฟล์จริง)
อำนาจกุมภ์

คำตอบ:


1052

ในข้ามแพลตฟอร์มตัวหารร่วมที่ต่ำที่สุดที่shคุณใช้:

#!/bin/sh
value=`cat config.txt`
echo "$value"

ในbashหรือzshเพื่ออ่านไฟล์ทั้งหมดลงในตัวแปรโดยไม่ต้องเรียกใช้cat:

#!/bin/bash
value=$(<config.txt)
echo "$value"

กล่าวอ้างcatในbashหรือzshจะ slurp ไฟล์ที่จะได้รับการพิจารณาการใช้ประโยชน์ของแมว

โปรดทราบว่าไม่จำเป็นต้องอ้างถึงการทดแทนคำสั่งเพื่อรักษาบรรทัดใหม่

ดู: ทุบตีแฮกเกอร์วิกิพีเดีย - เปลี่ยนตัวสั่ง - Specialties


4
โอเค แต่มันทุบตีไม่ใช่ sh; มันอาจไม่เหมาะกับทุกกรณี
moala

14
จะไม่value="`cat config.txt`"และvalue="$(<config.txt)"จะมีความปลอดภัยในกรณีที่ config.txt มีช่องว่าง?
Martin von Wittich

13
หมายเหตุว่าการใช้ดังกล่าวจะไม่ถือว่าเสมอการใช้งานไร้ประโยชน์cat catยกตัวอย่างเช่น< invalid-file 2>/dev/nullจะส่งผลให้เกิดข้อผิดพลาดที่ไม่สามารถส่งไป/dev/nullในขณะที่ไม่ได้รับการส่งอย่างถูกต้องเพื่อcat invalid-file 2>/dev/null /dev/null
Dejay Clayton

16
สำหรับ Scripters เชลล์ใหม่อย่างฉันโปรดทราบว่ารุ่น cat ใช้เห็บกลับไม่ใช่คำพูดเดียว! หวังว่านี่จะช่วยใครซักคนได้ครึ่งชั่วโมงโดยที่ฉันต้องใช้เวลาคิด
ericksonla

7
สำหรับ bashers ใหม่เช่นฉัน: โปรดทราบว่าvalue=$(<config.txt)ดี แต่value = $(<config.txt)ไม่ดี ระวังช่องว่างเหล่านั้น
ArtHare

88

หากคุณต้องการอ่านไฟล์ทั้งหมดลงในตัวแปร:

#!/bin/bash
value=`cat sources.xml`
echo $value

หากคุณต้องการอ่านทีละบรรทัด:

while read line; do    
    echo $line    
done < file.txt

2
@brain: ถ้าไฟล์เป็น Config.cpp และมีแบ็กสแลช คำพูดคู่และคำพูด?
user2284570

2
echo "$value"คุณควรอ้างดับเบิลตัวแปรใน มิฉะนั้นเชลล์จะดำเนินการโทเค็นช่องว่างและการขยายสัญลักษณ์แทนในค่า
tripleee

3
@ user2284570 ใช้read -rแทนเพียงread- เสมอเว้นแต่คุณจะต้องการพฤติกรรมแบบแปลก ๆ ที่คุณกำลังพูดถึง
tripleee

74

ข้อผิดพลาดที่สำคัญสองประการ

ซึ่งถูกปฏิเสธโดยคำตอบอื่น ๆ จนถึง:

  1. การลบบรรทัดใหม่ต่อท้ายจากการขยายคำสั่ง
  2. การลบอักขระ NUL

การลบบรรทัดใหม่ต่อท้ายจากการขยายคำสั่ง

นี่เป็นปัญหาสำหรับ:

value="$(cat config.txt)"

โซลูชันประเภท แต่ไม่ใช่สำหรับreadโซลูชันพื้นฐาน

การขยายคำสั่งลบบรรทัดใหม่ต่อท้าย:

S="$(printf "a\n")"
printf "$S" | od -tx1

ขาออก:

0000000 61
0000001

นี่เป็นการแบ่งวิธีที่ไร้เดียงสาของการอ่านจากไฟล์:

FILE="$(mktemp)"
printf "a\n\n" > "$FILE"
S="$(<"$FILE")"
printf "$S" | od -tx1
rm "$FILE"

วิธีแก้ปัญหา POSIX: ผนวกอักขระพิเศษในการขยายคำสั่งและลบในภายหลัง:

S="$(cat $FILE; printf a)"
S="${S%a}"
printf "$S" | od -tx1

ขาออก:

0000000 61 0a 0a
0000003

วิธีแก้ปัญหาเกือบ POSIX: เข้ารหัส ASCII ดูด้านล่าง

การลบอักขระ NUL

ไม่มีสติวิธีทุบตีเพื่อเก็บอักขระ NUL ในตัวแปรคือ

สิ่งนี้มีผลต่อทั้งการขยายตัวและreadการแก้ปัญหาและฉันไม่รู้วิธีแก้ปัญหาที่ดีสำหรับมัน

ตัวอย่าง:

printf "a\0b" | od -tx1
S="$(printf "a\0b")"
printf "$S" | od -tx1

ขาออก:

0000000 61 00 62
0000003

0000000 61 62
0000002

ฮา NUL ของเราหายไปแล้ว!

วิธีการแก้ปัญหา:

  • การเข้ารหัส ASCII ดูด้านล่าง

  • ใช้$""ตัวอักษรนามสกุลทุบตี:

    S=$"a\0b"
    printf "$S" | od -tx1

    ใช้ได้กับตัวอักษรเท่านั้นดังนั้นจึงไม่มีประโยชน์สำหรับการอ่านจากไฟล์

วิธีแก้ปัญหาสำหรับข้อผิดพลาด

เก็บไฟล์ที่เข้ารหัสรุ่น uuencode base64 ในตัวแปรและถอดรหัสก่อนการใช้งานทุกครั้ง:

FILE="$(mktemp)"
printf "a\0\n" > "$FILE"
S="$(uuencode -m "$FILE" /dev/stdout)"
uudecode -o /dev/stdout <(printf "$S") | od -tx1
rm "$FILE"

เอาท์พุท:

0000000 61 00 0a
0000003

uuencode และ udecode คือPOSIX 7แต่ไม่ใช่ใน Ubuntu 12.04 โดยค่าเริ่มต้น ( sharutilsแพ็คเกจ) ... ฉันไม่เห็นทางเลือก POSIX 7 สำหรับ<()ส่วนขยายการทดแทนกระบวนการทุบตียกเว้นการเขียนไปยังไฟล์อื่น ...

แน่นอนว่ามันช้าและไม่สะดวกดังนั้นฉันเดาว่าคำตอบที่แท้จริงคือ: อย่าใช้ Bash ถ้าไฟล์อินพุตอาจมีอักขระ NUL


2
ขอบคุณเฉพาะอันนี้ใช้ได้สำหรับฉันเพราะฉันต้องการขึ้นบรรทัดใหม่
Jason Livesay

1
@CiroSantilli: จะทำอย่างไรถ้า FILE เป็น Config.cpp และมีแบ็กสแลช คำพูดคู่และคำพูด?
user2284570

@ user2284570 ผมไม่ทราบ S="$(printf "\\\'\"")"; echo $Sแต่มันเป็นเรื่องง่ายที่จะหา: ผลลัพธ์: \'". ดังนั้นจึงใช้ได้ =)
Ciro Santilli i 冠状病六四事件法轮功

@CiroSantilli: บนเส้น 5511? คุณแน่ใจหรือว่าไม่มีวิธีอัตโนมัติ
user2284570

@ user2284570 ฉันไม่เข้าใจที่มี 5511 บรรทัด? ข้อผิดพลาดที่มาจาก$()การขยายตัวของการแสดงตัวอย่างของฉันที่ขยายตัวทำงานร่วมกับ$() \'"
Ciro Santilli 郝海东冠状病六四事件法轮功


2

ในฐานะที่เป็นCiro Santilli บันทึกโดยใช้การทดแทนคำสั่งจะวางบรรทัดใหม่ต่อท้าย วิธีแก้ปัญหาของพวกเขาในการเพิ่มตัวอักษรต่อท้ายนั้นยอดเยี่ยม แต่หลังจากใช้ไประยะหนึ่งแล้วฉันตัดสินใจว่าฉันต้องการโซลูชันที่ไม่ได้ใช้การทดแทนคำสั่งเลย

แนวทางของฉันตอนนี้ใช้readพร้อมกับการตั้งค่าสถานะprintfของ builtin เพื่ออ่านเนื้อหาของ stdin ลงในตัวแปรโดยตรง-v

# Reads stdin into a variable, accounting for trailing newlines. Avoids needing a subshell or
# command substitution.
read_input() {
  # Use unusual variable names to avoid colliding with a variable name
  # the user might pass in (notably "contents")
  : "${1:?Must provide a variable to read into}"
  if [[ "$1" == '_line' || "$1" == '_contents' ]]; then
    echo "Cannot store contents to $1, use a different name." >&2
    return 1
  fi

  local _line _contents
   while read -r _line; do
     _contents="${_contents}${_line}"$'\n'
   done
   _contents="${_contents}${_line}" # capture any content after the last newline
   printf -v "$1" '%s' "$_contents"
}

สิ่งนี้รองรับอินพุตที่มีหรือไม่มีการขึ้นบรรทัดใหม่

ตัวอย่างการใช้งาน:

$ read_input file_contents < /tmp/file
# $file_contents now contains the contents of /tmp/file

ที่ดี! ฉันแค่สงสัยว่าทำไมไม่ใช้สิ่งที่ต้องการ_contents="${_contents}${_line}\n "รักษาบรรทัดใหม่
Eenoku

1
คุณจะถามเกี่ยวกับ$'\n'? นั่นเป็นสิ่งที่จำเป็นมิฉะนั้นคุณจะต่อท้ายตัวอักษร\ และnอักขระ โค้ดบล็อกของคุณยังมีที่ว่างเพิ่มเติมในตอนท้ายไม่แน่ใจว่าเป็นไปโดยเจตนาหรือไม่
dimo414

ขอบคุณสำหรับคำอธิบาย!
Eenoku

-3

คุณสามารถเข้าถึงได้ครั้งละ 1 บรรทัดสำหรับการวนซ้ำ

#!/bin/bash -eu

#This script prints contents of /etc/passwd line by line

FILENAME='/etc/passwd'
I=0
for LN in $(cat $FILENAME)
do
    echo "Line number $((I++)) -->  $LN"
done

คัดลอกเนื้อหาทั้งหมดไปที่ไฟล์ (พูด line.sh); ปฏิบัติ

chmod +x line.sh
./line.sh

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