在 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 名称中的最后一个单词):没有这样的文件或目录

我尝试了所有方法让这个 SCP 获得完整路径,但它忽略了所有内容:将单引号、双引号、转义字符放入空格、双重、三重转义字符。将参数直接放入 SCP 中,将 SCP 的所有参数放入变量中,然后传递它。

在脚本外部运行时,命令运行无误。在脚本内运行时,命令会出错,并且只占用空格后的最后部分。

答案1

您的代码在很多方面存在缺陷。

-name *-flat.vmdk容易通配符;它扩展为什么取决于当前工作目录中的文件。*应该用引号引起来(例如-name '*-flat.vmdk')。

这不是您的代码唯一一次缺少引号。echo $line是有缺陷的,因为(和一般来说)。

read line至少应为。如果 所返回的任何路径包含换行符(这是文件名中的有效字符),IFS= read -r line它仍将失败。因此更好。您可以这样做:findfind … -exec … \;

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

这引入了另一个层次的引用;或者像这样:

find … -exec helper_script {} \;

这使得引用更helper_script容易。后一种方法是由这个答案但答案仍然不能解决其他问题。

您的变量dir1dir2似乎注入了一些繁琐的转义来处理空格。您不应该依赖这样的转义。即使您设法让它处理空格,通常也需要转义其他字符。正确的方法是引用适当地。

引用至少有三个层次:

  1. 在原始 shell 中find调用;
  2. 在由 生成的 shell 中-exec sh或在解释 的 shell 中helper_script
  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
$

将所有内容放在一个简单的变量中的问题是没有人能够再知道每个参数是什么。

相关内容