無法在 while 迴圈中轉義 scp 指令中的空格

無法在 while 迴圈中轉義 scp 指令中的空格

我正在嘗試建立一個腳本來將免費 ESXi 6.5 鏡像備份到另一台免費 ESXi 6.5 主機。我快到了,但這個問題讓我發瘋。這是腳本的一部分;我使用 Bash 作為腳本:

#!/bin/sh
find /vmfs/volumes/datastore1/ -regex '.*\.\(vmx\|nvram\|vmsd\|vmdk\)$' ! -name *-flat.vmdk | while read line; do
    dir1=$(dirname "${line}"| sed 's/ /\\ /g')
    dir2=$(dirname "${line}"| sed 's/ /\\\\ /g')
    ssh -n [email protected] "mkdir -p $dir1"
    cmd=$(echo $line "XX.XX.XX.XX:\""$dir2"/\"")
    echo $cmd
    scp -pr $cmd
done

輸出是:

  • 對於每個名稱中沒有空格的虛擬機,都會成功。
  • 對於名稱中帶有空格的每個虛擬機器(虛擬機器名稱中的最後一個單字):沒有這樣的檔案或目錄

我嘗試了一切方法讓這個 SCP 獲得完整路徑,但它忽略了所有內容:將單引號、雙引號、轉義字元轉義為空格、雙轉義字元、三轉義字元。將args直接放入SCP中,將SCP的所有args放入一個變數中並在後面傳遞。

運行外部腳本時,命令運行完美。在腳本中執行時,它會給出錯誤並且僅採用空格後的最後一部分。

答案1

您的程式碼在很多方面都有缺陷。

-name *-flat.vmdk很容易發生通配;它擴充的內容取決於目前工作目錄中的檔案。*應該被引用(例如-name '*-flat.vmdk')。

這並不是您的程式碼唯一一次缺少引號。echo $line是有缺陷的,因為(和一般來說)。

read line至少應該是IFS= read -r line.如果任何路徑(由 返回find)包含換行符(這是檔案名稱中的有效字元),它仍然會失敗。因為這個原因find … -exec … \;比較好。你可以這樣:

find … -exec sh -c '…' sh {} \;

這引入了另一個層次的引用;或者像這樣:

find … -exec helper_script {} \;

這使得引用變得更helper_script容易。後一種方法是由這個答案,答案仍然沒有解決其他問題。

您的變數dir1似乎dir2注入了一些麻煩的轉義來處理空格。你不應該依靠這樣的逃避。即使您設法使其與空格一起使用,通常也需要轉義其他字元。正確的方法是引用適當地。

引用至少分為三個等級:

  1. 在原始 shell 中find被呼叫的地方;
  2. 在由 ; 產生的 shell 中-exec sh或在解釋helper_script;的 shell 中
  3. 在遠端產生的 shell 中ssh … "whatever command"(與 處理的路徑類似scp)。

引入ahelper_script使得第一級不會幹擾其餘的。主要命令是:

find /vmfs/volumes/datastore1/ -regex '.*\.\(vmx\|nvram\|vmsd\|vmdk\)$' ! -name '*-flat.vmdk' -exec /path/to/helper_script {} \;

還有helper_script

#!/bin/sh
# no need for bash

addrs=XX.XX.XX.XX

pth="$1"
drctry="${pth%/*}"
# no need for dirname (separate executable)

ssh "root@$addrs" "mkdir -p '$drctry'"
scp -pr "$pth" "$addrs:'$drctry/'"

現在重要的是作為字串ssh獲取。mkdir -p 'whatever/the var{a,b}e/expand$t*'這被傳遞到遠端 shell 並解釋的。如果沒有內部單引號,它可能會以您不想要的方式解釋;我的例子誇大了這一點。你可以嘗試逃避每一個麻煩的角色,但這會很難;所以引用。

如果變數包含任何單引號,則某些子字串可以在遠端取消引用。這會打開一個程式碼注入漏洞。例如這條路徑:

…/foo/'$(nasty command)'bar/baz/…

當嵌入單引號並解釋時會非常危險。您應該$drctry提前消毒:

drctry="$(printf '%s' "${pth%/*}" | sed "s/'/'\"'\"'/g")"

範例危險路徑現在如下所示:

…/foo/'"'"'$(nasty command)'"'"'bar/baz/…

這與您的用法有些相似sed,但由於單引號字符現在是唯一麻煩的字符,因此應該更好。

scp出於基本相同的原因,需要在遠端路徑中進行類似的引用。同樣,使用反斜線進行正確的轉義會更麻煩(如果可能的話)。


一項細微的改進是允許幫助程式腳本處理多個物件。這將運行更少的 shell 進程:

find /vmfs/volumes/datastore1/ -regex '.*\.\(vmx\|nvram\|vmsd\|vmdk\)$' ! -name '*-flat.vmdk' -exec /path/to/helper_script_2 {} +

還有helper_script_2

#!/bin/sh

addrs=XX.XX.XX.XX

for pth; do
   drctry="$(printf '%s' "${pth%/*}" | sed "s/'/'\"'\"'/g")"
   ssh "root@$addrs" "mkdir -p '$drctry'"
   scp -pr "$pth" "$addrs:'$drctry/'"
done

可以使用-exec sh -c '…'(或-exec sh -c "…") 建置獨立命令(不引用任何幫助程式腳本)。由於最外面的引號,這會變成引用和/或逃避的瘋狂。以下命令替換技巧和此處文件有助於避免這種情況:

find /vmfs/volumes/datastore1/ \
   -type f \
   -regex '.*\.\(vmx\|nvram\|vmsd\|vmdk\)$' \
 ! -name '*-flat.vmdk' \
   -exec sh -c "$(cat << 'EOF'

addrs=XX.XX.XX.XX

for pth; do
   drctry="$(printf '%s' "${pth%/*}" | sed "s/'/'\"'\"'/g")"
   ssh "root@$addrs" "mkdir -p '$drctry'" \
   && scp -pr "$pth" "$addrs:'$drctry/'"
done

EOF
   )" sh {} +

要在變數擴展的上下文中充分理解這一點(以及前面片段中的一些片段),您需要了解引號內的引號為什麼EOF被引用(連結的答案引用了man bash但這更一般POSIX 行為)。另請注意,我添加了-type f排除可能與正規表示式相符的目錄;我寫道ssh … && scp …,所以如果前者失敗(包括mkdir -p失敗時),後者將不會運行。

答案2

將管道 ( ) 右側的內容移至|shell 腳本,然後執行以下操作

find /vmfs/volumes/datastore1/ -regex '.*\.\(vmx\|nvram\|vmsd\|vmdk\)$' ! -name *-flat.vmdk -exec /path/to/shell/script {} \;

{}正確轉義它成功的每個檔案名find,然後呼叫您的腳本,將轉義/引用的檔案名稱作為第一個參數傳遞。只需$1在腳本中訪問它即可。

答案3

見證數組的魔力:

$ line="meh bleh"
$ dir="hello\ world"
$ cmd=$(echo "$line" "$dir")
$ for i in $cmd; do echo "$i"; done
meh
bleh
hello\
world
$ for i in "$cmd"; do echo "$i"; done
meh bleh hello\ world
$ cmd=("$line" "$dir")
$ for i in "${cmd[@]}"; do echo "$i"; done
meh bleh
hello\ world
$

將所有內容都放在一個簡單變數中的問題是,沒有人能夠知道每個參數是什麼了。

相關內容