遍歷名稱中帶有空格的文件?
我編寫了以下腳本來區分兩個目錄的輸出,其中包含所有相同的文件:
#!/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 {} +
長答案
你有三個問題:
- 預設情況下,shell 將命令的輸出拆分為空格、製表符和換行符
- 文件名可能包含會被擴展的萬用字元
- 如果有一個名稱以 結尾的目錄
*.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
為一個換行符,因此它只在換行符上拆分,而不是空格和製表符。如果你使用
sh
ordash
代替ksh93
,bash
orzsh
,你需要這樣寫IFS=$'\n'
:IFS=' '
這可能足以讓您的腳本正常工作,但如果您有興趣正確處理其他一些極端情況,請繼續閱讀……
$file
2.不使用萬用字元進行擴展在你做的循環裡面
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 執行,這將導致錯誤,但我認為這在這種情況下並不重要。那麼,如果文件名包含換行符怎麼辦?
我們可以通過更改
-print0
並read -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
.一種更便攜的編寫方式,不需要
bash
或zsh
記住上述關於空字節的所有規則(再次感謝 Gilles):find . -name '*.csv' -exec sh -c ' file="$0" echo "$file" diff "$file" "/some/other/path/$file" read char </dev/tty ' exec-sh {} ';'
- 跳過以**.csv**結尾的目錄
find . -name "*.csv"
還將匹配被呼叫的目錄
something.csv
。為避免這種情況,請添加
-type f
到find
命令中。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
.