如何使用“find”指令自動轉義shell元字元?

如何使用“find”指令自動轉義shell元字元?

我在目錄樹下有一堆 XML 文件,我想將它們移動到同一目錄樹中具有相同名稱的相應資料夾中。

這是範例結構(在 shell 中):

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

find使用with參數時是否有任何通用的方法來轉義檔名-name,以便我可以更正我的命令以支援帶有元字元的檔案?

答案1

在這裡使用 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但請注意,名為, ..xmlor的檔案...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-rcmv執行rename("foo/lxde-rc.xml", "lxde-rc/lxde-rc.xml")foo也可以更改為該符號鏈接,使您移動到lxde-rc.xml其他地方)之間。

使用標準甚至 GNU 實用程式來解決這個問題可能是不可能的,您需要用適當的程式語言編寫它,進行一些安全的目錄遍歷並使用renameat()系統呼叫。

如果目錄樹夠深,達到了rename()系統呼叫的路徑長度限制(導致失敗並顯示),則上述所有解決方案也將失敗。使用的解決方案也可以解決該問題。mvrename()ENAMETOOLONGrenameat()

答案2

當您將內聯腳本與 一起使用時find ... -exec sh -c ...,您應該find透過位置參數將結果傳遞給 shell,這樣您就不必{}在內聯腳本中的任何地方使用。

如果有bashor zsh,您可以basename透過以下方式傳遞輸出printf '%q'

find . -name "*.xml" -exec bash -c '
  for f do
    BASENAME="$(printf "%q" "$(basename -- "$f" .xml)")"
    DST=$(find . -type d -name "$BASENAME" -print -quit)
    [ -d "$DST" ] && mv -v -- "$f" "$DST/"
  done
' bash {} +

有了bash,您就可以使用printf -v BASENAME,並且如果檔案名稱包含控製字符或非 ascii 字符,則此方法將無法正常工作。

如果你想讓它正常工作,你需要寫一個shell函數來只轉義[、、*?反斜線。

答案3

好消息:

find . -name '[ foo ].xml'

不被 shell 解釋,它透過這種方式傳遞給 find 程式。然而,Find 將參數解釋為-name一種glob模式,這一點需要考慮。

如果您喜歡呼叫find -exec \;或更好find -exec +,則不涉及 shell。

如果您想處理findshell 的輸出,我建議透過set -f在相關程式碼之前呼叫來停用 shell 中的檔案名稱通配符,並透過set +f稍後呼叫來再次開啟它。

答案4

以下是一個相對簡單、符合 POSIX 標準的管道。它會掃描層次結構兩次,首先掃描目錄,然後掃描 *.xml 常規檔案。掃描之間的空白行表示轉換的 AWK 訊號。

AWK 元件將基本名稱對應到目標目錄(如果存在多個具有相同基本名稱的目錄,則只記住第一次遍歷)。對於每個 *.xml 文件,它會列印一個製表符分隔的行,其中包含兩個欄位:1) 文件的路徑和 2) 相應的目標目錄。

{
    find . -type d
    echo
    find . -type f -name \*.xml
} |
awk -F/ '
    !NF { ++i; next }
    !i && !($NF".xml" in d) { d[$NF".xml"] = $0 }
    i { print $0 "\t" d[$NF] }
' |
while IFS='     ' read -r f d; do
    mv -- "$f" "$d"
done

在讀取之前分配給 IFS 的值是文字製表符,而不是空格。

這是使用原始問題的 touch/mkdir 框架的文字記錄:

$ touch foo.xml bar.xml "[ foo ].xml" "( bar ).xml"
$ mkdir -p foo bar "foo/[ foo ]" "bar/( bar )"
$ find .
.
./foo
./foo/[ foo ]
./bar.xml
./foo.xml
./bar
./bar/( bar )
./[ foo ].xml
./( bar ).xml
$ ../mv-xml.sh
$ find .
.
./foo
./foo/[ foo ]
./foo/[ foo ]/[ foo ].xml
./foo/foo.xml
./bar
./bar/( bar )
./bar/( bar )/( bar ).xml
./bar/bar.xml

相關內容