読者です 読者をやめる 読者になる 読者になる

すがブロ

sugamasaoのhatenablogだよ

gzip ライクなコマンドを Ruby の zlib で作成した

Ruby windows

Windows ではコマンドラインベースに zip が使えないっぽいので Rubygzip 的なものを実装した

Windows で何か処理を書く時に、 Zip 圧縮ができないのが不便でした*1
で、そんな時に Ruby の zlib ライブラリを発見。
これを使えば gzip 相当のコマンドを作成することは比較的容易であろう、と思ったので無駄に作ってみた。
一応、Windows でも Linux でも動くけれど、本家 gzip のある Linux で使ってありがたいことは無いだろう/(^o^)\

ソース

#!ruby

require 'zlib'
require "optparse"
require "fileutils"
require 'pp'

# 例外クラス定義
class RgzipInitError < StandardError; end
class RgzipInputError < StandardError; end
class RgzipOutputError < StandardError; end
class RgzipDecompInputError < StandardError; end

# エラー時の終了処理
def error(message)
  puts message
  exit 1
end

# 初期化処理
def init
  opts = {:in_file => []}
  argv = []

  begin
    opt = OptionParser.new
    opt.banner = "Usage: #{$0} [-Ddfrh] [filename ...]\n"
    opt.on('-D', '--delete', 'compressed input file delete'){|v| opts[:delete] = v }
    opt.on('-d', '--decompress=filename', 'decompress'){|v| opts[:decompress] = v }
    opt.on('-f', '--force', 'force write output file'){|v| opts[:force] = v }
    opt.on('-h', '--help', 'show this message') { error(opt.to_s) }
    opt.on('-r', '--recursive', 'directory structure recursively'){|v| opts[:recursive] = v }
    opt.on('-v', '--verbose', 'directory structure recursively'){|v| opts[:verbose] = v }
    argv = opt.parse!(ARGV)
  rescue
    raise RgzipInitError, opt.to_s
  end
  
  raise RgzipInitError, opt.to_s if argv.empty? && !opts[:decompress]
  
  argv.each do |input|
    opts[:in_file] << input
  end
  
  # 出力用ファイル名が省略された場合は、入力ファイル名 + .gz でファイル名を付ける
  return opts
end

# ** 圧縮処理 **
# in_file   : gz 圧縮対象ファイル/ディレクトリのString配列
# delete    : gz 圧縮後、元ファイルの削除をする
# recursive : ディレクトリを指定された場合、再帰的に読み込んでファイルを探すかどうか
# force     : gz 圧縮ファイルを作成する際に、既に同名のファイルがあっても上書きをする
# verbose   : gz 圧縮されたファイルを標準出力へ出力するかどうか
def compress(in_file, delete = false, recursive = false, force = false, verbose = false)
  
  in_file.each do |input|
    # 入力ファイルがディレクトリなのに -r オプションなしはエラー
    raise RgzipInputError, input if (File.directory?(input) && !recursive) # 入力ファイルが無ければエラー
    
    if File.directory?(input)
      if input =~ /\/$/ # 末尾がスラッシュかどうかで追加文字列を変更する
        input = input + '*'
      else
        input = input + '/*'
      end
      Dir.glob(input) do |list|
        # ディレクトリの中身に対して圧縮処理を掛けてゆく
        compress(list, delete, recursive, force, verbose)
      end
      return
    end
  
    output = input + '.gz'
    
    # 強制上書きフラグが True ではない場合、出力ファイルが既にあればエラー
    raise RgzipOutputError, output if (File.file?(output) && !force)
    begin
      Zlib::GzipWriter.open(output, Zlib::BEST_COMPRESSION) do |gz|
# FIXME:時刻が上手く設定できない
#        gz.mtime = File.mtime(input)
        gz.orig_name = input
        gz.puts File.open(input, 'rb'){|f| f.read }
      end
      # verbose オプション時
      puts "#{input} => #{output}" if verbose
    rescue => e
      raise RgzipOutputError, e.message
    end
  end
  
  # 圧縮前のファイルの削除を行う
  if delete
    FileUtils.rm(in_file)
  end
end

# ** 解凍処理 **
def decompress(filename, delete=false, verbose=false)
  # 解凍対象がファイルでなければエラー
  raise RgzipInputError, filename unless File.file?(filename)
  
  # .gz でなければエラー
  raise RgzipDecompInputError, filename unless filename =~ /(.+)\.gz$/
  basename =$1
  
  # ディレクトリ名(.gzと元拡張子を取り除いた名前)
  raise RgzipDecompInputError, basename if File.directory?(basename) or File.file?(basename)
  
  begin
    Zlib::GzipReader.open(filename) do |gz|
      orig_mtime = gz.mtime || Time.now # gz に時刻があればその時刻をタイムスタンプとして使用する
      orig_name = gz.orig_name || basename # gz にファイル名が無ければ、元ファイル名の .gz を除いた名前を用いる
      File.open(orig_name, "wb") do |f|
        f.print gz.read
      end
      
      File.utime(orig_mtime, orig_mtime, orig_name)
      # verbose オプション時
      puts "#{filename} => #{orig_name}" if verbose
    end
  rescue => e
    raise RgzipOutputError, e.message
  end
  
  # 解凍前のファイルの削除を行う
  if delete
    FileUtils.rm(filename)
  end
end

begin
  # 引数解析
  opts = init
  
  # 圧縮開始
  compress(opts[:in_file], opts[:delete], opts[:recursive], opts[:force], opts[:verbose]) unless opts[:in_file].empty?
  
  # 解凍開始
  decompress(opts[:decompress], opts[:delete], opts[:verbose]) if opts[:decompress]
  
  puts "#{$0} done."
rescue RgzipInitError => e
  error(e.message)
rescue RgzipInputError => e
  error("not found input file or directory. => #{e.message}")
rescue RgzipOutputError => e
  error("already existing output filename. => #{e.message}")
rescue RgzipInputError => e
  error("not found input file or directory. => #{e.message}")
rescue RgzipDecompInputError => e
  error("not found filename include '.gz' or already existing output filename. => #{e.message}")
end

使い方

こんな感じで引数を渡すと、対象を圧縮します。圧縮した後のファイル名は 引数で渡したファイル名 + .gz となります。
本家の gzip と同様で、tar の機能は無いので一ファイル単位での圧縮となります。
ただ、本家 gzip と異なるのはデフォルトでは圧縮元のファイルを削除しないことです。削除したければ -D を付けてください。

sugamasao% ls -l
total 16
-rw-r--r--  1 sugamasao  sugamasao  4981 11 27 15:48 rgzip.rb
drwxr-xr-x  9 sugamasao  sugamasao   306 12  2 20:24 test
sugamasao% ruby rgzip.rb ./rgzip.rb
rgzip.rb done.
sugamasao% ls -l
total 24
-rw-r--r--  1 sugamasao  sugamasao  4981 11 27 15:48 rgzip.rb
-rw-r--r--  1 sugamasao  sugamasao  1962 12  3 02:25 rgzip.rb.gz # <---- これ!
drwxr-xr-x  9 sugamasao  sugamasao   306 12  2 20:24 test

解凍は -d オプションでできます。

sugamasao% ls -l
total 24
drwxr-xr-x  3 sugamasao  sugamasao   102 12  3 02:35 aaa
-rw-r--r--  1 sugamasao  sugamasao  4981 11 27 15:48 rgzip.rb
drwxr-xr-x  9 sugamasao  sugamasao   306 12  2 20:24 test
-rw-r--r--  1 sugamasao  sugamasao  1959 12  3 02:36 test.rb.gz
sugamasao% ruby rgzip.rb -d test.rb.gz
rgzip.rb done.
sugamasao% ls -l
total 40
drwxr-xr-x  3 sugamasao  sugamasao   102 12  3 02:35 aaa
-rw-r--r--  1 sugamasao  sugamasao  4981 11 27 15:48 rgzip.rb
drwxr-xr-x  9 sugamasao  sugamasao   306 12  2 20:24 test
-rw-r--r--  1 sugamasao  sugamasao  4981 12  3 02:36 test.rb   # 解凍されたファイル
-rw-r--r--  1 sugamasao  sugamasao  1959 12  3 02:36 test.rb.gz

引数を渡さないで実行するとヘルプが見れます。
英語はとても妖しいです。というか、今これを書いていて気がついたのですが、ヘルプのメッセージが間違ってますね!*2><

sugamasao% ruby rgzip.rb
Usage: rgzip.rb [-Ddfrh] [filename ...]
    -D, --delete                     compressed input file delete
    -d, --decompress=filename        decompress
    -f, --force                      force write output file
    -h, --help                       show this message
    -r, --recursive                  directory structure recursively
    -v, --verbose                    directory structure recursively

問題点

Windows 上だからなのか、自分の実装のやり方がまずいのかわからないのですが、圧縮時の元ファイルの時刻を保持する事ができません。
圧縮前のファイルのタイムスタンプを File.mtime で取得しているのですが GzipWriter オブジェクトの mtime にうまくわたせていない><

*1:できないと思ってるんだけど、ほんとはできるのかな?

*2:-v のメッセージが -r と一緒だ・・・!