gzip 相同输入不同输出

gzip 相同输入不同输出

查看:

data/tmp$ gzip -l tmp.csv.gz
     compressed        uncompressed  ratio uncompressed_name
           2846               12915  78.2% tmp.csv
data/tmp$ cat tmp.csv.gz | gzip -l
     compressed        uncompressed  ratio uncompressed_name
             -1                  -1   0.0% stdout
data/tmp$ tmp="$(cat tmp.csv.gz)" && echo "$tmp" | gzip -l

gzip: stdin: unexpected end of file

好吧,显然输入不一样,但从逻辑上讲应该是一样的。我在这里缺少什么?为什么管道版本不起作用?

答案1

这个命令

$ tmp="$(cat tmp.csv.gz)" && echo "$tmp" | gzip -l

将 的内容分配tmp.csv.gz给 shell 变量并尝试使用echo管道将其传送到gzip。但 shell 的功能会妨碍(空字符被省略)。您可以通过测试脚本看到这一点:

#!/bin/sh
tmp="$(cat tmp.csv.gz)" && echo "$tmp" |cat >foo.gz
cmp foo.gz tmp.csv.gz

并进行更多工作,使用od(或hexdump) 并仔细查看这两个文件。例如:

0000000 037 213 010 010 373 242 153 127 000 003 164 155 160 056 143 163
        037 213  \b  \b 373 242   k   W  \0 003   t   m   p   .   c   s
0000020 166 000 305 226 141 157 333 066 020 206 277 367 127 034 012 014
          v  \0 305 226   a   o 333   6 020 206 277 367   W 034  \n  \f
0000040 331 240 110 246 145 331 362 214 252 230 143 053 251 121 064 026
        331 240   H 246   e 331 362 214 252 230   c   + 251   Q   4 026

在此输出的第一行中删除一个空值:

0000000 037 213 010 010 373 242 153 127 003 164 155 160 056 143 163 166
        037 213  \b  \b 373 242   k   W 003   t   m   p   .   c   s   v
0000020 305 226 141 157 333 066 020 206 277 367 127 034 012 014 331 240
        305 226   a   o 333   6 020 206 277 367   W 034  \n  \f 331 240
0000040 110 246 145 331 362 214 252 230 143 053 251 121 064 026 152 027
          H 246   e 331 362 214 252 230   c   + 251   Q   4 026   j 027

由于数据发生更改,它不再是有效的 gzip 文件,从而产生错误。

正如 @coffemug 所指出的,手册页指出 gzip 将-1为非 gzip 格式的文件报告 a 。但是,输入不再是压缩文件任何格式,因此手册页在某种意义上具有误导性:它没有将其归类为错误处理。

进一步阅读:

@wildcard 指出其他字符(例如反斜杠)可能会加剧问题,因为某些版本echo会将反斜杠解释为转义符并产生不同的字符(或不产生不同的字符,具体取决于对不在其指令库中的字符应用转义符的处理) 。对于 gzip(或大多数形式的压缩)的情况,各种字节值的可能性是相同的,并且因为全部空值将被省略,而一些反斜杠将导致数据被修改。

防止这种情况的方法不是尝试将压缩文件的内容分配给 shell 变量。如果您想这样做,请使用更适合的语言。下面是一个可以计算字符频率的 Perl 脚本,作为示例:

#!/usr/bin/perl -w

use strict;

our %counts;

sub doit() {
    my $file = shift;
    my $fh;
    open $fh, "$file" || die "cannot open $file: $!";
    my @data = <$fh>;
    close $fh;
    for my $n ( 0 .. $#data ) {
        for my $o ( 0 .. ( length( $data[$n] ) - 1 ) ) {
            my $c = substr( $data[$n], $o, 1 );
            $counts{$c} += 1;
        }
    }
}

while ( $#ARGV >= 0 ) {
    &doit( shift @ARGV );
}

for my $c ( sort keys %counts ) {
    if ( ord $c > 32 && ord $c < 127 ) {
        printf "%s:%d\n", $c, $counts{$c} if ( $counts{$c} );
    }
    else {
        printf "\\%03o:%d\n", ord $c, $counts{$c} if ( $counts{$c} );
    }
}

答案2

有关文件未压缩大小的信息(实际上是最后一个块的未压缩大小,因为 gzip 文件可以连接在一起)作为小端 32 位整数存储在文件的最后 4 个字节中。

要输出该信息,gzip -l请查找文件末尾,读取这 4 个字节(实际上,根据strace,它读取最后 8 个字节,即 CRC 和未压缩的大小)。

然后它会打印文件的大小和该数字。 (您会注意到给出的信息具有误导性,并且不会给出gunzip < file.gz | wc -c与串联 gzip 文件相同的结果)。

现在,如果文件是可查找的,那么它就可以工作,但是当它不是管道的情况时,它就不行了。并且gzip不够聪明,无法检测到它并完全读取文件以到达文件末尾。

现在,在以下情况下:

tmp="$(cat tmp.csv.gz)" && echo "$tmp" | gzip -l

还有一个问题是 shell 无法zsh在其变量中存储 NUL 字节,它$(...)会删除所有尾随换行符(0xa 字节),并echo转换其参数(如果它们以-或包含,\具体取决于echo实现)并添加额外的换行符。

因此,即使gzip -l能够使用管道,它收到的输出也会被损坏。

在小端系统(如 x86 系统)上,您可以使用:

tail -c4 < file.gz | od -An -tu4

获取最后一个块的未压缩大小。

tail,相反,gzip当无法查找输入时能够回退读取输入。

答案3

从管道获取输入时似乎gzip无法识别文件名。我做了这样的测试:

$ cat file.tar.gz | gzip -tv 
  OK

$ gzip -tv file.tar.gz
  file.tar.gz: OK

因此,在第一种情况下gzip,无法识别文件名,这似乎是 -l 标志所必需的(您可以在输出的最后一列中看到 uncompressed_name 是 stdout)。

gzip手册页中的一些更多信息(与您的问题不直接相关) :

对于非 gzip 格式的文件(例如压缩的 .Z 文件),未压缩的大小为 -1。要获取此类文件的未压缩大小,您可以使用:

     zcat file.Z | wc -c

相关内容