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 | uniq
。 awk
几乎可以自己完成所有事情(但如果让它做太多,就很难做到 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
- 我怀疑你首先结束了那个 因为 你正在捕获然后 echo
ing 输出,而不是让 cat
做它做的事 -- 打印到标准输出。
问题
为什么 $(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 | uniq
。awk
几乎可以自己完成所有事情(但如果让它做太多,就很难做到 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
- 我怀疑你首先结束了那个 因为 你正在捕获然后 echo
ing 输出,而不是让 cat
做它做的事 -- 打印到标准输出。