我想获取命令输出的字节数和 sha1sum。
原则上,人们总是可以做这样的事情:
BYTES="$( somecommand | wc -c )"
DIGEST="$( somecommand | sha1sum | sed 's/ .*//' )"
...但是,对于我感兴趣的用例,somecommand
相当耗时,并且会产生大量输出,所以我宁愿只调用它一次。
我想到的一种方法是这样的
evil() {
{
somecommand | \
tee >( wc -c | sed 's/^/BYTES=/' ) | \
sha1sum | \
sed 's/ .*//; s/^/DIGEST=/'
} 2>&1
}
eval "$( evil )"
...这似乎有效,但让我内心有点死了。
我想知道是否有更好(更稳健、更通用)的方法来将管道不同段的输出捕获到单独的变量中。
编辑:我目前正在解决的问题是bash
,所以我最感兴趣的是这个 shell 的解决方案,但我也做了很多zsh
编程,所以我对这些解决方案也有一些兴趣。
EDIT2:我尝试将 Stéphane Chazelas 的解决方案移植到bash
,但它不太有效:
#!/bin/bash
cmd() {
printf -- '%1000s'
}
bytes_and_checksum() {
local IFS
cmd | tee >(sha1sum > $1) | wc -c | read bytes || return
read checksum rest_ignored < $1 || return
}
set -o pipefail
unset bytes checksum
bytes_and_checksum "$(mktemp)"
printf -- 'bytes=%s\n' $bytes
printf -- 'checksum=%s\n' $checksum
当我运行上面的脚本时,我得到的输出是
bytes=
checksum=96d89030c1473585f16ec7a52050b410e44dd332
的值checksum
是正确的。我不明白为什么bytes
没有设置 的值。
EDIT3:好的,感谢@muru 的提示,我解决了问题:
#!/bin/bash
cmd() {
printf -- '%1000s'
}
bytes_and_checksum() {
local IFS
read bytes < <( cmd | tee >(sha1sum > $1) | wc -c ) || return
read checksum rest_ignored < $1 || return
}
set -o pipefail
unset bytes checksum
bytes_and_checksum "$(mktemp)"
printf -- 'bytes=%s\n' $bytes
printf -- 'checksum=%s\n' $checksum
现在:
bytes=1000
checksum=96d89030c1473585f16ec7a52050b410e44dd332
很遗憾...
...当产生的输出比上面的玩具示例中的情况多得多时,我的bytes_and_checksum
函数就会停止(死锁?) 。cmd
回到绘图板...
答案1
使用临时文件会更容易。在zsh
:
(){set -o localoptions -o pipefail; local IFS
{cmd} > >(sha1sum > $1) | wc -c | read bytes || return
read checksum rest_ignored < $1 || return
} =()
请注意,许多wc
实现在其输出的数字周围包含空格。read
默认值是$IFS
剥离它们。
请注意, 的退出状态sha1sum
会丢失。
创建=()
临时文件时根本不输出任何内容。当提供给该临时文件的命令(此处为匿名函数)返回时,该临时文件将自动删除。
在cmd > file | other-cmd
,cmd
的输出tee
内部由 d进行处理zsh
,因为它被重定向了两次,所以这里都是 tosha1sum
和 to wc
。我们进行包装cmd
以{...}
确保 zsh 等待进程重定向完成。
这里作为两者的输出sha1sum
,wc
保证不大于几个字节,它们也可以发送到管道,并且您不必同时从这些管道中读取(zsh 可以做到这一点,因为它有一个select()
/的接口poll()
,但是不是bash)。这可以按顺序完成而不会导致死锁,所以这是一个简单的版本进入不同的变量。
在基于 Linux 的系统上(当/dev/fd/x
管道x
的 fd 的行为类似于命名管道时):
{
IFS=$' \t' read bytes < <(cmd 3<&- | tee >(sha1sum > /dev/fd/3) | wc -c)
IFS=$' \t' read sum rest <&3
} 3< <(:)
(甚至可以在 bash 中工作)。
有关较大输出时遇到的死锁的详细信息,另请参阅tee + cat:多次使用输出,然后连接结果。
答案2
我正在使用备份巴什脚本,它具有以下帮助器“中间”函数,这些函数将“假定的文件名”作为参数(请参见下面的 tar.gz 示例):
function pipesum
{
tee >(sha1sum | awk --assign F="${1##*/}" '$2=F' > "${1?}.sha1")
}
function pipelen
{
tee >(wc -c > "${1?}.len")
}
function pipesumlen
{
tee >(sha1sum | awk --assign F="${1##*/}" '$2=F' > "${1?}.sha1") >(wc -c > "${1?}.len")
}
function pipechecksum
{
tee >(sha1sum --quiet -c <(awk '$2="-"' "${1?}") >&2)
}
例子:
$ echo 123 | pipesumlen filename
123
$ ls filename*
filename.len filename.sha1
$ cat filename*
4
a8fdc205a9f19cc1c7507a60c4f01b13d11d7fd0 filename
$ echo 123 | pipechecksum filename.sha1
123
$ echo 1234 | pipechecksum filename.sha1
1234
-: FAILED
sha1sum: WARNING: 1 computed checksum did NOT match
我在一个非常耗时、CPU 和 IO 消耗的脚本中使用它,如下所示:
tar |
pipesumlen mybackup.tar |
gzip > mybackup.tar.gz
<mybackup.tar.gz gunzip |
pipechecksum mybackup.tar.sha1 |
xz > mybackup.tar.xz
因此,我根据随机内存/磁盘位翻转检查了我的备份。它创建“mybackup.tar.sha1”文件,就像实际创建“mybackup.tar”并进行校验和一样,而实际上在此示例中,未压缩的数据从未写入磁盘上。
警告:pipechecksum
即使出现错误,也不会终止脚本set -euo pipefail
。pipechecksum
在校验和不匹配时返回非零的替代方案:
function pipechecksum
{
{ tee /dev/fd/$N | sha1sum --quiet -c <(awk '$2="-"' "${1?}") >&2; } {N}>&1
}
看起来不错,但我今天才带来它,还不能认为它已得到证实。