ファイルアクセスはFileクラスを使用します。

Fileクラスではファイル名やモードの変更などのファイル操作、ファイルの入出力を行うための機能を提供しています。Fileクラスは基本的な入出力機能を提供するIOクラスを継承しています。

ファイルをオープンする

組込み関数のopen、File::openメソッドによりファイルをオープンすることができます。

指定できるファイルオープンモードは基本的に以下の6種類ですが、それぞれバイナリモードでのオープンもありますので、合計で12種類のモードがあることになります。

モード 説明
“r” 読み込みモード
“r+” 読み書き両用モード
“w” 新規作成書き込みモード
“w+” 新規作成読み書き両用モード
“a” 追加書き込みモード
“a+” 追加書き込み読み書き両用モード
“rb”,”r+b”,”wb”,”w+b”,”ab”,”a+b” 上記6パターンのバイナリモード
 f = open("index,txt") # モード省略時は "r" でオープンされる
 f = open("index.html", "w") # 書き込みモード

 f = File::open("log.txt", "a") # 追加モード
組込み関数のopenでは” “で始まるパスを指定すると以降をコマンドとして実行し、そのプロセスとのストリームを返します。以下の例ではsortコマンドでfooの中身をソートした結果をFileクラスのインスタンスfでアクセスすることができます。
 f = open("|sort foo")

テキストファイルをオープンして内容を出力する

ファイルをオープンするには組込み関数のopen、File::openメソッドを、ファイルの読み込みは組込み関数のIO#eachメソッド、IO#getsメソッドを使用することができます。

 # 
 # IO#eachを使う例
 #
 f = open("foo")
 f.each {|line| print line}
 f.close
 
 #
 # IO#getsを使う例
 #
 f = open("foo")
 while line = f.gets
   print line
 end
 f.close
 
 #
 # IO#each_lineを使う例
 #
 f = open("foo")
   f.each_line {|line|
     print line
   }
 f.close

また、File::openメソッドにブロックを渡すと、ファイルオブジェクトを与えられてブロックが実行され、ブロック終了後にファイルが自動的にクローズされるため、上記の例は以下のように記述することもできます。

 #
 # IO#eachを使う例
 #
 File::open("index.html") {|f|
   f.each {|line| print line}
 }
 
 #
 # IO#getsを使う例
 #
 File::open("index.html") {|f|
   while line = f.gets
     print line
   end
 }
 
 #
 # IO#each_lineを使う例
 #
 File.open("foo") {|f|
   f.each_line {|line|
     print line
   }
 }

文字エンコーディングを指定してテキストファイルをオープンする。

Ruby 2.0 からは,文字エンコーディングを指定するマジックコメントは不要になりました。 しかし,その場合に,ファイルから入力したテキストがどのエンコーディングかわからないために,読み込んだ文字列に対する処理がうまく行かないことが あります。それを避けるには,以下のようにopen メソッドの第二引数にエンコーディング指定を入れてやります。

 File.open('hoge.txt', 'r:utf-8')

読み込む長さを指定する

IO#readメソッドは引数で読み込む長さを指定することができます。以下の例はfooをオープンし、100バイト読み込んで内容を標準出力へ出力します。

 f = open("foo")
 print f.read(100)
 f.close

これは1行で書いてしまうこともできます。

 print File.open("foo").read(100)

ファイルの内容を一度に読み込む

IO#readメソッドは引数で読み込む長さを指定できますが、省略するとファイルの内容を一度に入力することができます。下記の例はfooの内容を一度に読み込み、内容を標準出力へ出力します。

 f = open("foo")
 print f.read
 f.close

1行で書いてしまうこともできます。

 print File.open("foo").read

なお,上の read を使う書き方は簡便ですが,その後でファイルがクローズされません.それでも大抵の場合に問題は起きませんが,次のようにするとクローズされるので気持ちがいいかもしれません.

 print open(url, &:read) 

1行ずつ読み込みを行う

IO#getsメソッドによりテキストファイルから1行ずつ読み込むことができます。IO#getsメソッドはファイルの終わり(EOF)に達すると、nilを返します。下記の例はfoo.csvから1行ずつ読み込み、行数と総フィールド数をカウントするものです。

 lines = fields = 0
 
 open("foo.csv") {|file|
   while l = file.gets
     lines += 1
     fields += l.split(',').size
   end
 }
 
 puts "Total #{lines} lines, #{fields} fields"

テキストファイルの特定の行を読み込む

IO#readlinesメソッドは全ての行を一度に読み込み、配列へ格納して返却します。以下に、”foo”の100行目を標準出力へ出力する例を示します。

 open("foo") {|file|
   print file.readlines[99]
 }

配列の沿字は0から始まりますので、100行目を参照するために99を指定しています。

一時ファイルを作成する

Tempfileクラスを使用すると一時ファイルの作成、読み込みなどを行うことができます。作成された一時ファイルは、以下のタイミングで削除されます。

TempfileクラスはFileクラスの全てのインスタンスメソッドを使用することができます。なお、Tempfileクラスを使用するためにはrequire ‘tempfile’する必要があります。

下記は一時ファイルを使ってfoo.txtというファイルの中身を全て大文字に変換する例です。

 require 'tempfile'
 
 temp = Tempfile::new("foobar", "/home/take/tmp")
 
 open("foo.txt") {|f|
   f.each {|line|
     line.upcase!
     temp.puts(line)
   }
 }
 
 temp.close
 temp.open
 
 open("foo.txt", "w") {|f| temp.each {|line| f.puts(line) }}
 
 temp.close(true)

大まかな処理の流れを以下に示します。

  1. /home/take/tmp配下へ”foobar”をベースネームとした一時ファイルを作成する
  2. foo.txtの中身を全て大文字に変換して一時ファイルへ出力する
  3. 一時ファイルを再オープンし、foo.txtの内容を上書きする
  4. Tempfile#closeを引数trueで呼び出し、作成した一時ファイルを削除する

固定長レコードを読む

固定長レコードを処理する場合、レコードを処理するためのクラスを定義するとプログラムを簡潔に記述することができます。

また、クラス定義によりレコード形式が変更になった場合でもトータルとしてのプログラム修正量が減り、保守性が向上するというメリットもあります。

では、例として以下のような固定長レコード形式のファイルを処理するケースを考えてみましょう。

 ■レコード形式
  従業員番号 6桁|氏名 10桁|部課コード 4桁|入社年度 4桁
 
 ■レコード例
  100001鈴木一郎太12342001
 
   従業員番号: 100001
   氏名: 鈴木一郎太
   部課コード: 1234
   入社年度: 2001

このレコードを処理するためのRecordクラスを定義してみましょう。

 class Record
   def initialize(record)
     @record = record
   end
   
   def empno
     @record[0..5]
   end
   
   def name
     @record[6..15]
   end
  
   def deptno
     @record[16..19]
   end
   
   def year
     @record[20..23]
   end
 end

Recordクラスではインスタンス生成時、渡されたレコードをインスタンス変数@recordに格納します。

emono、name、deptno、yearメソッドはそれぞれ@recordの決まった位置からフィールドを切り出し、Stringクラスのインスタンスとして返却します。

では、Recordクラスを使用してfooという従業員情報が格納されているファイルのレコードを、入社年度でソートして標準出力へ出力するスクリプトを作成してみます。

 class Record
   def initialize(record)
     @record = record
   end
  
   def empno
     @record[0..5]
   end
   
   def name
     @record[6..15]
   end
  
   def deptno
     @record[16..19]
   end
   
   def year
     @record[20..23]
   end
 end
 
 # (1)
 emp = Array::new
 
 # (2) - (4)
 open("foo") {|file|
   while l = file.gets
     r = Record::new(l)
     emp << {"empno"=>r.empno, "name"=>r.name, "deptno"=>r.deptno, "year"=>r.year}
   end
 }
 
 # 入社年度でソート
 emp.sort! {|a, b| a["year"] <=> b["year"]}
 
 emp.each {|h|
   puts "従業員番号:" + h['empno']
   puts "氏名:" + h['name']
   puts "部課コード:" + h['deptno']
   puts "入社年度:" + h['year']
   puts '-' * 20
 }

このスクリプトの処理は大まかに以下のようになります。

  1. 従業員情報を格納するための配列empを確保
  2. file#getsメソッドでファイルを1行ずつ読み込む
  3. Recordクラスのインスタンスrを生成
  4. 1レコード分のHashを作成し、配列に追加
  5. ファイルに書き込む

Fileクラスのメソッドを使って、ファイルをオープンした後、組み込みメソッドのprintまたはputを使って、ファイルにデータを書き込みます。

 foo = File.open("foo.txt",'w')
 foo.puts 'bar'
 foo.close

これで、foo.txtに’bar’と書き込まれます。

ファイルをコピーする

IO::readとIO::writeメソッドを使用してファイルのコピーを行う例を以下に示します。

 source = open("foo")
 dest = open("bar", "w")
 
 contents = source.read
 dest.write(contents)
 
 dest.close
 source.close

このスクリプトはファイル”foo”をファイル”bar”へコピーします。イテレータを使って以下のように記述することもできます。

 open("foo") {|source|
   open("bar", "w") {|dest|
     dest.write(source.read)
   }
 }

Ruby 1.7 以降は fileutils.rb を利用すると以下のように簡単に記述できます。

 FileUtils.cp( src, dest, *options )

FileUtils? には他にも便利な機能がたくさんあるので、覚えておくとよいと思います。

フィルタ系のコマンドを作成する

コマンドライン引数または標準入力から渡されるファイルの内容になんらかの処理を施して、標準出力へ出力するような、いわゆるフィルタ系のコマンドを作成する場合、組込み関数のgets、readlineが便利です。

gets、readlineはスクリプトのコマンドライン引数で与えられたファイルから1行読み込み、文字列を返します。

もし、コマンドライン引数が与えられなければ標準入力から読み込みます。使用例として、ファイルの内容を標準出力へ出力する簡易版のcatコマンド(cat.rb)を以下に示します。

 while s = gets
   print s
 end

以下のようにするとfoo.txtとbar.txtの内容を標準出力へ出力します。

 % ruby cat.rb foo.txt bar.txt 

以下のようにするとnkfコマンドで漢字コードをEUCに変換したfoo.txtの内容を標準入力から受け取り、標準出力へ出力します。

 % nkf -euc foo.txt | ruby cat.rb

ファイルタイプを取得する

File::ftypeメソッドを使用する事で、ファイルの種類を取得する事ができます。

 File.ftype("/etc/passwd") #=> "file"
 File.ftype("/etc" )       #=> "directory"

また文字列ではなく、真偽を返すメソッドも、FileTest?モジュールにより提供されています。

 FileTest.exists?("/etc") #=> true
 FileTest.directory?("/etc") #=> true
 FileTest.file?("/etc") #=> false

これらは、Fileメソッドからでも使用可能です。

 File.exists?("/etc") #=> true
 File.directory?("/etc") #=> true
 File.file?("/etc") #=> false

ファイルの詳細情報を取得する

File::statメソッドを呼び出すとFile::Statオブジェクトが返却されます。File::Statオブジェクトの各種メソッドを呼び出すことで、ファイルの詳細情報を取得することができます。

 s = File::stat("/etc/passwd") 
 p s.dev # デバイス番号
 p s.ino # i-node番号
 p s.mode # ファイルモード
 p s.nlink # ハードリンクの数
 p s.uid # ファイル所有者のユーザID
 p s.gid # ファイル所有者のグループID
 p s.size # ファイルサイズ
 p s.blocks # 割り当てられているブロック数
 p s.atime # 最終アクセス時刻
 p s.mtime # 最終更新時刻
 p s.ftype # ファイルタイプ

ファイルモードを変更する

File::chmodメソッドを使うとファイルモードを変更することができます。

 s = File::stat("/home/take/public_html/index.html")
 p "%o" % s.mode #=> "100664"
 
 File::chmod(0100666, "/home/take/public_html/index.html")
 p "%o" % s.mode #=> "100666"

ファイルの所有者とグループを変更する

File::chownメソッドを使うとファイルの所有者とグループを変更することができます。

ただし、実際に変更できるのはスーパーユーザだけです。所有者だけを変更したい場合は、グループにnilまたは-1を指定します。同様に、グループだけを変更したい場合は所有者にnilまたは-1を指定します。

 # 所有者を100、グループを101に変更
 File::chown(100, 101, "/home/httpd/html/index.html")
 
 # 所有者を101に変更
 File::chown(101, nil, "/home/httpd/html/index.html")
 
 # グループを102に変更
 File::chown(-1, 102, "/home/httpd/html/index.html")

ファイルの最終アクセス時刻と最終更新日時を変更する

File::utimeメソッドを使うとファイルの最終アクセス日と最終更新日時を変更することができます。日時はTimeクラスのインスタンスで指定します。

 # index.htmlの最終アクセス日を2001-5-22 23:59:59(JST)、最終更新日を2001-5-1 00:00:00(JST)に変更する
 
 File::utime(Time.local(2001, 5, 22, 23, 59, 59), Time.local(2001, 5, 1, 0, 0, 0), "index.html")
 
 p File::atime("index.html") #=> "Tue May 22 23:59:59 JST 2001"
 p File::mtime("index.html") #=> "Tue May 01 00:00:00 JST 2001"

相対パスから絶対パスを求める

File::expand_pathメソッドを使うと相対パスを絶対パスに変換することができます。第2引数を省略した場合は、カレントディレクトリを基準にします。

 p File::expand_path('baz', '/foo/bar') #=> /foo/bar/baz
 
 # 第2引数を省略した場合
 Dir::chdir('/hoge/piyo')
 p File::expand_path('.') #=> /hoge/piyo
 p File::expand_path('..') #=> /hoge
 p File::expand_path('fuga') #=> /hoge/piyo/fuga

ファイル名解析

ファイルパスからディレクトリパスを抜き出す

File::dirnameを使うと、ファイルパスからディレクトリパスを抜き出すことができます。

 File::dirname('/hoge/piyo')     #=> /hoge
 
 #末尾に/が付いていても無視します。
 File::dirname('/hoge/piyo/')    #=> /hoge
 
 #Windowsの\区切りにも対応しています。
 File::dirname('C:\\hoge\\piyo') #=> C:\hoge

ファイルパスからファイル名を抜き出す

File::basenameを使うと、ファイルパスからファイル名を抜き出すことができます。

 File::basename('/hoge/piyo')            #=> piyo
 
 #指定した拡張子を無視することもできます。
 File::basename('/hoge/piyo.c',   '.c')  #=> piyo
 File::basename('/hoge/piyo.cpp', '.c')  #=> piyo.cpp
 
 #拡張子にはワイルドカードも指定できます。
 File::basename('/hoge/piyo.c',   '.*')  #=> piyo
 File::basename('/hoge/piyo.cpp', '.*')  #=> piyo
 
 #ワイルドカードは末尾からの最短一致でチェックしているようです。
 File::basename('/hoge/piyo.cpp', 'p*')  #=> piyo.cp
 File::basename('/hoge/piyo.cpp', 'c*')  #=> piyo.
 
 #ワイルドカードの前に2文字以上あると無視されるようです。
 File::basename('/hoge/piyo.cpp', '.c*') #=> piyo.cpp

パス名とファイル名を一度に抜き出す

File::splitを使うと、ファイルパスからディレクトリパスとファイル名を一度に抜き出すことができます。

 File::split('/hoge/piyo') #=> ["/hoge", "piyo"]
 
 dir, file = File::split('/hoge/piyo')
 p dir  #=> "/hoge"
 p file #=> "piyo"

拡張子を調べる

File::extnameを使うと、ファイルパスから拡張子を抜き出すことができます。

 File::extname('/hoge/piyo.c')  #=> .c
 
 #ファイル名先頭の.は拡張子と見なされません。
 File::extname('/hoge/.piyo')   #=> ""
 File::extname('/hoge/.piyo.c') #=> .c