我有一系列命令,例如:
ssh -i key 10.10.10.10 mkdir -p "${DEST_PATH}/subdir1" "${DEST_PATH}/subdir2"
rsync "${SOURCE_PATH}" "$DEST_HOST:${DEST_PATH}/subdir1"
...
这些被馈送变量为
DEST_PATH=$( myfun "foo" )
SOURCE_PATH=$( myfun "bar" )
依次将字符串馈送到
function myfun {
local VALUE=$( grep "$1" yadayada.txt | tail -1 | sed 's/someregex//' )
echo "$VALUE"
}
$VALUE
如果有一些空间,$VALUE
你会如何处理Hello World
?我以为一直使用""
就能解决所有问题,但我错了。其他几个问题建议转义或使用单引号,但我不确定最好的方法。
谢谢
答案1
如果您知道远程系统有xargs
支持某个-0
选项的命令,您可以执行以下操作:
printf '%s\0' "$DEST_PATH/subdir1" "$DEST_PATH/subdir2" |
ssh -i key 10.10.10.10 'xargs -0 mkdir -p --'
所有 shell 都会对这段xargs -0 mkdir -p --
shell 代码进行相同的解释(请记住,sshd
在远程计算机上运行远程用户的登录 shell 来解释给定的代码,ssh
该代码可以是任何内容)。
的参数列表mkdir
通过远程 shell 的 stdin 传递 NUL 分隔,由 继承xargs
。
该方法的另一个优点是您可以传递任意数量的参数。如果需要,xargs
将破坏该列表并运行多次调用来mkdir -p -- dirn dirn+1...
绕过参数+环境列表的大小限制,甚至可能在传输非常大的列表的完整列表之前开始目录创建。
看如何在不知道远程用户的登录 shell 的情况下通过 ssh 执行任意简单命令?对于其他方法。
rsync
默认情况下,它本身也有同样的问题,因为它传递远程文件/目录的名称作为远程 shell 命令行的一部分进行同步,更好的解决方案是使用-s
/--protect-args
它不是在 shell 代码中传递文件/目录名称,而是在rsync
服务器的标准输入上传递它。
所以:
rsync -s -- "$SOURCE_PATH" "$DEST_HOST:$DEST_PATH/subdir1/"
引用手册rsync
页:
如果需要传输包含空格的文件名,可以指定
--protect-args
(-s
) 选项,或者需要以远程 shell 能够理解的方式转义空格。例如:rsync -av host:'file\ name\ with\ spaces' /dest
$ rsync --rsync-path='set -x; rsync' /etc/issue localhost:'1 2'
+zsh:1> rsync --server -e.LsfxC . 1 2
$ rsync --rsync-path='set -x; rsync' /etc/issue localhost:'1;uname>&2'
+zsh:1> rsync --server -e.LsfxC . 1
+zsh:1> uname
Linux
查看文件名如何解释为 shell 代码。
和-s
:
~$ rsync -s --rsync-path='set -x; rsync' /etc/issue localhost:'1;uname>&2'
+zsh:1> rsync --server -se.LsfxC
远程 shell 命令行上没有文件名的踪迹。
~$ cat '1;uname>&2'
Ubuntu 20.04.1 LTS \n \l
答案2
使用双引号解决最常见的问题,但这并不能解决所有问题。在您的情况下,双引号可以防止本地 shell 中不必要的扩展,但随后您的ssh
命令将文件名直接注入到 shell 片段中而不受保护,而 rsync 在其命令行上传递的远程文件名的底层执行相同的操作。也就是说,如果DEST_PATH
是/path/with three spaces
,您正在运行命令
mkdir -p /path/with three spaces/subdir1 /path/with three spaces/subdir2
在远程机器上。双引号$DEST_PATH
确保运行脚本的 shell 不会过度分割该值,但空格在远程 shell 中是有意义的。
天真地使用嵌套引号确实如此不是解决这个问题。例如,ssh -i key 10.10.10.10 mkdir -p "'${DEST_PATH}/subdir1'" "'${DEST_PATH}/subdir2'"
可以使用带有空格的文件名,但包含空格的文件名将'
不起作用(如果对手控制了 的值,则可能是远程命令执行漏洞DEST_PATH
)。
最简单的出路就是跑步sshfs并将所有内容视为本地路径。如果您从不需要在ssh
或命令中传递文件名rsync
,则无需担心引用这些文件名。然而,sshfs 并不总是方便管理,并且无法通过 rsync 有效更新大文件。
如果简单的方法不适合您,您需要引用 的值DEST_PATH
。也就是说,您需要更改为类似或 的/path/with three spaces
内容。'/path/with three spaces'
/path/with\ three\ \ spaces
set_quoted () {
raw="$1"
quoted=
while case "$raw" in *\'*) true;; *) false;; esac; do
quoted="$quoted'\\''${raw%%\'*}"
raw="${raw#*\'}"
done
quoted="'$quoted$raw'"
}
set_quoted "$DEST_PATH"; quoted_DEST_PATH="$quoted"
ssh -i key 10.10.10.10 mkdir -p "${quoted_DEST_PATH}/subdir1" "${quoted_DEST_PATH}/subdir2"
rsync "${SOURCE_PATH}" "$DEST_HOST:${quoted_DEST_PATH}/subdir1"
1 Rsync 对于递归或本地文件中的任意文件名没有问题。但它将作为远程文件名的命令行参数传递给远程 shell,远程 shell 会扩展它们。
答案3
如果您有 GNU 系统,则printf
支持%q
,这使得参数可以作为 shell 输入重复使用。
所以你可以做
DEST_PATH_ESC="$(printf "%q" "$DEST_PATH")
ssh -i key 10.10.10.10 mkdir -p "${DEST_PATH_ESC}/subdir1" "${DEST_PATH_ESC}/subdir2"
rsync "${SOURCE_PATH}" "$DEST_HOST:${DEST_PATH_ESC}/subdir1" # not sure if you need it here
我们来测试一下:
a="some complicated thing with spaces
and such"
b=$(printf "%q" "$a")
echo "$b"
我们得到
$'some complicated thing with spaces\nand such'
所以这应该是非常安全的,不仅对于空格,而且对于换行符也是如此。