gzip ライクなコマンドを Ruby の zlib で作成した
Windows ではコマンドラインベースに zip が使えないっぽいので Ruby で gzip 的なものを実装した
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 にうまくわたせていない><