IO和文件系统
本章简要介绍了输入/输出机制和文件系统相关的任务,以及相关模块,如IO
,File
和Path
。
我们最初在本入门指南中草拟了本章,并提前做了介绍。然而,我们注意到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/2
和IO.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/2
和File.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
组长可以按流程进行配置,并在不同情况下使用。例如,在远程终端中执行代码时,它将保证远程节点中的消息被重定向并打印在触发请求的终端中。
iodata
和chardata
在上面的所有例子中,我们在写入文件时使用了二进制文件。在“二进制文件,字符串和字符列表”一章中,我们提到了字符串是如何由字节组成的,而字符列表是带有Unicode码点的列表。
在函数IO
和File
也允许作为参数列表。不仅如此,他们还允许给出一个列表,整数和二进制文件的混合列表:
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模块- IO
,File
, Path
, 和-StringIO
以及虚拟机如何使用过程中对潜在的IO机制,以及如何使用chardataio
和用于IOdata
操作。