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

相關內容