วิธีวนลูปผ่านไดเรคทอรีแบบวนซ้ำเพื่อลบไฟล์ที่มีนามสกุลบางอย่าง


157

ฉันต้องห่วงผ่านไดเรกทอรีซ้ำและลบไฟล์ทั้งหมดที่มีนามสกุลและ.pdf .docฉันจัดการวนลูปผ่านไดเรกทอรีซ้ำ แต่ไม่ได้จัดการเพื่อกรองไฟล์ด้วยนามสกุลไฟล์ที่กล่าวถึงข้างต้น

รหัสของฉันจนถึงตอนนี้

#/bin/sh

SEARCH_FOLDER="/tmp/*"

for f in $SEARCH_FOLDER
do
    if [ -d "$f" ]
    then
        for ff in $f/*
        do      
            echo "Processing $ff"
        done
    else
        echo "Processing file $f"
    fi
done

ฉันต้องการความช่วยเหลือในการกรอกรหัสเนื่องจากฉันไม่ได้รับทุกที่


68
ฉันรู้ว่ามันเป็นรูปแบบที่ไม่ดีในการรันโค้ดโดยที่ไม่เข้าใจ แต่มีผู้คนมากมายเข้ามาที่เว็บไซต์นี้เพื่อเรียนรู้การทุบตีสคริปต์ ฉันมาที่นี่โดย googling "bash scripting recursively" และเกือบวิ่งหนึ่งในคำตอบเหล่านี้ (เพียงเพื่อทดสอบการเรียกซ้ำ) โดยไม่ทราบว่ามันจะลบไฟล์ ฉันรู้ว่าrmเป็นส่วนหนึ่งของรหัสของ OP แต่จริง ๆ แล้วมันไม่เกี่ยวข้องกับคำถามที่ถาม ฉันคิดว่ามันจะปลอดภัยกว่าถ้าคำตอบนั้นถูกเขียนขึ้นโดยใช้คำสั่งที่ไม่เป็นอันตรายเช่นechoนั้น
Keith

คำถามที่คล้ายกันที่นี่: stackoverflow.com/questions/41799938/…
codeforester

1
@Keith มีประสบการณ์ที่คล้ายกันยอมรับและเปลี่ยนชื่ออย่างสมบูรณ์
idclev 463035818

คำตอบ:


146

find ทำเพื่อสิ่งนั้น

find /tmp -name '*.pdf' -or -name '*.doc' | xargs rm

19
หรือ-deleteตัวเลือกของการค้นหา
Matthew Flaschen

28
ควรใช้อย่างใดอย่างหนึ่งfind ... -print0 | xargs -0 ...ไม่ใช่การค้นหาแบบดิบ xargs เพื่อหลีกเลี่ยงปัญหากับชื่อไฟล์ที่มีการขึ้นบรรทัดใหม่
Grumbel

7
การใช้xargsตัวเลือกที่ไม่มีตัวเลือกนั้นเป็นคำแนะนำที่แย่เสมอไปและนี่ก็เป็นข้อยกเว้น ใช้find … -execแทน
Gilles 'หยุดชั่วร้าย'

211

ในการติดตามคำตอบของ mouviciel คุณสามารถทำเช่นนี้เพื่อ for loop แทนการใช้ xargs ฉันมักจะพบว่า xargs ยุ่งยากโดยเฉพาะอย่างยิ่งถ้าฉันต้องทำอะไรที่ซับซ้อนกว่าในการทำซ้ำแต่ละครั้ง

for f in $(find /tmp -name '*.pdf' -or -name '*.doc'); do rm $f; done

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

IFS=$'\n'; set -f
for f in $(find /tmp -name '*.pdf' -or -name '*.doc'); do rm "$f"; done
unset IFS; set +f

หากคุณมีการขึ้นบรรทัดใหม่ในชื่อไฟล์ของคุณแล้วที่จะไม่ทำงานอย่างใดอย่างหนึ่ง คุณจะดีกว่าด้วยโซลูชันที่ใช้ xargs:

find /tmp \( -name '*.pdf' -or -name '*.doc' \) -print0 | xargs -0 rm

(ต้องใช้เครื่องหมายวงเล็บในการหลบหนีที่นี่เพื่อให้มีการ-print0ใช้กับorคำสั่งทั้งสอง)

GNU และ * ค้นหา BSD นอกจากนี้ยังมี-deleteการดำเนินการซึ่งจะมีลักษณะเช่นนี้

find /tmp \( -name '*.pdf' -or -name '*.doc' \) -delete

27
สิ่งนี้ไม่ทำงานอย่างที่คาดไว้หากมีช่องว่างในชื่อไฟล์ (สำหรับห่วงแยกผลลัพธ์ของการค้นหาบนช่องว่าง)
trev

3
คุณจะทำอย่างไร avaoid แยกบนช่องว่าง? ฉันกำลังลองสิ่งที่คล้ายกันและฉันมีไดเรกทอรีจำนวนมากที่มีช่องว่างที่ทำให้เกิดการวนซ้ำ
คริสเตียน

3
เพราะมันเป็นคำตอบที่มีประโยชน์มาก?
zenperttu

1
@Christian แก้ไขการแยกช่องว่างโดยใช้เครื่องหมายคำพูดเช่นนี้: "$ (find ... )" ฉันได้แก้ไขคำตอบของ James เพื่อแสดง
Matthew

2
@ Matthew การแก้ไขของคุณไม่ได้แก้ไขอะไรเลยมันทำจริงคำสั่งการทำงานเฉพาะในกรณีที่มีการพบไฟล์ที่ไม่ซ้ำกัน อย่างน้อยรุ่นนี้ใช้งานได้หากไม่มีช่องว่างแท็บ ฯลฯ ในชื่อไฟล์ ผมย้อนกลับไปยังรุ่นเก่า การสังเกตที่สมเหตุสมผลสามารถแก้ไขfor f in $(find ...)ได้ อย่าใช้วิธีนี้
gniourf_gniourf

67

โดยไม่ต้องfind:

for f in /tmp/* tmp/**/* ; do
  ...
done;

/tmp/*เป็นไฟล์ใน dir และ/tmp/**/*เป็นไฟล์ในโฟลเดอร์ย่อย เป็นไปได้ว่าคุณต้องเปิดใช้งานตัวเลือก globstar ( shopt -s globstar) ดังนั้นสำหรับคำถามรหัสควรมีลักษณะเช่นนี้:

shopt -s globstar
for f in /tmp/*.pdf /tmp/*.doc tmp/**/*.pdf tmp/**/*.doc ; do
  rm "$f"
done

โปรดทราบว่าสิ่งนี้ต้องการ bash ≥4.0 (หรือ zsh ที่ไม่มีshopt -s globstarหรือ ksh ด้วยset -o globstarแทนที่จะเป็นshopt -s globstar) นอกจากนี้ใน bash <4.3 เส้นทางนี้จะเชื่อมโยงสัญลักษณ์ไปยังไดเรกทอรีรวมถึงไดเรกทอรีซึ่งโดยทั่วไปจะไม่ต้องการ


1
วิธีนี้ใช้ได้ผลสำหรับฉันแม้จะมีชื่อไฟล์ที่มีช่องว่างใน OSX
ideasasylum

2
น่าสังเกตว่า globstar นั้นมีให้เฉพาะใน Bash 4.0 หรือใหม่กว่า .. ซึ่งไม่ใช่รุ่นเริ่มต้นในหลาย ๆ เครื่อง
ทรอยฮาวเวิร์ด

1
ฉันไม่คิดว่าคุณจะต้องระบุอาร์กิวเมนต์แรก (อย่างน้อยก็วันนี้) for f in /tmp/**จะเพียงพอแล้ว รวมไฟล์จาก / tmp dir
phil294

1
มันจะดีกว่านี้ไหม for f in /tmp/*.{pdf,doc} tmp/**/*.{,pdf,doc} ; do
Ice-Blaze

1
**เป็นส่วนขยายที่ดี แต่ไม่สามารถพกพาไปยัง POSIX shได้ (คำถามนี้ถูกติดแท็กbashแต่มันดีที่จะชี้ให้เห็นว่าแตกต่างจากวิธีแก้ปัญหาที่นี่นี่คือ Bash-only หรือจริงๆมันใช้งานได้ในกระสุนแบบขยายอื่น ๆ อีกมากมาย)
tripleee

27

หากคุณต้องการทำอะไรซ้ำ ๆ ฉันขอแนะนำให้คุณใช้การเรียกซ้ำ (ใช่คุณสามารถทำได้โดยใช้กองซ้อนและอื่น ๆ แต่เฮ้)

recursiverm() {
  for d in *; do
    if [ -d "$d" ]; then
      (cd -- "$d" && recursiverm)
    fi
    rm -f *.pdf
    rm -f *.doc
  done
}

(cd /tmp; recursiverm)

ที่กล่าวว่าfindน่าจะเป็นทางเลือกที่ดีกว่าที่ได้รับการแนะนำแล้ว


15

นี่คือตัวอย่างการใช้เชลล์ ( bash):

#!/bin/bash

# loop & print a folder recusively,
print_folder_recurse() {
    for i in "$1"/*;do
        if [ -d "$i" ];then
            echo "dir: $i"
            print_folder_recurse "$i"
        elif [ -f "$i" ]; then
            echo "file: $i"
        fi
    done
}


# try get path from param
path=""
if [ -d "$1" ]; then
    path=$1;
else
    path="/tmp"
fi

echo "base path: $path"
print_folder_recurse $path

15

สิ่งนี้ไม่ได้ตอบคำถามของคุณโดยตรง แต่คุณสามารถแก้ปัญหาของคุณด้วยสายการบินเดียว

find /tmp \( -name "*.pdf" -o -name "*.doc" \) -type f -exec rm {} +

การค้นหาบางรุ่น (GNU, BSD) มีการ-deleteดำเนินการที่คุณสามารถใช้แทนการโทรrm:

find /tmp \( -name "*.pdf" -o -name "*.doc" \) -type f -delete

7

วิธีนี้จัดการช่องว่างได้ดี

files="$(find -L "$dir" -type f)"
echo "Count: $(echo -n "$files" | wc -l)"
echo "$files" | while read file; do
  echo "$file"
done

แก้ไขแก้ไขแบบแยกส่วน

function count() {
    files="$(find -L "$1" -type f)";
    if [[ "$files" == "" ]]; then
        echo "No files";
        return 0;
    fi
    file_count=$(echo "$files" | wc -l)
    echo "Count: $file_count"
    echo "$files" | while read file; do
        echo "$file"
    done
}

ฉันคิดว่าการตั้งค่าสถานะ "-n" หลังจาก echo ไม่จำเป็น แค่ทดสอบด้วยตัวเอง: ด้วย "-n" สคริปต์ของคุณให้ไฟล์ผิดจำนวน สำหรับหนึ่งไฟล์ในไดเรกทอรีมันจะแสดงผลเป็น "Count: 0"
Lopa

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

3

สำหรับ bash (ตั้งแต่รุ่น 4.0):

shopt -s globstar nullglob dotglob
echo **/*".ext"

นั่นคือทั้งหมดที่
นามสกุลต่อท้าย ".ext" ที่นั่นเพื่อเลือกไฟล์ (หรือ dirs) ด้วยนามสกุลนั้น

ตัวเลือก globstar เปิดใช้งาน ** (การค้นหาซ้ำ)
ตัวเลือก nullglob ลบ * เมื่อมันไม่ตรงกับไฟล์ / dir
ตัวเลือก dotglob รวมถึงไฟล์ที่เริ่มต้นด้วยจุด (ไฟล์ที่ซ่อนอยู่)

ระวังว่าก่อนที่จะทุบตี 4.3 **/ก็จะไปยังลิงก์สัญลักษณ์ไปยังไดเรกทอรีที่ไม่ต้องการ


1

ฟังก์ชั่นต่อไปนี้จะวนซ้ำไดเรกทอรีทั้งหมดใน\home\ubuntuไดเรกทอรี (โครงสร้างไดเรกทอรีทั้งหมดภายใต้ Ubuntu) และใช้การตรวจสอบที่จำเป็นในelseบล็อก

function check {
        for file in $1/*      
        do
        if [ -d "$file" ]
        then
                check $file                          
        else
               ##check for the file
               if [ $(head -c 4 "$file") = "%PDF" ]; then
                         rm -r $file
               fi
        fi
        done     
}
domain=/home/ubuntu
check $domain

1

นี่เป็นวิธีที่ง่ายที่สุดที่ฉันรู้ในการทำสิ่งนี้: rm **/@(*.doc|*.pdf)

** ทำให้งานนี้ซ้ำ

@(*.doc|*.pdf) ค้นหาไฟล์ที่ลงท้ายด้วย pdf OR doc

ง่ายต่อการทดสอบอย่างปลอดภัยโดยแทนที่rmด้วยls


0

ไม่มีเหตุผลใดที่จะทำให้การส่งออกของfindไปยังยูทิลิตี้อื่น findมี-deleteธงติดอยู่ภายใน

find /tmp -name '*.pdf' -or -name '*.doc' -delete

0

คำตอบอื่น ๆ ที่ให้ไว้จะไม่รวมไฟล์หรือไดเรกทอรีที่ขึ้นต้นด้วย ทำงานต่อไปนี้สำหรับฉัน:

#/bin/sh
getAll()
{
  local fl1="$1"/*;
  local fl2="$1"/.[!.]*; 
  local fl3="$1"/..?*;
  for inpath in "$1"/* "$1"/.[!.]* "$1"/..?*; do
    if [ "$inpath" != "$fl1" -a "$inpath" != "$fl2" -a "$inpath" != "$fl3" ]; then 
      stat --printf="%F\0%n\0\n" -- "$inpath";
      if [ -d "$inpath" ]; then
        getAll "$inpath"
      #elif [ -f $inpath ]; then
      fi;
    fi;
  done;
}

-1

แค่ทำ

find . -name '*.pdf'|xargs rm

4
ไม่ไม่ทำเช่นนี้ ตัวแบ่งนี้ถ้าคุณมีชื่อไฟล์ที่มีช่องว่างหรือสัญลักษณ์ตลกอื่น ๆ
gniourf_gniourf

-1

ต่อไปนี้จะวนซ้ำผ่านไดเรกทอรีที่กำหนดซ้ำและแสดงรายการเนื้อหาทั้งหมด:

for d in /home/ubuntu/*; do echo "listing contents of dir: $d"; ls -l $d/; done


ไม่ฟังก์ชั่นนี้ไม่ย้อนกลับไปทำอะไรซ้ำ ๆ จะแสดงรายการเนื้อหาของไดเรกทอรีย่อยเท่านั้น มันเป็นปุยไปรอบ ๆls -l /home/ubuntu/*/ดังนั้นมันไร้ประโยชน์สวย
Gilles 'หยุดชั่วร้าย'

-1

หากคุณสามารถเปลี่ยนเชลล์ที่ใช้เพื่อรันคำสั่งคุณสามารถใช้ ZSH เพื่อทำงาน

#!/usr/bin/zsh

for file in /tmp/**/*
do
    echo $file
done

สิ่งนี้จะวนซ้ำผ่านไฟล์ / โฟลเดอร์ทั้งหมด

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