[Ruby入門] 15. ファイルとディレクトリを操作する

&gt;&gt; 連載の目次は [こちら！](http://qiita.com/prgseek/items/237d02be844534063f1d)

今回は、ファイルとディレクトリを扱う方法について整理する。
ファイルはFileクラス、ディレクトリはDirクラスで扱う。
また、FileUtilsモジュールにて、UNIXコマンドライクにさまざまな操作が可能。

※ここでは例文をすっきりさせるため例外処理はしていない。実際にはメソッドによって例外を発生するので、適宜対応する。


### ■ Fileクラスでファイルを操作する

@see https://docs.ruby-lang.org/ja/latest/class/File.html

・Fileクラスは IOクラスを継承しているため、共通の入出力IFで操作できる

・Fileクラスは Enumerableモジュールを積んでるので、ループ系の処理ができる


#### ● ファイルのオープンと基本的な読み書き
```ruby

# 手動で開く、１行ずつ読む、手動で閉じる
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メソッドの第２引数はモード
r  読み込み専用でオープン。デフォルト。
w  書き込み専用でオープン。
a  追記モード
・上記は、それぞれ「+」を付けると読み書き両用でオープンする
・「w」「w+」は、ファイルが存在する場合に空にするので注意。新規作成用だな。
・「a」は、ファイルがなければ新規作成される。
・モードの後ろに「:UTF-8」のように文字エンコーディングを指定できる。今回はVSCode上のデバッグで「-Ku」が効いていなかったため明示した。

・openメソッドの第３引数には、ファイル新規生成の場合にパーミッションを指定できる。デフォルトは「0666」



#### ● Fileクラスのその他のメソッド

##### ・絶対パスの取得
```ruby
p File.absolute_path("doughnut.csv") 
   # "/Users/miro/Documents/RubyProjects/MyRuby/doughnut.csv"

# 第２引数に基準パスを指定
p File.absolute_path("../..", "/Users/miro/Documents/RubyProjects") 
   # "/Users/miro"
```

##### ・パスから、本体の名前（ファイル名/ディレクトリ名の部分）を取得
```ruby
# ファイル名は、存在しなくても良いらしい。単に文字列として扱っている

p File.basename("/path/to/doughnut.csv"); # "doughnut.csv"

# サフィックスを除去して取得
p File.basename("./doughnut.csv", ".csv"); # "doughnut"
```

##### ・パスから、本体の名前より前のパスの部分を取得
```ruby
# ファイル名は、存在しなくても良いらしい。単に文字列として扱っている
p File.dirname("/path/to/mytest.txt")   # "/path/to"
p File.dirname("/Users/miro/Documents") # "/Users/miro"
p File.dirname("list.txt")              # "."
```

##### ・パスから、本体の名前と、パスの部分を分割して配列で取得
```ruby
# ファイル名は、存在しなくても良いらしい。単に文字列として扱っている
p File.split("/path/to/mytest.txt")   # ["/path/to", "mytest.txt"]
p File.split("list.txt")              # [".", "list.txt"]
```

##### ・ファイルの情報を格納した「File::Stat」クラスのオブジェクトを取得
```ruby
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
```

##### ・パーミッションを変更
```ruby
File.chmod(0666, "./doughnut.csv")
   # -rw-rw-rw-@ 1 miro  staff     69  5  2 21:39 doughnut.csv
```

##### ・ファイルの削除
```ruby
File.delete("./doughnut.csv")
File.unlink("./cafe.csv")
```

##### ・ファイルか？ディレクトリか？存在するか？ など
```ruby
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
```

##### ・ファイル名のパターンマッチング
```ruby
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
  # 第２引数のファイル名は、存在しなくても良いらしい。単に文字列としてマッチングしている
  # ワイルドカードとして `*', `?', `[]', `{}' が使用できる
  # 第３引数でオプションを指定できる
  #   FNM_NOESCAPE  エスケープ文字 \ を普通の文字とみなす
  #   FNM_PATHNAME  ワイルドカードが / にマッチしなくなる
  #   FNM_CASEFOLD  大小文字を区別しない
  #   FNM_DOTMATCH  ワイルドカードが 先頭の「.」にマッチするようになる
  #   FNM_EXTGLOB   {} 内のカンマで区切られたいずれかの文字列でマッチングできる
```

##### ・拡張子を取得したい
```ruby
p File.extname("foo/foo.tar.gz")  # ".gz"
p File.extname("path/to/dir")     # ""
  # ファイル名は、存在しなくても良いらしい。単に文字列としてマッチングしている
```

##### ・ファイル名の変更
```ruby
File.rename("list.txt", "renamed.txt")
```

##### ・ファイルのサイズを取得
```ruby
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

```ruby

# ディレクトリの作成
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"]
  # 第２引数でパターンマッチングのオプションを指定できる。
  # オプションの種類は、前述の「ファイル名のパターンマッチング」の fnmatch メソッドと同じ

# glob にブロックを渡して処理する
Dir.glob("*.csv") {|fname|
  puts fname
}
```




### ■ FileUtilsモジュールの便利メソッド

@see https://docs.ruby-lang.org/ja/latest/class/FileUtils.html

FileUtilsのメソッドは、UNIXコマンドに対応しているのでわかりやすい

```ruby
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")
  # 第１引数は、ユーザ名かuid、第２引数はグループ名かgid
  # それぞれ、nil または -1 を渡すと変更されない
  # 第３引数の対象ファイルを複数指定したい場合は、配列で指定可能
  #「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 メソッドも同機能
```