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

时间:2017-11-17 07:01:00

标签: bash sudo subshell

问题

为什么$(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
  

-----开始证书-----
  XYZ ...
  -----结束证书-----

这不起作用,但不明白为什么:

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:没有这样的文件或目录”

修复

根据答案,修复如下并工作。非常感谢。

#!/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

2 个答案:

答案 0 :(得分:3)

问题出现了,因为命令和变量替换是在 传递给命令之前在here-documents 中执行的。因此,$(sudo cat /etc/origin/master/ca-bundle.crt; echo x)${RESULT%x}都在本地计算机上进行评估(其中/etc/origin/master/ca-bundle.crt可能不存在且RESULT不是&#39} ; t定义)。因此,这是发送到远程计算机的内容:

    RESULT="x"
    echo ""

这根本不是你想要的。

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

编辑:在考虑了@ tripleee的答案之后,我不确定我是否同意他所有建议的重写,但他肯定是正确的,因为部分循环更复杂和/或者比他们应该的更脆弱。以下是我对变更的建议:

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

    好的,变量的东西确实做了一件事。它在证书文件的内容之后添加了一个空行。这是因为&#34; x&#34;诀窍仔细保留文件内容中的最终换行符(通常,$( )删除尾随换行符),然后echo添加另一个换行符,从而产生一个空白行。但如果这是故意的,只需添加另一个没有参数的echo命令就可以更容易(也更清晰)。

列出主机名的管道要比它需要的时间长得多,但是这里需要在复杂性和可读性/清晰度之间进行权衡。 tripleee的版本要短得多,但要更好地了解它所做的事情(除非你能够流利地使用awk),这意味着&#39;更多的错误风险,维护难度等等。但我仍然建议进行一些简化:

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

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

    这似乎不如使用cat |那么自然,但它是最好(也是最标准)的做事方式。坦率地说,如果它看起来不自然或令人困惑,那就意味着你还没有做到这一点。所以做得更多。

  • 您的管道中的每个命令都只执行以下操作:cut获取第一个字段,grep选择以&#34开头的字段;#34;,{{1按顺序放置它们,sort删除重复项。但uniq完全能够删除重复项,因此我使用sort代替sort -u。并且sort | uniq几乎可以完成所有事情(但如果你做得太多,就很难阅读/理解)。我个人使用:

    awk

    awk '$1 ~ /^master-/ {print $1}' /etc/ansible/hosts | sort -u 脚本可以读为&#34;如果第一个字段以&#39; master - &#39;开头,则打印出来。)

然后是如何迭代主机列表的问题。 awk被广泛认为是反模式,因为可能出错的事情数量很多。它将命令的输出分解为&#34; words&#34; (可能或可能不对应于行),然后尝试将它找到的任何通配符展开到匹配文件列表中(可能具有真正奇怪的效果),然后迭代结果。

在这种特殊情况下,我认为您是安全的(除非您重新定义for var in $(somecommand))。您的条目中不应包含任何单词分隔符(空格,制表符和换行符),因为您已经只选择了第一个字段。条目中不应该有任何通配符,因为&#34; *&#34;,&#34;?&#34;和&#34; [&#34;通常不允许在主机名中使用。我想在&#34; [hexdigits:hexdigits :: hexdigits]&#34;中可能存在原始IPv6地址。格式 - 如果有匹配的文件, 是一个shell通配符而导致麻烦 - 但{{1}将不会选择这些格式模式。

替代方法是使用管道列表来$IFS命令(如在@ tripleee&#39; s答案中)或^master-循环。这两者都有一个不同的潜在问题,如果您不小心,xargs可以从输入流中窃取主机名。 while read会阻止这种情况发生。

在这种情况下,我想我只是使用ssh循环。所有这些,这是我建议的重写:

ssh -n

答案 1 :(得分:1)

你的循环驱动程序仍然有点可怕。你可以轻而易举地避免useless use of cat,但整个事情看起来像是在乞求重构

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

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