Bash

遍歷名稱中帶有空格的文件?

  • December 20, 2020

我編寫了以下腳本來區分兩個目錄的輸出,其中包含所有相同的文件:

#!/bin/bash

for file in `find . -name "*.csv"`  
do
    echo "file = $file";
    diff $file /some/other/path/$file;
    read char;
done

我知道還有其他方法可以實現這一目標。但奇怪的是,當文件中有空格時,此腳本會失敗。我該如何處理?

find 的範例輸出:

./zQuery - abc - Do Not Prompt for Date.csv

簡短答案(最接近您的答案,但處理空格)

OIFS="$IFS"
IFS=$'\n'
for file in `find . -type f -name "*.csv"`  
do
    echo "file = $file"
    diff "$file" "/some/other/path/$file"
    read line
done
IFS="$OIFS"

更好的答案(也處理文件名中的萬用字元和換行符)

find . -type f -name "*.csv" -print0 | while IFS= read -r -d '' file; do
   echo "file = $file"
   diff "$file" "/some/other/path/$file"
   read line </dev/tty
done

最佳答案(基於Gilles 的答案

find . -type f -name '*.csv' -exec sh -c '
 file="$0"
 echo "$file"
 diff "$file" "/some/other/path/$file"
 read line </dev/tty
' exec-sh {} ';'

或者更好的是,避免sh每個文件執行一個:

find . -type f -name '*.csv' -exec sh -c '
 for file do
   echo "$file"
   diff "$file" "/some/other/path/$file"
   read line </dev/tty
 done
' exec-sh {} +

長答案

你有三個問題:

  1. 預設情況下,shell 將命令的輸出拆分為空格、製表符和換行符
  2. 文件名可能包含會被擴展的萬用字元
  3. 如果有一個名稱以 結尾的目錄*.csv怎麼辦?

1. 僅在換行符處拆分

要弄清楚要設置什麼file,shell 必須獲取 的輸出find並以某種方式解釋它,否則file將只是find.

shell 讀取預設IFS設置為的變數。<space><tab><newline>

然後它查看輸出中的每個字元find。一旦它看到 中的任何字元IFS,它就認為它標誌著文件名的結尾,因此它設置file為它到目前為止看到的任何字元並執行循環。然後它從它停止的地方開始獲取下一個文件名,並執行下一個循環,等等,直到它到達輸出的末尾。

所以它有效地做到了這一點:

for file in "zquery" "-" "abc" ...

要告訴它只在換行符上拆分輸入,您需要這樣做

IFS=$'\n'

在你的for ... find命令之前。

這設置IFS為一個換行符,因此它只在換行符上拆分,而不是空格和製表符。

如果你使用shordash代替ksh93, bashor zsh,你需要這樣寫IFS=$'\n'

IFS='
'

這可能足以讓您的腳本正常工作,但如果您有興趣正確處理其他一些極端情況,請繼續閱讀……

$file2.不使用萬用字元進行擴展

在你做的循環裡面

diff $file /some/other/path/$file

外殼嘗試擴展$file(再次!)。

它可以包含空格,但由於我們已經在IFS上面設置了,這裡不會有問題。

但它也可能包含萬用字元,例如*or ?,這會導致不可預知的行為。(感謝 Gilles 指出這一點。)

要告訴 shell 不要擴展萬用字元,請將變數放在雙引號內,例如

diff "$file" "/some/other/path/$file"

同樣的問題也可能讓我們陷入困境

for file in `find . -name "*.csv"`

例如,如果你有這三個文件

file1.csv
file2.csv
*.csv

(非常不可能,但仍有可能)

就好像你跑了

for file in file1.csv file2.csv *.csv

這將擴大到

for file in file1.csv file2.csv *.csv file1.csv file2.csv

導致file1.csv並被file2.csv處理兩次。

相反,我們必須做

find . -name "*.csv" -print | while IFS= read -r file; do
   echo "file = $file"
   diff "$file" "/some/other/path/$file"
   read line </dev/tty
done

read從標準輸入中讀取行,將行拆分為單詞,IFS並將它們儲存在您指定的變數名中。

在這裡,我們告訴它不要將行拆分為單詞,並將行儲存在$file.

另請注意,read line已更改為read line </dev/tty.

這是因為在循環內部,標準輸入來自find管道。

如果我們只是這樣做read,它將消耗部分或全部文件名,並且會跳過一些文件。

/dev/tty是使用者從中執行腳本的終端。請注意,如果腳本通過 cron 執行,這將導致錯誤,但我認為這在這種情況下並不重要。

那麼,如果文件名包含換行符怎麼辦?

我們可以通過更改-print-print0read -d ''在管道末端使用來處理這個問題:

find . -name "*.csv" -print0 | while IFS= read -r -d '' file; do
   echo "file = $file"
   diff "$file" "/some/other/path/$file"
   read char </dev/tty
done

這使得find在每個文件名的末尾放置一個空字節。空字節是文件名中唯一不允許使用的字元,因此無論多麼奇怪,它都應該處理所有可能的文件名。

要獲取另一端的文件名,我們使用IFS= read -r -d ''.

在我們read上面使用的地方,我們使用了預設的換行符,但現在,find使用 null 作為行分隔符。在bash中,您不能將參數中的 NUL 字元傳遞給命令(甚至是內置命令),但bash可以理解為NUL delimited-d ''的含義。所以我們使用與.相同的行分隔符。請注意,順便說一句,它也可以工作,因為不支持 NUL 字節會將其視為空字元串。-d ''``read``find``-d $'\0'``bash

正確地說,我們還添加-r了 ,它表示不要專門處理文件名中的反斜杠。例如,沒有-r,\<newline>將被刪除,並\n轉換為n.

一種更便攜的編寫方式,不需要bashzsh記住上述關於空字節的所有規則(再次感謝 Gilles):

find . -name '*.csv' -exec sh -c '
 file="$0"
 echo "$file"
 diff "$file" "/some/other/path/$file"
 read char </dev/tty
' exec-sh {} ';'
    1. 跳過以**.csv**結尾的目錄
find . -name "*.csv"

還將匹配被呼叫的目錄something.csv

為避免這種情況,請添加-type ffind命令中。

find . -type f -name '*.csv' -exec sh -c '
 file="$0"
 echo "$file"
 diff "$file" "/some/other/path/$file"
 read line </dev/tty
' exec-sh {} ';'

正如glenn jackman 所指出的,在這兩個範例中,為每個文件執行的命令都在子 shell 中執行,因此如果您更改循環內的任何變數,它們將被遺忘。

如果您需要設置變數並在循環結束時仍然設置它們,您可以重寫它以使用這樣的程序替換:

i=0
while IFS= read -r -d '' file; do
   echo "file = $file"
   diff "$file" "/some/other/path/$file"
   read line </dev/tty
   i=$((i+1))
done < <(find . -type f -name '*.csv' -print0)
echo "$i files processed"

請注意,如果您嘗試在命令行複制和粘貼它,read line將消耗echo "$i files processed",因此該命令將不會執行。

為避免這種情況,您可以刪除read line </dev/tty並將結果發送到類似less.


筆記

;我刪除了循環內的分號 ( )。如果你願意,你可以把它們放回去,但它們不是必需的。

這些天來,$(command)command. 這主要是因為它$(command1 $(command2))比``command1 `command2```.

read char並沒有真正讀懂一個字元。它讀了一整行,所以我把它改成了read line.

引用自:https://unix.stackexchange.com/questions/9496