File

File

파일을 조작하는 함수들을 담고 있는 모듈이에요. 일부 함수는 저수준이라서 사용자가 open/2, copy/3 등으로 파일이나 IO 장치와 직접 상호작용할 수 있어요. 이 모듈은 또한 파일 이름과 함께 동작하면서 이름이 Unix 계열 변형에 기반한 고수준 함수들도 제공해요. 예를 들어 cp/3로 파일을 복사하고, rm_rf/1로 파일과 디렉터리를 재귀적으로 삭제할 수 있어요.

이 모듈의 함수에 주어지는 경로는 현재 작업 디렉터리(File.cwd/0가 돌려주는 값)에 상대적일 수도 있고, 절대 경로일 수도 있어요. ~ 같은 셸 관례는 자동으로 확장되지 않아요. ~/Downloads 같은 경로를 쓰려면 Path.expand/1이나 Path.expand/2로 절대 경로로 확장해야 해요.

출처: File

본문

인코딩 (Encoding)

파일을 쓰고 읽으려면 IO 모듈의 함수를 사용해야 해요. 기본적으로 파일은 바이너리 모드로 열리는데, 이 경우 IO.binread/2IO.binwrite/2 함수로 파일과 상호작용해야 해요. 파일을 열 때 :utf8을 옵션으로 넘기면, 그때부터는 더 느린 IO.read/2IO.write/2를 써야 해요. 이 함수들이 올바른 변환을 수행하고 데이터 보장을 제공하는 역할을 하기 때문이에요.

Elixir에서 charlist로 주어지는 파일 이름은 항상 UTF-8로 취급된다는 점에 주의하세요. 특히 셸과 운영체제가 UTF-8 인코딩을 쓰도록 설정되어 있다고 기대해요. 바이너리 파일 이름은 raw로 간주되어 운영체제에 그대로 전달돼요.

API

이 모듈의 대부분의 함수는 성공 시 :ok 또는 {:ok, result}를, 실패 시 {:error, reason}을 돌려줘요. 이 함수들은 또한 !로 끝나는 변형을 갖고 있는데, 성공 시에는 결과를 돌려주고({:ok, result} 튜플 대신) 실패 시에는 예외를 던져요. 예를 들어:

File.read("hello.txt")
#=> {:ok, "World"}

File.read("invalid.txt")
#=> {:error, :enoent}

File.read!("hello.txt")
#=> "World"

File.read!("invalid.txt")
#=> raises File.Error

일반적으로 파일이 존재하지 않을 때 반응하고 싶다면 전자(튜플 반환)를 쓰고, 파일을 읽지 못하면 소프트웨어가 실패해야 한다고 기대할 때(말 그대로 예외인 경우) 후자(!)를 쓰면 돼요.

프로세스와 raw 파일

파일이 열릴 때마다 Elixir는 새 프로세스를 만들어요. 파일에 쓰는 것은 파일 디스크립터에 쓰는 프로세스에 메시지를 보내는 것과 같아요. 이 말은 파일이 노드 사이를 오갈 수 있고, 메시지 전달 덕분에 네트워크에서 같은 파일에 쓸 수 있다는 뜻이에요.

하지만 이 추상화에 대한 비용을 항상 치르고 싶지는 않을 수 있어요. 그런 경우 파일을 :raw 모드로 열 수 있어요. :read_ahead:delayed_write 옵션도 큰 파일을 다룰 때나 빡빡한 루프에서 파일을 다룰 때 유용해요. 이런 옵션에 대한 자세한 내용과 성능 고려 사항은 :file.open/2를 확인하세요.

파일 내 위치 이동 (Seeking)

Elixir가 돌려준 파일과 상호작용할 때 :file 모듈의 함수를 사용할 수도 있어요. 예를 들어 파일의 특정 위치에서 읽으려면 :file.pread/3를 사용해요.

File.write!("example.txt", "Eats, Shoots & Leaves")
file = File.open!("example.txt")
:file.pread(file, 15, 6)
#=> {:ok, "Leaves"}

혹은 현재 위치를 계속 추적해야 한다면 :file.position/2:file.read/2를 사용할 수 있어요.

:file.position(file, 6)
#=> {:ok, 6}
:file.read(file, 6)
#=> {:ok, "Shoots"}
:file.position(file, {:cur, -12})
#=> {:ok, 0}
:file.read(file, 4)
#=> {:ok, "Eats"}

주요 타입

이 모듈은 파일 작업에 쓰이는 여러 타입을 정의해요. 그중 눈여겨볼 것들을 정리할게요.

  • mode/0 — 파일을 여는 모드예요. :read, :write, :append, :binary, :exclusive, :raw, :compressed, :delayed_write, :read_ahead 등과 인코딩 모드가 포함돼요.
  • encoding_mode/0 — 인코딩 옵션이에요. :utf8, {:encoding, :latin1 | :unicode | :utf8 | :utf16 | :utf32 | ...} 등이 있어요.
  • io_device/0file_descriptor/0 — 열린 파일을 나타내는 타입이에요.
  • posix/0 — 파일 시스템 오류 이유를 나타내는 타입이에요.
  • stat_options/0[{:time, :local | :universal | :posix}] 형태로 stat류 함수의 시간 반환 방식을 정해요.
  • stream_mode/0 — 스트리밍 전용 모드예요.
@type mode() ::
  :append | :binary | :charlist | :compressed | :delayed_write
  | :exclusive | :raw | :read | :read_ahead | :sync | :write
  | {:read_ahead, pos_integer()}
  | {:delayed_write, non_neg_integer(), non_neg_integer()}
  | encoding_mode()

@type encoding_mode() ::
  :utf8
  | {:encoding, :latin1 | :unicode | :utf8 | :utf16 | :utf32
    | {:utf16, :big | :little} | {:utf32, :big | :little}}

함수들

디렉터리 관련

  • cd/1, cd!/1 — 현재 작업 디렉터리를 바꿔요. 디렉터리는 BEAM 전역으로 설정되므로 여러 프로세스가 동시에 바꾸면 경쟁 조건이 생길 수 있어요. 전역 디렉터리를 바꾸지 않고 주어진 디렉터리에서 외부 명령을 실행하려면 System.cmd/3Port.open/2:cd 옵션을 쓰세요. cd!/2는 주어진 함수를 실행한 뒤 예외가 있든 없든 이전 경로로 되돌아와요.
  • cwd/0, cwd!/0 — 현재 작업 디렉터리를 가져와요. Unix 계열에서 드물게 실패할 수 있어요(현재 디렉터리의 부모에 읽기 권한이 없을 때).
  • ls/1, ls!/1 — 주어진 디렉터리의 파일 목록을 돌려줘요. 숨김 파일도 무시되지 않고, 결과는 정렬되지 않아요. 디렉터리도 파일로 간주되어 결과에 포함돼요.
  • mkdir/1, mkdir!/1 — 디렉터리를 만들려 해요. 누락된 부모 디렉터리는 만들지 않아요. 반면 mkdir_p/1, mkdir_p!/1은 누락된 부모 디렉터리까지 만들어요.
  • rmdir/1, rmdir!/1 — 디렉터리를 삭제하려 해요. 비어 있지 않으면 {:error, :eexist}를 돌려줘요.

파일 확인

  • dir?/2 — 주어진 경로가 디렉터리면 true를 돌려줘요. 심볼릭 링크를 따라가므로, 링크가 디렉터리를 가리키면 true가 나와요.
  • exists?/2 — 주어진 경로가 존재하면 true를 돌려줘요. 존재하지 않는 대상을 가리키는 심볼릭 링크는 false를 돌려줘요.
  • regular?/2 — 경로가 일반 파일이면 true를 돌려줘요. 심볼릭 링크를 따라가요.
  • lstat/2, lstat!/2 — 경로에 대한 정보를 돌려줘요. 심볼릭 링크라면 타입을 :symlink로 설정하고 링크에 대한 File.Stat struct를 돌려줘요. 다른 파일이라면 stat/2와 정확히 같은 값을 돌려줘요.
  • stat/2, stat!/2 — 경로에 대한 정보를 돌려줘요. 존재하면 {:ok, info} 튜플(infoFile.Stat struct)을 돌려줘요. :time 옵션으로 시간 반환 방식을 설정할 수 있는데, :universal(기본값, UTC), :local, :posix(epoch 이후 정수 초) 중에서 고를 수 있어요. 대부분 운영체제가 파일 시간을 POSIX 포맷으로 저장하므로 time: :posix가 더 빠르다는 점도 참고하세요.
  • write_stat/3, write_stat!/3 — 주어진 File.Stat를 파일 시스템에 다시 써요.

복사·이동·링크

  • copy/3, copy!/3source의 내용을 destination으로 복사해요. bytes_count(기본값 :infinity)로 복사할 바이트 수를 제한할 수 있어요.
  • cp/3, cp!/3 — 파일 내용을 복사하면서 모드를 보존해요.
  • cp_r/3, cp_r!/3source의 내용을 목적지까지 재귀적으로 복사하면서 소스 디렉터리 구조와 일반 파일 모드를 유지해요. cp!/3는 성공 시 :ok를, cp_r!/3는 복사된 파일 리스트를 돌려줘요. 옵션에는 :on_conflict(기존 파일을 덮어쓸지 결정하는 콜백, 기본값 true), :dereference_symlinks(기본 false; true면 심볼릭 링크를 역참조해 그 내용을 복사), :preserve_directory_permissions(기본 false; true면 소스 디렉터리 권한을 목적지에도 복사)가 있어요.
# Copies file "a.txt" to "b.txt"
File.cp_r("a.txt", "b.txt")
#=> {:ok, ["b.txt"]}

# Copies all files in "samples" to "tmp"
File.cp_r("samples", "tmp")
#=> {:ok, ["z.txt", "y.txt", "x.txt]}

# Copying into a subdirectory of source is not allowed
File.cp_r("src", "src/dest")
#=> {:error, :einval, "src/dest"}
  • ln/2, ln!/2existing 파일에 대한 하드 링크 new를 만들어요. 운영체제가 하드 링크를 지원하지 않으면 {:error, :enotsup}을 돌려줘요. ln_s/2, ln_s!/2는 심볼릭 링크를 만들어요. new가 이미 존재하면 {:error, :eexist}를 돌려줘요.
  • read_link/1, read_link!/1path의 심볼릭 링크를 읽어요. path가 존재하고 심볼릭 링크면 {:ok, target}을, 아니면 {:error, reason}을 돌려줘요.
  • rename/2, rename!/2source 파일을 destination 파일로 이름을 바꿔요. 디렉터리 사이에서 파일(과 디렉터리)을 옮길 때 쓸 수 있어요. 파일을 옮길 때는 목적지 파일 이름을 완전히 지정해야 해요.
  • touch/2, touch!/2 — 주어진 파일의 수정 시간(mtime)과 접근 시간(atime)을 갱신해요. 파일이 없으면 만들어요. 시간은 UTC datetime(:erlang.universaltime/0이 돌려주는 값)이나 POSIX 타임스탬프 정수(System.os_time(:second)가 돌려주는 값)를 요구해요.

읽기·쓰기·열기

  • open/2, open!/2, open/3, open!/3 — 주어진 경로를 열어요. modes_or_function은 모드 리스트이거나 함수일 수 있어요. 함수면 open(path, [], function)을 호출하는 것과 같아요. 지원 모드에는 :binary(기본, 유니코드 특수 처리 비활성), :read(존재해야 함), :write(없으면 생성, read와 결합하지 않으면 잘라냄), :append(쓰면 항상 끝에), :exclusive(없을 때만 생성, 있으면 {:error, :eexist}), :charlist(읽을 때 charlist 반환), :compressed(gzip 압축 파일), :utf8(데이터를 UTF-8로 자동 변환) 등이 있어요.
  • open!/2는 열지 못하면 File.Error를 던지고 IO 장치를 돌려줘요. open/3은 마지막 인자로 함수를 받아, 파일을 열어 함수에 넘기고 함수가 반환하면 에러가 있든 없든 자동으로 닫아요.

기본적으로 open/2IO 장치를 돌려줘요. IO 장치는 파일을 처리하는 프로세스이고 IO 모듈의 함수로 상호작용해요. 소유 프로세스가 종료하면 파일이 닫히고 프로세스도 종료돼요. 반면 :raw:ram 모드를 주면 저수준 파일 디스크립터를 돌려주는데, 프로세스를 만들지 않지만 :file 모듈의 함수로 상호작용해야 해요.

  • read/2, read!/2path의 내용을 담은 바이너리를 돌려줘요(실패 시 {:error, reason} 또는 File.Error). 대표적인 오류로 :enoent(없음), :eacces(권한 없음), :eisdir(디렉터리), :enotdir, :enomem이 있어요. :file.format_error/1로 설명 문자열을 얻을 수 있어요.
  • write/3, write!/3content를 파일에 써요. 파일이 없으면 만들고, 있으면 이전 내용을 덮어써요. content는 iodata(바이트 리스트나 바이너리)여야 해요. 🙋 경고: 이 함수를 호출할 때마다 파일 디스크립터가 열리고 새 프로세스가 생겨요. 루프 안에서 여러 번 쓴다면 File.open/2로 파일을 열고 IO 함수로 쓰는 편이 성능이 훨씬 좋아요.
File.write("hello.txt", "world!")
#=> :ok

File.write("temp", "world!")
#=> {:error, :eisdir}

삭제

  • rm/1, rm!/1 — 파일을 삭제하려 해요. 읽기 전용이어도 삭제돼요. :eperm은 파일이 디렉터리인 경우예요.
  • rm_rf/1, rm_rf!/1 — 주어진 경로에서 파일과 디렉터리를 재귀적으로 삭제해요. 심볼릭 링크는 따라가지 않고 그냥 삭제하며, 존재하지 않는 파일은 무시해요(실패로 만들지 않아요).

스트리밍

  • stream!/2, stream!/3 — 주어진 경로와 모드에 대한 File.Stream을 돌려줘요. 스트림은 EnumerableCollectable 프로토콜을 모두 구현하므로 읽기와 쓰기 둘 다에 쓸 수 있어요. line_or_bytes 인자는 어떻게 읽을지 정하는데, 기본은 :line이고 주어진 바이트 수만큼 읽을 수도 있어요. :line 옵션을 쓰면 CRLF 줄바꿈("\r\n")이 LF("\n")로 정규화돼요.
  • 스트림에 :trim_bom 모드를 넘기면 파일을 읽을 때 UTF-8/UTF-16/UTF-32 바이트 순서 표시(BOM)를 잘라내요. :read_offset을 넘기면 열거할 때 건너뛰는 오프셋이에요(BOM 뒤에 적용).
# Read a utf8 text file which may include BOM
File.stream!("./test/test.txt", [:trim_bom, encoding: :utf8])

# Read in 2048 byte chunks rather than lines
File.stream!("./test/test.data", 2048)

Elixir는 스트리밍 파일이 열리는 시점을 통제하므로, 같은 노드에서 열고 인코딩을 지정하지 않으면 성능을 위해 :raw 모드와 :read_ahead 옵션으로 스트림을 열어요. 이 경우 파일로 스트리밍되는 데이터는 iodata/0 타입으로 변환되어야 해요. {:encoding, :utf8} 등을 모드에 넘기면 IO.write/2String.Chars 프로토콜로 데이터를 변환해요.

더 알아보기

  • 경로를 절대 경로로 확장하는 방법은 Path 모듈의 expand/1, expand/2를 참고해 보세요.
  • 열린 파일과 상호작용하는 방법은 IO 모듈 문서를 확인해 보세요.
  • 파일 정보를 담는 File.Stat struct와 오류 struct(File.Error, File.CopyError, File.RenameError, File.LinkError) 문서도 함께 보면 좋아요.
  • 저수준 파일 함수에 대한 자세한 내용은 Erlang의 :file 모듈 문서를 참고해요.