问题背景
所有常用的文件复制工具(如ssh、scp、rsync)都会将文件名作为shell命令的一部分发送给远程系统进行解释。这使得问题变得非常复杂,因为远程shell通常会破坏文件名。
解决方案
处理该问题至少存在三种方法:
- NFS
- 对文件名进行适当编码
- 将文件名作为数据流的一部分进行提交
首先让我们看看不起作用的方法:
# Will not work
scp "my file" remote:"your file"
scp基本上是在ssh之上的一个薄包装层,它通过指示远程系统的shell打开一个写入文件来工作。由于文件名以最简单的方式传递给远程shell,远程shell将空格视为参数分隔符,并最终创建一个名为your的文件。
类似的问题很多人使用错误的解决方法,尝试使用其他工具解决该问题:
# Will not work
ssh remote cat \> "your file" < "my file"
# Will not work
rsync "my file" remote:"your file"
那么,什么方法有效?
NFS
如果你使用NFS(或任何其他能力强大的网络文件系统共享技术,包括sshfs,甚至可能是smbfs)将远程主机的文件系统挂载到本地主机上,那么你可以直接进行复制操作:
cp "my file" /remote/"your file"
谨慎地对远程名称进行编码
显然,如果在编写命令时已经知道远程名称,可以以一种能够被远程shell解析的方式进行编码。通常这意味着添加一个额外的引号层。例如,以下方法有效:
scp "my file" remote:"'your file'"
但是在一般情况下,我们无法在编写脚本时知道确切的远程文件名。它将作为参数、环境变量等传递给脚本。在这种情况下,我们必须足够聪明地编码任何可能的文件名。
问题进一步复杂化的原因是我们不一定知道远程用户使用的是哪个shell。你在客户工作站上使用bash,并不意味着远程系统的sshd会生成bash来解析你的命令。(请记住,scp通过ssh发送一个shell命令,某个未知的远程shell需要解析该命令。)因此,我们使用的任何解决方案都必须尽可能与shell无关。例如,bash的printf %q就被排除在外。
在这些限制下,唯一剩下的方法是在整个文件名周围加上单引号。这意味着我们还必须修改文件名中已有的任何单引号。因此,我们的编码如下所示:
q=\'
dest="'${dest//$q/$q\\$q$q}'"
这样我们得到一个修改后的dest,其起始和结束处有文字意义的单引号,并且用''替换了所有内部的'字符。当此内容传递给远程shell进行解析时,结果就是我们最初的文件名。
因此,完整的复制函数可能如下所示:
# copyto <sourcefile> <remotehost> <remotefile>
copyto() {
local q dest
q=\'
dest="'${3//$q/$q\\$q$q}'"
scp "$1" "$2":"$dest"
}
通过数据流发送文件名
这种方法稍微不太便携,因为它要求远程主机上安装了bash(尽管不一定是远程用户的登录Shell)。它是一种更通用的解决方案,因为理论上可以传递任何类型的数据流,只要您能编写一个解析器来解析它(但请记住,您必须将解析器发送到远程系统进行执行,因此它需要简单)。
在这个示例中,我们将发送一个数据流,其中包含两个内容:文件名和文件的内容。它们将由空字节(NUL byte)分隔。我们使用bash在远程系统上解析此数据流,因为它是极少数几个可以解析NUL分隔的数据流的shell之一。
# copyto <sourcefile> <remotehost> <remotefile>
copyto() {
{ printf '%s\0' "$3"; cat < "$1"; } |
ssh "$2" bash -c \''read -rd ""; cat > "$REPLY"'\'
}
我们的解析器是bash命令read -rd ""; cat > "$REPLY"。它会读取文件名(以NUL结尾)并将其存储在shell变量REPLY中,然后调用cat读取流的剩余部分。解析器周围有两层引号,因为我们需要对本地shell和远程shell进行引用。因此,我们避免在解析器中使用单引号,在本地层使用单引号,并在远程层使用转义的单引号。
这个版本不使用scp,因此不会复制文件的权限。如果你想做到这一点,你可以将权限作为数据流中的另一个对象传递,解析出来并调用chmod。(获取本地文件权限没有通用的方法,所以那实际上是最难的部分。)
最后
如果你想学习如何编写更加健壮和可靠的 Shell 脚本,减少生产环境中的错误和故障,那么关注我吧!我会分享 Shell 编程的最佳实践和建议,帮助你提高 Shell 脚本的鲁棒性和可维护性。如果你想深入了解 Shell 编程的实际应用和技巧,可以关注我的《Shell 脚本编程最佳实践》专栏,里面有我在一线互联网大厂的实际生产经验和最佳实践,帮助你高效完成各种自动化任务。