ฉันจะใช้ null bytes ใน Bash ได้อย่างไร


32

ฉันได้อ่านมาแล้วเนื่องจากเส้นทางของไฟล์ใน Bash สามารถมีตัวละครใด ๆ ยกเว้น null null (ไบต์ที่มีค่าเป็นศูนย์$'\0') จึงเป็นการดีที่สุดที่จะใช้ null null เป็นตัวคั่น ตัวอย่างเช่นหากผลลัพธ์ของfindจะถูกส่งไปยังโปรแกรมอื่นขอแนะนำให้ใช้-print0ตัวเลือก (สำหรับเวอร์ชันfindที่มี)

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

find -print0 \
  | while IFS= read -r -d $'\0' ; do echo "$REPLY" ; done

สิ่งนี้ไม่ได้ผล:

for file in * ; do echo -n "$file"$'\0' ; done \
  | while IFS= read -r -d $'\0' ; do echo "$REPLY" ; done

เมื่อฉันลองใช้เพียงforส่วน -loop ฉันพบว่ามันแค่พิมพ์ชื่อไฟล์ทั้งหมดเข้าด้วยกันโดยไม่มีไบต์ว่างระหว่างนั้น

ทำไมนี้ เกิดอะไรขึ้น?

คำตอบ:


43

Bash ใช้สตริง C-style ภายในซึ่งจะถูกยกเลิกด้วยไบต์ null ซึ่งหมายความว่าสตริง Bash (เช่นค่าของตัวแปรหรืออาร์กิวเมนต์ของคำสั่ง) ไม่สามารถมีค่าเป็น null จริง ๆ ตัวอย่างเช่นมินิสคริปต์นี้:

foobar=$'foo\0bar'    # foobar='foo' + null byte + 'bar'
echo "${#foobar}"     # print length of $foobar

จริง ๆ แล้วพิมพ์3เพราะ$foobarเป็นเพียงแค่'foo': barมาหลังจากจุดสิ้นสุดของสตริง

ในทำนองเดียวกันecho $'foo\0bar'เพียงพิมพ์fooเพราะechoไม่รู้เกี่ยวกับชิ้น\0barส่วน

อย่างที่คุณเห็น\0ลำดับนั้นเป็นจริงทำให้เข้าใจผิดมากใน$'...'สตริงสไตล์ ดูเหมือนว่าเป็น null ภายในสตริง แต่มันก็ไม่ได้ทำงานอย่างนั้น ในตัวอย่างแรกของคุณ, คุณคำสั่งมีread -d $'\0'ใช้งานได้ แต่เพียงเพราะ-d ''ยังใช้งานได้! (นั่นไม่ใช่คุณลักษณะที่มีการบันทึกไว้อย่างชัดเจนreadแต่ฉันคิดว่ามันใช้งานได้ด้วยเหตุผลเดียวกัน: ''เป็นสตริงว่างดังนั้นไบต์ null ที่ยุติลงจะมาทันทีมีการบันทึกไว้โดยใช้ "อักขระตัวแรกของdelim " และฉันเดาว่าแม้จะใช้งานได้ ถ้า "อักขระตัวแรก" ผ่านจุดสิ้นสุดของสตริง!)-d delim

แต่ดังที่คุณทราบจากfindตัวอย่างของคุณอาจเป็นไปได้ที่คำสั่งพิมพ์ไบต์ว่างและสำหรับไบต์นั้นจะถูกไพพ์ไปยังคำสั่งอื่นที่อ่านว่าเป็นอินพุต ส่วนหนึ่งของที่อาศัยการจัดเก็บไบต์โมฆะในสตริงภายในทุบตี ปัญหาเดียวของตัวอย่างที่สองของคุณคือเราไม่สามารถใช้$'\0'อาร์กิวเมนต์ในคำสั่งได้ echo "$file"$'\0'สามารถพิมพ์ไบต์ว่างได้อย่างมีความสุขในตอนท้ายถ้าเพียงรู้ว่าคุณต้องการ

ดังนั้นแทนที่จะใช้echoคุณสามารถใช้printfซึ่งสนับสนุนประเภทเดียวกันของลำดับหนีเป็น$'...'สตริงสไตล์ ด้วยวิธีนี้คุณสามารถพิมพ์ null โดยไม่ต้องมี null null ภายในสตริง ที่จะมีลักษณะเช่นนี้:

for file in * ; do printf '%s\0' "$file" ; done \
  | while IFS= read -r -d '' ; do echo "$REPLY" ; done

หรือเพียงแค่นี้:

printf '%s\0' * \
  | while IFS= read -r -d '' ; do echo "$REPLY" ; done

(หมายเหตุ: echoจริง ๆ แล้วยังมีการ-eตั้งค่าสถานะที่จะให้มันประมวลผล\0และพิมพ์ไบต์ที่ว่างเปล่า แต่จากนั้นก็จะพยายามประมวลผลลำดับพิเศษใด ๆ ในชื่อไฟล์ของคุณดังนั้นprintfวิธีนี้จึงแข็งแกร่งกว่า)


อนึ่งมีเปลือกบางอย่างที่จะช่วยให้ null ไบต์สตริงภายใน ตัวอย่างของคุณใช้งานได้ดีใน Zsh เช่น (สมมติว่าการตั้งค่าเริ่มต้น) อย่างไรก็ตามไม่ว่าเชลล์ของคุณระบบปฏิบัติการแบบ Unix จะไม่มีวิธีการรวมไบต์ว่างในอาร์กิวเมนต์ของโปรแกรม (เนื่องจากข้อโต้แย้งของโปรแกรมถูกส่งเป็นสตริง C-style) ดังนั้นจะมีข้อ จำกัด อยู่เสมอ (ตัวอย่างเช่นคุณสามารถทำงานใน zsh เพียงเพราะechoเป็น builtin เปลือกดังนั้น zsh สามารถเรียกมันโดยไม่ต้องอาศัยการสนับสนุนระบบปฏิบัติการสำหรับการเรียกโปรแกรมอื่น ๆ . ถ้าคุณใช้command echoแทนechoเพื่อที่จะข้าม builtin และใช้แบบสแตนด์อโลนechoโปรแกรมบน$PATH, คุณจะเห็นพฤติกรรมแบบเดียวกันใน Zsh เหมือนกับใน Bash)


2
เหตุใด IFS จึงถูกตั้งค่าเป็นไม่มีอะไรถ้า -d ''มีการกำหนดขอบเขตไว้แล้ว\0? ฉันพบคำอธิบายที่นี่: stackoverflow.com/questions/8677546/…
CMCDragonkai
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.