файлу "src", она выведет на экран полный абсолютный путь к файлу.
$ ./normalizer src
/Users/tfc/src
6. Когда мы снова запускаем программу в домашнем каталоге, но даем ей запутанное относительное описание пути к файлу, в котором сначала прописывается вход в папку Desktop, потом прописывается выход из нее с помощью косвенного адреса .., затем входим в папку Documents и выходим из нее, чтобы в конечном итоге попасть в каталог src, программа отображает тот же путь файла, что и ранее!
$ ./normalizer Desktop/../Documents/../src
/Users/tfc/src
Как это работает
Этот начальный пример работы с std::filesystem мы сделали относительно коротким и прямолинейным. Мы инициализировали объект класса path на основе строки, которая содержит описание пути к файлу. Класс std::filesystem::path имеет самое первостепенное значение в тех ситуациях, когда мы используем библиотеку, связанную с файловой системой, поскольку с ним связано большинство функций и классов.
Функция filesystem::exists позволяет проверить, существует ли указанный путь файла на самом деле. До сего момента мы не можем быть в этом уверены, поскольку возможно создавать объекты класса path, которые не относятся к реальному объекту файловой системы. Функция exists всего лишь принимает экземпляр класса path и возвращает значение true в том случае, если он действительно существует. Эта функция способна сама определить, какой путь мы ей передали (абсолютный или относительный), что делает ее очень комфортной в применении.
Наконец, мы использовали функцию filesystem::canonical для каталога, чтобы вывести на экран его нормализованную форму:
path canonical(const path& p, const path& base = current_path());
Функция canonical принимает путь к файлу и в качестве необязательного второго аргумента еще один путь к файлу. Второй путь base добавляется к пути файла p в том случае, если p является относительным. После этого функция canonical пытается убрать все косвенные адреса . и .. .
Для вывода результата на экран мы использовали метод .c_str(), которому передали канонический путь к файлу. Мы сделали так потому, что перегруженный оператор << для выходных потоков берет пути к файлам в кавычки, а это не всегда желательно.
Дополнительная информация
Функция canonical генерирует исключение типа fileystem_error, если путь, который мы хотим привести к каноническому виду, не существует. Для предотвращения этого мы проверили наш путь к файлу с помощью функции exists. Но было ли достаточно данной проверки, чтобы необработанные исключения не генерировались? Нет.
Обе функции, как exists, так и canonical, способны генерировать исключения типа bad_alloc. Если бы эти исключения сгенерировались, кто-то мог бы утверждать, что программа все равно обречена. Более важная, а также гораздо более вероятная проблема возникает, когда где-то между проверкой существования файла и процессом приведения его к каноническому виду некто переименовывает или удаляет основной файл! В этом случае функция canonical сгенерирует сообщение об ошибке filesystem_error, хотя мы ранее уже убедились в том, что файл существует.
Большая часть функций файловой системы имеет еще одну перегруженную версию, которая принимает те же аргументы, а также ссылку на std::error_code:
path canonical(const path& p, const path& base = current_path());
path canonical(const path& p, error_code& ec);
path canonical(const std::filesystem::path& p,
const std::filesystem::path& base,
std::error_code& ec);
Таким образом, можно выбрать, окружать ли запросы к функциям файловой системы конструктами try-catch, или же проверять наличие ошибок вручную. Обратите внимание: это изменяет только поведение ошибок, связанных с файловой системой! Если в системе заканчивается память, то возможна генерация более сложных исключений, таких как bad_alloc, которые могут иметь или не иметь параметр ec.
Получаем канонические пути к файлам из относительных путей
В последнем примере мы уже приводили к каноническому виду/нормализовали пути файлов. Конечно же, класс filesystem::path способен не только хранить и проверять пути к файлам. Это помогает легко создавать пути к файлу из строк, а также снова разбивать их на составные части.
На данном этапе класс path позволяет абстрагироваться от деталей работы операционной системы, но в некоторых случаях все же о них следует помнить.
Мы увидим, как обращаться с путями и их композицией/декомпозицией, на примере работы с абсолютными и относительными путями.
Как это делается
В данном примере мы будем работать с абсолютными и относительными путями к файлам, чтобы увидеть сильные стороны класса path и связанных с ним вспомогательных функций.
1. Сначала включаем все необходимые заголовочные файлы и объявляем, что используем пространства имен std и filesystem:
#include <iostream>
#include <filesystem>
using namespace std;
using namespace filesystem;
2. Затем объявляем пример пути к файлу. На данный момент неважно, существует ли текстовый файл, на который он ссылается. Тем не менее есть функции, генерирующие исключения, если требуемого файла нет.
int main()
{
path p {"testdir/foobar.txt"};
3. Сейчас мы познакомимся с четырьмя разными функциями библиотеки для работы с файловой системой. Функция current_path возвращает путь, в котором в данный момент выполняется программа, — так называемый рабочий каталог. Функция absolute принимает относительный путь к файлу наподобие нашего пути p и возвращает абсолютный, однозначный путь во всей файловой системе.
Функция system_complete делает практически то же самое, что и функция absolute в Linux, MacOS или других UNIX-подобных операционных системах. В Windows мы получим абсолютный путь к файлу, только вначале будет добавлено буквенное обозначение тома диска (например, "C:"). Функция canonical опять же делает то же самое, что и функция absolute, но потом дополнительно убирает все косвенные адреса, такие как "." (сокращение для «текущий каталог») или ".." (сокращение для «один каталог вверх»). Мы рассмотрим работу с данными косвенными адресами в следующих шагах.
cout << "current_path : " << current_path()
<< "nabsolute_path : " << absolute(p)
<< "nsystem_complete : " << system_complete(p)
<< "ncanonical(p) : " << canonical(p)
<< 'n';
4. Еще одна приятная особенность класса path заключается в том, что он перегружает оператор /. Таким образом, можно сцеплять имена папок и имена файлов с помощью данного оператора и составлять из них пути файлов. Попробуем это и отобразим составной путь:
cout