ปัญหา
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) )
ผู้ที่ชอบsh
FreeBSD (อย่างน้อย 11 คน) หรือmksh -o utf8-mode
ที่รองรับหลายไบต์ แต่สำหรับ UTF-8 เท่านั้น
หมายเหตุ
1เพื่อความสมบูรณ์เราสามารถพูดถึงวิธีแฮ็กในzsh
การวนลูปไฟล์โดยใช้การวนซ้ำแบบซ้ำโดยไม่เก็บรายการทั้งหมดในหน่วยความจำ:
process() {
something with $REPLY
false
}
: **/*(ND.m-1+process)
+cmd
เป็นรอบคัดเลือกที่เรียก glob cmd
(โดยปกติจะเป็นฟังก์ชั่น) $REPLY
กับเส้นทางของไฟล์ในปัจจุบัน ฟังก์ชันส่งกลับค่าจริงหรือเท็จเพื่อตัดสินใจว่าควรเลือกไฟล์หรือไม่ (และอาจแก้ไข$REPLY
หรือคืนค่าหลายไฟล์ใน$reply
อาร์เรย์) ที่นี่เราทำการประมวลผลในฟังก์ชั่นนั้นและคืนค่าเท็จเพื่อไม่ได้เลือกไฟล์