Bash - 为什么 $(sudo cat file) 找不到一个存在的文件?

Bash - Why $(sudo cat file) cannot find a file which exists?

问题

为什么 $(sudo cat) 找不到一个存在的文件?

这个有效:

for host in $(cat /etc/ansible/hosts | cut -d ' ' -f 1 | grep -P '^master-' | sort | uniq)
do
    ssh ${host} /bin/bash << EOF
        sudo cat /etc/origin/master/ca-bundle.crt
EOF 
done

-----BEGIN CERTIFICATE-----
XYZ...
-----END CERTIFICATE-----

这不起作用,但不知道为什么:

for host in $(cat /etc/ansible/hosts | cut -d ' ' -f 1 | grep -P '^master-' | sort | uniq)
do
    ssh ${host} /bin/bash << EOF
        RESULT="$(sudo cat /etc/origin/master/ca-bundle.crt; echo x)"
        echo "${RESULT%x}"
EOF
done

"cat: /etc/origin/master/ca-bundle.crt: No such file or directory"

修复

根据答案,如下修复并成功。非常感谢。

#!/bin/bash
set -u
for host in $(cat /etc/ansible/hosts | cut -d ' ' -f 1 | grep -P '^master-' | sort | uniq)
do
    ssh ${host} /bin/bash <<'EOF'
RESULT="$(sudo cat /etc/origin/master/ca-bundle.crt; echo x)"
echo "${RESULT%x}"
EOF
done

出现问题是因为命令和变量替换是在此处文档中执行的,然后它们被传递给命令。因此,$(sudo cat /etc/origin/master/ca-bundle.crt; echo x)${RESULT%x} 都在本地计算机上计算(其中 /etc/origin/master/ca-bundle.crt 可能不存在并且 RESULT 未定义)。结果,这是发送到远程计算机的内容:

    RESULT="x"
    echo ""

这根本不是您想要的。

为避免这种情况,您可以转义 here-document 中的 $ 字符,或引用 here-document 分隔符 (<<'EOF')。顺便说一句,here-document 中的缩进字符将通过 ssh 连接传递,这可能会产生不幸的后果(例如调用远程 shell 的自动完成功能)。我要么缩进这里的文件,要么在分隔符前加上“-”(<<-'EOF')并使用制表符(而不是空格)进行缩进。

编辑:在考虑了@tripleee 的回答后,我不确定我是否同意他建议的所有重写,但他肯定是正确的,循环的某些部分比它们应该的更复杂 and/or 脆弱.以下是我的更改建议:

  • 除非在远程计算机上完成的工作比问题中显示的要多,否则 ssh 部分可以(并且应该)大大简化。不需要把证书文件的内容放到一个变量中然后echo这个变量,直接输出即可。请注意,整个问题的出现是因为将其传入和传出变量的复杂性;如果远程部分只是 sudo cat /etc/origin/master/ca-bundle.crt,那么整个事情就会成功。此外,无需显式 运行 远程 bash shell 并使用重定向将命令提供给 运行,只需使用 ssh ${host} sudo cat /etc/origin/master/ca-bundle.crt.

    好的,变量的东西确实做了一件事。它在证书文件的内容之后添加了一个空行。发生这种情况是因为 "x" 技巧小心地保留了文件内容中的最后一个换行符(通常,$( ) 删除尾随的换行符),然后 echo 添加 另一个 换行符,导致空行。但如果这是故意的,那么只需添加另一个不带参数的 echo 命令就可以更容易(也更清晰)。

列出主机名的管道比需要的要长得多,但这里需要在复杂性和 readability/clarity 之间进行权衡。 tripleee 的版本要短得多,但更难读懂它在做什么(除非你精通 awk),这意味着存在更多错误风险,更难维护等。但仍有一些我推荐的简化方法:

  • 不要使用 cat 将单个文件送入管道。使用 <filename 重定向,或者(如果命令支持它)只给它命令文件名并让它从中读取:

    cut -d ' ' -f 1 /etc/ansible/hosts | ...
    cut -d ' ' -f 1 </etc/ansible/hosts | ...
    

    这可能看起来不如使用 cat | 自然,但这是最好(也是最标准)的做事方式。坦率地说,如果它看起来不自然或令人困惑,那只是意味着你做得还不够。所以多做一些。

  • 您的管道让每个命令只做一件事情:cut 获取第一个字段,grep 选择那些以 "master-"、[=33= 开头的字段] 将它们按顺序排列,uniq 删除重复项。但是 sort 本身完全能够删除重复项,所以我会使用 sort -u 而不是 sort | uniqawk 几乎可以自己完成所有事情(但如果让它做太多,就很难做到 read/understand)。就个人而言,我会使用:

    awk ' ~ /^master-/ {print }' /etc/ansible/hosts | sort -u
    

    (那个 awk 脚本可以读作“如果第一个字段以 'master-' 开头,打印它。)

然后是如何遍历主机列表的问题。 for var in $(somecommand) 被广泛认为是一种反模式,因为可能出错的地方很多。它将命令的输出分成 "words"(可能对应也可能不对应行),然后尝试将它找到的任何通配符扩展到匹配文件列表(可以有 really 奇怪的效果),然后迭代那个结果。

在这种特殊情况下,我认为你是安全的(除非你重新定义 $IFS)。您的条目中不应有任何单词分隔符(空格、制表符和换行符),因为您已经只选择了第一个字段。条目中不应有任何通配符,因为主机名中通常不允许使用“*”、“?”和“[”。我想可能有一个“[hexdigits:hexdigits::hexdigits]”格式的原始 IPv6 地址——一个shell通配符, 如果有匹配的文件会造成麻烦——但是 ^master- 模式不会选择这些文件。

替代方法是将列表通过管道传递给 xargs 命令(如@tripleee 的回答)或 while read 循环。它们都有一个不同的潜在问题,如果您不小心,ssh 可以从输入流中窃取主机名。 ssh -n 将阻止这种情况。

在这种情况下,我想我只需要使用 for 循环。因此,综上所述,这是我建议的重写:

#!/bin/bash
set -u
for host in $(awk ' !~ /^master-/ {print }' /etc/ansible/hosts | sort -u); do
    ssh "${host}" sudo cat /etc/origin/master/ca-bundle.crt
    echo
done

你的循环驱动程序仍然有点糟糕。你可以简单地避免 useless use of cat 但整个事情看起来像是在乞求重构

#!/bin/bash
awk ' !~ /^master-/ && !a[] { print ; ++a[] }' /etc/ansible/hosts |
xargs -r -n 1 ssh {} sudo cat /etc/origin/master/ca-bundle.crt

这消除了添加然后删除尾随 x - 我怀疑你首先结束了那个 因为 你正在捕获然后 echoing 输出,而不是让 cat 做它做的事 -- 打印到标准输出。