std::filesystem

Note

This topic is usually live hacked.

Overview

  • File system related stuff (obviously)

    • Path manipulation

    • Directory creation and iteration ⟶ even recursive!

    • Stats like size, access time

    • Permissions

    • Support routines like copy, remove, …

  • Since C++ 17

  • Continually improved since then

Fixture

(Not central to this topic, just for completeness)

This topic is mainly livehacked, along the test cases that follow. Each test case is executed withing a test fixture; that fixture works as follows:

  • Every test case has its own temporary directory, by a std::filesystem::path type variable, dirname

  • The test case run inside that directory; i.e. the process’s current working directory is set to dirname before the test runs, and reset to whatever it was before.

  • dirname is removed after each test run ⟶ fixture

#pragma once

#include <fixture-tmpdir-cwd.h>

struct filesystem_suite : public cd_to_tmpdir_fixture {};

Paths: Composition, Comparison

  • Paths are OS dependent

    • ⟶ directory separators, for example (/ on Unixen, \ on Doze)

  • std::filesystem::path does not access filesystem

  • Used as input to higher level routines

  • Main operators: /=, /

  • Lexical comparison

  • Largely compatible with std::string

#include "suite.h"

#include <gtest/gtest.h>
#include <filesystem>

TEST_F(filesystem_suite, path_compose_compare)
{
    std::filesystem::path fn = "/";
    fn /= "etc";
    fn /= "passwd";

    ASSERT_EQ(fn, "/etc/passwd");
}
#include "suite.h"

#include <gtest/gtest.h>
#include <filesystem>

TEST_F(filesystem_suite, path_compose_compare_2)
{
    std::filesystem::path fn = "/etc";
    std::filesystem::path passwd = fn / "passwd";

    ASSERT_EQ(passwd, "/etc/passwd");
}

Absolute and Relative Paths

Method

Description

path.is_absolute()

Is path a absolute path

path.is_relative()

Is path a relative path

#include "suite.h"

#include <gtest/gtest.h>
#include <filesystem>


TEST_F(filesystem_suite, path_abs_rel)
{
    std::filesystem::path abspath = "/etc/passwd";
    ASSERT_TRUE(abspath.is_absolute());
    ASSERT_FALSE(abspath.is_relative());

    std::filesystem::path relpath = "etc/passwd";
    ASSERT_FALSE(relpath.is_absolute());
    ASSERT_TRUE(relpath.is_relative());
}

Path Component Access

Method

Description

path.filename()

extract filename part

path.parent_path()

extract parent directory

path.remove_filename()

remove filename part, leaving parent directory path in place

path.replace_filename()

replace filename part

path.replace_extension()

replace extension

#include "suite.h"

#include <gtest/gtest.h>
#include <filesystem>
#include <string>

TEST_F(filesystem_suite, path_component_access)
{
    std::filesystem::path path = "/a/b/c/blah.txt";
    std::string filename_part = path.filename();

    ASSERT_EQ(filename_part, "blah.txt");

    std::string dir_part = path.parent_path();
    ASSERT_EQ(dir_part, "/a/b/c");

    path.replace_extension("TXT");
    ASSERT_EQ(path, "/a/b/c/blah.TXT");
}

Iterating Over Path Components

  • A path is basically a list of strings

  • Separated by OS specific directory separators

  • Absolute paths start with a separator

#include "suite.h"

#include <gtest/gtest.h>
#include <filesystem>
#include <string>

TEST_F(filesystem_suite, path_iteration)
{
    std::filesystem::path path = "/etc/passwd";

    std::string components[3];

    size_t i = 0;
    for (const auto& component: path)
        components[i++] = component;

    ASSERT_EQ(components[0], "/");
    ASSERT_EQ(components[1], "etc");
    ASSERT_EQ(components[2], "passwd");
}

Current Working Directory

  • Current working directory: process attribute

  • Process can change CWD

  • … and get it of course

#include "suite.h"

TEST_F(filesystem_suite, cwd_chdir)
{
    std::filesystem::current_path(dirname);

    std::filesystem::path cwd = std::filesystem::current_path();
    ASSERT_EQ(cwd, dirname);
}

Directory Creation: std::filesystem::create_directory()

  • Os wise, creating a directory fail if the parent directory does not exist (or has no permissions)

  • ⟶ E.g. mkdir -p allows you to create paths of multiple levels

#include "suite.h"

#include <gtest/gtest.h>
#include <filesystem>

TEST_F(filesystem_suite, create_directory_error)
{
    try {
        std::filesystem::create_directory(dirname / "parent/child");
        ASSERT_FALSE(true);
    }
    catch (const std::filesystem::filesystem_error&) {}

    // this would be more to the point ...

    //     ASSERT_THROW(std::filesystem::create_directory(dirname / "parent/child"),
    //                  std::filesystem::filesystem_error);

    // ... but (using -O3) ...

    // In file included from /usr/include/c++/12/ios:40,
    //                  from /usr/include/c++/12/ostream:38,
    //                  from /usr/include/c++/12/bits/unique_ptr.h:41,
    //                  from /usr/include/c++/12/memory:76,
    //                  from /home/jfasch/work/jfasch-home/googletest/googletest/include/gtest/gtest.h:55,
    //                  from /home/jfasch/work/jfasch-home/trainings/material/soup/cxx-code/fixtures/./fixture-tmpdir.h:3,
    //                  from /home/jfasch/work/jfasch-home/trainings/material/soup/cxx-code/fixtures/./fixture-tmpdir-cwd.h:3,
    //                  from /home/jfasch/work/jfasch-home/trainings/material/soup/cxx11/filesystem/code/suite.h:3,
    //                  from /home/jfasch/work/jfasch-home/trainings/material/soup/cxx11/filesystem/code/create_directory_error.cpp:1:
    // In static member function ‘static constexpr std::char_traits<char>::char_type* std::char_traits<char>::copy(char_type*, const char_type*, std::size_t)’,
    //     inlined from ‘static constexpr void std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::_S_copy(_CharT*, const _CharT*, size_type) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]’ at /usr/include/c++/12/bits/basic_string.h:423:21,
    //     inlined from ‘constexpr std::__cxx11::basic_string<_CharT, _Traits, _Allocator>& std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::_M_replace(size_type, size_type, const _CharT*, size_type) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]’ at /usr/include/c++/12/bits/basic_string.tcc:532:22,
    //     inlined from ‘constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>& std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::replace(size_type, size_type, const _CharT*, size_type) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]’ at /usr/include/c++/12/bits/basic_string.h:2171:19,
    //     inlined from ‘constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>& std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::replace(size_type, size_type, const _CharT*) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]’ at /usr/include/c++/12/bits/basic_string.h:2196:22,
    //     inlined from ‘std::string testing::internal::CanonicalizeForStdLibVersioning(std::string)’ at /home/jfasch/work/jfasch-home/googletest/googletest/include/gtest/internal/gtest-type-util.h:83:14:
    // /usr/include/c++/12/bits/char_traits.h:431:56: error: ‘void* __builtin_memcpy(void*, const void*, long unsigned int)’ accessing 9223372036854775810 or more bytes at offsets [2, 9223372036854775807] and 1 may overlap up to 9223372036854775813 bytes at offset -3 [-Werror=restrict]
    //   431 |         return static_cast<char_type*>(__builtin_memcpy(__s1, __s2, __n));
    //       |                                        ~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~
}
  • Create directories one at a time

#include "suite.h"

#include <gtest/gtest.h>

#include <filesystem>
#include <stdexcept>
#include <fstream>

TEST_F(filesystem_suite, create_directory_single)
{
    std::filesystem::create_directory(dirname / "parent");
    std::filesystem::create_directory(dirname / "parent/child");

    std::ofstream(dirname / "parent/child/file.txt");

    ASSERT_TRUE(std::filesystem::is_regular_file(dirname / "parent/child/file.txt"));
}

Directory Creation: std::filesystem::create_directories()

  • Creating multiple level of directories in one shot

  • Like mkdir -p ...

#include "suite.h"

#include <gtest/gtest.h>
#include <filesystem>
#include <fstream>

TEST_F(filesystem_suite, create_directory_multiple)
{
    std::filesystem::create_directories(dirname / "parent/child");
    std::ofstream(dirname / "parent/child/file.txt");

    ASSERT_TRUE(std::filesystem::is_regular_file(dirname / "parent/child/file.txt"));
}

Directory Entry Classification: std::filesystem::is_xxx()

  • Directories may contain files and directories

  • Symbolic links, named pipes, character devices, block devices, …

  • ⟶ classification routines

#include "suite.h"

#include <gtest/gtest.h>
#include <filesystem>
#include <fstream>

TEST_F(filesystem_suite, is_xxx)
{
    std::filesystem::create_directory(dirname / "subdir");
    ASSERT_TRUE(std::filesystem::is_directory(dirname / "subdir"));

    std::ofstream(dirname / "file");
    ASSERT_TRUE(std::filesystem::is_regular_file(dirname / "file"));
}

Function

Description

is_block_file

checks whether the given path refers to block device

is_character_file

checks whether the given path refers to a character device

is_directory

checks whether the given path refers to a directory

is_empty

checks whether the given path refers to an empty file or directory

is_fifo

checks whether the given path refers to a named pipe

is_other

checks whether the argument refers to an other file

is_regular_file

checks whether the argument refers to a regular file

is_socket

checks whether the argument refers to a named IPC socket

is_symlink

checks whether the argument refers to a symbolic link

Directory Entries: Basics

  • Represents a thing that can be contained in a directory

  • OS dependent (as everything in std::filesystem)

  • Contains

    • File type (regular file, directory, symlink, character/block device, pipe, …)

    • File status (permissions, etc.)

  • Tries to implement the greatest common divisor across different OSen

    • … and fails (in my opinion)

  • Unix ls -l

$ ls -l /etc/passwd
-rw-r--r--. 1 root root 2691 Nov 15 15:45 /etc/passwd
  • More information: Unix stat

$ stat /etc/passwd
  File: /etc/passwd
  Size: 2691         Blocks: 8          IO Block: 4096   regular file
Device: 0,36 Inode: 919153      Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Context: system_u:object_r:passwd_file_t:s0
Access: 2022-12-06 15:00:15.111282886 +0100
Modify: 2022-11-15 15:45:06.737858445 +0100
Change: 2022-11-15 15:45:06.743858359 +0100
 Birth: 2022-11-15 15:45:06.737858445 +0100

Directory Entries: std::filesystem::directory_entry

  • Commonly used when iterating over directories (see below)

  • Carries information about what’s being iterated over

Method

Description

entry.path()

returns the path the entry refers to

entry.exists()

checks whether directory entry refers to existing file system object

entry.is_block_file()

checks whether the directory entry refers to block device

entry.is_character_file()

checks whether the directory entry refers to a character device

entry.is_directory()

checks whether the directory entry refers to a directory

entry.is_fifo()

checks whether the directory entry refers to a named pipe

entry.is_other()

checks whether the directory entry refers to an other file

entry.is_regular_file()

checks whether the directory entry refers to a regular file

entry.is_socket()

checks whether the directory entry refers to a named IPC socket

entry.is_symlink()

checks whether the directory entry refers to a symbolic link

entry.file_size()

returns the size of the file to which the directory entry refers

entry.hard_link_count()

returns the number of hard links referring to the file to which the directory entry refers

entry.last_write_time()

gets or sets the time of the last data modification of the file to which the directory entry refers

entry.status()

status of the file designated by this directory entry

#include "suite.h"

#include <gtest/gtest.h>
#include <filesystem>
#include <fstream>

TEST_F(filesystem_suite, directory_entry)
{
    std::filesystem::create_directory(dirname / "subdir");
    std::ofstream(dirname / "file").write("Hallo", 5);

    std::filesystem::directory_entry subdir(dirname / "subdir");
    ASSERT_TRUE(subdir.is_directory());
    ASSERT_EQ(subdir.path(), dirname / "subdir");

    std::filesystem::directory_entry file(dirname / "file");
    ASSERT_TRUE(file.is_regular_file());
    ASSERT_EQ(file.path(), dirname / "file");
    ASSERT_EQ(file.file_size(), 5);
}

Iterating Over Directory Entries

  • Directories contain entries

  • OS wise, directories are not read in any particular order

  • File system specific (e.h. ext4 might be different from btrfs from ntfs)

#include "suite.h"

#include <gtest/gtest.h>
#include <filesystem>
#include <fstream>
#include <iostream>

TEST_F(filesystem_suite, directory_iterator)
{
    std::filesystem::create_directory(dirname / "subdir1");
    std::filesystem::create_directory(dirname / "subdir2");
    std::ofstream(dirname / "subdir1/file");
    std::ofstream(dirname / "subdir2/file");
    std::ofstream(dirname / "file1");
    std::ofstream(dirname / "file2");

    std::set<std::filesystem::path> entries;         // <--- iteration order is not (necessarily) creation order
    auto diriter = std::filesystem::directory_iterator(dirname);
    for (const std::filesystem::directory_entry& entry: diriter)
        entries.insert(entry);

    ASSERT_EQ(entries.size(), 4);                    // <--- not 6! (no recursion)

    ASSERT_TRUE(entries.contains(dirname / "subdir1"));
    ASSERT_TRUE(entries.contains(dirname / "subdir2"));
    ASSERT_TRUE(entries.contains(dirname / "file1"));
    ASSERT_TRUE(entries.contains(dirname / "file2"));
}

Recursive Directory Iteration

#include "suite.h"

#include <gtest/gtest.h>
#include <filesystem>
#include <fstream>

TEST_F(filesystem_suite, recursive_directory_iterator)
{
    std::filesystem::create_directory(dirname / "subdir1");
    std::filesystem::create_directory(dirname / "subdir2");
    std::ofstream(dirname / "subdir1/file");
    std::ofstream(dirname / "subdir2/file");
    std::ofstream(dirname / "file1");
    std::ofstream(dirname / "file2");

    auto diriter = std::filesystem::recursive_directory_iterator(dirname);
    std::set<std::filesystem::path> entries;
    for (const auto& entry: diriter)
        entries.insert(entry);

    ASSERT_EQ(entries.size(), 6);                  // <--- *recursive*: hitting entries in subdirs too

    ASSERT_TRUE(entries.contains(dirname / "subdir1"));
    ASSERT_TRUE(entries.contains(dirname / "subdir2"));
    ASSERT_TRUE(entries.contains(dirname / "subdir1/file"));
    ASSERT_TRUE(entries.contains(dirname / "subdir2/file"));
    ASSERT_TRUE(entries.contains(dirname / "file1"));
    ASSERT_TRUE(entries.contains(dirname / "file2"));
}