IO和文件系统

本章简要介绍了输入/输出机制和文件系统相关的任务,以及相关模块,如IOFilePath

我们最初在本入门指南中草拟了本章,并提前做了介绍。然而,我们注意到IO系统提供了一个很好的机会来阐述Elixir和VM的一些哲学和好奇心。

IO模块

IO模块是Elixir中用于读写标准输入/输出(:stdio),标准错误(:stderr),文件和其他IO设备的主要机制。模块的使用非常简单:

iex> IO.puts "hello world"
hello world
:ok
iex> IO.gets "yes or no? "
yes or no? yes
"yes\n"

默认情况下,IO模块中的功能将从标准输入读取并写入标准输出。例如,我们可以通过传递:stderr参数(为了写入标准错误设备)来改变:

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

File模块

File模块包含的功能允许我们打开文件作为IO设备。默认情况下,文件以二进制模式打开,这要求开发人员使用模块中的特定功能IO.binread/2IO.binwrite/2功能IO

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/2File.rm_rf/1分别复制和递归地删除文件和目录(即,复制和删除过的目录中的内容)。

您还会注意到,File模块中的功能有两种变体:一种是“常规”变体,另一种变体是尾随式(!)。例如,当我们"hello"在上面的例子中读取文件时,我们使用File.read/1。或者,我们可以使用File.read!/1

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

请注意,该版本会!返回文件的内容而不是元组,并且如果出现任何错误,该函数会引发错误。

!当您想要使用模式匹配处理不同的结果时,首选版本是首选:

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

但是,如果您希望文件在那里,则爆炸变体更有用,因为它会引发有意义的错误消息。避免写作:

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

如果发生错误,File.read/1将返回{:error,reason}并且模式匹配将失败。你仍然会得到想要的结果(一个引发的错误),但是这个消息将是关于不匹配的模式(因此对于错误实际上是什么而言是神秘的)。

因此,如果您不想处理错误结果,请使用File.read!/1

Path模块

File模块中的大部分函数都将路径作为参数。通常,这些路径将是常规的二进制文件。该Path模块提供了处理这些路径的工具:

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

Path由于Path模块透明地处理不同的操作系统,因此使用模块中的功能而不是直接操作字符串是首选。最后,请记住,在执行文件操作时,Elixir会在Windows上自动将斜杠(/)转换为反斜杠(\)。

有了这个,我们已经介绍了Elixir为处理IO并与文件系统交互而提供的主要模块。在接下来的部分中,我们将讨论关于IO的一些高级主题。为了编写Elixir代码,这些部分不是必需的,因此可以随意跳过它们,但它们确实提供了IO系统在VM和其他好奇心中的实现方式。

流程和组长

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

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

发生这种情况是因为IO模块实际上与进程一起工作(参见第11章)。当您编写程序时IO.write(pid, binary)IO模块会向所标识的进程发送一条消息,并pid提供所需的操作。让我们看看如果我们使用我们自己的过程会发生什么:

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在字符串之上提供了设备消息的实现:

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

通过使用进程对IO设备进行建模,Erlang VM允许同一网络中的不同节点交换文件进程,以读取/写入节点之间的文件。在所有IO设备中,有一个对每个过程都是特殊的:组长

写入时:stdio,实际上是向组长发送消息,该消息写入标准输出文件描述符:

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

组长可以按流程进行配置,并在不同情况下使用。例如,在远程终端中执行代码时,它将保证远程节点中的消息被重定向并打印在触发请求的终端中。

iodatachardata

在上面的所有例子中,我们在写入文件时使用了二进制文件。在“二进制文件,字符串和字符列表”一章中,我们提到了字符串是如何由字节组成的,而字符列表是带有Unicode码点的列表。

在函数IOFile也允许作为参数列表。不仅如此,他们还允许给出一个列表,整数和二进制文件的混合列表:

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模块- IOFilePath, 和-StringIO以及虚拟机如何使用过程中对潜在的IO机制,以及如何使用chardataio和用于IOdata操作。