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

出力は次のようになります。

  • 名前にスペースが含まれていないすべての VM で成功しました。
  • 名前にスペースが含まれるすべての VM について (VM 名の最後の単語): そのようなファイルまたはディレクトリはありません

この SCP が完全なパスを取得するようにあらゆることを試しましたが、すべてが無視されます。一重引用符、二重引用符、スペースへのエスケープ文字、二重、三重エスケープ文字を入力します。引数を SCP に直接入力し、SCP のすべての引数を変数に入れてその後に渡します。

スクリプトの外部で実行すると、コマンドは問題なく実行されます。スクリプト内で実行すると、エラーが発生し、スペースの後の最後の部分のみが取得されます。

答え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後者のアプローチは、この答えしかし、その答えでは他の問題は解決されません。

変数dir1dir2スペースを扱うために、面倒なエスケープ処理が挿入されているようです。このようなエスケープ処理に頼るべきではありません。スペースを扱うことができたとしても、一般的にエスケープする必要のある文字は他にもあります。正しい方法は、引用きちんと。

引用には少なくとも 3 つのレベルがあります。

  1. findが呼び出される元のシェルでは、
  2. によって生成されたシェル内-exec sh、または を解釈するシェル内helper_script
  3. によってリモート側で生成されたシェル内ssh … "whatever command"( によって処理されるパスについても同様scp)。

を導入すると、helper_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*'。これはリモートシェルに渡され、解釈された内側の一重引用符がないと、望ましくない方法で解釈される可能性があります。私の例では、この点が強調されています。問題のある文字をすべてエスケープしようとすると、困難になるため、引用符を使用します。

しかし変数に一重引用符が含まれている場合、リモート側で一部の文字列が引用符で囲まれなくなる可能性があります。これにより、コード インジェクションの脆弱性が生じます。例:

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

シングルクォートで囲まれて解釈されると非常に危険です。$drctry事前にサニタイズする必要があります。

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

危険なパスの例は次のようになります。

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

これは の使用法と多少似ていますsedが、問題となる文字はシングルクォーテーション文字だけになったので、より良いはずです。

scp基本的に同じ理由で、リモート パスでも同様の引用符が必要です。繰り返しますが、バックスラッシュで適切にエスケープするのは (可能であれば) 面倒です。


ちょっとした改善として、ヘルパー スクリプトが複数のオブジェクトを処理できるようになります。これにより、シェル プロセスの実行回数が少なくなります。

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

パイプ( )の右側の部分を|シェルスクリプトに移動し、次のようにします。

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
$

すべてを単純な変数に入れることの問題は、それぞれの引数が何であるか誰もわからなくなってしまうことです。

関連情報