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


290

ใครก็ได้โปรดให้รหัสที่จะทำต่อไปนี้: สมมติว่ามีไดเรกทอรีของไฟล์ทั้งหมดที่จะต้องทำงานผ่านโปรแกรม โปรแกรมจะแสดงผลลัพธ์ออกมาเป็นมาตรฐาน ฉันต้องการสคริปต์ที่จะเข้าไปในไดเรกทอรีรันคำสั่งในแต่ละไฟล์และต่อผลลัพธ์เป็นไฟล์ขนาดใหญ่ไฟล์เดียว

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

$ cmd [option] [filename] > results.out

3
ฉันอยากจะเพิ่มคำถาม มันสามารถทำได้โดยใช้ xargs? เช่น ls <directory> | xargs cmd [options] {filenames put in here automatically by xargs} [more arguments] > results.out
Ozair Kafray

2
มันสามารถ แต่คุณอาจไม่ต้องการที่จะใช้lsxargsกับไดรฟ์ ถ้าcmdเป็นที่ทุกคนเขียน competently cmd <wildcard>บางทีคุณก็สามารถทำได้
tripleee

คำตอบ:


425

รหัสทุบตีต่อไปนี้จะส่ง $ file ไปยังคำสั่งที่ $ file จะเป็นตัวแทนของทุกไฟล์ใน / dir

for file in /dir/*
do
  cmd [option] "$file" >> results.out
done

ตัวอย่าง

el@defiant ~/foo $ touch foo.txt bar.txt baz.txt
el@defiant ~/foo $ for i in *.txt; do echo "hello $i"; done
hello bar.txt
hello baz.txt
hello foo.txt

23
หากไม่มีไฟล์อยู่/dir/แสดงว่าลูปยังคงทำงานหนึ่งครั้งด้วยค่า '*' สำหรับ$fileซึ่งอาจไม่พึงประสงค์ เพื่อหลีกเลี่ยงปัญหานี้ให้เปิดใช้งาน nullglob ในช่วงระยะเวลาของการวนซ้ำ เพิ่มบรรทัดนี้ก่อนที่จะห่วงและสายนี้หลังจากที่วงshopt -s nullglob shopt -u nullglob #revert nullglob back to it's normal default state
Stew-au

43
+1 และมันก็ทำให้ฉันเสียค่าใช้จ่ายในการสะสมภาพพื้นหลัง ทุกคนหลังจากฉันใช้ doublequotes "$ file"
Behrooz

หากไฟล์ที่ส่งออกจะเหมือนกันภายในวงก็มีประสิทธิภาพมากขึ้นในการเปลี่ยนเส้นทางนอกวงdone >results.out(และอาจจะแล้วคุณสามารถเขียนทับแทนการผนวกเช่นฉันได้สันนิษฐานที่นี่)
tripleee

คุณจะรับไฟล์ผลลัพธ์แต่ละไฟล์ที่กำหนดชื่อเองให้กับไฟล์อินพุตได้อย่างไร
Timothy Swan

1
ระวังด้วยการใช้คำสั่งนี้สำหรับไฟล์จำนวนมากใน dir ใช้ find -exec แทน
kolisko

182

เกี่ยวกับสิ่งนี้:

find /some/directory -maxdepth 1 -type f -exec cmd option {} \; > results.out
  • -maxdepth 1อาร์กิวเมนต์ป้องกันการค้นหาไม่ให้ซ้ำลงในไดเรกทอรีย่อยใด ๆ (หากคุณต้องการให้ไดเรกทอรีที่ซ้อนกันประมวลผลคุณสามารถละเว้นสิ่งนี้ได้)
  • -type -f ระบุว่าจะประมวลผลไฟล์ธรรมดาเท่านั้น
  • -exec cmd option {}บอกให้มันทำงานcmdด้วยการระบุoptionสำหรับแต่ละไฟล์ที่พบโดยใช้ชื่อไฟล์แทน{}
  • \; หมายถึงการสิ้นสุดคำสั่ง
  • ในที่สุดผลลัพธ์จากการcmdประมวลผลแต่ละรายการจะถูกเปลี่ยนเส้นทางไปยัง results.out

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


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

1
หากฉันต้องการเรียกใช้สองคำสั่งฉันจะเชื่อมโยงคำสั่งเหล่านั้นหลังจาก-execตัวเลือกได้อย่างไร ฉันต้องห่อมันด้วยเครื่องหมายอัญประกาศเดี่ยวหรือเปล่า?
Frei

findเป็นตัวเลือกที่ดีที่สุดเสมอเพราะคุณสามารถกรองตามรูปแบบชื่อไฟล์พร้อมตัวเลือก-nameและคุณสามารถทำได้ในคำสั่งเดียว
João Pimentel Ferreira

3
@frei คำตอบสำหรับคำถามของคุณอยู่ที่นี่: stackoverflow.com/a/6043896/1243247แต่โดยทั่วไปเพียงแค่เพิ่ม-execตัวเลือก:find . -name "*.txt" -exec echo {} \; -exec grep banana {} \;
João Pimentel Ferreira

2
คุณจะอ้างอิงชื่อไฟล์เป็นตัวเลือกได้อย่างไร?
Toskan

54

ฉันทำเช่นนี้ในราสเบอร์รี่ pi ของฉันจากบรรทัดคำสั่งโดยวิ่ง:

for i in *;do omxplayer "$i";done

7

คำตอบที่ได้รับการยอมรับ / โหวตสูงนั้นยอดเยี่ยม แต่พวกเขายังขาดรายละเอียดเล็ก ๆ น้อย ๆ โพสต์นี้ครอบคลุมถึงกรณีเกี่ยวกับวิธีการจัดการที่ดีขึ้นเมื่อการขยายชื่อเส้นทางเชลล์ (glob) ล้มเหลวเมื่อชื่อไฟล์มีสัญลักษณ์บรรทัดใหม่ / เส้นประฝังตัวและย้ายคำสั่งเอาต์พุตทิศทางใหม่ออกจาก for-loop เมื่อเขียนผลลัพธ์ไปยัง ไฟล์.

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

 shopt -s nullglob

 for file in ./*; do
     cmdToRun [option] -- "$file"
 done

สิ่งนี้ช่วยให้คุณออกจากห่วงอย่างปลอดภัยเมื่อนิพจน์./*ไม่ส่งคืนไฟล์ใด ๆ (หากไดเรกทอรีว่างเปล่า)

หรือในทางที่สอดคล้องกับ POSIX ( nullglobเป็นbashเฉพาะ)

 for file in ./*; do
     [ -f "$file" ] || continue
     cmdToRun [option] -- "$file"
 done

สิ่งนี้จะช่วยให้คุณเข้าไปในลูปเมื่อนิพจน์ล้มเหลวหนึ่งครั้งและ[ -f "$file" ]ตรวจสอบเงื่อนไขว่าสตริงที่./*ไม่ถูกขยายเป็นชื่อไฟล์ที่ถูกต้องในไดเรกทอรีนั้นซึ่งจะไม่เป็นเช่นนั้น ดังนั้นในสภาวะนี้ล้มเหลวการใช้continueเราดำเนินการกลับไปที่forวงซึ่งจะไม่ทำงานในภายหลัง

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

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


การอ้างชื่อไฟล์สองครั้งอย่างถูกต้องจะช่วยแก้ปัญหากรณีที่ชื่อมีอักขระ glob หรือช่องว่างสีขาว แต่ชื่อไฟล์ * ระวังยังสามารถขึ้นบรรทัดใหม่ในพวกเขา ดังนั้นเราจึง จำกัด ชื่อไฟล์ด้วยอักขระเท่านั้นที่ไม่สามารถเป็นส่วนหนึ่งของชื่อไฟล์ที่ถูกต้อง - null byte ( \0) เนื่องจากbashใช้ภายในCสตริงสไตล์ซึ่งใช้ null เป็นไบต์เพื่อระบุจุดสิ้นสุดของสตริงจึงเป็นตัวเลือกที่เหมาะสมสำหรับสิ่งนี้

ดังนั้นการใช้printfตัวเลือกของเชลล์เพื่อกำหนดขอบเขตไฟล์ด้วย NULL ไบต์นี้โดยใช้-dตัวเลือกของreadคำสั่งเราสามารถทำด้านล่าง

( shopt -s nullglob; printf '%s\0' ./* ) | while read -rd '' file; do
    cmdToRun [option] -- "$file"
done

nullglobและprintfถูกห่อรอบ(..)ซึ่งหมายความว่าพวกเขาจะถูกเรียกใช้โดยทั่วไปในการย่อยเปลือก (เปลือกเด็ก) เพราะเพื่อหลีกเลี่ยงการnullglobเลือกที่จะสะท้อนให้เห็นถึงเปลือกแม่เมื่อออกคำสั่ง -d ''ตัวเลือกของreadคำสั่งไม่ได้ตาม POSIX ดังนั้นจำเป็นต้องมีbashเปลือกนี้จะทำได้ ใช้findคำสั่งนี้สามารถทำได้

while IFS= read -r -d '' file; do
    cmdToRun [option] -- "$file"
done < <(find -maxdepth 1 -type f -print0)

สำหรับfindการใช้งานที่ไม่รองรับ-print0(นอกเหนือจาก GNU และการใช้งาน FreeBSD) สิ่งนี้สามารถเลียนแบบได้โดยใช้printf

find . -maxdepth 1 -type f -exec printf '%s\0' {} \; | xargs -0 cmdToRun [option] --

การแก้ไขที่สำคัญอีกอย่างคือการย้ายทิศทางใหม่ออกจาก for-loop เพื่อลดจำนวนไฟล์ I / O ที่สูง เมื่อใช้ภายในลูปเชลล์จะต้องเรียกใช้งานระบบเรียกสองครั้งสำหรับการวนซ้ำแต่ละครั้งของ for-loop ครั้งเดียวสำหรับเปิดและอีกครั้งสำหรับปิดไฟล์ descriptor ที่เชื่อมโยงกับไฟล์ สิ่งนี้จะกลายเป็นคอขวดในการแสดงของคุณสำหรับการทำซ้ำซ้ำซ้อน คำแนะนำที่แนะนำคือการย้ายออกนอกวง

คุณสามารถขยายรหัสข้างต้นด้วยการแก้ไขนี้ได้

( shopt -s nullglob; printf '%s\0' ./* ) | while read -rd '' file; do
    cmdToRun [option] -- "$file"
done > results.out

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

while IFS= read -r -d '' file; do
    cmdToRun [option] -- "$file"
done < <(find -maxdepth 1 -type f -print0) > results.out

1
+1 สำหรับการตรวจสอบว่ามีไฟล์อยู่ หากการค้นหาใน dir ที่ไม่มีอยู่ไฟล์ $ มีสตริง regex "/ invald_dir / *" ไม่ใช่ชื่อไฟล์ที่ถูกต้อง
cdalxndr

3

วิธีหนึ่งที่รวดเร็วและสกปรกซึ่งทำให้งานสำเร็จได้ในบางครั้งคือ

find directory/ | xargs  Command 

ตัวอย่างเช่นการค้นหาจำนวนบรรทัดในไฟล์ทั้งหมดในไดเรกทอรีปัจจุบันคุณสามารถทำได้:

find . | xargs wc -l

8
@Hubert ทำไมคุณมีบรรทัดใหม่ในชื่อไฟล์ของคุณ!
musicin3d

2
ไม่ใช่คำถามที่ว่า "ทำไม" เป็นคำถามของความถูกต้อง - ชื่อไฟล์ไม่จำเป็นต้องมีตัวอักษรที่พิมพ์ได้พวกเขาไม่จำเป็นต้องมีลำดับ UTF-8 ที่ถูกต้อง นอกจากนี้สิ่งที่ขึ้นบรรทัดใหม่จะขึ้นอยู่กับการเข้ารหัสอย่างมากหนึ่งการเข้ารหัส♀คือการขึ้นบรรทัดใหม่ของอีก ดูรหัสหน้า 437
Hubert Kario

2
cmon จริงเหรอ? วิธีนี้ใช้งานได้ 99.9% ของเวลาและเขาก็พูดว่า "รวดเร็วและสกปรก"
Edoardo

ฉันไม่ใช่แฟนตัวยงของสคริปต์ Bash ที่ "รวดเร็วและสกปรก" (AKA "broken") ไม่ช้าก็เร็วมันก็จะจบลงในสิ่งที่โด่งดัง "Moved ~/.local/share/steam. Ran steam. มันลบทุกอย่างในระบบที่ผู้ใช้เป็นเจ้าของ" รายงานข้อผิดพลาด
กิจกรรมลด

สิ่งนี้จะไม่ทำงานกับไฟล์ที่มีช่องว่างในชื่อ
Shamas S - Reinstate Monica

2

ฉันต้องการคัดลอกไฟล์. md ทั้งหมดจากไดเรกทอรีหนึ่งไปยังอีกไดเรกทอรีหนึ่งดังนั้นนี่คือสิ่งที่ฉันทำ

for i in **/*.md;do mkdir -p ../docs/"$i" && rm -r ../docs/"$i" && cp "$i" "../docs/$i" && echo "$i -> ../docs/$i"; done

อ่านยากไปไหนมาไหนกันดีกว่า

cd แรกในไดเรกทอรีด้วยไฟล์ของคุณ

for i in **/*.md; สำหรับแต่ละไฟล์ในรูปแบบของคุณ

mkdir -p ../docs/"$i"ทำไดเรกทอรีนั้นในโฟลเดอร์เอกสารนอกโฟลเดอร์ที่มีไฟล์ของคุณ ซึ่งสร้างโฟลเดอร์พิเศษที่มีชื่อเดียวกับไฟล์นั้น

rm -r ../docs/"$i" ลบโฟลเดอร์พิเศษที่สร้างขึ้นจาก mkdir -p

cp "$i" "../docs/$i" คัดลอกไฟล์จริง

echo "$i -> ../docs/$i" สะท้อนสิ่งที่คุณทำ

; done ใช้ชีวิตอย่างมีความสุขตลอดไป


หมายเหตุ: สำหรับ**การทำงานglobstarจำเป็นต้องตั้งค่าตัวเลือกเชลล์:shopt -s globstar
Hubert Kario

2

คุณสามารถใช้ได้ xarg

ls | xargs -L 1 -d '\n' your-desired-command

-L 1 ทำให้เกิดการส่งผ่าน 1 รายการพร้อมกัน

-d '\n'ทำให้การส่งออกของlsถูกแยกขึ้นอยู่กับบรรทัดใหม่


1

ตามแนวทางของ @Jim Lewis:

นี่เป็นวิธีแก้ปัญหาอย่างรวดเร็วโดยใช้findและเรียงลำดับไฟล์ตามวันที่แก้ไข:

$ find  directory/ -maxdepth 1 -type f -print0 | \
  xargs -r0 stat -c "%y %n" | \
  sort | cut -d' ' -f4- | \
  xargs -d "\n" -I{} cmd -op1 {} 

สำหรับการเรียงลำดับดู:

http://www.commandlinefu.com/commands/view/5720/find-files-and-list-them-sorted-by-modification-time


สิ่งนี้จะไม่ทำงานหากไฟล์มีการขึ้นบรรทัดใหม่ในชื่อของพวกเขา
Hubert Kario

1
@HubertKario คุณอาจต้องการอ่านเพิ่มเติมเกี่ยวกับ-print0สำหรับfindและ-0สำหรับxargsที่ใช้อักขระ null แทนช่องว่างใด ๆ (รวมทั้งการขึ้นบรรทัดใหม่)
tuxdna

ใช่การใช้-print0เป็นสิ่งที่ช่วยได้ แต่ท่อทั้งหมดจำเป็นต้องใช้สิ่งนี้และsortไม่ใช่
Hubert Kario

1

ฉันคิดว่าทางออกง่าย ๆ คือ:

sh /dir/* > ./result.txt

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

1

maxdepth

ฉันพบว่ามันใช้งานได้ดีกับคำตอบของ Jim Lewisเพียงเพิ่มเล็กน้อยเช่นนี้:

$ export DIR=/path/dir && cd $DIR && chmod -R +x *
$ find . -maxdepth 1 -type f -name '*.sh' -exec {} \; > results.out

เรียงลำดับ

หากคุณต้องการดำเนินการตามลำดับให้แก้ไขเช่นนี้:

$ export DIR=/path/dir && cd $DIR && chmod -R +x *
find . -maxdepth 2 -type f -name '*.sh' | sort | bash > results.out

ตัวอย่างเช่นนี้จะดำเนินการตามคำสั่งต่อไปนี้:

bash: 1: ./assets/main.sh
bash: 2: ./builder/clean.sh
bash: 3: ./builder/concept/compose.sh
bash: 4: ./builder/concept/market.sh
bash: 5: ./builder/concept/services.sh
bash: 6: ./builder/curl.sh
bash: 7: ./builder/identity.sh
bash: 8: ./concept/compose.sh
bash: 9: ./concept/market.sh
bash: 10: ./concept/services.sh
bash: 11: ./product/compose.sh
bash: 12: ./product/market.sh
bash: 13: ./product/services.sh
bash: 14: ./xferlog.sh

ความลึกไม่ จำกัด

หากคุณต้องการดำเนินการในระดับความลึกไม่ จำกัด ตามเงื่อนไขบางอย่างคุณสามารถใช้สิ่งนี้:

export DIR=/path/dir && cd $DIR && chmod -R +x *
find . -type f -name '*.sh' | sort | bash > results.out

จากนั้นวางบนสุดของแต่ละไฟล์ในไดเร็กทอรีย่อยดังนี้:

#!/bin/bash
[[ "$(dirname `pwd`)" == $DIR ]] && echo "Executing `realpath $0`.." || return

และบางแห่งในเนื้อหาของไฟล์พาเรนต์:

if <a condition is matched>
then
    #execute child files
    export DIR=`pwd`
fi
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.