>> 連載の目次は こちら!
今回は、ファイルとディレクトリを扱う方法について整理する。
ファイルはFileクラス、ディレクトリはDirクラスで扱う。
また、FileUtilsモジュールにて、UNIXコマンドライクにさまざまな操作が可能。
※ここでは例文をすっきりさせるため例外処理はしていない。実際にはメソッドによって例外を発生するので、適宜対応する。
■ Fileクラスでファイルを操作する
@see https://docs.ruby-lang.org/ja/latest/class/File.html
・Fileクラスは IOクラスを継承しているため、共通の入出力IFで操作できる
・Fileクラスは Enumerableモジュールを積んでるので、ループ系の処理ができる
● ファイルのオープンと基本的な読み書き
# 手動で開く、1行ずつ読む、手動で閉じる
file = File.open("doughnut.csv", "r:UTF-8")
file.each do |line|
puts "商品名:%s(価格:%d円)" % line.split(",")
end
file.close
# 商品名:プレーン(価格:129円)
# 商品名:チョコレート(価格:108円)
# 商品名:エンゼルフレンチ(価格:140円)
# ブロックで処理し、自動で閉じる
File.open("doughnut.csv", "r:UTF-8") do |file|
file.each do |line|
puts "商品名:%s(価格:%d円)" % line.split(",")
end
end
# 商品名:プレーン(価格:129円)
# 商品名:チョコレート(価格:108円)
# 商品名:エンゼルフレンチ(価格:140円)
# Kernelモジュールにもopenメソッドがあるので、関数のようにも書ける
open("doughnut.csv", "r:UTF-8") do |file|
file.each do |line|
puts "商品名:%s(価格:%d円)" % line.split(",")
end
end
# 商品名:プレーン(価格:129円)
# 商品名:チョコレート(価格:108円)
# 商品名:エンゼルフレンチ(価格:140円)
# ファイルを新規作成して書き込む
File.open("cafe.csv", "w", 0644) do |file|
file.puts("カフェオレ,380")
end
# 追記モードで開いて書き込む
File.open("cafe.csv", "a") do |file|
file.puts("ココア,400")
end
# ファイル全体を一度に読み込む
File.open("cafe.csv", "r:UTF-8") do |file|
puts file.read
end
# カフェオレ,380
# ココア,400
・openメソッドにブロックを渡して処理した場合は、ブロック終了時に自動でcloseしてくれる
・openメソッドの第2引数はモード
r 読み込み専用でオープン。デフォルト。
w 書き込み専用でオープン。
a 追記モード
・上記は、それぞれ「+」を付けると読み書き両用でオープンする
・「w」「w+」は、ファイルが存在する場合に空にするので注意。新規作成用だな。
・「a」は、ファイルがなければ新規作成される。
・モードの後ろに「:UTF-8」のように文字エンコーディングを指定できる。今回はVSCode上のデバッグで「-Ku」が効いていなかったため明示した。
・openメソッドの第3引数には、ファイル新規生成の場合にパーミッションを指定できる。デフォルトは「0666」
● Fileクラスのその他のメソッド
・絶対パスの取得
p File.absolute_path("doughnut.csv")
# "/Users/miro/Documents/RubyProjects/MyRuby/doughnut.csv"
# 第2引数に基準パスを指定
p File.absolute_path("../..", "/Users/miro/Documents/RubyProjects")
# "/Users/miro"
・パスから、本体の名前(ファイル名/ディレクトリ名の部分)を取得
# ファイル名は、存在しなくても良いらしい。単に文字列として扱っている
p File.basename("/path/to/doughnut.csv"); # "doughnut.csv"
# サフィックスを除去して取得
p File.basename("./doughnut.csv", ".csv"); # "doughnut"
・パスから、本体の名前より前のパスの部分を取得
# ファイル名は、存在しなくても良いらしい。単に文字列として扱っている
p File.dirname("/path/to/mytest.txt") # "/path/to"
p File.dirname("/Users/miro/Documents") # "/Users/miro"
p File.dirname("list.txt") # "."
・パスから、本体の名前と、パスの部分を分割して配列で取得
# ファイル名は、存在しなくても良いらしい。単に文字列として扱っている
p File.split("/path/to/mytest.txt") # ["/path/to", "mytest.txt"]
p File.split("list.txt") # [".", "list.txt"]
・ファイルの情報を格納した「File::Stat」クラスのオブジェクトを取得
stat = File.stat("/Users/miro/Documents/RubyProjects/MyRuby/doughnut.csv")
p stat.file? # true
p stat.atime # 2017-05-03 11:19:50 +0900
# など。File::Stat の詳細は、
# https://docs.ruby-lang.org/ja/latest/class/File=3a=3aStat.html
・パーミッションを変更
File.chmod(0666, "./doughnut.csv")
# -rw-rw-rw-@ 1 miro staff 69 5 2 21:39 doughnut.csv
・ファイルの削除
File.delete("./doughnut.csv")
File.unlink("./cafe.csv")
・ファイルか?ディレクトリか?存在するか? など
p File.file?("./list.txt") # true
p File.directory?("./list.txt") # false
p File.exist?("./list.txt") # true 「exists?」は非推奨なので注意
p File.symlink?("./list.txt") # false
・ファイル名のパターンマッチング
p File.fnmatch("list*", "list.txt") # true
p File.fnmatch("*list*", "./list.txt") # false
p File.fnmatch("*list*", "./list.txt", File::FNM_DOTMATCH) # true
p File.fnmatch("{my,your}file*", "myfile.txt", File::FNM_EXTGLOB) # true
# 第2引数のファイル名は、存在しなくても良いらしい。単に文字列としてマッチングしている
# ワイルドカードとして `*', `?', `[]', `{}' が使用できる
# 第3引数でオプションを指定できる
# FNM_NOESCAPE エスケープ文字 \ を普通の文字とみなす
# FNM_PATHNAME ワイルドカードが / にマッチしなくなる
# FNM_CASEFOLD 大小文字を区別しない
# FNM_DOTMATCH ワイルドカードが 先頭の「.」にマッチするようになる
# FNM_EXTGLOB {} 内のカンマで区切られたいずれかの文字列でマッチングできる
・拡張子を取得したい
p File.extname("foo/foo.tar.gz") # ".gz"
p File.extname("path/to/dir") # ""
# ファイル名は、存在しなくても良いらしい。単に文字列としてマッチングしている
・ファイル名の変更
File.rename("list.txt", "renamed.txt")
・ファイルのサイズを取得
p File.size("/Users/miro/.rvm/rubies/ruby-2.4.0/bin/ruby") # 8960
■ Dirクラスでディレクトリを操作する
@see https://docs.ruby-lang.org/ja/latest/class/Dir.html
# ディレクトリの作成
Dir::mkdir("tmp", 0700) # パーミッションは省略したら0777
Dir::mkdir("tmp") # 既に存在したら Uncaught exception: File exists 〜
# 空のディレクトリの削除
Dir::rmdir("tmp")
Dir::delete("tmp") # 存在しなかったら、
Dir::unlink("tmp") # Uncaught exception: No such file or directory 〜
# ディレクトリの中身の一覧を取得
p Dir::entries("./")
# [".", "..", ".vscode", "cafe.csv", "doughnut.csv", "mytest.rb"]
# 存在するか
p Dir.exist?("/path/to/none") # false
# 現在のユーザのホームディレクトリを取得
p Dir.home # "/Users/miro"
# カレントディレクトリを取得
p Dir.pwd # "/Users/miro/Documents/RubyProjects/MyRuby"
# 指定のディレクトリへ移動
Dir.chdir("/Users/miro/Temp/"); p Dir.pwd # "/Users/miro/Temp"
# パターンマッチングでディレクトリ内のエントリ一覧を取得
items = Dir.glob("*.csv"); p items
# ["baibai.csv", "cafe.csv", "doughnut.csv"]
# 第2引数でパターンマッチングのオプションを指定できる。
# オプションの種類は、前述の「ファイル名のパターンマッチング」の fnmatch メソッドと同じ
# glob にブロックを渡して処理する
Dir.glob("*.csv") {|fname|
puts fname
}
■ FileUtilsモジュールの便利メソッド
@see https://docs.ruby-lang.org/ja/latest/class/FileUtils.html
FileUtilsのメソッドは、UNIXコマンドに対応しているのでわかりやすい
require 'fileutils'
# 空ファイルの作成
FileUtils.touch("list.txt")
# ファイルのコピー
FileUtils.cp("cafe.csv", "cafe2.csv")
# ファイルの削除
FileUtils.rm("cafe2.csv")
# ファイルの強制削除
FileUtils.rm_f("cafe.csv")
# ディレクトリの作成
FileUtils.mkdir("test");
# 複階層のディレクトリの作成
FileUtils.mkdir_p("./test/path/to/mydir")
# ディレクトリのコピー
FileUtils.cp_r("./test/path/to/mydir", "./test/path/to/mydir2")
# ディレクトリの削除
FileUtils.rm_r("./test/path/to")
# ディレクトリの強制削除
FileUtils.rm_rf("./test")
# カレントディレクトリを取得
p FileUtils.pwd # "/Users/miro/Temp"
# 指定のディレクトリへ移動
FileUtils.cd("/Users/miro/Documents/RubyProjects/MyRuby")
# パーミッションを変更
FileUtils.chmod(0666, "./cafe.csv")
# -rw-rw-rw- 1 miro staff 34 5 3 11:35 cafe.csv
#「chmod_R」なら、ディレクトリ以下を再帰的にchmodできる
# オーナーを変更する
FileUtils.chown("nobody", "nogroup", "./cafe.csv")
# 第1引数は、ユーザ名かuid、第2引数はグループ名かgid
# それぞれ、nil または -1 を渡すと変更されない
# 第3引数の対象ファイルを複数指定したい場合は、配列で指定可能
#「chown_R」なら、ディレクトリ以下を再帰的にchownできる
# ファイルの内容が同じか?
FileUtils.cp("doughnut.csv", "copy.txt")
p FileUtils.compare_file("doughnut.csv", "copy.txt") # true
# cmp, compare_file, identical? は同機能のメソッド
# シンボリックリンクの作成
FileUtils.ln_s("doughnut.txt", "sym_to_doughnut")
# lrwxr-xr-x 1 miro staff 12 5 3 11:55 sym_to_doughnut -> doughnut.txt
# symlink メソッドも同機能