Elixir入門 12: 入出力とファイルシステム
Tags: elixir, webdev, tutorial, programming
Published: 2018-12-04 01:03:40

---
title: Elixir入門 12: 入出力とファイルシステム
published: true
description: Elixir公式サイトの許諾を得て「IO and the file system」の解説にもとづき、加筆補正を加えて、Elixirにおける入出力の仕方やファイルシステムに関わる操作、IO、File、Pathなどの関連モジュールを簡単に紹介します。
tags: #elixir, #webdev, #tutorial, #programming
---

本稿はElixir公式サイトの許諾を得て「[IO and the file system](https://elixir-lang.org/getting-started/io-and-the-file-system.html)」の解説にもとづき、加筆補正を加えて、Elixirにおける入出力の仕方やファイルシステムに関わる操作、`IO`、`File`、`Path`などの関連モジュールを簡単に紹介します。


# IOモジュール
[`IO`モジュール](https://hexdocs.pm/elixir/IO.html)は、入出力のためのおもな機能を提供します。読み書き先は、標準入出力(`:stdio`)や標準エラー(`:stderr`)、ファイル、その他の入出力デバイスなどです。

[`IO.puts/2`](https://hexdocs.pm/elixir/IO.html#puts/2)は、引数の項目を出力します。[`IO.gets/2`](https://hexdocs.pm/elixir/IO.html#gets/2)は入出力デバイスからの読み込みです。ともに第1引数のデバイスは、標準入出力(`:stdio`)がデフォルトになっています。

```elixir
iex> IO.puts "hello world"
hello world
:ok
iex> IO.gets "yes or no? "
yes or no? hello  #<- helloとタイプして[enter]
"hello\n"
```

第1引数に`:stderr`を与えると、標準エラーが入出力先になります。

```elixir
iex> IO.puts :stderr, "hello world"
hello world
:ok
```


# Fileモジュール
[`File`モジュール](https://hexdocs.pm/elixir/File.html)には、入出力デバイスとしてファイルを扱うための関数が備わっています。つぎのコードは、ファイルを開け閉じして、文字列を読み書きする例です。ファイルはデフォルトではバイナリのモード(`:binary`)で開かれます。データの読み書きには用いるのは、`IO`モジュールの関数です。

```elixir
iex> {:ok, file} = File.open("hello", [:write])
{:ok, #PID<0.90.0>}
iex> IO.binwrite(file, "world")
:ok
iex> File.close(file)
:ok
iex> File.read("hello")
{:ok, "world"}
iex> {:ok, file} = File.open("hello", [:read])
{:ok, #PID<0.95.0>}
iex> IO.binread(file, :line)
"world"
iex> File.close(file)
:ok
```

- [`File.open/2`](https://hexdocs.pm/elixir/File.html#open/2): 第1引数のパスのファイルを開きます。第2引数はモードのリストで、`:write`は書き込み、`:read`は読み込みです。
- [`File.close/1`](https://hexdocs.pm/elixir/File.html#close/1): 引数のファイルを閉じます。
- [`File.read/1`](https://hexdocs.pm/elixir/File.html#read/1): `{:ok, binary}`のタプルを返します。`binary`は引数のパスのバイナリデータです。読み込めなかったときは`{:error, reason}`が返ります。
- [`IO.binwrite/2`](https://hexdocs.pm/elixir/IO.html#binwrite/2): 第1引数がデバイスで、デフォルト値は標準入出力の`:stdio`です。第2引数の項目をバイナリとして書き込みます。
- [`IO.binread/2`](https://hexdocs.pm/elixir/IO.html#binread/2): 第1引数がデバイスで、デフォルト値は標準入出力の`:stdio`です。第2引数で読み込むのが1行(`:line`)かすべて(`:all`)かを定めます。

ファイルを開くモードには`:utf8`も加えられます。`File`モジュールに、ファイルから読み込むバイトをUTF-8のエンコードで解析するための指定です。

`File`モジュールには、ファイルシステムを操作する関数も備わっています。関数の名前はUNIXのコマンドに沿ってつけられました。たとえば、つぎのような関数です。

- [`File.rm/1`](https://hexdocs.pm/elixir/File.html#rm/1): ファイルのパスを削除します。
- [`File.mkdir/1`](https://hexdocs.pm/elixir/File.html#mkdir/1): パスのディレクトリをつくります。
- [`File.mkdir_p/1`](https://hexdocs.pm/elixir/File.html#mkdir_p/1): パスのディレクトリを、親チェーンも含めてつくります。
- [`File.cp_r/2`](https://hexdocs.pm/elixir/File.html#cp_r/3): ディレクトリの内容をサブディレクトリまで含めてコピーします。
- [`File.rm_rf/1`](https://hexdocs.pm/elixir/File.html#rm_rf/1): ディレクトリの内容をサブディレクトリまで含めて削除します。

`File`モジュールの関数には、同じ名前で最後に`!`記号のつくものとつかないものがあることに気づくでしょう。たとえば、`File.read/1`と`File.read!/1`です。違いは戻り値にあります。前者はタプルを返します。それに対して、後者はファイルの中身が返るか、そうでなければエラーが起こるのです。

```elixir
iex> File.read("hello")
{:ok, "world"}
iex> File.read!("hello")
"world"
iex> File.read("unknown")
{:error, :enoent}
iex> File.read!("unknown")
** (File.Error) could not read file "unknown": no such file or directory
    (elixir) lib/file.ex:310: File.read!/1
```

タプルを返す関数は、戻り値にパターンマッチングが使えます。

```elixir
defmodule Example do
  def read_file(file) do
    case File.read(file) do
      {:ok, body} -> IO.puts(body)
      {:error, reason} -> IO.puts("error: #{reason}")
    end
  end
end
```

```elixir
iex> Example.read_file("hello")
world
:ok
iex> Example.read_file("unknown")
error: enoent
:ok
```

ファイルが存在する前提であれば、`File.read!/1`の方が端的です。万が一ファイルがなかったとき、パターンマッチングではマッチしないというエラーになります。`File.read!/1`であれば、エラーメッセージにファイルの存在しないことが示されるからです。

```elixir
iex> {:ok, body} = File.read("unknown")
** (MatchError) no match of right hand side value: {:error, :enoent}
```


# Pathモジュール
`File`モジュールの多くの関数は引数にパスが含まれます。パスは通常バイナリです。[`Path`モジュール](https://hexdocs.pm/elixir/Path.html)には、パスとしてバイナリを扱うための関数が備わっています。

[`Path.join/2`](https://hexdocs.pm/elixir/Path.html#join/2)は、ふたつの引数をパスとしてつなぎます。また、[`Path.expand/1`](https://hexdocs.pm/elixir/Path.html#expand/1)はパスを展開して絶対パスにします。

```elixir
iex> Path.join("elixir", "test")
"elixir/test"
iex> Path.expand("/elixir/test/../config")
"/elixir/config"
```

パスの扱いは、文字列を直に操作するのでなく、`Path`モジュールの関数を使う方がよいでしょう。オペレーティングシステムの違いが吸収されるからです。ファイルを操作するとき、パスの中のスラッシュ`/`は、Windowsでは自動的にバックスラッシュ`\`に変換されます。

入出力とファイルシステムの操作に関わるおもなモジュールのご説明は以上です。このあとは、入出力についての少し進んだ話題を扱います。Elixirのコードを書くための解説ではないので、飛ばしても構いません。VMの中でIOシステムがどのように実装されているかを解説します。


# プロセスとグループリーダー
`File.open/2`はタプルを返します。これは`IO`モジュールが、内部的にプロセスに働きかけるからです。

```elixir
iex> {:ok, file} = File.open "hello", [:write]
{:ok, #PID<0.47.0>}
```

たとえば、[`IO.write/2`](https://hexdocs.pm/elixir/IO.html#write/2)を使うと、 `IO`モジュールがPIDで参照されるプロセスにメッセージを送り、操作が行われます。出力される4要素のタプルがそのメッセージです。そのあと、`IO`モジュールの求める結果が与えられなかったために失敗しています。

```elixir
iex> pid = spawn fn ->
...>   receive do: (msg -> IO.inspect msg)
...> end
#PID<0.89.0>
iex> IO.write(pid, "hello")
{:io_request, #PID<0.84.0>, #Reference<0.3003912951.510132227.213667>,
 {:put_chars, :unicode, "hello"}}
** (ErlangError) Erlang error: :terminated
    (stdlib) :io.put_chars(#PID<0.89.0>, :unicode, "hello")
```

[`StringIO`モジュール](https://hexdocs.pm/elixir/StringIO.html)は、文字列に入出力デバイスの操作を実装します。`StringIO`は入出力デバイスになり、`IO`モジュールの関数が使えるのです。[`StringIO.open/2`](https://hexdocs.pm/elixir/StringIO.html#open/2)は入出力デバイスをつくり、[`IO.read/2`](https://hexdocs.pm/elixir/IO.html#read/2)はその参照から第2引数の文字数を読み込みます。

```elixir
iex> {:ok, pid} = StringIO.open("hello")
{:ok, #PID<0.96.0>}
iex> IO.read(pid, 4)
"hell"
```

Erlang VMは入出力デバイスをプロセスでモデル化することにより、同じネットワークの異なるノードがファイルの操作をやり取りして、ノード間でファイルを読み書きできるようにしています。

すべてのIOデバイスは、プロセスにひとつ特別なグループリーダーをもちます。`:stdio`に書き込みをしたとき、メッセージはグループリーダーに送られるのです。グループリーダーは標準出力[ファイル記述子](https://ja.wikipedia.org/wiki/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E8%A8%98%E8%BF%B0%E5%AD%90)に書き込みます。[`Process.group_leader/0`](https://hexdocs.pm/elixir/Process.html#group_leader/0)は、呼び出しもとプロセスのグループリーダーのPIDを返します。

```elixir
iex> IO.puts :stdio, "hello"
hello
:ok
iex> IO.puts Process.group_leader, "hello"
hello
:ok
```

グループリーダーはプロセスごとにつくられ、さまざまな状況で使われます。たとえば、リモート端末でコードを実行したとき、リモートノード内のメッセージは、リクエストを発した端末にリダイレクトされて出力されることが保証されます。


# 入出力データと文字データ
Elixirにおける文字列はバイトの集まりで、文字リストはUnicodeのコードポイントを納めたリストです(「[Elixir入門 06: バイナリと文字列および文字リスト](https://dev.to/gumi/elixir-06--35na)」参照)。

`IO`と`File`モジュールの関数には、引数にリストも与えられます。それだけでなく、リストに整数や入れ子リスト、さらにバイナリを混在させても構いません。

```elixir
iex> IO.puts '拝啓' ++ [25964, 20855]
拝啓敬具
:ok
iex> IO.puts [104, 'ello', ?\s, "world"]
hello world
:ok
```

ただし、入出力の操作にリストを使うときは、注意しなければなりません。リストはバイトの集まりも、文字列も示せるからです。どちらを用いるかは、入出力デバイスのエンコーディングによります。

エンコーディングを定めずに開かれたファイルは、rawモードとみなされます。`IO`モジュールから使う関数は、`bin`で始まるものでなければなりません。これらの関数は、引数に入出力データを受け取ります。つまり、バイトとバイナリを表す整数のリストとして扱うのです。

`:stdio`と`:utf8`のモードにより、UTF-8エンコーディングで開いたファイルは、`bin`のつかない`IO`モジュールの関数も使えるようになります。これらの関数は引数に文字データを受け取ります。つまり、文字または文字列のリストとして扱うのです。

細かな違いとはいえ、入出力の関数にリストを渡すときには注意してください。バイナリはすでにバイトにもとづいて表されているので、つねにrawモードで扱われます。


#### Elixir入門もくじ
- [Elixir入門 01: コードを書いて試してみる](https://dev.to/gumi/elixir-01--2585)
- [Elixir入門 02: 型の基本](https://dev.to/gumi/elixir-02--30n1)
- [Elixir入門 03: 演算子の基本](https://dev.to/gumi/elixir-03--33im)
- [Elixir入門 04: パターンマッチング](https://dev.to/gumi/elixir-04--1346)
- [Elixir入門 05: 条件 - case/cond/if](https://dev.to/gumi/elixir-05----casecondif-60o)
- [Elixir入門 06: バイナリと文字列および文字リスト](https://dev.to/gumi/elixir-06--35na)
- [Elixir入門 07: キーワードリストとマップ](https://dev.to/gumi/elixir-07--39hi)
- [Elixir入門 08: モジュールと関数](https://dev.to/gumi/elixir-08--1c4c)
- [Elixir入門 09: 再帰](https://dev.to/gumi/elixir-09--1a0p)
- [Elixir入門 10: EnumとStream](https://dev.to/gumi/elixir-10-enumstream-4fpb)
- [Elixir入門 11: プロセス](https://dev.to/gumi/elixir-11--2mia)
- Elixir入門 12: 入出力とファイルシステム
- [Elixir入門 13: aliasとrequireおよびimport](https://dev.to/gumi/elixir-13-aliasrequireimport-55c1)
- [Elixir入門 14: モジュールの属性](https://dev.to/gumi/elixir-14--3511)
- [Elixir入門 15: 構造体](https://dev.to/gumi/elixir-15--4f43)
- [Elixir入門 16: プロトコル](https://dev.to/gumi/elixir-16--lif)
- [Elixir入門 17: 内包表記](https://dev.to/gumi/elixir-17--5gci)
- [Elixir入門 18: シギル](https://dev.to/gumi/elixir-18--5791)
- [Elixir入門 19: tryとcatchおよびrescue](https://dev.to/gumi/elixir-19-trycatchrescue-50i8)
- [Elixir入門 20: 型の仕様とビヘイビア](https://dev.to/gumi/elixir-20--j50)
- [Elixir入門 21: デバッグ](https://dev.to/gumi/elixir-21--21a1)
- [Elixir入門 22: Erlangライブラリ](https://dev.to/gumi/elixir-22-erlang-2492)
- [Elixir入門 23: つぎのステップ](https://dev.to/gumi/elixir-23--50ik)

##### 番外
- [Elixir入門: Plugを使うには](https://dev.to/gumi/elixir-plug-40lb)