เหตุใดการวนซ้ำของผลลัพธ์ที่ไม่เหมาะสมของ find find


170

คำถามนี้ได้รับแรงบันดาลใจจาก

เหตุใดการใช้เชลล์ลูปเพื่อประมวลผลข้อความจึงถือว่าไม่ดี?

ฉันเห็นโครงสร้างเหล่านี้

for file in `find . -type f -name ...`; do smth with ${file}; done

และ

for dir in $(find . -type d -name ...); do smth with ${dir}; done

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

ทำไมวนลูปfindของการส่งออกปฏิบัติไม่ดีและสิ่งที่เป็นวิธีการที่เหมาะสมในการทำงานอย่างน้อยหนึ่งคำสั่งสำหรับแต่ละชื่อไฟล์ / เส้นทางกลับโดยfind?


12
ฉันคิดว่านี่เป็นเหมือน "ไม่แยกวิเคราะห์ ls output!" - คุณสามารถทำสิ่งใดสิ่งหนึ่งบนพื้นฐานที่แน่นอน แต่พวกเขากำลังแฮ็คที่รวดเร็วกว่าคุณภาพการผลิต หรือโดยทั่วไปแล้วจะไม่มีวันดื้อรั้นอย่างแน่นอน
Bruce Ediger


นี่ควรเป็นคำตอบที่ยอมรับได้
Zaid

6
เพราะประเด็นของการค้นหาคือการวนซ้ำสิ่งที่พบ
OrangeDog

2
จุดเสริมหนึ่งจุด - คุณอาจต้องการส่งออกไปยังไฟล์แล้วประมวลผลในภายหลังในสคริปต์ วิธีนี้รายการไฟล์จะสามารถตรวจสอบได้หากคุณต้องการดีบักสคริปต์
user117529

คำตอบ:


87

ปัญหา

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 ประเภทหลักในเรื่องนั้น:

  1. 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

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

    find . -exec yash -c 'echo "$1"' sh {} \;
    

    ในโลแคล UTF-8 จะล้มเหลวใน ISO-8859-1 ของเราcôté.txtก่อนหน้านี้เช่น

  3. ผู้ที่ชอบbashหรือzshที่การสนับสนุนหลายไบต์ได้รับการเพิ่มความก้าวหน้า สิ่งเหล่านั้นจะย้อนกลับไปยังการพิจารณาไบต์ที่ไม่สามารถแมปกับตัวละครราวกับว่าพวกเขาเป็นตัวละคร พวกเขายังมีข้อผิดพลาดเล็กน้อยที่นี่และโดยเฉพาะอย่างยิ่งที่มีชุดอักขระแบบหลายไบต์น้อยเช่น GBK หรือ BIG5-HKSCS (ซึ่งค่อนข้างน่ารังเกียจเนื่องจากอักขระหลายไบต์จำนวนมากมีจำนวนไบต์ในช่วง 0-127 (เช่นอักขระ ASCII) )

  4. ผู้ที่ชอบ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อาร์เรย์) ที่นี่เราทำการประมวลผลในฟังก์ชั่นนั้นและคืนค่าเท็จเพื่อไม่ได้เลือกไฟล์


ถ้า zsh และ bash พร้อมใช้งานคุณอาจจะดีกว่าโดยใช้การสร้างแบบวงกลมและเชลล์แทนการพยายามที่จะใช้findพฤติกรรมอย่างปลอดภัย Globbing มีความปลอดภัยโดยค่าเริ่มต้นในขณะที่การค้นหาไม่ปลอดภัยตามค่าเริ่มต้น
เควิน

@ เควินดูการแก้ไข
Stéphane Chazelas

182

เหตุใดการวนซ้ำfindของการปฏิบัติที่ไม่เหมาะสมของเอาต์พุต

คำตอบง่ายๆคือ:

เพราะชื่อไฟล์สามารถมีตัวละครใด ๆ

ดังนั้นจึงไม่มีตัวอักษรที่พิมพ์ได้ที่คุณสามารถใช้เพื่อกำหนดชื่อไฟล์ได้อย่างน่าเชื่อถือ


ขึ้นบรรทัดใหม่มักจะใช้ (ไม่ถูกต้อง) เพื่อคั่นชื่อไฟล์เนื่องจากเป็นเรื่องผิดปกติที่จะรวมอักขระบรรทัดใหม่ในชื่อไฟล์

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

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

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


มีความเฉพาะเจาะจงมากขึ้น: บนระบบ UNIX หรือ Linux ชื่อไฟล์อาจมีอักขระใด ๆ ยกเว้น/(ซึ่งใช้เป็นตัวคั่นองค์ประกอบพา ธ ) และอาจไม่มีไบต์ว่าง

ไบต์ null จึงเป็นเพียงวิธีที่ถูกต้องเพื่อกำหนดชื่อไฟล์


เนื่องจาก GNU findมีชุดข้อมูล-print0หลักซึ่งจะใช้ไบต์ว่างเพื่อกำหนดชื่อไฟล์ที่พิมพ์ดังนั้น GNU find จึงสามารถใช้กับ GNU ได้อย่างปลอดภัยxargsและ-0ตั้งค่าสถานะ (และ-rตั้งค่าสถานะ) เพื่อจัดการเอาต์พุตของfind:

find ... -print0 | xargs -r0 ...

อย่างไรก็ตามไม่มีเหตุผลที่ดีที่จะใช้แบบฟอร์มนี้เพราะ:

  1. มันเพิ่มการพึ่งพา GNU findutils ซึ่งไม่จำเป็นต้องอยู่ที่นั่นและ
  2. findถูกออกแบบมาเพื่อให้สามารถเรียกใช้คำสั่งในไฟล์ที่พบ

นอกจากนี้ GNU xargsต้องการ-0และ-rในขณะที่ FreeBSD xargsต้องการเพียงแค่-0(และไม่มี-rตัวเลือก) และบางคนxargsไม่สนับสนุน-0เลย ดังนั้นจึงเป็นที่ดีที่สุดเพียงแค่ติดกับคุณลักษณะของ POSIX find(ดูหัวข้อถัดไป) xargsและข้าม

สำหรับจุดที่ 2 - findความสามารถในการเรียกใช้คำสั่งในไฟล์ที่พบ - ฉันคิดว่า Mike Loukides พูดได้ดีที่สุด:

findธุรกิจกำลังประเมินนิพจน์ไม่ใช่ค้นหาไฟล์ ใช่findค้นหาไฟล์อย่างแน่นอน แต่นั่นเป็นเพียงผลข้างเคียงจริงๆ

- เครื่องมือไฟฟ้ายูนิกซ์


POSIX การใช้งานที่ระบุของ find

วิธีที่เหมาะสมในการเรียกใช้คำสั่งอย่างน้อยหนึ่งคำสำหรับfindผลลัพธ์แต่ละรายการคืออะไร

ในการรันคำสั่งเดียวสำหรับไฟล์แต่ละไฟล์ที่พบให้ใช้:

find dirname ... -exec somecommand {} \;

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

find dirname ... -exec somecommand {} \; -exec someothercommand {} \;

ในการรันคำสั่งเดียวในหลายไฟล์พร้อมกันให้ทำดังนี้

find dirname ... -exec somecommand {} +

find ร่วมกับ sh

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

  • อย่าฝังลง{}ในshรหัสโดยตรง วิธีนี้ช่วยให้สามารถเรียกใช้รหัสโดยอำเภอใจจากชื่อไฟล์ที่ออกแบบมาเพื่อประสงค์ร้าย นอกจากนี้จริง ๆ แล้วมันไม่ได้ระบุโดย POSIX ว่ามันจะทำงานได้เลย (ดูประเด็นต่อไป)

  • อย่าใช้{}หลายครั้งหรือใช้เป็นส่วนหนึ่งของอาร์กิวเมนต์ที่ยาวกว่า มันไม่ได้พกพา ตัวอย่างเช่นอย่าทำสิ่งนี้:

    find ... -exec cp {} somedir/{}.bak \;

    หากต้องการอ้างอิงข้อมูลจำเพาะ POSIX สำหรับfind :

    หากยูทิลิตี้ชื่อสตริงหรืออาร์กิวเมนต์ประกอบด้วยอักขระสองตัว "{}" แต่ไม่ใช่แค่อักขระสองตัว "{}" มันจะถูกกำหนดตามการนำไปใช้งานว่าการค้นหาจะแทนที่อักขระสองตัวนั้นหรือใช้สตริงโดยไม่มีการเปลี่ยนแปลง

    ... หากมีมากกว่าหนึ่งอาร์กิวเมนต์ที่มีอักขระสองตัว "{}" ปรากฏขึ้นพฤติกรรมจะไม่ได้รับการระบุ

  • ข้อโต้แย้งต่อไปนี้สตริงคำสั่งเปลือกส่งผ่านไปยัง-cตัวเลือกที่มีการตั้งค่าพารามิเตอร์เปลือกตำแหน่งที่เริ่มต้นด้วย $0$1ไม่ได้เริ่มต้นด้วย

    ด้วยเหตุนี้จึงเป็นการดีที่จะรวมค่า "จำลอง" $0เช่นfind-shซึ่งจะใช้สำหรับการรายงานข้อผิดพลาดจากภายในเปลือกหอยที่วางไข่ นอกจากนี้จะช่วยให้การใช้งานของโครงสร้างเช่น"$@"เมื่อผ่านหลายไฟล์ไปยังเปลือกในขณะที่ถนัดค่าสำหรับ$0จะหมายถึงไฟล์แรกผ่านจะถูกตั้งค่าและทำให้ไม่รวมอยู่ใน$0"$@"


หากต้องการรันคำสั่งเชลล์เดี่ยวต่อไฟล์ให้ใช้:

find dirname ... -exec sh -c 'somecommandwith "$1"' find-sh {} \;

อย่างไรก็ตามโดยทั่วไปแล้วจะให้ประสิทธิภาพที่ดีกว่าในการจัดการไฟล์ใน shell loop เพื่อให้คุณไม่วางไข่ shell สำหรับทุก ๆ ไฟล์ที่พบ:

find dirname ... -exec sh -c 'for f do somecommandwith "$f"; done' find-sh {} +

(โปรดทราบว่าfor f doเทียบเท่าfor f in "$@"; doและจัดการพารามิเตอร์ตำแหน่งแต่ละตำแหน่งตามลำดับ - อีกนัยหนึ่งคือใช้แต่ละไฟล์ที่ค้นพบโดยfindไม่คำนึงถึงอักขระพิเศษใด ๆ ในชื่อของพวกเขา)


ตัวอย่างเพิ่มเติมของfindการใช้งานที่ถูกต้อง:

(หมายเหตุ: อย่าลังเลที่จะขยายรายการนี้)


5
มีกรณีหนึ่งที่ฉันไม่รู้ทางเลือกในการแยกวิเคราะห์findเอาต์พุต - ซึ่งคุณต้องรันคำสั่งในเชลล์ปัจจุบัน (เช่นเพราะคุณต้องการตั้งค่าตัวแปร) สำหรับแต่ละไฟล์ ในกรณีนี้while IFS= read -r -u3 -d '' file; do ... done 3< <(find ... -print0)เป็นสำนวนที่ดีที่สุดที่ฉันรู้ หมายเหตุ: <( )ไม่ใช่อุปกรณ์พกพา - ใช้ bash หรือ zsh นอกจากนี้ยังมี-u3และ3<ในกรณีที่มีอะไรในวงพยายามที่จะอ่าน stdin
Gordon Davisson

1
@GordonDavisson บางที - แต่คุณต้องการตั้งค่าตัวแปรเหล่านั้นเพื่ออะไร ฉันจะยืนยันว่าสิ่งที่มันควรจะจัดการในการfind ... -execโทร หรือเพียงแค่ใช้เปลือก glob ถ้ามันจะจัดการกรณีการใช้งานของคุณ
Wildcard

1
ฉันมักจะต้องการพิมพ์ข้อมูลสรุปหลังจากประมวลผลไฟล์ ("แปลง 2 ครั้ง, ข้าม 3 ครั้ง, ไฟล์ต่อไปนี้มีข้อผิดพลาด: ... ") และจำนวน / รายการเหล่านั้นต้องถูกสะสมในตัวแปรเชลล์ นอกจากนี้ยังมีสถานการณ์ที่ฉันต้องการสร้างอาร์เรย์ของชื่อไฟล์เพื่อให้ฉันสามารถทำสิ่งที่ซับซ้อนกว่าซ้ำตามลำดับ (ในกรณีของมันfilelist=(); while ... do filelist+=("$file"); done ...)
Gordon Davisson

3
คำตอบของคุณถูกต้อง อย่างไรก็ตามฉันไม่ชอบความเชื่อ ถึงแม้ว่าผมจะรู้ดีกว่ามีหลาย (interactive เป็นพิเศษ) กรณีการใช้งานที่เป็นที่ปลอดภัยและง่ายขึ้นเพียงแค่พิมพ์วนลูปกับการส่งออกหรือแม้แต่เลวใช้find lsฉันทำสิ่งนี้ทุกวันโดยไม่มีปัญหา ฉันรู้เกี่ยวกับ -print0, --null, -z หรือ -0 ตัวเลือกของเครื่องมือทุกชนิด แต่ฉันจะไม่เสียเวลาที่จะใช้มันใน shell prompt ของฉันเว้นแต่จำเป็นจริงๆ นี่อาจเป็นข้อสังเกตในคำตอบของคุณ
rudimeier

16
@rudimeier ข้อโต้แย้งเกี่ยวกับความเชื่อกับแนวปฏิบัติที่ดีที่สุดได้ถูกทำให้เป็นตายแล้ว ไม่สนใจ. หากคุณใช้มันแบบโต้ตอบและใช้งานได้ดีและดีสำหรับคุณ - แต่ฉันจะไม่ส่งเสริมการทำเช่นนั้น เปอร์เซ็นต์ของผู้เขียนสคริปต์ที่สนใจที่จะเรียนรู้ว่าโค้ดที่มีประสิทธิภาพคืออะไรและทำอย่างนั้นเมื่อเขียนสคริปต์ที่ใช้งานจริงแทนที่จะทำสิ่งที่พวกเขาเคยทำในเชิงโต้ตอบนั้นมีน้อยมาก การจัดการคือการส่งเสริมการปฏิบัติที่ดีที่สุดตลอดเวลา ผู้คนจำเป็นต้องเรียนรู้ว่ามีวิธีที่ถูกต้องในการทำสิ่งต่าง ๆ
Wildcard

10

คำตอบนี้มีไว้สำหรับชุดผลลัพธ์ที่มีขนาดใหญ่มากและคำนึงถึงประสิทธิภาพเป็นหลักเช่นเมื่อรับรายการไฟล์ผ่านเครือข่ายที่ช้า สำหรับไฟล์จำนวนเล็กน้อย (พูดได้ 100 หรืออาจเป็น 1,000 ในโลคัลดิสก์) ส่วนใหญ่เป็น moot

การใช้งานแบบขนานและหน่วยความจำ

นอกเหนือจากคำตอบอื่น ๆ ที่ได้รับเกี่ยวข้องกับปัญหาการแยกและยังมีปัญหาอื่นอีกด้วย

for file in `find . -type f -name ...`; do smth with ${file}; done

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

วิธียูนิกซ์ที่ต้องการคือการทำงานกับไพพ์ซึ่งทำงานแบบขนานโดยเนื้อแท้และไม่ต้องการบัฟเฟอร์ขนาดใหญ่โดยพลการโดยทั่วไป นั่นหมายความว่าคุณจะชอบมากสำหรับfindการทำงานแบบขนานที่คุณsmthและเพียง smthแต่ให้ชื่อไฟล์ปัจจุบันในแรมในขณะที่มันมือที่ออกไป

ทางออกหนึ่งอย่างน้อยส่วนหนึ่ง OKish find -exec smthสำหรับที่อยู่ดังกล่าว มันไม่จำเป็นต้องเก็บชื่อไฟล์ทั้งหมดไว้ในหน่วยความจำและทำงานได้อย่างขนาน น่าเสียดายที่มันยังเริ่มหนึ่งsmthกระบวนการต่อไฟล์ หากsmthสามารถใช้ได้กับไฟล์เดียวนั่นก็คือวิธีที่มันเป็น

ถ้าเป็นไปได้ทุกทางออกที่ดีที่สุดจะfind -print0 | smthมีsmthความสามารถในการประมวลผลชื่อไฟล์บน STDIN ของมัน จากนั้นคุณมีเพียงsmthกระบวนการเดียวไม่ว่าจะมีไฟล์กี่ไฟล์ก็ตามและคุณต้องบัฟเฟอร์เพียงเล็กน้อยในจำนวนไบต์ (ไม่ว่าการบัฟเฟอร์ไพน์ภายในจะเกิดขึ้น) ระหว่างสองกระบวนการ แน่นอนว่านี่เป็นสิ่งที่ไม่สมจริงถ้าsmthเป็นคำสั่ง Unix / POSIX มาตรฐาน แต่อาจเป็นวิธีการที่คุณเขียนด้วยตัวเอง

หากเป็นไปไม่ได้นั่นก็find -print0 | xargs -0 smthเป็นหนึ่งในทางออกที่ดีกว่า ตามที่ @ dave_thompson_085 ที่กล่าวถึงในความคิดเห็นxargsแยกข้อโต้แย้งข้ามการทำงานหลายครั้งsmthเมื่อถึงขีด จำกัดของระบบ (โดยค่าเริ่มต้นในช่วง 128 KB หรือขีด จำกัด ใด ๆ ที่กำหนดโดยexecระบบ) และมีตัวเลือกที่จะมีอิทธิพลต่อจำนวน ไฟล์ถูกกำหนดให้หนึ่งการเรียกใช้smthดังนั้นการค้นหาสมดุลระหว่างจำนวนsmthกระบวนการและการหน่วงเวลาเริ่มต้น

แก้ไข: ลบความคิดของ "ดีที่สุด" - มันยากที่จะพูดว่าสิ่งที่ดีกว่าจะครอบตัด ;)


find ... -exec smth {} +เป็นทางออก
Wildcard

find -print0 | xargs smthไม่ทำงานเลย แต่find -print0 | xargs -0 smth(หมายเหตุ-0) หรือfind | xargs smthถ้าชื่อไฟล์ไม่ได้มีคำพูดหรือช่องว่างทับขวาทำงานหนึ่งsmthที่มีชื่อไฟล์จำนวนมากที่สุดเท่าที่มีอยู่และพอดีในรายการอาร์กิวเมนต์หนึ่ง ; หากคุณเกิน maxargs มันจะทำงานsmthหลาย ๆ ครั้งตามที่จำเป็นเพื่อจัดการ args ทั้งหมดที่กำหนด (ไม่ จำกัด ) คุณสามารถตั้งค่าขนาดเล็ก 'ชิ้น' (จึงค่อนข้างก่อนหน้านี้ขนาน) -L/--max-lines -n/--max-args -s/--max-charsด้วย
dave_thompson_085


4

เหตุผลหนึ่งคือช่องว่างพ่นเครื่องมือในงานทำให้ไฟล์ 'foo bar' ได้รับการประเมินว่า 'foo' และ 'bar'

$ ls -l
-rw-rw-r-- 1 ec2-user ec2-user 0 Nov  7 18:24 foo bar
$ for file in `find . -type f` ; do echo filename $file ; done
filename ./foo
filename bar
$

ใช้งานได้ดีถ้า -exec ใช้แทน

$ find . -type f -exec echo filename {} \;
filename ./foo bar
$ find . -type f -exec stat {} \;
  File: ‘./foo bar’
  Size: 0               Blocks: 0          IO Block: 4096   regular empty file
Device: ca01h/51713d    Inode: 9109        Links: 1
Access: (0664/-rw-rw-r--)  Uid: (  500/ec2-user)   Gid: (  500/ec2-user)
Access: 2016-11-07 18:24:42.027554752 +0000
Modify: 2016-11-07 18:24:42.027554752 +0000
Change: 2016-11-07 18:24:42.027554752 +0000
 Birth: -
$

โดยเฉพาะอย่างยิ่งในกรณีที่findมีตัวเลือกในการรันคำสั่งในทุกไฟล์มันเป็นตัวเลือกที่ดีที่สุด
Centimane

1
ลองพิจารณา-exec ... {} \;เทียบกับ-exec ... {} +
thrig

1
ถ้าคุณใช้for file in "$(find . -type f)" และecho "${file}"จากนั้นก็ทำงานได้แม้จะมีช่องว่าง, อักขระพิเศษอื่น ๆ ผมคิดว่าทำให้เกิดปัญหามากขึ้นแม้ว่า
mazs

9
@ มาส - ไม่การอ้างอิงไม่ได้ทำในสิ่งที่คุณคิด ในไดเรกทอรีกับหลายแฟ้มลองfor file in "$(find . -type f)";do printf '%s %s\n' name: "${file}";doneที่ควร (ตามที่คุณ) name:พิมพ์ชื่อไฟล์แต่ละบรรทัดที่แยกต่างหากนำหน้าด้วย มันไม่ได้
don_crissti

2

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

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

ตัวอย่างทุบตี:

shopt -s nullglob globstar
for i in **
do
    echo «"$i"»
done

เหมือนกันในปลา:

for i in **
    echo «$i»
end

หากคุณต้องการคุณสมบัติของfindให้แน่ใจว่าจะแยกเฉพาะใน NUL (เช่นfind -print0 | xargs -r0สำนวน)

ปลาสามารถย้ำเอาท์พุทที่คั่นด้วย NUL ดังนั้นอันนี้จริง ๆ แล้วไม่เลว:

find -print0 | while read -z i
    echo «$i»
end

ในฐานะที่เป็น gotcha เล็ก ๆ น้อย ๆ สุดท้ายในเชลล์จำนวนมาก (ไม่ใช่ Fish แน่นอน) การวนลูปมากกว่าคำสั่งจะทำให้ลูปย่อยเป็นsubshell (หมายถึงคุณไม่สามารถตั้งค่าตัวแปรในลักษณะใด ๆ ที่มองเห็นได้หลังจากลูปสิ้นสุด) ซึ่งเป็น ไม่เคยสิ่งที่คุณต้องการ


@don_crissti แม่นยำ มันไม่ได้โดยทั่วไปทำงาน ฉันพยายามที่จะประชดประชันโดยบอกว่า "ใช้งานได้" (พร้อมเครื่องหมายคำพูด)
user2394284

โปรดทราบว่าการวนซ้ำแบบซ้ำเกิดขึ้นในzshช่วงต้นยุค 90 (แม้ว่าคุณต้องการ**/*) fishเช่นเดียวกับการใช้งานก่อนหน้านี้ของคุณสมบัติที่เทียบเท่าของ bash นั้นจะตามด้วย symlink เมื่อเลื่อนลงมาที่แผนผังไดเรกทอรี ดูผลลัพธ์ของ ls *, ls ** และ ls ***สำหรับความแตกต่างระหว่างการนำไปใช้งาน
Stéphane Chazelas

1

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

TLDR / cbf: find | parallel stuff

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