การทำความเข้าใจกับตัวเลือก -exec ของ `find '


53

ฉันพบว่าตัวเองกำลังค้นหาไวยากรณ์ของ

find . -name "FILENAME"  -exec rm {} \;

ส่วนใหญ่เป็นเพราะฉันไม่เห็นว่า-execชิ้นส่วนทำงานอย่างไร เครื่องหมายวงเล็บคือเครื่องหมายแบ็กสแลชและเครื่องหมายอัฒภาคคืออะไร มีกรณีการใช้งานอื่นสำหรับไวยากรณ์นั้นหรือไม่


11
@Phippippos: ฉันเห็นจุดของคุณ โปรดจำไว้ว่า man pages เป็นข้อมูลอ้างอิงเช่นมีประโยชน์สำหรับผู้ที่มีความเข้าใจในเรื่องการค้นหาไวยากรณ์ สำหรับคนใหม่ในหัวข้อพวกเขามักจะเป็นความลับและเป็นทางการที่จะเป็นประโยชน์ คุณจะพบว่าคำตอบที่ได้รับการยอมรับนั้นมีความยาวประมาณ 10 เท่าของการป้อน man page และนั่นก็เป็นเหตุผล
Zsolt Szilagy

6
แม้แต่manหน้าPOSIX เก่าก็อ่านยูทิลิตี้ชื่อหรืออาร์กิวเมนต์ที่มีเพียงอักขระสองตัว "{}" เท่านั้นที่จะถูกแทนที่ด้วยชื่อพา ธ ปัจจุบันซึ่งดูเหมือนว่าจะไม่เหมาะสมสำหรับฉัน นอกจากนี้ยังมีตัวอย่างด้วย-exec rm {} \;เช่นเดียวกับในคำถามของคุณ ในสมัยของฉันแทบจะไม่มีทรัพยากรอื่นใดนอกเหนือจาก "กำแพงสีเทาขนาดใหญ่" ซึ่งเป็นหนังสือของmanหน้ากระดาษที่พิมพ์ออกมา ดังนั้นฉันรู้ว่านี่เพียงพอสำหรับคนใหม่ในหัวข้อ แม้ว่าคำถามสุดท้ายของคุณจะถามที่นี่ @Kusalananda และตัวฉันเองไม่มีคำตอบสำหรับเรื่องนี้
Philippos

1
Comeon @Phippippos คุณกำลังบอก Kusalananda จริง ๆ ว่าเขาไม่ได้ปรับปรุง manpage หรือไม่? :-)
Zsolt Szilagy

1
@allo แม้ว่าxargsบางครั้งก็มีประโยชน์ แต่findสามารถส่งอาร์กิวเมนต์หลายพา ธ ไปยังคำสั่งโดยไม่ต้องใช้ -exec command... {} +(โดยมี+แทนที่จะเป็น\;) ผ่านเส้นทางมากcommand...ที่สุดเท่าที่จะเหมาะสม (แต่ละระบบปฏิบัติการมีข้อ จำกัด ของตัวเองว่าจะสามารถใช้บรรทัดคำสั่งได้นานเท่าใด) และชอบxargsที่+รูปแบบของ -terminated findของ-execการดำเนินการนอกจากนี้ยังจะทำงานcommand...หลายครั้งในกรณีที่หายากว่ามีเส้นทางมากเกินไปที่จะพอดีกับขีด จำกัด
Eliah Kagan

2
@ZsoltSzilagy ฉันไม่ได้บอกว่าไม่ได้หมายความว่า เขาเลี้ยงดูคุณได้ดีมากฉันแค่คิดว่าคุณแก่พอที่จะกินด้วยตัวเอง (-;
Philippos

คำตอบ:


90

คำตอบนี้มาในส่วนต่าง ๆ ต่อไปนี้:

  • การใช้งานขั้นพื้นฐานของ -exec
  • ใช้-execร่วมกับsh -c
  • การใช้ -exec ... {} +
  • การใช้ -execdir

การใช้งานขั้นพื้นฐานของ -exec

-execตัวเลือกที่จะใช้เวลาสาธารณูปโภคภายนอกที่มีข้อโต้แย้งที่ไม่จำเป็นเป็นอาร์กิวเมนต์และรัน

หากสตริง{}มีอยู่ที่ใดก็ได้ในคำสั่งที่กำหนดแต่ละอินสแตนซ์ของมันจะถูกแทนที่ด้วยชื่อพา ธ ที่กำลังดำเนินการในขณะนี้ (เช่น./some/path/FILENAME) ในเชลล์ส่วนใหญ่{}ไม่จำเป็นต้องอ้างอิงอักขระสองตัว

คำสั่งจะต้องถูกยกเลิกด้วย;เพื่อfindให้ทราบว่ามันจะจบลงที่ใด (เนื่องจากอาจมีตัวเลือกเพิ่มเติมภายหลัง) เพื่อปกป้อง;เชลล์จากนั้นจะต้องมีการยกมาเป็น\;หรือ';'มิฉะนั้นเชลล์จะเห็นว่ามันเป็นจุดสิ้นสุดของfindคำสั่ง

ตัวอย่าง ( \ในตอนท้ายของสองบรรทัดแรกนั้นมีไว้เพื่อต่อเนื่องของบรรทัด):

find . -type f -name '*.txt'      \
   -exec grep -q 'hello' {} ';'   \
   -exec cat {} ';'

นี้จะค้นหาไฟล์ปกติทั้งหมด ( -type f) ที่มีชื่อตรงกับรูปแบบ*.txtในหรือด้านล่างไดเรกทอรีปัจจุบัน จากนั้นจะทดสอบว่าสตริงhelloเกิดขึ้นในไฟล์ใด ๆ ที่พบโดยใช้grep -q(ซึ่งไม่ได้สร้างเอาต์พุตใด ๆ เพียงแค่สถานะออก) สำหรับไฟล์ที่มีสตริงcatจะถูกดำเนินการเพื่อส่งออกเนื้อหาของไฟล์ไปยังสถานี

แต่ละคน-execยังทำหน้าที่เหมือน "การทดสอบ" ในชื่อพา ธ ที่พบfindเช่นเดียวกับ-typeและ-nameไม่ หากคำสั่งส่งคืนสถานะจบการทำงานเป็นศูนย์ (หมายถึง "สำเร็จ") ส่วนถัดไปของfindคำสั่งจะถูกพิจารณามิฉะนั้นfindคำสั่งจะดำเนินการกับชื่อพา ธ ถัดไป ใช้ในตัวอย่างด้านบนเพื่อค้นหาไฟล์ที่มีสตริงhelloแต่ไม่ต้องสนใจไฟล์อื่นทั้งหมด

ตัวอย่างข้างต้นแสดงให้เห็นถึงสองกรณีการใช้งานที่พบบ่อยที่สุดของ-exec:

  1. เป็นการทดสอบเพื่อ จำกัด การค้นหาเพิ่มเติม
  2. หากต้องการดำเนินการบางอย่างกับชื่อพา ธ ที่พบ (โดยทั่วไป แต่ไม่จำเป็นในตอนท้ายของfindคำสั่ง)

ใช้-execร่วมกับsh -c

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

ถ้าbashคุณสมบัติจะต้องแล้วใช้ในสถานที่ของbash -csh -c

sh -cทำงาน/bin/shกับสคริปต์ที่กำหนดในบรรทัดคำสั่งตามด้วยอาร์กิวเมนต์บรรทัดคำสั่งเผื่อเลือกสำหรับสคริปต์นั้น

ตัวอย่างง่ายๆของการใช้งานsh -cโดยไม่มีfind:

sh -c 'echo  "You gave me $1, thanks!"' sh "apples"

สิ่งนี้จะส่งผ่านสองอาร์กิวเมนต์ไปยังสคริปต์เปลือกลูก:

  1. shสตริง สิ่งนี้จะพร้อมใช้งาน$0ภายในสคริปต์และหากเชลล์ภายในแสดงข้อความแสดงข้อผิดพลาดมันจะขึ้นต้นด้วยสตริงนี้

  2. อาร์กิวเมนต์applesสามารถใช้ได้เป็น$1ในสคริปต์และเคยมีการขัดแย้งมากขึ้นแล้วเหล่านี้จะได้รับการบริการเป็น$2, $3ฯลฯ และยังจะมีในรายการ"$@"(ยกเว้น$0ซึ่งจะไม่เป็นส่วนหนึ่งของ"$@")

นี้จะเป็นประโยชน์ในการทำงานร่วมกับ-execมันช่วยให้เราเพื่อให้สคริปต์ที่ซับซ้อนโดยพลที่ทำหน้าที่ใน pathnames findที่พบโดย

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

from=text  #  Find files that have names like something.text
to=txt     #  Change the .text suffix to .txt

find . -type f -name "*.$from" -exec sh -c 'mv "$3" "${3%.$1}.$2"' sh "$from" "$to" {} ';'

ภายในสคริปต์ภายใน$1จะเป็นสตริงtext, $2จะเป็นสตริงtxtและ$3จะเป็นสิ่งที่ชื่อพา ธfindได้พบสำหรับเรา การขยายพารามิเตอร์${3%.$1}จะใช้ชื่อพา ธ และลบคำต่อท้าย.textออกจากมัน

หรือใช้dirname/ basename:

find . -type f -name "*.$from" -exec sh -c '
    mv "$3" "$(dirname "$3")/$(basename "$3" ".$1").$2"' sh "$from" "$to" {} ';'

หรือด้วยตัวแปรเพิ่มเติมในสคริปต์ภายใน:

find . -type f -name "*.$from" -exec sh -c '
    from=$1; to=$2; pathname=$3
    mv "$pathname" "$(dirname "$pathname")/$(basename "$pathname" ".$from").$to"' sh "$from" "$to" {} ';'

โปรดทราบว่าในรูปแบบที่เปลี่ยนแปลงล่าสุดนี้ตัวแปรfromและtoในเปลือกเด็กจะแตกต่างจากตัวแปรที่มีชื่อเดียวกันในสคริปต์ภายนอก

ข้างต้นเป็นวิธีที่ถูกต้องของการเรียกสคริปต์ที่ซับซ้อนโดยพลการจากที่มี-exec findใช้findในวงเหมือน

for pathname in $( find ... ); do

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

ดูสิ่งนี้ด้วย:


การใช้ -exec ... {} +

ในตอนท้ายอาจถูกแทนที่ด้วย; +ซึ่งทำให้findการดำเนินการคำสั่งที่กำหนดด้วยอาร์กิวเมนต์มากที่สุด (พบชื่อพา ธ ) เป็นไปได้มากกว่าหนึ่งครั้งสำหรับแต่ละพา ธ ที่พบ สตริง{} จะต้องเกิดขึ้นก่อนที่สิ่ง+นี้จะใช้งานได้

find . -type f -name '*.txt' \
   -exec grep -q 'hello' {} ';' \
   -exec cat {} +

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

find . -type f -name "*.txt" \
   -exec grep -q "hello" {} ';' \
   -exec mv -t /tmp/files_with_hello/ {} +

เช่นเดียวกันที่นี่mvจะถูกประหารชีวิตสองสามครั้งเท่าที่จะทำได้ ตัวอย่างสุดท้ายนี้ต้องใช้ GNU mvจาก coreutils (ซึ่งรองรับ-tตัวเลือก)

การใช้-exec sh -c ... {} +เป็นวิธีที่มีประสิทธิภาพในการวนลูปมากกว่าชุดของชื่อพา ธ ด้วยสคริปต์ที่ซับซ้อนโดยพลการ

ข้อมูลเบื้องต้นเหมือนกับเมื่อใช้-exec sh -c ... {} ';'แต่ตอนนี้สคริปต์ใช้รายการอาร์กิวเมนต์ที่ยาวกว่ามาก สิ่งเหล่านี้สามารถวนซ้ำโดยวนซ้ำ"$@"ภายในสคริปต์

ตัวอย่างจากส่วนสุดท้ายที่เปลี่ยนคำต่อท้ายชื่อไฟล์:

from=text  #  Find files that have names like something.text
to=txt     #  Change the .text suffix to .txt

find . -type f -name "*.$from" -exec sh -c '
    from=$1; to=$2
    shift 2  # remove the first two arguments from the list
             # because in this case these are *not* pathnames
             # given to us by find
    for pathname do  # or:  for pathname in "$@"; do
        mv "$pathname" "${pathname%.$from}.$to"
    done' sh "$from" "$to" {} +

การใช้ -execdir

นอกจากนี้ยังมี-execdir(ใช้งานโดยfindตัวแปรส่วนใหญ่แต่ไม่ใช่ตัวเลือกมาตรฐาน)

นี้ทำงานเหมือน-execมีความแตกต่างที่ว่าคำสั่งของเชลล์ที่ได้รับจะถูกดำเนินการกับไดเรกทอรีของพา ธ ที่พบเป็นไดเรกทอรีการทำงานในปัจจุบันและที่{}จะมี basename ของพา ธ ที่พบโดยไม่ต้องเส้นทางของมัน ( แต่ GNU findจะยังคงนำหน้า basename ด้วย./ในขณะที่ BSD findจะไม่ทำอย่างนั้น)

ตัวอย่าง:

find . -type f -name '*.txt' \
    -execdir mv {} done-texts/{}.done \;

นี้จะย้ายพบกัน*.txtแฟ้ม: การที่มีอยู่ก่อนdone-textsไดเรกทอรีย่อยในไดเรกทอรีเดียวกันเป็นที่ไฟล์นั้นถูกพบ ไฟล์ที่จะได้รับการเปลี่ยนชื่อโดยการเพิ่มคำต่อท้าย.doneไป

นี่อาจเป็นเรื่องที่ยุ่งยากกว่า-execเพราะเราจะต้องได้รับชื่อไฟล์ที่ค้นพบออกมา{}เพื่อสร้างชื่อใหม่ของไฟล์ นอกจากนี้เรายังต้องการชื่อไดเรกทอรีจาก{}เพื่อค้นหาdone-textsไดเรกทอรีอย่างถูกต้อง

ด้วย-execdirบางสิ่งเช่นนี้จะง่ายขึ้น

การดำเนินการที่สอดคล้องกันโดยใช้-execแทนที่จะ-execdirต้องใช้เปลือกลูก:

find . -type f -name '*.txt' -exec sh -c '
    for name do
        mv "$name" "$( dirname "$name" )/done-texts/$( basename "$name" ).done"
    done' sh {} +

หรือ,

find . -type f -name '*.txt' -exec sh -c '
    for name do
        mv "$name" "${name%/*}/done-texts/${name##*/}.done"
    done' sh {} +

7
-execใช้โปรแกรมและการขัดแย้งและรันมัน; คำสั่งเชลล์บางคำสั่งประกอบด้วยโปรแกรมและอาร์กิวเมนต์เพียงอย่างเดียว แต่หลายคำสั่งไม่มี คำสั่งเชลล์สามารถรวมการเปลี่ยนเส้นทางและการไพพ์ -execไม่สามารถ (แม้ว่าจะfindสามารถเปลี่ยนเส้นทางทั้งหมด) คำสั่งเชลล์สามารถใช้; && ifฯลฯ -execไม่สามารถถึงแม้ว่าจะ-a -oสามารถทำได้ คำสั่ง shell สามารถเป็นฟังก์ชัน alias หรือ shell หรือ builtin; -execไม่ได้. คำสั่งเชลล์สามารถขยาย vars; -execไม่สามารถ (แม้ว่าเปลือกนอกที่รันfindกระป๋อง) คำสั่ง shell สามารถทดแทนได้$(command)ในแต่ละครั้ง -execไม่ได้. ...
dave_thompson_085

... คำสั่ง shell สามารถทำให้กลมกลืนกัน-execไม่ได้ - แม้ว่าfindสามารถวนซ้ำไฟล์ในแบบเดียวกับที่ globs ส่วนใหญ่ต้องการดังนั้นจึงไม่ค่อยต้องการ
dave_thompson_085

@ dave_thompson_085 แน่นอนว่าคำสั่ง shell สามารถเป็นของshตัวเองซึ่งสามารถทำสิ่งต่าง ๆ เหล่านี้ได้อย่างสมบูรณ์
Tavian Barnes

2
สมมติว่ามันเป็นคำสั่งของเชลล์ผิดที่นี่find -exec cmd arg \;ไม่ได้เรียกเชลล์เพื่อตีความบรรทัดคำสั่งของเชลล์มันทำงานexeclp("cmd", "arg")โดยตรงไม่ใช่execlp("sh", "-c", "cmd arg")(ซึ่งเชลล์จะจบลงด้วยการทำเทียบเท่าexeclp("cmd", "arg")ถ้าcmdไม่ได้อยู่ในตัว)
Stéphane Chazelas

2
คุณสามารถชี้แจงได้ว่าfindข้อโต้แย้งทั้งหมดหลังจาก-execและขึ้น;หรือ+สร้างคำสั่งเพื่อดำเนินการพร้อมกับข้อโต้แย้งโดยแต่ละ{}ข้อโต้แย้งจะถูกแทนที่ด้วยไฟล์ปัจจุบัน (ด้วย;) และ{}เป็นอาร์กิวเมนต์สุดท้ายก่อนที่จะ+ถูกแทนที่ด้วยรายการไฟล์ เป็นอาร์กิวเมนต์ที่แยกต่างหาก (ใน{} +กรณี) IOW -execใช้เวลาหลายข้อโต้แย้งยกเลิกโดยหรือ; {} +
Stéphane Chazelas
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.