cp に進捗表示機能を付ける cp.rb

 勤務先から帰る方向が同じ友人がいるので、ラーメン屋で夕食。その帰りの電車で、この cp.rb をコーディングしました。

 cp.rb は、既存の cp に対して、wget のような進捗を表示するように機能を拡張するものです。

 ただ、コマンドラインで操作していて cp が登場する機会はそう多くなく、実際ラーメン屋で友人にヒアリングした cp のユースケースを列挙すると、



(1) 設定ファイルのバックアップ

(2) デバイス間のファイルの移動



という 2 種類でした。

 しかし、殊に (2) のような場合は、cp を押してからシェルに操作が戻るまでに多くの時間がかかるので、進捗表示機能は嬉しいとのこと。

 インタラクションデザインの原則として、ユーザは 0.3 秒以上レスポンスが返ってこないと、システムに対して苛立ちを覚えます。そこで、この 0.3 秒以内に何らかのレスポンス(「ただいま処理しています」でも可)を返す事で、ユーザの不安を和らげようというのが、cp.rb 製作の意図です。



使い方

 引数の与え方は、通常の cp と一緒です。alias cp="ruby /path/to/cp.rb" とすると幸せになれるよ!('(゚∀゚∩




$ ruby cp.rb -r tmp/hogehoge tmp/piyopiyo
[==> ] 5% ETA: 34sec

ソースコード


#FIXME copy 0 byte file
#FIXME --debug mode
#FIXME --view=off mode

DEBUG = false

def getSizeTable(dirs)
sizetable = {};
dirs = [dirs].flatten
args = argsToStr(dirs)
command = "du -a #{args}";
puts "executing: #{command}" if DEBUG
IO.popen(command, "r") {|io|
filename_prev = ""
while line = io.gets
size, filename = line.chomp.split("\t")
size = size.to_i
size = 0 if filename_prev[0..filename.length-1+1] == filename + "/"

sizetable[filename] = size
filename_prev = filename
end
}

return sizetable
end

def showProgress(rate, eta)
barwidth = (rate * 50).to_i
percentage = (rate * 100).to_i
bar = "[" + "="*barwidth + ">" + " "*([50-barwidth, 0].max) + "]";
if eta >= 0
STDOUT.write sprintf("%s %d%% ETA: %.0fsec \r", bar, percentage, eta)
else
STDOUT.write sprintf("%s %d%% ETA: --- \r", bar, percentage)
end
end

def shellesc(str, opt = {})
str = str.dup
if opt[:erace]
opt[:erace] = [opt[:erace]] unless Array === opt[:erace]
opt[:erace].each do |i|
case i
when :ctrl then str.gsub!(/[\x00-\x08\x0a-\x1f\x7f]/, '')
when :hyphen then str.gsub!(/^-+/, '')
else str.gsub!(i, '')
end
end
end
str.gsub!(/[\!\"\$\&\'\(\)\*\,\:\;\<\=\>\?\[\\\]\^\`\{\|\}\t ]/, '\\\\\\&')
str
end

def argsToStr(argv)
str = ""
argv.each{|raw| str += shellesc(raw) + " "}
return str
end

def extractFirstArg(argv=ARGV)
argv.each{|arg| return arg if arg[0..0] != '-';}
return nil
end

def extractFirstArgs(argv=ARGV)
res = []
argv.each{|arg| res << arg if arg[0..0] != '-'}
res.pop
return res
end

def applyCopy(args, sizetable)
total = 0
sizetable.each{|filename, size|
total += size
}

argsstr = argsToStr(args)

done = 0
t0 = Time.now
command = "/bin/cp -v #{argsstr}";
puts "executing: #{command}" if DEBUG
showProgress(0, -1); # ugokanai
IO.popen(command, "r") {|io|
while line = io.gets
from, to = line.chomp.split(" -> ");

done += sizetable[from]

if done > 0
unittime = (Time.now - t0).to_f / done
rest = total - done
rate = done.to_f / total.to_f
eta = rest * unittime
showProgress(rate, eta)
end
end
}
STDOUT.puts "\n"
end

from = extractFirstArgs
if from.length > 0
applyCopy(ARGV, getSizeTable(from))
else
puts `cp`
end



 0 バイトのファイルをコピーすると進捗が表示されない。UI がダサいなど、まだ少しだけ手を加えたいところはありますが、まったり直していこうと思います。