Lặp lại danh sách các tệp có khoảng trắng


201

Tôi muốn lặp lại một danh sách các tập tin. Danh sách này là kết quả của một findlệnh, vì vậy tôi đã đưa ra:

getlist() {
  for f in $(find . -iname "foo*")
  do
    echo "File found: $f"
    # do something useful
  done
}

Sẽ ổn trừ khi một tệp có khoảng trắng trong tên của nó:

$ ls
foo_bar_baz.txt
foo bar baz.txt

$ getlist
File found: foo_bar_baz.txt
File found: foo
File found: bar
File found: baz.txt

Tôi có thể làm gì để tránh sự phân chia trên không gian?


Đây về cơ bản là một trường hợp con cụ thể của Khi nào cần trích dẫn dấu ngoặc kép quanh một biến shell?
tripleee

Câu trả lời:


253

Bạn có thể thay thế phép lặp dựa trên từ bằng một phép lặp dựa trên dòng:

find . -iname "foo*" | while read f
do
    # ... loop body
done

31
Điều này là vô cùng sạch sẽ. Và làm cho tôi cảm thấy đẹp hơn thay đổi IFS kết hợp với vòng lặp for
Derrick

15
Điều này sẽ phân chia một đường dẫn tệp duy nhất chứa \ n. OK, những người không nên ở đây nhưng họ có thể được tạo ra:touch "$(printf "foo\nbar")"
Ollie Saunders

4
Để ngăn chặn mọi giải thích về đầu vào (dấu gạch chéo ngược, khoảng trắng ở đầu và cuối), IFS= while read -r fthay vào đó , hãy sử dụng .
mkuity0

2
Câu trả lời này cho thấy sự kết hợp an toàn hơn findvà vòng lặp while.
moi

5
Có vẻ như chỉ ra điều hiển nhiên, nhưng trong hầu hết các trường hợp đơn giản, -execsẽ sạch hơn một vòng lặp rõ ràng : find . -iname "foo*" -exec echo "File found: {}" \;. Ngoài ra, trong nhiều trường hợp, bạn có thể thay thế lần cuối \;bằng +cách đặt nhiều tệp vào một lệnh.
ness101

152

Có một số cách khả thi để thực hiện điều này.

Nếu bạn muốn bám sát phiên bản gốc của mình, bạn có thể thực hiện theo cách này:

getlist() {
        IFS=$'\n'
        for file in $(find . -iname 'foo*') ; do
                printf 'File found: %s\n' "$file"
        done
}

Điều này sẽ vẫn thất bại nếu tên tệp có dòng mới theo nghĩa đen, nhưng không gian sẽ không phá vỡ nó.

Tuy nhiên, gây rối với IFS là không cần thiết. Đây là cách ưa thích của tôi để làm điều này:

getlist() {
    while IFS= read -d $'\0' -r file ; do
            printf 'File found: %s\n' "$file"
    done < <(find . -iname 'foo*' -print0)
}

Nếu bạn thấy < <(command)cú pháp không quen thuộc, bạn nên đọc về quá trình thay thế . Ưu điểm của việc này for file in $(find ...)là các tệp có dấu cách, dòng mới và các ký tự khác được xử lý chính xác. Điều này hoạt động vì findvới -print0sẽ sử dụng null(aka \0) làm dấu kết thúc cho mỗi tên tệp và, không giống như dòng mới, null không phải là ký tự hợp pháp trong tên tệp.

Lợi thế này so với phiên bản gần tương đương

getlist() {
        find . -iname 'foo*' -print0 | while read -d $'\0' -r file ; do
                printf 'File found: %s\n' "$file"
        done
}

Có phải bất kỳ phép gán biến trong phần thân của vòng lặp while đều được giữ nguyên. Đó là, nếu bạn đặt ống whilenhư trên thì phần thân của whilenó nằm trong một khung con có thể không phải là thứ bạn muốn.

Ưu điểm của phiên bản thay thế quá trình find ... -print0 | xargs -0là tối thiểu: xargsPhiên bản vẫn ổn nếu tất cả những gì bạn cần là in một dòng hoặc thực hiện một thao tác trên tệp, nhưng nếu bạn cần thực hiện nhiều bước thì phiên bản vòng lặp sẽ dễ dàng hơn.

EDIT : Đây là một kịch bản thử nghiệm hay để bạn có thể biết được sự khác biệt giữa các nỗ lực khác nhau trong việc giải quyết vấn đề này

#!/usr/bin/env bash

dir=/tmp/getlist.test/
mkdir -p "$dir"
cd "$dir"

touch       'file not starting foo' foo foobar barfoo 'foo with spaces'\
    'foo with'$'\n'newline 'foo with trailing whitespace      '

# while with process substitution, null terminated, empty IFS
getlist0() {
    while IFS= read -d $'\0' -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done < <(find . -iname 'foo*' -print0)
}

# while with process substitution, null terminated, default IFS
getlist1() {
    while read -d $'\0' -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done < <(find . -iname 'foo*' -print0)
}

# pipe to while, newline terminated
getlist2() {
    find . -iname 'foo*' | while read -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}

# pipe to while, null terminated
getlist3() {
    find . -iname 'foo*' -print0 | while read -d $'\0' -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}

# for loop over subshell results, newline terminated, default IFS
getlist4() {
    for file in "$(find . -iname 'foo*')" ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}

# for loop over subshell results, newline terminated, newline IFS
getlist5() {
    IFS=$'\n'
    for file in $(find . -iname 'foo*') ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}


# see how they run
for n in {0..5} ; do
    printf '\n\ngetlist%d:\n' $n
    eval getlist$n
done

rm -rf "$dir"

1
Được chấp nhận câu trả lời của bạn: hầu hết đầy đủ và thú vị - Tôi không biết về $IFS< <(cmd)cú pháp. Vẫn còn một điều mơ hồ với tôi, tại sao lại $vào $'\0'? Cảm ơn rất nhiều.
gregseth

2
+1, nhưng bạn nên thêm ... while IFS= read... để xử lý các tệp bắt đầu hoặc kết thúc bằng khoảng trắng.
Gordon Davisson

1
Có một cảnh báo cho giải pháp thay thế quá trình. Nếu bạn có bất kỳ lời nhắc nào bên trong vòng lặp (hoặc đang đọc từ STDIN theo bất kỳ cách nào khác), đầu vào sẽ được điền bởi những thứ bạn nạp vào vòng lặp. (có lẽ điều này nên được thêm vào câu trả lời?)
andsens 12/12/13

2
@uvsmtid: Câu hỏi này đã được gắn thẻ bashnên tôi cảm thấy an toàn khi sử dụng các tính năng dành riêng cho bash. Quá trình thay thế không thể di chuyển sang các shell khác (bản thân sh không có khả năng nhận được bản cập nhật quan trọng như vậy).
Bọ Cạp

2
Kết hợp IFS=$'\n'với forviệc ngăn chặn phân tách từ bên trong dòng, nhưng vẫn làm cho các dòng kết quả có thể bị toàn cầu hóa, vì vậy cách tiếp cận này không hoàn toàn mạnh mẽ (trừ khi bạn cũng tắt tính năng quảng cáo trước). Trong khi read -d $'\0'tác phẩm, nó là hơi gây hiểu lầm ở chỗ nó cho thấy rằng bạn có thể sử dụng $'\0'để tạo ra NUL - bạn không thể: một \0trong một chuỗi ANSI C-trích dẫn một cách hiệu quả chấm dứt chuỗi, do đó -d $'\0'là một cách hiệu quả giống như -d ''.
mkuity0

29

Ngoài ra còn có một giải pháp rất đơn giản: dựa vào bash continbing

$ mkdir test
$ cd test
$ touch "stupid file1"
$ touch "stupid file2"
$ touch "stupid   file 3"
$ ls
stupid   file 3  stupid file1     stupid file2
$ for file in *; do echo "file: '${file}'"; done
file: 'stupid   file 3'
file: 'stupid file1'
file: 'stupid file2'

Lưu ý rằng tôi không chắc hành vi này là mặc định nhưng tôi không thấy bất kỳ cài đặt đặc biệt nào trong shopt của mình nên tôi sẽ nói rằng nó phải "an toàn" (được thử nghiệm trên osx và ubfox).


13
find . -iname "foo*" -print0 | xargs -L1 -0 echo "File found:"

6
như một lưu ý phụ, điều này sẽ chỉ hoạt động nếu bạn muốn thực thi một lệnh. Một vỏ dựng sẵn sẽ không hoạt động theo cách này.
Alex

11
find . -name "fo*" -print0 | xargs -0 ls -l

Xem man xargs.


6

Vì bạn không thực hiện bất kỳ loại lọc nào khác find, bạn có thể sử dụng các cách sau kể từ bash4.0:

shopt -s globstar
getlist() {
    for f in **/foo*
    do
        echo "File found: $f"
        # do something useful
    done
}

Các **/sẽ phù hợp với zero hoặc nhiều thư mục, vì vậy mô hình đầy đủ sẽ phù hợp foo*trong thư mục hiện tại hoặc bất kỳ thư mục con.


3

Tôi thực sự thích các vòng lặp và lặp mảng, vì vậy tôi nghĩ rằng tôi sẽ thêm câu trả lời này vào hỗn hợp ...

Tôi cũng thích ví dụ tập tin ngu ngốc của marchelbled. :)

$ mkdir test
$ cd test
$ touch "stupid file1"
$ touch "stupid file2"
$ touch "stupid   file 3"

Trong thư mục kiểm tra:

readarray -t arr <<< "`ls -A1`"

Điều này thêm từng dòng liệt kê tệp vào một mảng bash có tên arrvới bất kỳ dòng mới nào bị xóa.

Hãy nói rằng chúng tôi muốn đặt cho các tệp này tên tốt hơn ...

for i in ${!arr[@]}
do 
    newname=`echo "${arr[$i]}" | sed 's/stupid/smarter/; s/  */_/g'`; 
    mv "${arr[$i]}" "$newname"
done

$ {! Array [@]} mở rộng thành 0 1 2 vì vậy "$ {Array [$ i]}" là phần tử thứ i của mảng. Các trích dẫn xung quanh các biến là quan trọng để bảo tồn các không gian.

Kết quả là ba tệp được đổi tên:

$ ls -1
smarter_file1
smarter_file2
smarter_file_3

2

findcó một -execđối số lặp lại kết quả tìm và thực hiện một lệnh tùy ý. Ví dụ:

find . -iname "foo*" -exec echo "File found: {}" \;

Ở đây {}đại diện cho các tệp được tìm thấy và gói nó vào ""cho phép lệnh shell kết quả xử lý các khoảng trắng trong tên tệp.

Trong nhiều trường hợp, bạn có thể thay thế lệnh cuối cùng \;(bắt đầu một lệnh mới) bằng một lệnh \+sẽ đặt nhiều tệp trong một lệnh (không nhất thiết phải tất cả chúng cùng một lúc, xem man findđể biết thêm chi tiết).


0

Trong một số trường hợp, ở đây nếu bạn chỉ cần sao chép hoặc di chuyển danh sách các tệp, bạn cũng có thể chuyển danh sách đó sang awk.
Quan trọng \"" "\"xung quanh trường $0(nói ngắn gọn là các tệp của bạn, một dòng-list = một tệp).

find . -iname "foo*" | awk '{print "mv \""$0"\" ./MyDir2" | "sh" }'

0

Ok - bài viết đầu tiên của tôi về Stack Overflow!

Mặc dù vấn đề của tôi với vấn đề này luôn nằm ở csh không làm hỏng giải pháp tôi trình bày, tôi chắc chắn, sẽ làm việc trong cả hai. Vấn đề là với cách giải thích của shell về lợi nhuận "ls". Chúng tôi có thể loại bỏ "ls" khỏi sự cố bằng cách sử dụng mở rộng shell của *ký tự đại diện - nhưng điều này gây ra lỗi "không khớp" nếu không có tệp nào trong thư mục hiện tại (hoặc thư mục được chỉ định) - để giải quyết vấn đề này, chúng tôi chỉ cần mở rộng mở rộng để bao gồm các tệp chấm do đó: * .*- điều này sẽ luôn mang lại kết quả kể từ các tệp. và .. sẽ luôn có mặt. Vì vậy, trong csh chúng ta có thể sử dụng cấu trúc này ...

foreach file (* .*)
   echo $file
end

nếu bạn muốn lọc ra các tệp chấm tiêu chuẩn thì điều đó đủ dễ dàng ...

foreach file (* .*)
   if ("$file" == .) continue
   if ("file" == ..) continue
   echo $file
end

Mã trong bài viết đầu tiên về chủ đề này sẽ được viết như sau: -

getlist() {
  for f in $(* .*)
  do
    echo "File found: $f"
    # do something useful
  done
}

Hi vọng điêu nay co ich!


0

Một giải pháp khác cho công việc ...

Mục tiêu là:

  • chọn / lọc tên tệp đệ quy trong thư mục
  • xử lý từng tên (bất kỳ khoảng trắng nào trong đường dẫn ...)
#!/bin/bash  -e
## @Trick in order handle File with space in their path...
OLD_IFS=${IFS}
IFS=$'\n'
files=($(find ${INPUT_DIR} -type f -name "*.md"))
for filename in ${files[*]}
do
      # do your stuff
      #  ....
done
IFS=${OLD_IFS}


Thx cho nhận xét mang tính xây dựng, nhưng: 1- đây là một vấn đề thực tế, 2- shell có thể đã phát triển trong thời gian ... như mọi người tôi giả định; 3- Không có câu trả lời nào ở trên có thể đáp ứng độ phân giải TRỰC TIẾP của pb mà không thay đổi vấn đề hoặc luận điểm :-)
Vince B
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.