如何使用 find
命令自動轉義 shell 元字元?
我在一個目錄樹下有一堆 XML 文件,我想將它們移動到同一目錄樹中具有相同名稱的相應文件夾。
這是範例結構(在外殼中):
touch foo.xml bar.xml "[ foo ].xml" "( bar ).xml" mkdir -p foo bar "foo/[ foo ]" "bar/( bar )"
所以我的方法是:
find . -name "*.xml" -exec sh -c ' DST=$( find . -type d -name "$(basename "{}" .xml)" -print -quit ) [ -d "$DST" ] && mv -v "{}" "$DST/"' ';'
給出以下輸出:
‘./( bar ).xml’ -> ‘./bar/( bar )/( bar ).xml’ mv: ‘./bar/( bar )/( bar ).xml’ and ‘./bar/( bar )/( bar ).xml’ are the same file ‘./bar.xml’ -> ‘./bar/bar.xml’ ‘./foo.xml’ -> ‘./foo/foo.xml’
但是帶有方括號 (
[ foo ].xml
) 的文件並沒有被移動,就好像它被忽略了一樣。我已經檢查並
basename
(例如basename "[ foo ].xml" ".xml"
)正確轉換了文件,但是find
括號有問題。例如:find . -name '[ foo ].xml'
不會正確找到文件。但是,當轉義括號 (
'\[ foo \].xml'
) 時,它可以正常工作,但不能解決問題,因為它是腳本的一部分,我不知道哪些文件具有這些特殊(shell?)字元。通過 BSD 和 GNU 測試find
。使用 with 參數時是否有任何通用的方法來轉義文件名
find
,-name
以便我可以更正我的命令以支持帶有元字元的文件?
在這裡使用 glob 會容易得多
zsh
:for f (**/*.xml(.)) (mv -v -- $f **/$f:r:t(/[1]))
或者,如果您想包含隱藏的 xml 文件並查看隱藏目錄,例如
find
:for f (**/*.xml(.D)) (mv -v -- $f **/$f:r:t(D/[1]))
但請注意名為
.xml
,..xml
or的文件...xml
會成為問題,因此您可能需要排除它們:setopt extendedglob for f (**/(^(|.|..)).xml(.D)) (mv -v -- $f **/$f:r:t(D/[1]))
使用 GNU 工具,另一種避免掃描每個文件的整個目錄樹的方法是掃描一次並查找所有目錄和
xml
文件,記錄它們的位置並在最後進行移動:(export LC_ALL=C find . -mindepth 1 -name '*.xml' ! -name .xml ! \ -name ..xml ! -name ...xml -type f -printf 'F/%P\0' -o \ -type d -printf 'D/%P\0' | awk -v RS='\0' -F / ' { if ($1 == "F") { root = $NF sub(/\.xml$/, "", root) F[root] = substr($0, 3) } else D[$NF] = substr($0, 3) } END { for (f in F) if (f in D) printf "%s\0%s\0", F[f], D[f] }' | xargs -r0n2 mv -v -- )
如果您想允許任意文件名,您的方法會出現許多問題:
- 嵌入
{}
到 shell 程式碼中總是錯誤的。例如,如果有一個文件$(rm -rf "$HOME").xml
怎麼辦?正確的方法是將這些{}
作為參數傳遞給內聯 shell 腳本 (-exec sh -c 'use as "$1"...' sh {} \;
)。- 使用 GNU
find
(在您使用 時在此處暗示-quit
),*.xml
只會匹配由一系列有效字元組成的文件,然後是.xml
,因此不包括在目前語言環境中包含無效字元的文件名(例如錯誤字元集中的文件名)。解決方法是將語言環境修復為C
每個字節都是有效字元的位置(這意味著錯誤消息將以英文顯示)。- 如果這些
xml
文件中的任何一個是目錄或符號連結類型,則會導致問題(影響目錄的掃描,或在移動時破壞符號連結)。您可能想添加一個-type f
以僅移動正常文件。- 命令替換 (
$(...)
) 去除所有尾隨換行符。這會導致一個名為foo.xml
例如的文件出現問題。解決這個問題是可能的,但很痛苦:base=$(basename "$1" .xml; echo .); base=${base%??}
. 您至少可以basename
用${var#pattern}
運算符替換。並儘可能避免命令替換。- 您的文件名包含萬用字元(
?
、[
和*
反斜杠;它們不是 shell 所特有的,而是模式匹配(fnmatch()
),find
它恰好與 shell 模式匹配非常相似)。你需要用反斜杠轉義它們。- 上面提到的
.xml
,..xml
,的問題。...xml
所以,如果我們解決以上所有問題,我們最終會得到類似的結果:
LC_ALL=C find . -type f -name '*.xml' ! -name .xml ! -name ..xml \ ! -name ...xml -exec sh -c ' for file do base=${file##*/} base=${base%.xml} escaped_base=$(printf "%s\n" "$base" | sed "s/[[*?\\\\]/\\\\&/g"; echo .) escaped_base=${escaped_base%??} find . -name "$escaped_base" -type d -exec mv -v "$file" {\} \; -quit done' sh {} +
呸…
現在,這還不是全部。隨著
-exec ... {} +
,我們盡可能少地執行sh
。如果幸運的話,我們將只執行一個,但如果沒有,在第一次sh
呼叫之後,我們將移動一些xml
文件,然後find
繼續尋找更多文件,很可能會找到我們擁有的文件再次在第一輪中移動(並且很可能嘗試將它們移動到它們所在的位置)。除此之外,它與 zsh 的方法基本相同。其他一些顯著的差異:
- 用
zsh
一個,文件列表是排序的(按目錄名和文件名),所以目標目錄或多或少是一致的和可預測的。使用find
,它基於目錄中文件的原始順序。- ,
zsh
如果沒有找到將文件移動到的匹配目錄,您將收到一條錯誤消息,而不是使用上述find
方法。- 使用
find
,如果無法遍歷某些目錄,您將收到錯誤消息,而不是使用那個zsh
。最後一點警告。如果你得到一些文件名不正確的文件的原因是因為目錄樹可以被對手寫入,那麼請注意,如果對手可能在該命令的腳下重命名文件,上述解決方案都不安全。
例如,如果您使用的是 LXDE,攻擊者可以製作惡意的
foo/lxde-rc.xml
,創建一個lxde-rc
文件夾,檢測您何時執行您的命令,並在比賽視窗期間將其替換lxde-rc
為指向您的符號連結~/.config/openbox/
(可以根據需要放大在許多方面)在find
發現lxde-rc
和mv
執行之間rename("foo/lxde-rc.xml", "lxde-rc/lxde-rc.xml")
(foo
也可以更改為該符號連結,讓您將您的移動lxde-rc.xml
到其他地方)。使用標準甚至 GNU 實用程序可能無法解決這個問題,您需要用適當的程式語言編寫它,進行一些安全的目錄遍歷並使用
renameat()
系統呼叫。
rename()
如果目錄樹足夠深以至於達到系統呼叫的路徑長度限制mv
(導致rename()
失敗ENAMETOOLONG
),上述所有解決方案也將失敗。使用的解決方案renameat()
也可以解決該問題。