입출력
입출력 (Input/Output)
파일을 읽고 쓰는 일은 어떤 언어를 쓰든 피할 수 없는 기본기예요. Raku는 전통적인 방식을 그대로 쓰게 해주면서도, 더 간결하고 읽기 좋은 방법도 함께 제공해요. 이 문서에서 파일 관련 입출력을 빠르게 훑어볼게요. 자세한 내용은 IO 롤, IO::Handle, IO::Path 타입 문서에서 확인할 수 있어요.
본문
파일에서 읽기 (Reading from files)
파일 내용을 읽는 한 가지 방법은 open 함수를 :r(읽기) 파일 모드 옵션과 함께 써서 파일을 연 다음, 내용을 slurp하는 거예요.
my $fh = open "testfile", :r;
my $contents = $fh.slurp;
$fh.close;
여기서는 IO::Handle 객체에 close 메서드를 호출해 파일핸들을 명시적으로 닫아요. 이건 파일 내용을 읽는 매우 전통적인 방식이에요. 그런데 같은 일을 더 쉽고 명확하게 이렇게 할 수도 있어요.
my $contents = "testfile".IO.slurp;
# 또는 절차적 형태로:
$contents = slurp "testfile";
파일 이름 문자열에 IO 롤을 붙이면, 그 문자열을 파일 객체 자체처럼 참조할 수 있어서 내용을 바로 slurp할 수 있어요. 이때 slurp가 파일 열기와 닫기를 알아서 처리해준다는 점을 기억하세요.
줄 단위로 읽기 (Line by line)
파일을 줄 단위로 읽는 옵션도 있어요. 이때 새 줄 구분자(즉 $*IN.nl-in)는 제외돼요.
for 'huge-csv'.IO.lines -> $line {
# $line으로 뭔가 처리
}
# 또는 나중에 처리할 거라면
my @lines = 'huge-csv'.IO.lines;
파일에 쓰기 (Writing to files)
파일에 데이터를 쓸 때도, 이번에는 :w(쓰기) 옵션으로 open 함수를 호출하는 전통적인 방법을 고를 수 있어요. 그리고 데이터를 파일에 출력하면 돼요.
my $fh = open "testfile", :w;
$fh.print("data and stuff\n");
$fh.close;
say를 쓰면 명시적인 줄바꿈이 더 필요 없어져요.
my $fh = open "testfile", :w;
$fh.say("data and stuff");
$fh.close;
spurt를 쓰면 쓰기 모드로 파일을 열고 데이터를 쓰고 다시 닫는 일까지 한 번에 처리돼요.
spurt "testfile", "data and stuff\n";
기본적으로 모든 (텍스트) 파일은 UTF-8로 쓰여요. 필요하다면 :enc 옵션으로 인코딩을 명시할 수도 있어요.
spurt "testfile", "latin1 text: äöüß", enc => "latin1";
서식화된 문자열을 파일에 쓰고 싶다면 IO::Handle의 printf 함수를 사용해요.
my $fh = open "testfile", :w;
$fh.printf("formatted data %04d\n", 42);
$fh.close;
파일에 덧붙이기(append)를 하려면 파일핸들을 열 때 :a 옵션을 지정하면 돼요.
my $fh = open "testfile", :a;
$fh.print("more data\n");
$fh.close;
say를 쓰면 명시적인 줄바꿈이 필요 없어지고요.
my $fh = open "testfile", :a;
$fh.say("more data");
$fh.close;
spurt 호출에 :append 옵션을 주면 더 단순해져요.
spurt "testfile", "more data\n", :append;
이진(binary) 데이터를 명시적으로 쓰려면 :bin 옵션으로 파일을 열어요. 그러면 입출력 동작이 Str 타입 대신 Buf 타입으로 이뤄져요.
파일 복사·이름 바꾸기·삭제 (Copying, renaming, and removing files)
저수준 시스템 명령을 쓰지 않도록 copy, rename, move, unlink 루틴이 준비돼 있어요. 몇 가지 예시를 볼게요.
my $filea = 'foo';
my $fileb = 'foo.bak';
my $filec = '/disk1/foo';
# 참고: 'diskN'은 물리 저장 장치로 가정됨
copy $filea, $fileb; # $fileb가 있으면 덮어씀
copy $filea, $fileb, :createonly; # $fileb가 있으면 실패
rename $filea, 'new-foo'; # 'new-foo'가 있으면 덮어씀
rename $filea, 'new-foo', :createonly; # 'new-foo'가 있으면 실패
# 시스템 레벨 rename이 동작하지 않을 때는 move를 사용
move $fileb, '/disk2/foo'; # '/disk2/foo'가 있으면 덮어씀
move $fileb, '/disk2/foo', :createonly; # '/disk2/foo'가 있으면 실패
unlink $filea;
$fileb.IO.unlink;
두 unlink 문장은 인자가 존재하면 그 인자를 삭제해요. 다만 사용자에게 그렇게 할 올바른 권한이 없으면 예외를 던져요.
파일·디렉터리 확인 (Checking files and directories)
IO::Path 객체의 e 메서드를 쓰면 파일이나 디렉터리가 존재하는지 검사할 수 있어요.
if "nonexistent_file".IO.e {
say "file exists";
}
else {
say "file doesn't exist";
}
콜론 쌍(colon pair) 문법으로 같은 일을 할 수도 있어요.
if "path/to/file".IO ~~ :e {
say 'file exists';
}
my $file = "path/to/file";
if $file.IO ~~ :e {
say 'file exists';
}
파일 존재 검사와 비슷하게, 경로가 디렉터리인지도 확인할 수 있어요. 예를 들어 testfile 파일과 lib 디렉터리가 존재한다고 가정하면, 존재 검사 메서드 e로는 두 대상 모두 존재한다는 같은 결과를 얻어요.
say "testfile".IO.e; # OUTPUT: «True»
say "lib".IO.e; # OUTPUT: «True»
하지만 둘 중 하나만 디렉터리이므로, 디렉터리 검사 메서드 d는 다른 결과를 줘요.
say "testfile".IO.d; # OUTPUT: «False»
say "lib".IO.d; # OUTPUT: «True»
경로가 파일인지 확인하는 f 메서드로 검사하면 당연히 결과가 뒤집혀요.
say "testfile".IO.f; # OUTPUT: «True»
say "lib".IO.f; # OUTPUT: «False»
파일이나 디렉터리를 조회하는 데 쓸 수 있는 다른 메서드도 있어요. 유용한 것 몇 가지를 볼게요.
my $f = "file";
say $f.IO.modified; # 마지막 파일(또는 디렉터리) 변경 시각 반환
say $f.IO.accessed; # 파일(또는 디렉터리)을 마지막으로 읽은 시각 반환
say $f.IO.s; # 파일(또는 디렉터리 inode) 크기를 바이트 단위로 반환
더 많은 메서드와 자세한 내용은 IO::Path 문서를 참고하세요.
디렉터리 목록 얻기 (Getting a directory listing)
현재 디렉터리의 내용을 나열하려면 dir 함수를 사용해요. 이 함수는 IO::Path 객체의 목록을 반환해요.
say dir; # OUTPUT: «"/path/to/testfile".IO "/path/to/lib".IO»
주어진 디렉터리의 파일과 디렉터리를 나열하려면 dir에 경로를 인자로 전달하기만 하면 돼요.
say dir "/etc/"; # OUTPUT: «"/etc/ld.so.conf".IO "/etc/shadow".IO ....»
디렉터리 만들기·제거하기 (Creating and removing directories)
새 디렉터리를 만들려면 mkdir 함수에 디렉터리 이름을 인자로 넘기면 돼요.
mkdir "newdir";
이 함수는 성공하면 만든 디렉터리 이름을, 실패하면 Nil을 반환해요. 그래서 Perl의 표준 관용구가 기대한 대로 동작해요.
mkdir "newdir" or die "$!";
빈 디렉터리를 제거할 때는 rmdir을 사용해요.
rmdir "newdir" or die "$!";