天天看点

为什么shell中要注意文件的命名方式

作者:SuperOps
为什么shell中要注意文件的命名方式

‬问题背景

所有常用的文件复制工具(如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 脚本编程最佳实践》专栏,里面有我在一线互联网大厂的实际生产经验和最佳实践,帮助你高效完成各种自动化任务。