Elixir入门教程-IO和文件系统

  1. IO模块
  2. File模块
  3. Path模块
  4. 进程和组领导
  5. iodata和chardata

本章是对输入/输出机制和文件系统相关的任务以及相关模块,比如:IOFilePath的快速介绍。

我们原本计划这章在本系列教程中更早地出现。然而,我们注意到 IO 系统提供一个非常好的机会来阐明Elixir和VM的一些哲学和奇特之处。

IO模块

IO模块是Elixir里读写标准输入输出(:stdio)、标准错误(:stderr)、文件和其他IO设备的主要机制。这个模块的使用方式非常简单:

1
2
3
4
5
6
iex> IO.puts "hello world"
hello world
:ok
iex> IO.gets "yes or no? "
yes or no? yes
"yes\n"

默认情况下,IO模块的函数从标准输入读数据并往标准输出写数据。我们可以改变这个默认方式,例如,通过传递 :stderr 作为一个入参(来写数据到标准错误):

1
2
3
iex> IO.puts :stderr, "hello world"
hello world
:ok

File模块

File模块包含函数允许我们打开文件当做IO文件。默认地,文件以二进制模式打开,这些文件就需要开发者使用IO模式的特定函数 IO.binread/2 和 IO.binwrite/2 来读写。

1
2
3
4
5
6
7
8
iex> {:ok, file} = File.open "hello", [:write]
{:ok, #PID<0.47.0>}
iex> IO.binwrite file, "world"
:ok
iex> File.close file
:ok
iex> File.read "hello"
{:ok, "world"}

文件也可以用 :utf8 编码方式打开,这就告诉File模块解析从文件读取的字节为UTF-8编码的字节。

除了打开、读写文件的函数,File模块还有许多处理文件系统的函数。那些函数命名方式和Unix的函数相对应。例如,File.rm/1 可以用来删除文件,File.mkdir/1 用来创建目录,File.mkdir_p/1 用来创建目录和它所有的父目录。甚至还有 File.cp_r/2 和 File.rm_rf/1 ,分别递归地拷贝和删除文件和目录(即也复制和删除目录的内容)。

你可能也注意到了File模块里的函数有两种类型:一种是“正常”的,另一种是尾部有一个!号的。例如,上面例子里当我们读”hello”文件的时候,我们使用 File.read/1 。相应地,我们可以用File.read!/1 :

1
2
3
4
5
6
7
8
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

注意:带叹号的版本返回文件的内容而不是元组,而如果有任何错误,这个函数就抛出一个错误。

当你想要用模式匹配处理不同的输出的时候,不带叹号的版本是首选:

1
2
3
4
case File.read(file) do
{:ok, body} -> # do something with the `body`
{:error, reason} -> # handle the error caused by `reason`
end

但是,如果你期望文件就在那里,带叹号版本更加有用,因为它抛出有意义的错误。避免这样写:

1
{:ok, body} = File.read(file)

因为,一旦有错误,File.read/1 将返回 {:error, reason} 而且模式匹配将失败。你将仍然得到你想要的结果(一个抛出的错误),但是消息却是关于没有匹配上的模式(因此,错误实际上是什么就显得很神秘)。

因此,如果你不想处理错误结果,就优先使用 File.read!/1 。

Path模块

File模块里的绝大多数函数都期望用路径做入参。最常见的是,这些路径将是普通二进制数据。Path模块提供处理这样路径的工具:

1
2
3
4
iex> Path.join("foo", "bar")
"foo/bar"
iex> Path.expand("~/hello")
"/Users/jose/hello"

使用Path模块的函数,而不是直接操纵字符串是首选,因为Path模块透明地处理不同的操作系统之间的差异。最后,记住当在Windows执行文件操作的时候,Elixir将自动将斜线(/)转成反斜线(\)。

到此,我们已经讲完了Elixir提供的处理IO和与文件系统交互的主要模块。下面的部分,我们将讨论关于IO的一些高级话题。这些部分不是写Elixir代码所必需的,因此可以略过它们,不过它们对VM里IO系统是如何实现的以及其他特性提供了一个好的概貌。

进程和组领导

你可能已经注意到 File.open/2 返回一个元组 {:ok, pid} :

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

之所以是如此,是因为IO模块实际上是和进程打交道(参见11章)。当你写 IO.write(pid, binary) 的时候,IO模块将发送一个消息给被pid标识的进程,消息里同时带着期望的操作。让我们看看如果我们用我们自己的进程会发生什么:

1
2
3
4
5
6
7
8
iex> pid = spawn fn ->
...> receive do: (msg -> IO.inspect msg)
...> end
#PID<0.57.0>
iex> IO.write(pid, "hello")
{:io_request, #PID<0.41.0>, #Reference<0.0.8.91>,
{:put_chars, :unicode, "hello"}}
** (ErlangError) erlang error: :terminated

上述例子中,在 IO.write/2 后,我们可以看到IO模块发送的请求(四元素元组)被打印出来。紧跟其后,我们看到了失败,因为IO模块所期望的结果我们没有支持。

StringIO模块提供了基于字符串之上的IO设备消息的实现:

1
2
3
4
iex> {:ok, pid} = StringIO.open("hello")
{:ok, #PID<0.43.0>}
iex> IO.read(pid, 2)
"he"

通过用进程模型化IO设备,Erlang虚拟机允许同一个网络里的不同节点互访文件进程来在不同节点间读写文件。在所有IO设备中,有一个对所有进程都很特别,就是:组领导

当你写数据给 :stdio 的时候,你实际上是发消息给组领导,由它写数据给标注输出文件描述符:

1
2
3
4
5
6
iex> IO.puts :stdio, "hello"
hello
:ok
iex> IO.puts Process.group_leader, "hello"
hello
:ok

每个进程都可以配置组领导,组领导在不同的情况下使用。例如,当在一个远程终端上执行代码,它保证远程节点上的消息被重定向并打印在触发请求的终端上。

iodata和chardata

在上面所有的例子里,当写数据到文件的时候,我们使用二进制数据。在“二进制数据、字符串和字符列表”那一章,我们提到字符串是怎样由二进制数据构成而字符列表unicode代码点的列表。

IO模块和File模块里的函数也允许列表作为入参。不仅如此,它们也运行混合列表,列表里包含整数和二进制数据:

1
2
3
4
5
6
iex> IO.puts 'hello world'
hello world
:ok
iex> IO.puts ['hello', ?\s, "world"]
hello world
:ok

然而,在IO操作中使用列表需要一些注意。一个列表可能表示一串二进制数据或者一串字符,而到底使用哪一个依赖于IO设备的编码。如果一个文件没有指定编码方式打开,它被期望是原始模式,则IO模块里以 bin* 开头的函数就必须被使用。这些函数期望一个 iodata 作为入参,也就是,它们期望一个表示字节和二进制数据的整数列表被传入。

另一方面,:stdio 和以 :utf8 编码打开的文件将用其余的IO模块中的函数处理。这些函数期望一个 char_data 作为入参,也就是,一个字符或者字符串列表。

虽然这是一个微妙的区别,你只需要担心这些细节,如果你打算传递列表给这些函数。二进制数据已经由底层字节表示,因此它们的表示总是“原始”的。

到此我们结束了IO设备和IO相关功能之旅。我们已经学了四个Elixir模块:IOFilePathStringIO,也学了Erlang虚拟机如何用进程来处理底层IO机制以及如何使用 chardata 和 iodata 来进行IO操作。

原文链接: http://elixir-lang.org/getting-started/io-and-the-file-system.html