您将如何优雅地处理此代码片段以允许目录中存在空格?

您将如何优雅地处理此代码片段以允许目录中存在空格?

我有一系列命令,例如:

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'

所以这应该是非常安全的,不仅对于空格,而且对于换行符也是如此。

相关内容