Cách lặp qua thư mục một cách đệ quy để xóa các tệp có phần mở rộng nhất định


157

Tôi cần lặp qua thư mục một cách đệ quy và xóa tất cả các tệp có phần mở rộng .pdf.doc. Tôi đang quản lý để lặp qua thư mục một cách đệ quy nhưng không quản lý để lọc các tệp có phần mở rộng tệp được đề cập ở trên.

Mã của tôi cho đến nay

#/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

Tôi cần giúp đỡ để hoàn thành mã, vì tôi không đi đâu cả.


68
Tôi biết đó là hình thức xấu để thực thi mã mà không hiểu nó, nhưng rất nhiều người đến trang web này để tìm hiểu kịch bản bash. Tôi đã đến đây bằng cách googling "bash scripting đệ quy" và gần như chạy một trong những câu trả lời này (chỉ để kiểm tra đệ quy) mà không nhận ra rằng nó sẽ xóa các tệp. Tôi biết rmlà một phần của mã OP, nhưng nó không thực sự liên quan đến câu hỏi được hỏi. Tôi nghĩ sẽ an toàn hơn nếu câu trả lời được thực hiện bằng cách sử dụng một lệnh vô hại như thế nào echo.
Keith

Câu hỏi tương tự ở đây: stackoverflow.com/questions/41799938/ Cách
codeforester

1
@Keith đã có kinh nghiệm tương tự, hoàn toàn đồng ý và thay đổi tiêu đề
idclev 463035818

Câu trả lời:


146

find chỉ được thực hiện cho điều đó.

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

19
Hoặc tìm -deletetùy chọn.
Matthew Flaschen

28
Một nên luôn luôn sử dụng find ... -print0 | xargs -0 ..., không tìm thấy thô | xargs để tránh các vấn đề với tên tệp có chứa dòng mới.
Grumbel

7
Sử dụng xargskhông có tùy chọn hầu như luôn luôn là lời khuyên tồi và điều này cũng không ngoại lệ. Sử dụng find … -execthay thế.
Gilles 'SO- ngừng trở nên xấu xa'

211

Để theo dõi câu trả lời của mouviciel, bạn cũng có thể làm điều này như một vòng lặp for, thay vì sử dụng xargs. Tôi thường thấy xargs cồng kềnh, đặc biệt nếu tôi cần làm một cái gì đó phức tạp hơn trong mỗi lần lặp.

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

Như một số người đã bình luận, điều này sẽ thất bại nếu có không gian trong tên tệp. Bạn có thể giải quyết vấn đề này bằng cách tạm thời đặt IFS (bộ tách trường nội bộ) thành ký tự dòng mới. Điều này cũng thất bại nếu có các ký tự đại diện \[?*trong tên tệp. Bạn có thể giải quyết vấn đề đó bằng cách tạm thời vô hiệu hóa mở rộng ký tự đại diện (globalbing).

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

Nếu bạn có dòng mới trong tên tệp của mình, thì nó cũng không hoạt động. Bạn tốt hơn với một giải pháp dựa trên xargs:

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

(Các dấu ngoặc thoát được yêu cầu ở đây để -print0áp dụng cho cả hai ormệnh đề.)

GNU và * BSD find cũng có một -deletehành động, trông như thế này:

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

27
Điều này không hoạt động như mong đợi nếu có một khoảng trắng trong tên tệp (vòng lặp for phân chia kết quả tìm kiếm trên khoảng trắng).
trev

3
Làm thế nào để bạn avaoid chia tách trên khoảng trắng? Tôi đang thử một thứ tương tự và tôi có rất nhiều thư mục với các khoảng trắng làm hỏng vòng lặp này.
Christian

3
bởi vì đó là một câu trả lời rất hữu ích?
zenperttu

1
@Christian Khắc phục sự phân tách khoảng trắng bằng cách sử dụng dấu ngoặc kép như sau: "$ (find ...)". Tôi đã chỉnh sửa câu trả lời của James để hiển thị.
Matthew

2
@Matthew bản chỉnh sửa của bạn không khắc phục được gì cả: nó thực sự khiến lệnh chỉ hoạt động nếu có một tệp tìm thấy duy nhất . Ít nhất phiên bản này hoạt động nếu không có khoảng trắng, tab, v.v. trong tên tệp. Tôi quay trở lại phiên bản cũ. Ghi chú hợp lý thực sự có thể sửa chữa a for f in $(find ...). Chỉ không sử dụng phương pháp này.
gniourf_gniourf

67

Không có find:

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

/tmp/*là các tệp trong thư mục và /tmp/**/*là các tệp trong thư mục con. Có thể bạn phải bật tùy chọn globalstar ( shopt -s globstar). Vì vậy, đối với câu hỏi, mã sẽ trông như thế này:

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

Lưu ý rằng điều này yêu cầu bash ≥4.0 (hoặc zsh không có shopt -s globstarhoặc ksh set -o globstarthay vì shopt -s globstar). Hơn nữa, trong bash <4.3, điều này đi qua các liên kết tượng trưng đến các thư mục cũng như các thư mục, thường không được mong muốn.


1
Phương pháp này hiệu quả với tôi, ngay cả với tên tệp chứa khoảng trắng trên OSX
ideaasylum

2
Đáng lưu ý rằng globalstar chỉ có sẵn trong Bash 4.0 hoặc mới hơn .. đây không phải là phiên bản mặc định trên nhiều máy.
Troy Howard

1
Tôi không nghĩ bạn cần chỉ định đối số đầu tiên. (Ít nhất là cho đến ngày hôm nay,) for f in /tmp/**sẽ là đủ. Bao gồm các tập tin từ / tmp dir.
phil294

1
Sẽ không tốt hơn như thế này sao? for f in /tmp/*.{pdf,doc} tmp/**/*.{,pdf,doc} ; do
Ice-Blaze

1
**là một phần mở rộng đẹp nhưng không khả chuyển sang POSIX sh. (Câu hỏi này được gắn thẻ bash . Nhưng nó sẽ được tốt đẹp để chỉ ra rằng không giống như một số các giải pháp ở đây, điều này thực sự là Bash chỉ Hoặc, tốt, nó hoạt động ở một số vỏ mở rộng khác, quá.)
tripleee

27

Nếu bạn muốn làm một cái gì đó đệ quy, tôi khuyên bạn nên sử dụng đệ quy (vâng, bạn có thể làm điều đó bằng cách sử dụng ngăn xếp, v.v., nhưng hey).

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

(cd /tmp; recursiverm)

Điều đó nói rằng, findcó lẽ là một lựa chọn tốt hơn như đã được đề xuất.


15

Dưới đây là một ví dụ sử dụng shell ( 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

Điều này không trả lời trực tiếp câu hỏi của bạn, nhưng bạn có thể giải quyết vấn đề của mình bằng một lớp lót:

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

Một số phiên bản find (GNU, BSD) có -deletehành động mà bạn có thể sử dụng thay vì gọi rm:

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

7

Phương pháp này xử lý không gian tốt.

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

Chỉnh sửa, sửa lỗi từng cái một

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
}

Tôi nghĩ cờ "-n" sau tiếng vang không cần thiết. Chỉ cần tự kiểm tra: với "-n" tập lệnh của bạn cung cấp sai số lượng tệp. Đối với chính xác một tệp trong thư mục, nó xuất ra "Đếm: 0"
Lopa

1
Điều này không hoạt động với tất cả các tên tệp: nó không thành công với khoảng trắng ở cuối tên, với tên tệp chứa dòng mới và với một số tên tệp chứa dấu gạch chéo ngược. Những khiếm khuyết này có thể được sửa chữa nhưng toàn bộ cách tiếp cận phức tạp không cần thiết nên không đáng bận tâm.
Gilles 'SO- ngừng trở nên xấu xa'

3

Đối với bash (kể từ phiên bản 4.0):

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

Đó là tất cả.
Phần mở rộng theo sau ".ext" ở đó để chọn tệp (hoặc thư mục) với phần mở rộng đó.

Tùy chọn globalstar kích hoạt ** (tìm kiếm đệ quy).
Tùy chọn nullglob xóa * khi nó không khớp với tệp / dir.
Tùy chọn dotglob bao gồm các tệp bắt đầu bằng dấu chấm (tệp ẩn).

Coi chừng rằng trước bash 4.3, **/cũng truyền qua các liên kết tượng trưng đến các thư mục không mong muốn.


1

Hàm sau sẽ lặp lại đệ quy thông qua tất cả các thư mục trong \home\ubuntuthư mục (toàn bộ cấu trúc thư mục trong ubfox) và áp dụng các kiểm tra cần thiết trong elsekhối.

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

Đây là cách đơn giản nhất mà tôi biết để làm điều này: rm **/@(*.doc|*.pdf)

** làm cho công việc này đệ quy

@(*.doc|*.pdf) tìm kiếm một tập tin kết thúc bằng pdf HOẶC doc

Dễ dàng kiểm tra an toàn bằng cách thay thế rmbằngls


0

Không có lý do để dẫn đầu ra của findmột tiện ích khác. findcó một -deletelá cờ được xây dựng trong đó.

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

0

Các câu trả lời khác được cung cấp sẽ không bao gồm các tệp hoặc thư mục bắt đầu bằng a. Những điều sau đây làm việc cho tôi:

#/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

Cứ làm đi

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

4
Không, đừng làm điều này. Điều này phá vỡ nếu bạn có tên tập tin với không gian hoặc các biểu tượng vui nhộn khác.
gniourf_gniourf

-1

Sau đây sẽ lặp qua thư mục đã cho đệ quy và liệt kê tất cả các nội dung:

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


Không, chức năng này không đi qua bất cứ điều gì đệ quy. Nó chỉ liệt kê nội dung của các thư mục con. Nó chỉ là xung quanh ls -l /home/ubuntu/*/, vì vậy nó khá vô dụng.
Gilles 'SO- ngừng trở nên xấu xa'

-1

Nếu bạn có thể thay đổi shell được sử dụng để chạy lệnh, bạn có thể sử dụng ZSH để thực hiện công việc.

#!/usr/bin/zsh

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

Điều này sẽ lặp lại đệ quy thông qua tất cả các tập tin / thư mục.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.