查看:
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