我正在嘗試建立一個腳本來將免費 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
注入了一些麻煩的轉義來處理空格。你不應該依靠這樣的逃避。即使您設法使其與空格一起使用,通常也需要轉義其他字元。正確的方法是引用適當地。
引用至少分為三個等級:
- 在原始 shell 中
find
被呼叫的地方; - 在由 ; 產生的 shell 中
-exec sh
或在解釋helper_script
;的 shell 中 - 在遠端產生的 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
$
將所有內容都放在一個簡單變數中的問題是,沒有人能夠知道每個參數是什麼了。