如何单次获取字节数和 sha1sum?

如何单次获取字节数和 sha1sum?

我想获取命令输出的字节数和 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 等待进程重定向完成。

这里作为两者的输出sha1sumwc保证不大于几个字节,它们也可以发送到管道,并且您不必同时从这些管道中读取(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 pipefailpipechecksum在校验和不匹配时返回非零的替代方案:

function pipechecksum
{
  { tee /dev/fd/$N | sha1sum --quiet -c <(awk '$2="-"' "${1?}") >&2; } {N}>&1
}

看起来不错,但我今天才带来它,还不能认为它已得到证实。

相关内容