ปัญหา
for f in $(find .)
รวมสองสิ่งที่เข้ากันไม่ได้
findพิมพ์รายการพา ธ ไฟล์ที่คั่นด้วยอักขระบรรทัดใหม่ ในขณะที่โอเปอเรเตอร์แยก + glob ที่ถูกเรียกใช้เมื่อคุณปล่อยให้ไม่มี$(find .)เครื่องหมายอัญประกาศในบริบทรายการนั้นให้แยกอักขระของ$IFS(โดยค่าเริ่มต้นจะมีการขึ้นบรรทัดใหม่ แต่ยังมีช่องว่างและแท็บ (และ NUL ในzsh)) และทำการวนรอบคำแต่ละคำ ในzsh) (และแม้กระทั่งการขยายรั้งใน ksh93 หรืออนุพันธ์ pdksh!)
แม้ว่าคุณจะทำมัน:
IFS='
' # split on newline only
set -o noglob # disable glob (also disables brace expansion in pdksh
# but not ksh93)
for f in $(find .) # invoke split+glob
ยังคงผิดเนื่องจากอักขระขึ้นบรรทัดใหม่มีความถูกต้องเหมือนกับที่อยู่ในพา ธ ไฟล์ ผลลัพธ์ของการfind -printโพสต์ไม่สามารถประมวลผลได้อย่างน่าเชื่อถือ (ยกเว้นโดยใช้เคล็ดลับบางอย่างที่ซับซ้อนดังแสดงที่นี่ )
นั่นหมายความว่าเชลล์จำเป็นต้องเก็บเอาท์พุทfindเต็มที่แล้วแยก + glob มัน (ซึ่งหมายถึงการเก็บเอาท์พุทเป็นครั้งที่สองในหน่วยความจำ) ก่อนที่จะเริ่มวนรอบไฟล์
โปรดทราบว่าfind . | xargs cmdมีปัญหาที่คล้ายกัน (นั่น, ว่าง, ขึ้นบรรทัดใหม่, อัญประกาศเดี่ยว, อัญประกาศคู่และแบ็กสแลช (และด้วยxargการใช้งานบางไบต์ไม่ก่อตัวเป็นส่วนหนึ่งของตัวละครที่ถูกต้อง) เป็นปัญหา)
ทางเลือกที่ถูกต้องมากขึ้น
วิธีเดียวที่จะใช้การforวนซ้ำในผลลัพธ์ของการfindจะใช้zshที่สนับสนุนIFS=$'\0'และ:
IFS=$'\0'
for f in $(find . -print0)
(แทนที่-print0ด้วย-exec printf '%s\0' {} +สำหรับfindการใช้งานที่ไม่สนับสนุนมาตรฐานที่ไม่ได้มาตรฐาน (แต่ค่อนข้างบ่อยในปัจจุบัน) -print0)
ที่นี่วิธีที่ถูกต้องและพกพาคือการใช้-exec:
find . -exec something with {} \;
หรือหากsomethingสามารถรับมากกว่าหนึ่งอาร์กิวเมนต์:
find . -exec something with {} +
หากคุณต้องการรายการไฟล์ที่จะจัดการโดยเชลล์:
find . -exec sh -c '
for file do
something < "$file"
done' find-sh {} +
(ระวังอาจเริ่มมากกว่าหนึ่งsh)
ในบางระบบคุณสามารถใช้:
find . -print0 | xargs -r0 something with
แต่ที่มีความได้เปรียบน้อยกว่าไวยากรณ์มาตรฐานและวิธีการsomething's เป็นทั้งท่อหรือstdin/dev/null
เหตุผลหนึ่งที่คุณอาจต้องการใช้นั่นคือใช้-Pตัวเลือกของ GNU xargsสำหรับการประมวลผลแบบขนาน stdinปัญหานอกจากนี้ยังสามารถทำงานรอบกับ GNU xargsกับ-aตัวเลือกด้วยเปลือกหอยสนับสนุนเปลี่ยนตัวกระบวนการ:
xargs -r0n 20 -P 4 -a <(find . -print0) something
ตัวอย่างเช่นการเรียกใช้มากถึง 4 การเรียกใช้พร้อมกันของsomethingแต่ละการรับ 20 อาร์กิวเมนต์ไฟล์
ด้วยzshหรือbashอีกวิธีหนึ่งในการวนซ้ำเอาต์พุตของfind -print0คือ:
while IFS= read -rd '' file <&3; do
something "$file" 3<&-
done 3< <(find . -print0)
read -d '' อ่านระเบียนที่คั่นด้วย NUL แทนที่จะเป็นตัวคั่นที่ขึ้นบรรทัดใหม่
bash-4.4และด้านบนสามารถเก็บไฟล์ที่ส่งคืนโดยfind -print0อาเรย์ด้วย:
readarray -td '' files < <(find . -print0)
ค่าzshเทียบเท่า (ซึ่งมีข้อดีของการรักษาfindสถานะการออกของ):
files=(${(0)"$(find . -print0)"})
ด้วยzshคุณสามารถแปลfindการแสดงออกส่วนใหญ่เป็นการรวมกันของการวนซ้ำแบบซ้ำด้วยตัวระบุแบบกลม ตัวอย่างเช่นการวนซ้ำfind . -name '*.txt' -type f -mtime -1จะเป็น:
for file (./**/*.txt(ND.m-1)) cmd $file
หรือ
for file (**/*.txt(ND.m-1)) cmd -- $file
(ระวังความต้องการของ--เช่นเดียวกับ**/*เส้นทางของไฟล์ที่ไม่ได้เริ่มต้นด้วย./ดังนั้นอาจเริ่มต้นด้วย-ตัวอย่าง)
ksh93และbashในที่สุดก็เพิ่มการสนับสนุนสำหรับ**/(แม้ว่าจะไม่ใช่รูปแบบของการวนซ้ำแบบซ้ำ) แต่ก็ยังไม่ใช่ตัวระบุแบบกลมซึ่งทำให้การใช้งาน**มี จำกัด มาก นอกจากนี้โปรดระวังว่าbashก่อนหน้า 4.3 ตาม symlink เมื่อจากมากไปน้อยแผนผังไดเรกทอรี
เหมือนวนลูป$(find .)ที่ยังหมายถึงการจัดเก็บรายชื่อทั้งหมดของไฟล์ในหน่วยความจำ1 อาจเป็นที่ต้องการ แต่ในบางกรณีเมื่อคุณไม่ต้องการให้การกระทำของคุณในไฟล์มีผลต่อการค้นหาไฟล์ (เช่นเมื่อคุณเพิ่มไฟล์อื่น ๆ ที่อาจพบท้ายด้วยตนเอง)
ข้อควรพิจารณาด้านความน่าเชื่อถือ / ความปลอดภัยอื่น ๆ
สภาพการแข่งขัน
ตอนนี้ถ้าเรากำลังพูดถึงความน่าเชื่อถือเราต้องพูดถึงสภาพการแข่งขันระหว่างเวลาfind/ zshพบไฟล์และตรวจสอบว่ามันตรงตามเกณฑ์และเวลาที่มีการใช้งาน ( TOCTOU เรซ )
แม้ว่าจะลดทอนไดเรกทอรีต้นไม้ก็ตามคุณต้องตรวจสอบให้แน่ใจว่าไม่ได้ติดตาม symlink และทำเช่นนั้นหากไม่มีการแข่งขัน TOCTOU find( findอย่างน้อยGNU ) ทำเช่นนั้นโดยการเปิดไดเรกทอรีที่ใช้openat()พร้อมกับO_NOFOLLOWแฟล็กที่ถูกต้อง(ที่สนับสนุน) และเปิดไฟล์ตัวให้คำอธิบายสำหรับแต่ละไดเรกทอรีzsh/ bash/ kshไม่ทำเช่นนั้น ดังนั้นในหน้าผู้โจมตีที่สามารถแทนที่ไดเรกทอรีด้วย symlink ในเวลาที่เหมาะสมคุณสามารถจบลงไดเรกทอรีผิด
แม้ว่าfindจะลงไดเรกทอรีได้อย่างถูกต้องด้วย-exec cmd {} \;และมากยิ่งขึ้นเพื่อให้มี-exec cmd {} +ครั้งหนึ่งcmdจะถูกดำเนินการเช่นเป็นcmd ./foo/barหรือcmd ./foo/bar ./foo/bar/bazตามเวลาที่cmdทำให้การใช้./foo/barแอตทริบิวต์ของbarอาจไม่เป็นไปตามเกณฑ์การจับคู่โดยfindแต่ยิ่งแย่ลง./fooอาจจะได้รับ แทนที่ด้วย symlink ไปยังสถานที่อื่น ๆ (และหน้าต่างการแข่งขันนั้นใหญ่กว่ามากโดย-exec {} +ที่findจะรอให้มีไฟล์เพียงพอที่จะโทรออกcmd)
findการใช้งานบางอย่างมีเพรดิเคต (ที่ไม่ได้มาตรฐาน) -execdirเพื่อบรรเทาปัญหาที่สอง
ด้วย:
find . -execdir cmd -- {} \;
find chdir()s cmdลงในไดเรกทอรีแม่ของไฟล์ก่อนที่จะใช้ แทนที่จะเรียกcmd -- ./foo/barมันเรียกcmd -- ./bar( cmd -- barด้วยการใช้งานบางอย่างดังนั้น--) ดังนั้นปัญหา./fooการเปลี่ยนเป็น symlink จะหลีกเลี่ยง สิ่งนี้ทำให้การใช้คำสั่งอย่างrmปลอดภัยยิ่งขึ้น (มันยังสามารถลบไฟล์ที่แตกต่างกัน แต่ไม่ใช่ไฟล์ในไดเรกทอรีอื่น) แต่ไม่ใช่คำสั่งที่อาจแก้ไขไฟล์เว้นแต่ว่าพวกเขาได้รับการออกแบบให้ไม่ทำตาม symlink
-execdir cmd -- {} +บางครั้งยังใช้งานได้ แต่มีหลายการใช้งานรวมทั้งบางรุ่น GNU ก็จะเทียบเท่ากับfind-execdir cmd -- {} \;
-execdir ยังมีประโยชน์ในการแก้ไขปัญหาที่เกี่ยวข้องกับไดเรกทอรีต้นไม้ที่ลึกเกินไป
ใน:
find . -exec cmd {} \;
ขนาดของเส้นทางที่กำหนดcmdจะเพิ่มขึ้นตามความลึกของไดเรกทอรีที่ไฟล์นั้นมีหากขนาดนั้นใหญ่กว่าPATH_MAX(บางอย่างเช่น 4k บน Linux) การเรียกใช้ระบบใด ๆ ที่cmdทำบนเส้นทางนั้นจะล้มเหลวโดยมีENAMETOOLONGข้อผิดพลาด
ด้วย-execdirเพียงชื่อไฟล์ (อาจจะนำหน้าด้วย./) cmdถูกส่งไปยัง ชื่อไฟล์ของตัวเองในระบบไฟล์ส่วนใหญ่มีขีด จำกัด ต่ำกว่ามาก ( NAME_MAX) PATH_MAXดังนั้นENAMETOOLONGข้อผิดพลาดจึงมีโอกาสน้อยกว่าที่จะเกิดขึ้น
ไบต์เทียบกับอักขระ
นอกจากนี้มักจะมองข้ามเมื่อพิจารณาถึงความปลอดภัยรอบ ๆfindและโดยทั่วไปด้วยการจัดการชื่อไฟล์โดยทั่วไปคือความจริงที่ว่าบนระบบ Unix ที่เหมือนกันมากที่สุดชื่อไฟล์เป็นลำดับของไบต์ (ค่าไบต์ใด ๆ แต่ 0 ในเส้นทางไฟล์และในระบบส่วนใหญ่ ASCII ที่ใช้เราจะไม่สนใจ EBCDIC ที่หายากสำหรับตอนนี้) 0x2f เป็นตัวคั่นเส้นทาง
มันขึ้นอยู่กับแอพพลิเคชั่นที่จะตัดสินใจว่าพวกเขาต้องการพิจารณาไบต์เหล่านั้นเป็นข้อความหรือไม่ และโดยทั่วไปแล้วพวกเขาทำ แต่โดยทั่วไปการแปลจากไบต์เป็นอักขระจะทำตามสถานที่ของผู้ใช้ขึ้นอยู่กับสภาพแวดล้อม
หมายความว่าชื่อไฟล์ที่กำหนดอาจมีการแสดงข้อความที่แตกต่างกันขึ้นอยู่กับสถานที่ ยกตัวอย่างเช่นลำดับไบต์63 f4 74 e9 2e 74 78 74จะเป็นcôté.txtสำหรับการประยุกต์ใช้การตีความชื่อไฟล์ว่าในสถานที่ตั้งตัวเป็น ISO-8859-1 หนึ่งและcєtщ.txtในสถานที่ charset เป็น IS0-8859-5 แทน
แย่ลง ในสถานที่ที่ชุดอักขระเป็น UTF-8 (มาตรฐานปัจจุบัน) 63 f4 74 e9 2e 74 78 74 ไม่สามารถแมปกับตัวละครได้!
findเป็นหนึ่งในแอปพลิเคชันดังกล่าวที่พิจารณาว่าชื่อไฟล์เป็นข้อความสำหรับ-name/ ภาค-pathแสดง (และอื่น ๆ เช่น-inameหรือ-regexมีการใช้งานบางอย่าง)
สิ่งที่มีความหมายเช่นนั้นมีหลายfindการใช้งาน (รวมถึง GNU find)
find . -name '*.txt'
จะไม่พบ63 f4 74 e9 2e 74 78 74ไฟล์ของเราด้านบนเมื่อถูกเรียกในโลแคล UTF-8 ว่า*(ซึ่งตรงกับ 0 ตัวอักษรหรือมากกว่าไม่ใช่ไบต์) ไม่สามารถตรงกับที่ไม่ใช่ตัวอักษรเหล่านั้น
LC_ALL=C find... จะแก้ปัญหาได้เนื่องจาก C locale บอกถึงหนึ่งไบต์ต่อตัวอักษรและ (โดยทั่วไป) รับประกันได้ว่าค่าไบต์ทั้งหมดจะจับคู่กับอักขระ (แม้ว่าอาจไม่ได้กำหนดไว้สำหรับบางค่าไบต์)
ตอนนี้เมื่อพูดถึงการวนลูปมากกว่าชื่อไฟล์เหล่านั้นจากเชลล์อักขระไบต์เทียบกับก็อาจกลายเป็นปัญหา โดยทั่วไปเราจะเห็นเปลือกหอย 4 ประเภทหลักในเรื่องนั้น:
dashคนที่ยังไม่ได้หลายไบต์ตระหนักเช่น สำหรับพวกเขาแผนที่ไบต์กับตัวละคร ตัวอย่างเช่นใน UTF-8 côtéคือ 4 ตัวอักษร แต่ 6 ไบต์ ในสถานที่ที่ UTF-8 เป็นชุดอักขระใน
find . -name '????' -exec dash -c '
name=${1##*/}; echo "${#name}"' sh {} \;
findจะค้นหาไฟล์ที่มีชื่อประกอบด้วยอักขระ 4 ตัวที่เข้ารหัสใน UTF-8 ได้สำเร็จ แต่dashจะรายงานความยาวระหว่าง 4 ถึง 24
yash: ตรงข้าม. มันเกี่ยวข้องกับตัวละครเท่านั้น อินพุตทั้งหมดที่ใช้จะถูกแปลเป็นอักขระภายใน มันทำเพื่อเปลือกที่สอดคล้องกันมากที่สุด แต่ก็หมายความว่ามันไม่สามารถรับมือกับลำดับไบต์โดยพลการ (ที่ไม่ได้แปลตัวละครที่ถูกต้อง) แม้แต่ในโลแคล C ก็ไม่สามารถรับมือกับค่าไบต์ที่สูงกว่า 0x7f
find . -exec yash -c 'echo "$1"' sh {} \;
ในโลแคล UTF-8 จะล้มเหลวใน ISO-8859-1 ของเราcôté.txtก่อนหน้านี้เช่น
ผู้ที่ชอบbashหรือzshที่การสนับสนุนหลายไบต์ได้รับการเพิ่มความก้าวหน้า สิ่งเหล่านั้นจะย้อนกลับไปยังการพิจารณาไบต์ที่ไม่สามารถแมปกับตัวละครราวกับว่าพวกเขาเป็นตัวละคร พวกเขายังมีข้อผิดพลาดเล็กน้อยที่นี่และโดยเฉพาะอย่างยิ่งที่มีชุดอักขระแบบหลายไบต์น้อยเช่น GBK หรือ BIG5-HKSCS (ซึ่งค่อนข้างน่ารังเกียจเนื่องจากอักขระหลายไบต์จำนวนมากมีจำนวนไบต์ในช่วง 0-127 (เช่นอักขระ ASCII) )
ผู้ที่ชอบshFreeBSD (อย่างน้อย 11 คน) หรือmksh -o utf8-modeที่รองรับหลายไบต์ แต่สำหรับ UTF-8 เท่านั้น
หมายเหตุ
1เพื่อความสมบูรณ์เราสามารถพูดถึงวิธีแฮ็กในzshการวนลูปไฟล์โดยใช้การวนซ้ำแบบซ้ำโดยไม่เก็บรายการทั้งหมดในหน่วยความจำ:
process() {
something with $REPLY
false
}
: **/*(ND.m-1+process)
+cmdเป็นรอบคัดเลือกที่เรียก glob cmd(โดยปกติจะเป็นฟังก์ชั่น) $REPLYกับเส้นทางของไฟล์ในปัจจุบัน ฟังก์ชันส่งกลับค่าจริงหรือเท็จเพื่อตัดสินใจว่าควรเลือกไฟล์หรือไม่ (และอาจแก้ไข$REPLYหรือคืนค่าหลายไฟล์ใน$replyอาร์เรย์) ที่นี่เราทำการประมวลผลในฟังก์ชั่นนั้นและคืนค่าเท็จเพื่อไม่ได้เลือกไฟล์