プログラムの不具合には様々な段階がある。 文法的なエラーなどプログラムの実行そのものができない単純なもの, 実行はできるが問題解決法が違うなど論理的な誤りのものについては, いずれもプログラムを作成している場所で判断して修正することができる。 ところがそれらの問題がないプログラムでも,実際に実行するときの 外界条件によって予期した動きを取れなくなるような不具合がある。 たとえば,データをファイルに保存しようとしたのに ディスク容量不足などが原因で書き込みができないような状況では プログラムは目的を達成できない。
このように,特定の条件によって想定した処理を進められないような 事態のことを例外 という。プログラム実行時にそうした 事態が生じたときには例外を意味する Exception が発生される。
文法的にはエラーのない以下のプログラムを動かしてみよ。
#!/usr/bin/env ruby file = ARGV.shift || "nonexistent.txt" open(file, "r") do |f| while line = f.gets print line end end
実行すると例外が発生する。
openerr.rb:4:in `initialize': No such file or directory @ rb_sysopen - nonexistent.txt (Errno::ENOENT) from openerr.rb:4:in `open' from openerr.rb:4:in `<main>'
おみくじプログラムの例に戻る。 改良すべき2つの点,
にはそれぞれ落し穴がある。
既存の結果ファイル fortune.txt
をバックアップファイル
fortune.bak
に移動できるかの確認は何を確かめればよいのだろう。
場合によってはバックアップファイルである fortune.bak
が存在するかや,そのファイルに書き込みできるかを確かめたくなるが,
実際にはこれは「バックアップファイルを作れるかどうか」には関係ない。
以下の実験で,移動先のファイルを書き込み禁止にして移動を行なってみる。
mkdir dirtest cd dirtest echo This is foo > foo echo This is backup file > foo.bak (foo.bakがバックアップファイル) chmod -w foo.bak (書き込み禁止にする) ls -lF total 1 -rw-r--r-- 1 yuuji wheel 12 Mar 15 12:59 foo -r--r--r-- 1 yuuji wheel 15 Mar 15 12:59 foo.bak echo hoge > foo.bak zsh: permission denied: foo.bak (直接ファイルに書こうとするとエラーになるが...) mv foo foo.bak override r--r--r-- yuuji/wheel for foo.bak? n (mvの場合は警告が出るがnと答える) irb irb> File.rename("foo", "foo.bak") => 0 irb> exit ls -lF total 1 -rw-r--r-- 1 yuuji wheel 12 Mar 15 12:59 foo.bak (12バイトなので元々fooだったものがfoo.bakになっている)
ファイルをrename
する場合は,
移動先ファイルを修正するのではないので移動先ファイルのパーミッションは
関係ない。元のファイルを消してそのファイルを作る権利があるかが
意味を持つので,ディレクトリに書き込み権があるかが意味を持つ。
touch foo foo.bak chmod -w . (ディレクトリを書き込み禁止にする) irb irb> File.rename("foo", "foo.bak") Errno::EACCES: Permission denied @ sys_fail2 - (foo, foo.bak) from (irb):1:in `rename' from (irb):1 from /usr/local/bin/irb21:11:in `<main>'
これは,ファイルを新規作成するときも同様の要件となる。
以上のことから,おみくじの例での改良2点,
は,ディレクトリへの書き込み権限を検査する必要があることが分かる。
#!/usr/bin/env ruby
# -*- coding: utf-8 -*-
#Foretune teller - version 1
ftune = %w,大吉 吉 中吉 小吉 半吉 末吉 小凶 凶,
outfile = "fortune.txt"
srand
result = ftune[rand(ftune.length)]
if test(?w, ".") then # ディレクトリの書き込み権限を確認
if test(?s, outfile) then
ext = File.extname(outfile)
bak = File.basename(outfile, ext)+".bak"
File.rename(outfile, bak)
end
open(outfile, "w") do |f|
f.printf("今日の運勢は %s\n", result)
end
STDERR.puts "#{outfile}を見よ。"
end
しかしこれでも不十分で,ファイルシステムが溢れるなど,
書き込みに失敗する要因は他にいくらでもある。
だからといって,それらに対する事前予想を if
で書き連ねてもプログラムの本筋が見づらくなる。
このような場合begin〜rescue〜end
構文を用いる。
エラーが起きない理想的な場合の処理のみをまず書き,エラーが起きた
場合の処理をまとめて別箇所に書くことで,処理の流れがはっきりし,
エラーが起きた場合の対処ももれなく書くことが容易になる。
#!/usr/bin/env ruby # -*- coding: utf-8 -*- #Foretune teller - version 2 ftune = %w,大吉 吉 中吉 小吉 半吉 末吉 小凶 凶, outfile = "fortune.txt" srand result = ftune[rand(ftune.length)] begin if test(?s, outfile) then ext = File.extname(outfile) bak = File.basename(outfile, ext)+".bak" # File.rename(outfile, bak) end open(outfile, "w") do |f| f.printf("今日の運勢は %s\n", result) end STDERR.puts "#{outfile}を見よ。" rescue abort(<<MSG) 書き込みに失敗しました。ディレクトリへの書き込み権限を確認してください。 MSG end
この場合は例外発生時に復旧せず,異常終了している。
そのためのメソッドが abort
である。