모두의 코드
씹어먹는 C++ - <17 - 4. C++ 파일 시스템(<filesystem>) 라이브러리 소개>

작성일 : 2020-03-11 이 글은 2855 번 읽혔습니다.

이번 강좌에서는

  • <filesystem> 라이브러리를 활용한

    • 파일 존재 유무 확인

    • 디렉토리 안에 파일들 순회

    • 디렉토리 생성하기

    • 파일과 폴더 복사/삭제 하기

에 대해서 알아봅니다.

안녕하세요 여러분! 이번 강좌에서는 C++ 17 에 비로소 도입된 파일 시스템 라이브러리 (<filesystem>) 를 간단히 소개하는 시간을 가지도록 하겠습니다.

파일 시스템 라이브러리는 파일 데이터 의 입출력을 담당하는 파일 입출력 라이브러리 (<fstream>) 과는 다릅니다. <fstream> 의 경우, 파일 하나가 주어지면 해당 파일의 데이터를 읽어내는 역할을 합니다만 그 외에 파일에 관한 정보 (파일 이름, 위치, 등등) 에 관한 데이터를 수정할 수 는 없습니다. 반면에 파일 시스템 라이브러리의 경우, 파일에 관한 정보 (파일 메타데이타)에 대한 접근을 도와주는 역할을 수행하며, 파일 자체를 읽는 일은 수행하지 않습니다.

쉽게 말해서 하드 디스크 어딘가에 있는 a.txt 라는 파일을 찾고 싶다면 filesystem 라이브러리를 사용하게 되고, 해당 파일을 찾은 이후에 a.txt읽고 싶다면 fstream 라이브러리를 사용하면 되겠습니다.

뿐만 아니라 파일 시스템 라이브러리를 통해서 원하는 경로에 폴더를 추가한다던지, 파일을 삭제 한다던지, 아니면 파일의 정보 - 예를 들어서 파일의 생성 시간이러던지, 권한이라던지와 같은 것들을 보는 데에도 사용할 수 있습니다.

자 그렇다면 파일 시스템 라이브러리를 어떻게 사용하는지 차근 차근 살펴보도록 하겠습니다.

파일을 찾아보자

먼저 파일 시스템 라이브러리를 어떻게 사용하는지 간단한 예제로 살펴보도록 하겠습니다.

#include <filesystem>
#include <iostream>

int main() {
  std::filesystem::path p("./some_file");

  std::cout << "Does " << p << " exist? [" << std::boolalpha
            << std::filesystem::exists(p) << "]" << std::endl;
  std::cout << "Is " << p << " file? [" << std::filesystem::is_regular_file(p)
            << "]" << std::endl;
  std::cout << "Is " << p << " directory? [" << std::filesystem::is_directory(p)
            << "]" << std::endl;
}

성공적으로 컴파일 하였다면

실행 결과

Does "./some_file" exist? [true]
Is "./some_file" file? [true]
Is "./some_file" directory? [false]

와 같이 나옵니다. 참고로 g++ 로 컴파일 하시는 분들은 꼭 8 버전 이상의 컴파일러가 설치되어 있어야 <filesystem> 을 사용하실 수 있습니다. 그 이하 버전의 경우 <experimental/filesystem> 을 사용하셔야 합니다 (없을 수도 있음) 특히 컴파일 시에 반드시 아래와 같이 컴파일 옵션을 줘야 합니다.

g++-9 test.cc -o test --std=c++17

또한 필요에 따라서 -lstdc++fs 를 추가해야 할 수 도 있습니다.

파일 시스템 라이브러리의 경우 모든 클래스와 함수들이 std::filesystem 이름 공간 안에 정의되어 있습니다. 예를 들어서 파일 시스템의 path 클래스를 사용하기 위해서는 위와 같이

std::filesystem::path

와 같이 써야 합니다. 이는 기존의 chrono 라이브러리에서 std::chrono 안에 정의되어 있는 것과 일맥 상통합니다. std::filesystem 를 매번 일일히 쓰는 것이 번거롭기 때문에 편의상 그냥

namespace fs = std::filesystem;

와 같이 정의해놓고, fs::path 와 같이 간단하게 쓰는 것이 보통입니다.

경로 (path)

자 위 코드를 다시 살펴봅시다. 먼저 path 클래스의 객체를 선언하는 부분부터 봅시다.

std::filesystem::path p("./some_file");

컴퓨터 상의 모든 파일에는 해당 파일의 위치를 나타내는 고유의 주소가 있는데 이를 경로(path) 라고 합니다. 왜 이를 주소가 아니라 경로라고 부르냐면, 컴퓨터에서 해당 파일을 참조할 때 가장 맨 첫 번째 디렉토리

부터 순차적으로 찾아가기 때문입니다. 예를 들어서 /a/b/c 라는 경로를 따라가기 위해서는 맨 처음에 /a 디렉토리를 찾고, 그 디렉토리 안에 b 라는 디렉토리를 찾고 맨 마지막으로 b 안에 c 라는 파일을 찾기 때문이지요.

이 때 경로를 지정하는 방식에는 두 가지가 있는데, 바로 절대 경로 (absolute path) 와 상대 경로 (relative path) 가 있습니다.

  • 절대 경로는 가장 최상위 디렉토리 (이를 보통 root 디렉토리라고 합니다) 에서 내가 원하는 파일까지의 경로를 의미하는 말입니다. 윈도우의 경우 root 디렉토리는 C:\ 나 D:\ 와 같은 것들이 되겠고, 리눅스의 경우 간단히 / 가 될 것입니다. 즉, 경로의 맨 앞에 / 거나 C:\ 이면 절대 경로라 생각하시면 됩니다.

  • 상대 경로의 경우 반대로 현재 프로그램이 실행되고 있는 위치 에서 해당 파일을 찾아가는 경로 입니다. 예를 들어서 경로를 그냥 a/b 라고 했다면 이는 현재 프로그램의 실행 위치에서 a 라는 디렉토리를 찾고 그 안에 b 라는 파일을 찾는 식이지요.
    따라서 만약에 현재 프로그램의 실행 절대 경로가 /foo/bar 라면 b 의 절대 경로는 /foo/bar/a/b 가 될 것입니다.

그렇다면 우리가 전달한 ./some_file 의 경우는 어떨까요? 맨 앞이 / 가 아니므로 이는 상대 경로 입니다. 참고로 . 은 현재 디렉토리를 의미하는 문자 이므로 결과적으로 위 경로는 현재 프로그램이 실행되고 있는 위치에 존재하는 some_file 를 나타내겠지요.

filesystem 라이브러리에서 파일이나 디렉토르를 다루는 모든 함수들은 파일을 나타내기 위해서 path 객체를 인자로 받습니다. 따라서 보통

  1. 원하는 경로에 있는 파일/디렉토리의 path 를 정의

  2. 해당 path 로 파일/디렉토리 정보 수집

의 순서로 작업을 하게 됩니다. 한 가지 중요한 점은 path 객체 만으로는 실제 해당 경로에 파일이 존재하는지 아닌지 알 수 없습니다. path 클래스는 그냥 경로를 나타낼 뿐 실제 파일을 지칭하는 것은 아닙니다.

만약에 해당 경로에 파일이 실제로 존재하는지 아닌지 보려면 아래와 같이 exists 함수를 사용해야 합니다.

std::cout << "Does " << p << " exist? [" << std::boolalpha
          << std::filesystem::exists(p) << "]" << std::endl;

위 경우 p 에 파일이 존재한다면 true 라고 표시 됩니다.

std::cout << "Is " << p << " file? [" << std::filesystem::is_regular_file(p)
          << "]" << std::endl;
std::cout << "Is " << p << " directory? [" << std::filesystem::is_directory(p)
          << "]" << std::endl;

비슷하게 해당 위치에 있는 것이 파일인지 아니면 디렉토리인지 is_regular_fileis_directory 함수로 확인할 수 있습니다.

참고로 왜 그냥 is_file 이 아니라 굳이 regular 파일인지 궁금하실 수 있는데 이는 리눅스 상에서 주변 장치(device) 나 소켓(socket) 들도 다 파일로 취급하기 때문입니다. 추후에 시간이 되면 "Everything is a File" 이라는 글을 한 번 읽어보세요!

여러 경로 관련 함수들

파일시스템 라이브러리에서는 경로를 가지고 여러가지 작업을 할 수 있는 함수들을 지원합니다.

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

int main() {
  fs::path p("./some_file.txt");

  std::cout << "내 현재 경로 : " << fs::current_path() << std::endl;
  std::cout << "상대 경로 : " << p.relative_path() << std::endl;
  std::cout << "절대 경로 : " << fs::absolute(p) << std::endl;
  std::cout << "공식적인 절대 경로 : " << fs::canonical(p) << std::endl;
}

성공적으로 컴파일 하였다면

실행 결과

내 현재 경로 : "/Users/jblee/Test"
상대 경로 : "./some_file.txt"
절대 경로 : "/Users/jblee/Test/./some_file.txt"
공식적인 절대 경로 : "/Users/jblee/Test/some_file.txt"

와 같이 나옵니다.

먼저 current_path() 함수의 경우 프로그램이 실행되는 경로를 리턴하게 됩니다. 모든 상대 경로는 이 프로그램의 현재 실행 경로를 기반으로 해서 구해집니다. 예를 들어서 우리의 프로그램의 경로가 /Users/jblee/Test 였으므로 ./some_file.txt 의 절대 경로는 /Users/jblee/Test/some_file.txt 가 되겠죠.

물론 주어진 경로를 바로 절대 경로로 바꾸고 싶다면 absolute 함수를 사용하면 됩니다. 하지만 absolute 의 단점이라 하면 주어진 경로를 절대 경로로 바꿔주기는 하지만 . 이나 .. 와 같은 불필요한 요소들을 포함할 수 있습니다.

따라서 좀 더 깔끔하게 표현하고자 한다면 canonical 함수를 사용하면 됩니다. canonical 의 경우 해당 파일의 경로를 가장 짧게 나타낼 수 있는 공식적인 절대 경로를 제공합니다.

위 모든 함수들의 경우 입력 받는 경로에 파일이 존재하지 않는다면 모두 예외를 throw 합니다. 따라서 위 함수들을 호출하기 전에 반드시 exist 를 통해서 파일이 존재하는지 확인해야 합니다.

만약에 예외를 처리하고 싶지 않다면 마지막 인자로 발생한 오류를 받는 std::error_code 객체를 전달하면 됩니다. 이 경우 예외를 던질 상황이 생기면 예외를 던지는 대신에 error_code 객체에 발생한 오류를 설정합니다. 참고로 filesystem 에서 예외를 던지는 함수들의 경우 이 처럼 마지막 인자로 error_code 를 받는 오버로딩이 제공됩니다.

디렉토리 관련 작업들

파일 시스템 라이브러리를 통해서 디렉토리에 여러가지 작업들을 할 수 있습니다. 예를 들어서

  • 해당 디렉토리 안에 있는 파일/폴더들 살펴보기

  • 해당 디렉토리 안에 폴더 생성하기 (파일 생성은 ofstream 으로 할 수 있죠!)

  • 해당 디렉토리 안에 파일/폴더 복사하기

  • 해당 디렉토리 안에 파일/폴더 삭제하기

등등을 말입니다.

디렉토리 안에 모든 파일들 순회하기

가장 먼저 디렉토리 안에 있는 파일들을 접근하는 방법을 살펴봅시다. 디렉토리는 그냥 쉽게 생각하면 책의 목차라고 보시면 됩니다. 즉, 디렉토리는 해당 디렉토리에 어떠한 파일들이 정의 되어 있는지 쭈르륵 써져 있는 파일이라 볼 수 있습니다. 이를 쉽게 접근하기 위해서 filesystem 라이브러리에서는 directory_iterator 라는 반복자를 제공합니다.

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

int main() {
  fs::directory_iterator itr(fs::current_path() / "a");
  while (itr != fs::end(itr)) {
    const fs::directory_entry& entry = *itr;
    std::cout << entry.path() << std::endl;
    itr++;
  }
}

성공적으로 컴파일 하였다면

실행 결과

"/Users/jblee/Test/a/3.txt"
"/Users/jblee/Test/a/2.txt"
"/Users/jblee/Test/a/1.txt"
"/Users/jblee/Test/a/b"

와 같이 나옵니다. 먼저 directory_iterator 를 정의하는 부분부터 살펴봅시다.

fs::directory_iterator itr(fs::current_path() / "a");

기존에 vector 와 같은 컨테이너를 생각했을 때 반복자를 정의하기 위해서는 v.begin() 처럼 컨테이너 자체의 함수를 통해서 정의하였지 직접 반복자를 정의하지 않았는데요, directory_iterator 는 특이하게 반복자 자체를 스스로 정의해야 하고, 반복자의 생성자에 우리가 탐색할 경로를 전달해줘야 합니다.

참고로 path 에는 operator/ 가 정의되어 있어서

fs::current_path() / "a"

위와 같이 현재 경로에 /a 를 편리하게 추가할 수 있습니다. (매우 직관적이죠!) 즉 우리 프로그램의 실행 경로가 /Users/jblee/Test 이므로 위 반복자는 /Users/jblee/Test/a 안에 있는 파일들을 탐색하게 됩니다.

자 그럼 이 반복자의 끝은 어떻게 나타낼까요?

while (itr != fs::end(itr)) {

바로 filesystem 에 정의되어 있는 end 함수에 현재 반복자를 전달하면 해당 반복자의 끝을 얻어낼 수 있습니다.

const fs::directory_entry& entry = *itr;

그리고 각각의 반복자들은 디렉토리에 정의되어 있는 개개의 파일을 나타내는 directory_entry 를 가리키고 있습니다. directory_entry 에는 여러가지 정보들이 저장되어 있는데 파일의 이름이나, 크기 등등을 알 수 있습니다.

std::cout << entry.path() << std::endl;

그리고 마지막으로 해당 파일의 경로를 위와 같이 출력하였습니다.

위 코드는 C++ 11 에 도입된 range for 문을 사용하면 더욱 간단히 작성할 수 있습니다.

#include <iostream>

namespace fs = std::filesystem;

int main() {
  for (const fs::directory_entry& entry :
       fs::directory_iterator(fs::current_path() / "a")) {
    std::cout << entry.path() << std::endl;
  }
}

성공적으로 컴파일 하였다면

실행 결과

"/Users/jblee/Test/a/3.txt"
"/Users/jblee/Test/a/2.txt"
"/Users/jblee/Test/a/1.txt"
"/Users/jblee/Test/a/b"

와 같이 동일하게 나옵니다. 매우 깔끔하죠?

directory_iterator 의 한 가지 단점은 해당 디렉토리 안에 다른 디렉토리가 있을 경우 그 안까지는 살펴보지 않는다는 점입니다. 예를 들어서 위 경우 b 안에 여러 파일들이 있는데 이들은 순회 대상에서 제외되었습니다.

만약에 여러분이 디렉토리 안에 서브 디렉토리까지 모두 순회할 수 있는 반복자를 사용하고 싶다면 recursive_directory_iterator 를 사용하면 됩니다.

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

int main() {
  for (const fs::directory_entry& entry :
       fs::recursive_directory_iterator(fs::current_path() / "a")) {
    std::cout << entry.path() << std::endl;
  }
}

성공적으로 컴파일 하였다면

실행 결과

"/Users/jblee/Test/a/3.txt"
"/Users/jblee/Test/a/2.txt"
"/Users/jblee/Test/a/1.txt"
"/Users/jblee/Test/a/b"
"/Users/jblee/Test/a/b/5.txt"
"/Users/jblee/Test/a/b/4.txt"
"/Users/jblee/Test/a/b/6.txt"

와 같이 잘 나옵니다.

디렉토리 생성하기

이전에 ofstream 라이브러리를 사용했으면 파일을 간단하게 생성할 수 있었습니다. 간단히 아래 처럼

std::ofstream out("a.txt");
out << "hi";

를 하면 만약에 a.txt 라는 파일이 존재하지 않을 시에 파일이 생성 되며 내용이 작성됩니다.

하지만 ofstream 라이브러리를 통해서는 디렉토리를 생성할 수 없습니다. 예를 들어서

std::ofstream out("./b/a.txt");
out << "hi";

만약에 b 라는 폴더가 없다면 a.txt 도 생성되지 않고 위 out << 은 실패하게 됩니다.

따라서 디렉토리를 생성하고 싶다면 filesystem 에서 제공하는 create_directory 함수를 사용해야 합니다. 아래 예제를 보시죠.

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

int main() {
  fs::path p("./a/c");
  std::cout << "Does " << p << " exist? [" << std::boolalpha << fs::exists(p)
            << "]" << std::endl;

  fs::create_directory(p);

  std::cout << "Does " << p << " exist? [" << fs::exists(p) << "]" << std::endl;
  std::cout << "Is " << p << " directory? [" << fs::is_directory(p) << "]"
            << std::endl;
}

성공적으로 컴파일 하였다면

실행 결과

Does "./a/c" exist? [false]
Does "./a/c" exist? [true]
Is "./a/c" directory? [true]

와 같이 잘 나옵니다.

fs::create_directory("./a/c");

create_directory 함수는 주어진 경로를 인자로 받아서 디렉토리를 생성합니다. 다만 한 가지 주의할 점은 생성하는 디렉토리의 부모 디렉토리는 반드시 존재 하고 있어야 합니다. 위 경우 ./a 라는 디렉토리가 이미 존재하고 있기 때문에 create_directory 가 성공적으로 수행되었습니다.

예를 들어서

fs::path p("./c/d/e/f");  // ./c 는 존재하고 있지 않는 디렉토리
fs::create_directory(p);

위를 실행한다면

실행 결과

terminate called after throwing an instance of 'std::filesystem::__cxx11::filesystem_error'
  what():  filesystem error: cannot create directory: No such file or directory [./c/d/e/f]
[1]    15954 abort (core dumped)  ./test

와 같이 예외를 던지게 됩니다.

그렇다면 만약에 부모 디렉토리들까지 한꺼번에 만들고 싶다면 어떨까요? 이를 위해선 create_directories 함수를 사용하면 됩니다.

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

int main() {
  fs::path p("./c/d/e/f");
  std::cout << "Does " << p << " exist? [" << std::boolalpha << fs::exists(p)
            << "]" << std::endl;

  fs::create_directories(p);

  std::cout << "Does " << p << " exist? [" << fs::exists(p) << "]" << std::endl;
  std::cout << "Is " << p << " directory? [" << fs::is_directory(p) << "]"
            << std::endl;
}

성공적으로 컴파일 하였다면

실행 결과

Does "./c/d/e/f" exist? [false]
Does "./c/d/e/f" exist? [true]
Is "./c/d/e/f" directory? [true]

위와 같이 c, d, e, f 디렉토리 전부가 잘 생성된 것을 볼 수 있습니다. 실제로 tree 명령어를 통해 디렉토리를 살펴보면

실행 결과

$ tree
├── c
│   └── d
│       └── e
│           └── f

이쁘게 잘 나옵니다!

파일과 폴더 복사/삭제하기

마지막으로 파일 시스템 라이브러리에서 살펴볼 기능으로 복사와 삭제 기능 입니다. 물론 기존의 ofstream 을 통해서 파일 간의 복사는 가능했습니다. (파일 내용 전체를 읽어들인 뒤에 다른 파일로 출력하는 방식으로). 하지만 디렉토리를 복사하거나, 디렉토리 안의 모든 파일들을 복사하는 등의 작업들은 불가능하였는데, filesystemcopy 를 사용하면 간단히 수행할 수 있습니다.

실행 결과

$ tree
├── a
│   ├── a.txt
│   └── b
│       └── c.txt
├── c

예를 들어서 현재 파일들 상황이 위와 같다고 해봅시다. a 디렉토리 안에 여러 파일과 폴더들이 있고 c 는 비어있는 디렉토리 입니다. 만약에 a 안의 모든 파일들을 c 에 복사하고 싶다면 어떨까요? 아주 간단합니다.

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

int main() {
  fs::path from("./a");
  fs::path to("./c");

  fs::copy(from, to, fs::copy_options::recursive);
}

성공적으로 컴파일 하였다면 아래와 같이 파일과 폴더들이 잘 복사된 것을 볼 수 있습니다.

실행 결과

├── a
│   ├── a.txt
│   └── b
│       └── c.txt
├── c
│   ├── a.txt
│   └── b
│       └── c.txt

copy 함수에 복사할 대상과, 복사할 위치를 차례대로 인자로 전달하면 됩니다. 그리고 마지막 인자로 어떠한 방식으로 복사할 지 지정해줘야 합니다.

실행 결과

  fs::copy(from, to, fs::copy_options::recursive);

우리의 경우 recursive 옵션을 주었는데 이는 복사할 대상에 존재하는 모든 디렉토리와 파일들을 복사하게 됩니다. 만일 recursive 옵션을 주지 않는다면 a 에 존재하는 파일들, 즉 a.txt 딱 하나만 복사됩니다.

만약에 복사할 대상이 이미 존재하고 있다면 예외를 던지게 됩니다. 예를 들어서 위 상태에서 a/a.txtc 에 복사한다고 해봅시다.

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

int main() {
  fs::path from("./a/a.txt");
  fs::path to("./c");

  fs::copy(from, to);
}

컴파일 하였다면

실행 결과

terminate called after throwing an instance of 'std::filesystem::__cxx11::filesystem_error'
  what():  filesystem error: cannot copy: File exists [./a/a.txt] [./c]
[1]    21859 abort (core dumped)  ./test

위와 같이 c 에 이미 a.txt 가 존재한다고 예외가 던져집니다.

다행이도 filesystem 라이브러리에는 이 경우 여러분이 택할 수 있는 선택지를 3 개나 제공하고 있습니다.

  • skip_existing : 이미 존재하는 파일은 무시 (예외 안던지고)

  • overwrite_existing : 이미 존재하는 파일은 덮어 씌운다.

  • update_existing : 이미 존재하는 파일이 더 오래되었을 경우 덮어 씌운다.

예를 들어서

fs::copy(from, to, fs::copy_options::overwrite_existing);

와 같이 한다면 a.txt 를 잘 덮어씌우겠지요.

파일 / 디렉토리 삭제하기

자 그러면 마지막으로 살펴볼 기능은 파일이나 폴더를 삭제하는 작업입니다. 파일 삭제는 remove 함수를 통해서 간단히 경로만 전달하면 수행할 수 있습니다.

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

int main() {
  fs::path p("./a/b.txt");
  std::cout << "Does " << p << " exist? [" << std::boolalpha
            << std::filesystem::exists(p) << "]" << std::endl;
  fs::remove(p);
  std::cout << "Does " << p << " exist? [" << std::boolalpha
            << std::filesystem::exists(p) << "]" << std::endl;
}

성공적으로 컴파일 하였다면

실행 결과

Does "./a/b.txt" exist? [true]
Does "./a/b.txt" exist? [false]

와 같이 잘 나옵니다.

remove 함수를 통해서 디렉토리 역시 지울 수 있습니다. 단, 해당 디렉토리는 반드시 빈 디렉토리여야 합니다. 만일 비어있지 않은 디렉토르를 삭제하고 싶다면 remove_all 함수를 사용하면 됩니다.

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

int main() {
  fs::path p("./c/b");

  std::error_code err;
  fs::remove(p, err);  // 실패
  std::cout << err.message() << std::endl;

  fs::remove_all(p);  // 성공!
}

성공적으로 컴파일 하였다면

실행 결과

Directory not empty

와 같이 나옵니다. 이 메세지는 fs::remove 에서 발생한 메세지 이죠.

하지만 remove_all 을 통해서 제대로 c/b 가 지워진 것을 확인할 수 있습니다.

실행 결과

$ tree
├── c
│   ├── a.txt
│   └── b
│       └── c.txt

위 상태에서

실행 결과

$ tree
├── c
│   └── a.txt

로 제대로 b 가 지워졌습니다.

directory_iterator 사용시 주의할 점

예를 들어서 해당 디렉토리 안에 확장자가 txt 인 파일을 모두 삭제하는 프로그램을 만들고 싶다고 해봅시다. 이 경우 아마 아래와 같이 코드를 작성할 것입니다.

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

int main() {
  fs::path p("./a");
  for (const auto& entry : fs::directory_iterator("./a")) {
    const std::string ext = entry.path().extension();
    if (ext == ".txt") {
      fs::remove(entry.path());
    }
  }
}

하지만 사실 이 코드는 한 가지 문제점이 있습니다.

fs::remove(entry.path());

./a 디렉토리에서 파일을 하나 삭제할 때 마다 해당 디렉토리의 구조가 바뀌게 됩니다. 그런데 directory_iterator디렉토리의 구조가 바뀔 때 마다 무효화 됩니다! 따라서 fs::remove 후에 entry 는 사용할 수 없는 반복자가 됩니다. 따라서 ++entry 가 다음 디렉토리를 가리키는 것을 보장할 수 없게 되죠. 따라서 이 경우 어쩔 수 없이;

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

int main() {
  fs::path p("./a");
  while (true) {
    bool is_modified = false;
    for (const auto& entry : fs::directory_iterator("./a")) {
      const std::string ext = entry.path().extension();
      if (ext == ".txt") {
        fs::remove(entry.path());
        is_modified = true;
        break;
      }
    }

    if (!is_modified) {
      break;
    }
  }
}

위와 같이 파일을 삭제할 때 마다 반복자를 초기화 해줘야만 합니다.

자 그럼 이것으로 filesystem 라이브러리를 살펴보았습니다. 파일 시스템 라이브러리를 통해서 기존에 파일 입출력 라이브러리로는 할 수 없었던 디렉토리들을 다루는 것과 파일 삭제, 복사 등등을 간단히 수행할 수 있게 되었습니다.

이제 정말 C++ 강좌도 끝을 향해서 달려갑니다. (마지막이 될 수 도 있는) 다음 강좌에서는 C++ 의 utility 라이브러리에 정의된 여러가지 도구들을 살펴볼 것입니다.

뭘 배웠지?

filesystem 라이브러리를 통해서 파일과 디렉토리를 다룰 수 있다.

path 객체를 통해 파일과 폴더의 경로에 대한 작업을 손쉽게 할 수 있다.

directory_iterator 을 통해 디렉토리 안에 존재하는 파일들을 살펴볼 수 있고, recursive_directory_iterator 을 사용해서 디렉토리 안에 있는 모든 파일/폴더들을 탐색할 수 있다.

create_directory 를 통해 디렉토리를 생성하고 copy, remove 함수를 통해 파일/폴더를 복사/삭제 할 수 있다.

강좌를 보다가 조금이라도 궁금한 것이나 이상한 점이 있다면 꼭 댓글을 남겨주시기 바랍니다. 그 외에도 강좌에 관련된 것이라면 어떠한 것도 질문해 주셔도 상관 없습니다. 생각해 볼 문제도 정 모르겠다면 댓글을 달아주세요.

현재 여러분이 보신 강좌는 <17 - 4. C++ 파일 시스템(<filesystem>) 라이브러리 소개> 입니다. 이번 강좌의 모든 예제들의 코드를 보지 않고 짤 수준까지 강좌를 읽어 보시기 전까지 다음 강좌로 넘어가지 말아주세요
댓글이 3 개 있습니다!
프로필 사진 없음
강좌에 관련 없이 궁금한 내용은 여기를 사용해주세요