データの処理を行なうことはコンピュータに求められる重要な仕事だが, そのデータの入れ物であるファイルの処理も同様に重要である。 たとえばゲームを作る場合にハイスコアを記録したい場合を考える。 データの入出力の方法も大事だが,保存するファイル名をどう決めるか, 書き込みできる場所をどうやって探すか,などについても知る必要がある。
ここではファイルやディレクトリに関する操作をプログラムから行なうための メソッドや,そのときに注意すべきシステム上の事項について説明を進める。
ファイル名操作やファイルそのものの属性を操作したりするメソッドは, File クラスに集まっている。
以下のものはクラスメソッドであり,オブジェクトの 確保なしにどのタイミングでも使える。
File.expand_path(file [, dir ])
ファイル名 file を絶対パスに展開する。
省略可能な第2引数 dir を指定するとそのディレクトリを
基準に展開する。先頭の ~
記号はホームディレクトリに,
~user
は,ユーザuser
のホームディレクトリに展開される。
File.expand_path(".zshrc", "~") => "/home/yuuji/.zshrc" File.expand_path(".zshrc", "~/../skel") => "/home/skel/.zshrc"
シェル上で利用できる ~
記号は,
システムにとっては単なる記号であり,
それをホームディレクトリに展開するのは
アプリケーションの判断による処理である。
第1引数に絶対パス(~
で始まるものを含む)を
指定した場合は,第2引数の如何に関らず第1引数を展開したものを返す。
基準ディレクトリにあるファイル名を生成したいときには
ディレクトリ名とファイル名を単に文字列として連結するのではなく
File.expand_path()
を用いる方がよい。
File.basename(path)
File.basename(path, ext)
ファイル名 path のディレクトリ名を除いた部分を返す。 最後のスラッシュより後ろの部分。省略可能な第2引数 ext を指定するとファイル名の末尾がそれと一致した場合 のみ ext をさらに削除する。
以下のプログラムを filebn.rb
という名前で保存し,実行した様子を示す。
#!/usr/bin/env ruby printf("$0 = [%s].\n", $0) # $0はこのプログラムを起動したときの名前 printf("My name is [%s].\n", File.basename($0)) printf("My root name is [%s].\n", File.basename($0, ".rb"))
プログラムがカレントディレクトリにある状態で起動したときと,
コマンド検索パス(環境変数PATH
)に設定された
ディレクトリから直接起動したときの結果を示す。
(カレントディレクトリに置いて起動) ./filebn.rb $0 = [filebn.rb]. My name is [filebn.rb]. My root name is [filebn]. ($PATHにホームディレクトリを追加してそこから起動) cp filebn.rb ~ PATH=$HOME:$PATH filebn.rb $0 = [/home/yuuji/filebn.rb]. My name is [filebn.rb]. My root name is [filebn]. rm ~/filebn.rb
後者の例のように,コマンド検索パスにより起動された場合は,
たとえユーザがプログラム名しか打っていなくても,フルパス名が
$0
変数に代入される。
File.dirname(path)
ファイル名 path のディレクトリ名部分を返す。
最後のスラッシュより前の部分。スラッシュを含まない場合は
"."
を返す。
File.basename
と組み合わせて,動作中Rubyプログラムの
格納ディレクトリとプログラム名を得るときに利用することが多い。
グローバル変数 $0
に動作中Rubyプログラムの名前が
入っていることを利用する。
mydir = File.dirname($0) myname = File.basename($0, ".rb") configfile = File.expand_path(".#{myname}rc", "~") scoredir = File.expand_path("../share/#{myname}", mydir) scorefile = File.expand_path("#{myname}.score", scoredir) # (不完全版でのちに修正あり)
一般的に,実行時に複数のファイルを利用するアプリケーション プログラムはインストールプレフィクス prefixを定め,
prefix/bin ... 実行ファイル prefix/lib ... ライブラリファイル prefix/man ... マニュアル prefix/share ... アーキテクチャ非依存ファイル(データなど)
というディレクトリ構造でインストールすることが多い。 プログラムの利用者がそのプログラムをどの prefix にインストールするかは自由なので,プログラムでは自分で 自分の prefix を検出し,データが格納されるべき 適切なディレクトリを探し出す必要がある。
File.chmod(mode, file)
file の属性を mode に変更する。 失敗したときは例外が発生する。 ファイルの属性は8進数で最大4桁の値で指定する。
特殊属性 | 所有者 | グループ | その他 | ||||||||
4 | 2 | 1 | 4 | 2 | 1 | 4 | 2 | 1 | 4 | 2 | 1 |
setuid | setgid | sticky | r | w | x | r | w | x | r | w | x |
各桁の意味は以下のとおり。
プログラム実行時に,プログラム自身のファイル所有者の user id でプロセスを起動する。
プログラム実行時に,プログラム自身のファイル所有者の group id でプロセスを起動する。
対象がディレクトリの場合,そのディレクトリに書き込み権限を 持つユーザであっても,他者の所有するファイル消すことはできない。
そのファイルに対して, 所有者/同一グループ所属ユーザ/それ以外のユーザ が読み出し操作を行なえるか。
そのファイルに対して, 所有者/同一グループ所属ユーザ/それ以外のユーザ が書き込み操作を行なえるか。
そのファイルに対して, 所有者/同一グループ所属ユーザ/それ以外のユーザ が実行操作を行なえるか。ただし, 対象がディレクトリの場合はchdirできるか。
Rubyでは0(ゼロ)で始まる数値は8進数指定となる。 たとえば,
File.chmod(0754, "newprog.rb")
とすると,newprog.rb
というファイルに対し,
所有者は,読み・書き・実行すべて,同一グループユーザは読みと実行,
その他のユーザは読み取りのみできるように属性設定する。
なお,ファイルそのものの作成や,ファイル名の変更は そのファイルが置かれるディレクトリの属性が可否を決める。
File.unlink(filename)
ファイルを削除する。厳密には,ファイルを消すわけではなく, ファイル名 filename の,実体との結合を削除する。
Unixファイルシステムではファイルの実体に結び付いている名前を 複数持てる。ファイルの新規作成では1つ目の名前が付き, 2つ目以降は ln コマンドで作れる。
以下のように続けてコマンド起動した流れを説明する。
echo Hello > hoge ln hoge hero ln hero heso echo world > hoge rm hoge
まず,文字列 Hello という内容で hoge
ファイルを作成する。
echo Hello > hoge ls -l hoge -rw-r--r-- 1 yuuji wheel 6 Mar 11 13:30 hoge (第2フィールドの 1 はリンクカウント)
ファイルシステムの中で "Hello"
という内容を持つ領域が確保され,そこを指すように
hoge
という名前がディレクトリに作成される。
続いて,ln コマンドを用いて同一ファイルの内容に別のリンクを作成する。
ln hoge hero ls -l h* -rw-r--r-- 2 yuuji wheel 6 Mar 11 13:30 hero -rw-r--r-- 2 yuuji wheel 6 Mar 11 13:30 hoge ls -li h* 1510107 -rw-r--r-- 2 yuuji wheel 6 Mar 11 13:30 hero 1510107 -rw-r--r-- 2 yuuji wheel 6 Mar 11 13:30 hoge (ls -i オプションでinode番号も出力)
hoge
ファイルの指す内容と同一のものを指すように,
hero
という名前が同じディレクトリに作成される。上の例で ls
コマンドに指定した -i オプションはファイルの内容のinode番号も出力する。
inode番号は同一ファイルシステムで唯一となるように割り当てられる番号である。
2つのファイル hoge
,hero
ともに
1510107番で示される実体を指していることが分かる。
さらに続けて3つ目のリンク heso
も作成してみる。
ln hero heso ls -li h* 1510107 -rw-r--r-- 3 yuuji wheel 6 Mar 11 13:30 hero 1510107 -rw-r--r-- 3 yuuji wheel 6 Mar 11 13:30 heso 1510107 -rw-r--r-- 3 yuuji wheel 6 Mar 11 13:30 hoge (リンクカウントが3になる)
3つのファイルがすべて inode 1510107 の実体を指している。
ファイルの中味を修正してみる。"Hello" だった内容を "world" に変更する。
echo world > hoge ls -li h* 1510107 -rw-r--r-- 3 yuuji wheel 6 11 Mar 13:32 hero 1510107 -rw-r--r-- 3 yuuji wheel 6 11 Mar 13:32 heso 1510107 -rw-r--r-- 3 yuuji wheel 6 11 Mar 13:32 hoge (すべて変わる(タイムスタンプに注目)) rm hoge (hogeという名前だけを消す) ls -li h* 1510107 -rw-r--r-- 2 yuuji wheel 6 11 Mar 13:32 hero 1510107 -rw-r--r-- 2 yuuji wheel 6 11 Mar 13:32 heso
一般的に「ファイルを消す」という行為は,ファイルへの
1リンクを消しているに過ぎない。リンクカウント1のときに
unlink
するとファイルを参照できなくなる,つまり
実質的に「削除される」が,実体のデータがディスク上から消えたわけではない。
ちなみにrmはファイルへのリンクを消すだけであるが, BSD系OSのrmにある-P オプション,Linux系OSの shred コマンドはファイルの中味を上書きすることを試みる。これらを用いると, 伝統的なファイルシステム上のファイルであれば データをディスク上から抹消できる。
File.link(old, new)
old が指すファイルを指す新しいリンク new を作成する。同一のファイルに直接結び付けるリンクを ハードリンク という。上記実行例の ln コマンドによって作られているリンクがハードリンクである。
File.symlink(old, new)
old という名前を指す new
というシンボリックリンクを作成する。ln
コマンドの
-s
オプションでもシンボリックリンクは作成できる。
ハードリンクに対するものとして,シンボリックリンクのことを
ソフトリンク ということもある。
ln -s foo bar ls -lF bar lrwxr-xr-x 1 yuuji wheel 3 Jun 8 07:23 bar@ -> foo
bar
というファイル名へのアクセスは
foo
へのアクセスに変換される。もちろん foo
がなければアクセスは失敗する。
ls foo ls: foo: No such file or directory cat bar cat: bar: No such file or directory date > foo cat bar Mon Jun 8 07:26:34 JST 2014 (fooがあればbarへのアクセスも成功)
File.rename(from, to)
from というファイル名を to に変える。 to という名前で別のファイルが存在するときは 上書きされる。失敗すると例外が発生する。
File.stat(filename)
filename の情報を取得する。 File::Stat オブジェクトが返る。
単一ファイルを何度も読み書きする場合は,その都度閉じたり開いたりを 繰り返すのではなく,対象ファイルのうち次に読み書きする位置(ファイルポインタ) を移動したり,書き込んだ内容を確実にディスクに書き出す処理が必要になる。 以下のメソッドは IOクラスの非クラスメソッドである。既に open しているIOオブジェクトから利用する。
seek(offset [, whence])
ファイルポインタを offset だけ移動する。 省略可能な第2引数で移動の基準位置を指定する。
IO::SEEK_SET | ファイルの先頭 |
IO::SEEK_CUR | 現在のファイルポインタ位置 |
IO::SEEK_END | ファイルの末尾 |
デフォルトは IO::SEEK_SET
。
rewind
ファイルポインタを先頭に移動する。
pos
tell
現在のファイルポインタ位置を返す。
以下の例はファイル読み取りが末尾まで達するたびに, きまぐれ(random)な位置にファイルポインタを変えて 何度もファイルを読むプログラムである。
#!/usr/bin/env ruby # -*- coding: utf-8 -*- thisfile = "randseek.rb" # このファイルの名前 sz = File.size(thisfile) open(thisfile, "r") do |file| while true while line = file.gets print line end puts "読み終わりました。[Enter]でもう一度。やめたい場合は C-c" gets file.seek(rand(sz)) # 乱数でファイルポインタを移動 printf("%dバイト目から読み直します。\n", file.pos) end end
flush
IOポートの内部バッファをフラッシュする。STDOUT
のようにバッファリングされるIOポートへの出力をフラッシュすることで
その時点までのデータが書き込まれる。
fsyncが必要な場合もある。
sync
出力同期モードを真偽値で指定する。
IO.sync=true
なら同期モード,
IO.sync=false
とすると非同期(バッファリングされる)モードになる。
プログラムから出力を行なう場合,print などの書き込み操作をしても すぐに実際に対象デバイスにデータが書き込まれるとは限らない。 効率を上げるため一定量を溜めてからまとめて書き出す処理が行なわれる。 このため,目の前の短期的な応答性が求められるプログラムでは, 出力先のバッファリングを制御する必要がある。
バッファリングの有無による違いを確認するために, Ruby1.8での挙動を用いた例を示す。 システムの標準的な設定として, 標準出力はデフォルトでバッファリングされ, 標準エラー出力はデフォルトでバッファリングされない。 他の言語処理系を使う場合にもこのような注意が必要な点は同じである。
Ruby1.8用の以下のプログラムは, 標準出力から "O" を,標準エラー出力から "E" を1字ずつ交互に出力する。
#!/usr/bin/env ruby18 3.times do # 全体を3回繰り返す 12.times do # STDOUT/STDERR 交互出力を12回繰り返す STDOUT.print "O" STDERR.print "E" end STDOUT.puts "" # 12回終わったらSTDOUTに改行出力 end STDOUT.puts "Done."
このプログラムでは,交互出力を12回行なったあと標準出力に改行文字を 出力している。NetBSD6/amd64で実行すると以下のようになった。
oeoe.rb
EEEEEEEEEEEEOOOOOOOOOOOO
EEEEEEEEEEEEOOOOOOOOOOOO
EEEEEEEEEEEEOOOOOOOOOOOO
Done.
この例から,標準出力は改行文字までがまとめて出力されていることが分かる。
プロセスと通信を行なう場合などで,出力先にデータを即時に送りたい場合は
flush
メソッドで書き出しバッファを一掃するか,常に即時送りをした場合は
sync=true
で出力同期モードに変える。flush
を用いるように変えたプログラムと実行例を示す。
#!/usr/bin/env ruby18
3.times do # 全体を3回繰り返す
12.times do # STDOUT/STDERR 交互出力を12回繰り返す
STDOUT.print "O"
STDERR.print "E"
STDOUT.flush
end
STDOUT.puts "" # 12回終わったらSTDOUTに改行出力
end
STDOUT.puts "Done."
oeflush.rb
EOEOEOEOEOEOEOEOEOEOEOEO
EOEOEOEOEOEOEOEOEOEOEOEO
EOEOEOEOEOEOEOEOEOEOEOEO
Done.
ただし,即時書き込みはプログラムの動作性能を落とす可能性があることや, 実際に書き出されるかどうかは受け取り側の状況によることに注意する必要がある。
ファイルはディレクトリに格納する。 保存ファイルを書き込める場所を探したり, 既存のファイルを特定のディレクトリから探したりするためには ディレクトリを操作するためのメソッドの集まった Dir クラスを利用する。
Dir[pattern]
Dir.glob(pattern)
pattern にシェルパターンとしてマッチする ファイル名一覧を含む配列を返す。
パターンの一部に利用して特別な意味を持つ記号は以下のとおり。
* | 0文字以上の任意の文字列にマッチする。 |
? | 任意の1字とマッチする。 |
[ ] |
括弧内に列挙した任意の1字とマッチする。
ハイフンで繋ぐとその範囲の文字のどれか(例: 0-9),
括弧内の先頭が ^ のときは指定以外の文字とマッチする。 |
{ } |
組み合わせ展開。 例: file.{c,h,rb} は file.c file.h file.rb に展開される。 |
**/ |
ディレクトリ再帰指定。**/foo とするとカレントディレクトリ以下 すべてのディレクトリを対象に foo を探す。 |
Dir.entries(dir)
ディレクトリ dir の持つファイルエントリを
配列として返す。dir ディレクトリを基準とした
ファイル名なのでアクセスする場合は dir に
Dir.chdir
するか,File.expand_path
で絶対パスに展開する必要がある。
すべてのディレクトリのファイルエントリは [".", ".."] で始まる。 irb で Dir.entries(".") を確認してみる。
Dir.entries(".")
=> [".", "..", "direntries.rb", "dirfile.html", "exception.html"]
得られたリストを元に,各ファイルに関する情報を表示する プログラムを示す。File::Stat の詳細は次項で説明する。
#!/usr/bin/env ruby # -*- coding: utf-8 -*- checkdir = ARGV.shift || "." printf("(1)ここは%s\n", Dir.pwd) printf(%Q/"%s" ディレクトリのエントリ一覧\n/, checkdir) files = Dir.entries(checkdir) printf(%Q/先頭要素は必ず "%s"\n/, files.shift) printf(%Q/2番目要素は必ず "%s"\n/, files.shift) print("残りは登録順: ") $KCODE=ENV["LC_ALL"] || ENV["LANG"] || 'u' # for 1.8 p files puts "-"*15+"サイズ一覧"+"-"*15 files.each do |f| file = File.expand_path(f, checkdir) size = File.stat(file).size printf("%-30s %6d\n", f, size) end
Dir.pwd
Dir.getwd
現行Rubyプロセスのカレントディレクトリのフルパスを返す。
Dir.chdir(dir)
カレントディレクトリを dir に変更する。 後ろにブロックを指定するとブロック実行中のみカレントディレクトリを 変更する。
#!/usr/bin/env ruby # -*- coding: utf-8 -*- printf("(1)ここは%s\n", Dir.pwd) Dir.chdir(File.expand_path("~")) do printf("(2)ここは%s\n", Dir.pwd) end printf("(3)ここは%s\n", Dir.pwd)
実行すると以下のようになる。
./pwd.rb
(1)ここは/home/yuuji/Ruby
(2)ここは/home/yuuji
(3)ここは/home/yuuji/Ruby
Dir.mkdir(newdir [,
mode ])
新しいディレクトリ newdir を作成する。
省略可能な第2引数 mode を指定すると,
その時点の umask 値でマスクした値の属性値となる。
umask とは,新規にファイルやディレクトリを作成する場合に
ファイル属性のどのビットを落とす(0にする)かを決める値である。
一般的には umask を 022(8進値)にしておいて,
「グループ」と「その他」の2のビット(書き込み権)を出さないのを
デフォルトのファイル属性とする(表
ファイルの属性値 参照)。
Rubyでは umask 値を
File.umask
が保持している。したがって,Dir.mkdir
に mode
を指定した場合,実際に作成されるディレクトリの属性値は
mode & ~File.umask
となる。
Dir.rmdir(dir)
ディレクトリ dir を削除する。成功すると0が返され, 失敗すると例外が発生する。
ファイルはリンクカウント1のときの名前が消えても,そのファイルへの
アクセスがある限りファイルシステムの領域を占め続ける。
プログラム中で利用する一時ファイルを作成後,すぐに
unlink
しても close
するまではアクセスし続けられる。
#!/usr/bin/env ruby # -*- coding: utf-8 -*- tmpfile = ARGV.shift || "00TEMPFILE" tmpdir = File.dirname(tmpfile) STDERR.puts "最初のこのディレクトリの使用量:" system "du -sk #{tmpdir}" open(tmpfile, "w+") do |tf| 1.upto(10) do |lineno| tf.printf("これは%02d行目\n", lineno) end tf.puts "-"*1024*1024 # 1MBのダミーデータ tf.flush # バッファを書き込み tf.rewind # ファイルポインタを先頭に system "ls -lF #{tmpfile}" STDERR.puts "ファイル作成直後のこのディレクトリの使用量:" system "du -sk #{tmpdir}" STDERR.puts "改行を押すとこのファイルを消します。" STDIN.gets File.unlink(tmpfile) system "ls -lF #{tmpfile}" STDERR.puts "closeする前のこのディレクトリの使用量:" system "du -sk #{tmpdir}" STDERR.puts "1行目から読みます。" STDERR.puts "別の端末でlsして#{tmpfile}がないことを確認しましょう。" 10.times do |l| print tf.gets sleep 1 end end STDERR.puts "closeされたあとのこのディレクトリの使用量:" system "du -sk #{tmpdir}"
このプログラムを実行すると以下の結果が得られる。
./unlinktest.rb 最初のこのディレクトリの使用量: 116 . -rw-r--r-- 1 yuuji wheel 1048757 Mar 13 16:00 00TEMPFILE ファイル作成直後のこのディレクトリの使用量: 1156 . 改行を押すとこのファイルを消します。 ls: 00TEMPFILE: No such file or directory closeする前のこのディレクトリの使用量: 1156 . 1行目から読みます。 別の端末でlsして00TEMPFILEがないことを確認しましょう。 これは01行目 これは02行目 これは03行目 これは04行目 これは05行目 これは06行目 これは07行目 これは08行目 これは09行目 これは10行目 closeされたあとのこのディレクトリの使用量: 116 .
4つの数字が出されているタイミングは,
のとおりで,3のときにはディレクトリエントリからデータファイルの 名前がなくなっているにも関らず,ディスク上に確保された領域が残っている 状態であることが分かる
ユーザに見せる必要がなく, プログラム実行終了に不要となるファイルはopen直後に unlink するとよい。