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/2와 IO.binwrite/2 함수로 파일과 상호작용해야 해요. 파일을 열 때 :utf8을 옵션으로 넘기면, 그때부터는 더 느린 IO.read/2와 IO.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/0와file_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/3와Port.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.Statstruct를 돌려줘요. 다른 파일이라면stat/2와 정확히 같은 값을 돌려줘요.stat/2,stat!/2— 경로에 대한 정보를 돌려줘요. 존재하면{:ok, info}튜플(info는File.Statstruct)을 돌려줘요.:time옵션으로 시간 반환 방식을 설정할 수 있는데,:universal(기본값, UTC),:local,:posix(epoch 이후 정수 초) 중에서 고를 수 있어요. 대부분 운영체제가 파일 시간을 POSIX 포맷으로 저장하므로time: :posix가 더 빠르다는 점도 참고하세요.write_stat/3,write_stat!/3— 주어진File.Stat를 파일 시스템에 다시 써요.
복사·이동·링크
copy/3,copy!/3—source의 내용을destination으로 복사해요.bytes_count(기본값:infinity)로 복사할 바이트 수를 제한할 수 있어요.cp/3,cp!/3— 파일 내용을 복사하면서 모드를 보존해요.cp_r/3,cp_r!/3—source의 내용을 목적지까지 재귀적으로 복사하면서 소스 디렉터리 구조와 일반 파일 모드를 유지해요.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!/2—existing파일에 대한 하드 링크new를 만들어요. 운영체제가 하드 링크를 지원하지 않으면{:error, :enotsup}을 돌려줘요.ln_s/2,ln_s!/2는 심볼릭 링크를 만들어요.new가 이미 존재하면{:error, :eexist}를 돌려줘요.read_link/1,read_link!/1—path의 심볼릭 링크를 읽어요.path가 존재하고 심볼릭 링크면{:ok, target}을, 아니면{:error, reason}을 돌려줘요.rename/2,rename!/2—source파일을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/2는 IO 장치를 돌려줘요. IO 장치는 파일을 처리하는 프로세스이고 IO 모듈의 함수로 상호작용해요. 소유 프로세스가 종료하면 파일이 닫히고 프로세스도 종료돼요. 반면 :raw나 :ram 모드를 주면 저수준 파일 디스크립터를 돌려주는데, 프로세스를 만들지 않지만 :file 모듈의 함수로 상호작용해야 해요.
read/2,read!/2—path의 내용을 담은 바이너리를 돌려줘요(실패 시{:error, reason}또는File.Error). 대표적인 오류로:enoent(없음),:eacces(권한 없음),:eisdir(디렉터리),:enotdir,:enomem이 있어요.:file.format_error/1로 설명 문자열을 얻을 수 있어요.write/3,write!/3—content를 파일에 써요. 파일이 없으면 만들고, 있으면 이전 내용을 덮어써요.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을 돌려줘요. 스트림은Enumerable과Collectable프로토콜을 모두 구현하므로 읽기와 쓰기 둘 다에 쓸 수 있어요.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/2와 String.Chars 프로토콜로 데이터를 변환해요.
더 알아보기
- 경로를 절대 경로로 확장하는 방법은
Path모듈의expand/1,expand/2를 참고해 보세요. - 열린 파일과 상호작용하는 방법은
IO모듈 문서를 확인해 보세요. - 파일 정보를 담는
File.Statstruct와 오류 struct(File.Error,File.CopyError,File.RenameError,File.LinkError) 문서도 함께 보면 좋아요. - 저수준 파일 함수에 대한 자세한 내용은 Erlang의
:file모듈 문서를 참고해요.