【问题标题】:Shell: Capturing output files in variablesShell:在变量中捕获输出文件
【发布时间】:2023-12-26 17:37:02
【问题描述】:

openssl 之类的命令具有用于输出文件的-out <file> 之类的参数。我想在 shell 变量中捕获这些输出文件的内容,以便在其他命令中使用,而无需创建临时文件。例如,要生成自签名证书,可以使用:

openssl req -new -newkey rsa:2048 -subj / -days 365 -nodes -x509 -keyout KEYFILE -out CERTFILE 2>/dev/null

我要捕获两个输出文件的最接近方法是通过进程替换将它们回显到标准输出,但这并不理想,因为仍然需要将它们分开解析。

openssl req -new -newkey rsa:2048 -subj / -days 365 -nodes -x509 -keyout >(key=$(cat -); echo $key) -out >(cert=$(cat -); echo $cert) 2>/dev/null

有没有一种干净的方法可以在 shell 变量中捕获输出文件的内容?

【问题讨论】:

  • 请仅标记一个外壳。 shbashzsh在很多方面互不兼容;只有最低公分母的答案(使用大量额外代码——例如设置命名的 FIFO——以实现兼容性)适用于所有三个。 >(...) 等进程替换在 sh 中根本不起作用,f/e;您在我们的示例中使用的一些代码(即echo $key)仅在 zsh 中是安全的,而在其他任何一个中都不安全。
  • ...也就是说,尽管您可能不喜欢它,但这里最简单、最可靠的解决方案是使用临时文件。即使是命名的 FIFO 方法也会有一些令人讨厌的失败案例(其中 openssl 根本不写入 FIFO 的失败让 shell 挂起等待输出)。

标签: bash shell sh zsh


【解决方案1】:

Gilbert's answer 的扩展提供了额外的偏执:

eval "$( openssl req -new -newkey rsa:2048 -subj / -days 365 -nodes -x509 \
         -keyout >(printf 'keyout=%q\n' "$(</dev/stdin)") \
         -out >(printf 'out=%q\n' "$(</dev/stdin)") )"

(请注意,如果您的数据包含 NUL,这是不合适的,因为 bash 无法将其存储在本机 shell 变量中;在这种情况下,您需要以 base64 编码的形式将内容分配给您的变量)。

echo "keyout='$(cat)'" 不同,printf 'keyout=%q\n' "$(cat)" 确保即使是恶意内容也不能被 shell eval用作命令替换、重定向或文字数据以外的其他内容。


为了解释为什么这是必要的,我们来看一个简化的案例:

write_to_two_files() { printf 'one\n' >"$1"; printf 'two\n' >"$2"; }
write_to_two_files >(echo "one='$(cat)'") >(echo "two='$(cat)'")

...我们得到类似于(但没有特定顺序)的输出:

two='two'
one='one'

...当evaled 时,它设置了两个变量:

$ eval "$(write_to_two_files >(echo "one='$(cat)'") >(echo "two='$(cat)'"))"
$ declare -p one two
declare -- one="one"
declare -- two="two"

但是,假设我们的程序的行为有点不同:

## Demonstrate why eval'ing content created by echoing data is dangerous
write_to_two_files() {
  printf "'%s'\n" '$(touch /tmp/i-pwned-your-box)' >"$1"
  echo "two" >"$2"
}
eval "$(write_to_two_files >(echo "one='$(cat)'") >(echo "two='$(cat)'"))"
ls -l /tmp/i-pwned-your-box

我们不只是将输出分配给变量,而是将其作为代码进行评估


如果您对确保两个打印操作在不同时间发生(防止它们的输出混合)进一步感兴趣,那么进一步添加锁定很有用。这确实涉及一个临时文件,但将您的密钥材料写入磁盘(避免使用临时文件是最有说服力的理由):

lockfile=$(mktemp -t output.lck.XXXXXX)
eval "$( openssl req -new -newkey rsa:2048 -subj / -days 365 -nodes -x509 \
         -keyout >(in=$(cat); exec 99>"$lockfile" && flock -x 99 && printf 'keyout=%q\n' "$in") \
         -out    >(in=$(cat); exec 99>"$lockfile" && flock -x 99 && printf 'out=%q\n' "$in") )"

请注意,我们只阻止写入,而不是读取,因此我们无法进入竞争条件(即,openssl 未完成写入文件-A,因为它在写入文件时被阻止- B,它永远无法完成,因为文件读取端的子shell-A写入持有锁)。

【讨论】:

  • 如果你只是个男人,看吧,我想你现在知道你的毛巾在哪里了。无论如何,感谢互动我已经有一段时间没有找到可以认真学习更多 shell 知识的人了。不确定你会看到这个,但谢谢。在其他地方浏览你的一些答案。吉尔伯特。
【解决方案2】:

大多数现代 shell 现在支持 /dev/stdout 作为文件名以重定向到标准输出。这对于单文件解决方案来说已经足够了,但对于两个输出文件,您需要使用“进程替换”。

eval "$( openssl req -new -newkey rsa:2048 -subj / -days 365 -nodes -x509 -keyout >(echo "keyout='$( cat )'" ) -out >(echo out=" '$( 猫 )'" ) )"

这使用进程替换将每个“文件”定向到一个单独的进程,该进程将计算值的分配打印到标准输出。然后将整个事情传递给 eval 以执行实际分配。

保留 stderr 的输出以显示弹出的任何错误消息。在遇到麻烦时记录它很有用。

编辑:合并Charles Duffy'sgood paranoia:

flockf="$(mktemp -t tmp.lock.$$.XXXXXX )" ||退出 $?;

eval "$( openssl req -new -newkey rsa:2048 -subj / -days 365 -nodes -x509 \
        -keyout >( 设置 -x; 99>"$flockf" && \
                群 -x "$flockf" printf "keyout=%q" "$( cat )"; ) \
        -out >( set -x; 99>"$flockf" && \
                群 -x "$flockf" printf "out=%q" "$( cat )"; ) \
        )" ;
rm -f "$flockf"

【讨论】:

  • @CharlesDuffy - 我完全赞成偏执狂 - 是的,应该使用 printf %q..
  • 顺便说一句——在exit $? 中不需要$?,因为$?exit 使用的默认状态。
  • 很高兴看到更改被采纳;留下我的答案只是为了让任何想知道为什么这是必要的人的安全漏洞的复制器记录在案。
  • 也就是说,我很惊讶地看到 99&gt;"$flockf" 没有前面的 exec 让 FD 为将来的命令打开,而不是只是暂时打开它。这是在哪个 shell 上测试的?
  • ...哦——我明白为什么这似乎有效了。糟糕——这是我在回答中犯的一个错误;现在编辑为通过 FD 锁定,而不是名称,因此锁定是通过 printf 持有的,而不是在它之前的短暂时间内。