The purpose of the STL filesystem library is to normalize file system operations across platforms. The filesystem library bridges irregularities and normalizes operations between POSIX/Unix, Windows, and other file systems.
The history of file systems is filled with disparity and inconsistency. Differences in file naming conventions, directory separators, directory structures, permissions conventions, and more, have made cross-platform file system usage difficult in the extreme. Even today, the three major operating systems have significant incompatibilities: Linux uses POSIX conventions including the forward slash ( / ) to separate directory names; Windows uses a backward slash ( \ ) as a directory separator, limits filename extensions to three characters, and has its own unique permissions system; and while earlier versions of Apple's OS used a colon ( : ) as a directory separator, the current macOS uses POSIX naming conventions but adds extensions to the permissions system.
The filesystem library was adapted from the corresponding Boost library and incorporated into the STL with C++17. While there may still be some gaps in the various implementations, the recipes in this chapter have been tested on Linux, Windows, and macOS file systems, and compiled with the latest available versions of the GCC, MSVC, and Clang compilers, respectively.
C++23 introduced several refinements to the filesystem library including improved noexcept specifications, clarified error-handling semantics, and some defect-report corrections for improved implementation consistency. Problems resolved in the C++23 updates addressed portability and encoding-related issues, including improvements to wide-character and UTF-8 path handling across different platforms.
The library uses the <filesystem> header, and the std::filesystem namespace is commonly aliased as fs:
namespace fs = std::filesystem; The fs::path class is at the core of the filesystem library. It provides normalized filename and directory path representation across disparate environments. A path object may represent a file, a directory, or any object in a file system, even a non-existent or impossible object.
In the following recipes, we cover tools for working with files and directories using the filesystem library:
std::formatter for the path class regex and directory_iterator The path class is used throughout the filesystem library to represent a file or directory path. On POSIX-conformant systems, like macOS and Linux, the path object uses a value_type of char to represent filenames. On Windows, the value_type is wchar_t . On Windows, format() and print() will not display primitive strings of wchar_t characters. This means there is no simple out-of-the-box way to write code that uses the filesystem library and is portable across POSIX and Windows.
We could use preprocessor directives to write specific versions of code for Windows. That may be a reasonable solution for some code bases, but for this book it's messy and does not serve the purpose of simple, portable, reusable recipes.
The elegant solution is to write a formatter specialization for the path class. This allows us to display path objects simply and portably across systems.
In this recipe we'll write a formatter specialization for use with the fs::path class:
filesystem names are in the std::filesystem namespace: namespace fs = std::filesystem; Our formatter specialization for the path class is simple and succinct:
template<> struct std::formatter<fs::path>: std::formatter<std::string> { template<typename FormatContext> auto format(const fs::path& p, FormatContext& ctx) const { return std::formatter<std::string, char>::format(p.string(), ctx); } }; There's a more complete explanation of formatter specialization in Chapter 7 of this book. Here we're specializing the formatter for the fs::path type, using its string() method to get a printable representation.
main() function, we use the command line to pass a filename or path: int main(const int argc, const char** argv) { if (argc != 2) { fs::path fn {argv[0]}; println("usage: {} <path>", fn.filename()); return 0; } fs::path dir {argv[1]}; if (!fs::exists(dir)) { println("path: {} does not exist", dir); return 1; } println("path: {}", dir); println("filename: {}", dir.filename()); println("cannonical: {}", fs::canonical(dir)); } The argc and argv parameters expose the standard command line arguments.
argv[0] is always the full directory path and filename for the executable itself. If we don't have the correct number of arguments, we use a filesystem::path object to display the filename part from argv[0] as part of our usage message.
The fs::exists() function checks if a directory or file exists.
dir is a path object. Thanks to our formatter specialization, we can now pass it directly to println() . This will display the string representation of the path.
The filename() method returns a new path object, which we can pass directly to println() because of our specialization.
The fs::cannonical() function takes a path object and returns a new path object with the canonical absolute directory path. We pass this path object directly to println() and it displays the directory path returned from cannonical() .
Output:
$ ./formatter ./formatter.cpp path: ./formatter.cpp filename: formatter.cpp cannonical: /home/billw/working/chap10/formatter.cpp The fs::path class is used throughout the filesystem library to represent directory paths and filenames. By providing a formatter specialization, we can easily display path objects consistently across platforms.
The path class provides some useful methods. We can iterate through a path to see its component parts:
fs::path p {"~/include/foo.h"}; println("{}", p); for (const auto& x : p) print("[{}] ", x); print("\n"); Output:
~/include/foo.h [~] [include] [foo.h] The iterator returns a path object for each element of the path .
Notice that the path object does not care if the filename exists in the file system. In this context, it's just parsing the provided string.
We can also get different parts of the path:
fs::path p {"~/include/foo.h"}; println("{}", p); println("{}", p.stem()); println("{}", p.extension()); println("{}", p.filename()); println("{}", p.parent_path()); Output:
~/include/foo.h foo .h foo.h ~/include Each line of the output represents a distinct component of the filename.
p gives us the full path p.stem() gives us just the filename without the extension p.extension() gives us just the extension p.filename() gives us the full filename without the directory path p.parent_path() gives us the directory path part without the filename We will continue to use this formatter specialization for the path class throughout the rest of this chapter.
The filesystem library includes functions for manipulating the contents of path objects. In this recipe, we will consider a few of these tools, including:
absolute() function which returns an absolute path path object canonical() function which returns a full canonical directory equivalent() function which tests if two relative paths resolve to the same file system object In this recipe, we will examine some functions that manipulate the contents of path objects.
namespace directive and our formatter specialization. We do this in every recipe in this chapter: template<> struct std::formatter<fs::path>: std::formatter<std::string> { template<typename FormatContext> auto format(const fs::path& p, FormatContext& ctx) const { return std::formatter<std::string, char>::format(p.string(), ctx); } }; current_path() function, which returns a path object: println("current_path: {}", fs::current_path()); Output:
current_path: /home/billw/chap10 absolute() function returns an absolute path from a relative path : println("absolute(p): {}", fs::absolute(p)); Output:
absolute(p): /home/billw/chap10/testdir/foo.txt absolute() will also dereference symbolic links.
+= operator concatenates a string to the end of a path : println("concatenate: {}", fs::path {"testdir"} += "foo.txt"); Output:
concatenate: testdirfoo.txt path object: println("concatenate: {}", fs::path {"testdir"} /= "foo.txt"); Output:
append: testdir/foo.txt The canonical() function returns the full canonical directory path :
println("canonical: {}", fs::canonical(fs::path{ "." } /= "testdir")); Output:
canonical: /home/billw/chap10/testdir equivalent() function tests whether two relative paths resolve to the same file system entity: println("equivalent: {}", fs::equivalent("testdir/foo.txt", "testdir/../testdir/foo.txt")); Output:
equivalent: true filesystem library includes the filesystem_error class for exception handling: try { fs::path p{ fp }; println("p: {}", p); ... println("equivalent: {}", fs::equivalent("testdir/foo.txt", "testdir/../testdir/foo.txt")); } catch (const fs::filesystem_error& e) { println("{}", e.what()); println("path1: {}", e.path1()); println("path2: {}", e.path2()); } The filesystem_error class includes methods for displaying the error message, and for getting the path (s) involved in the error.
If we introduce an error into the equivalent() call, we can see the results of the fileystem_error class:
println("equivalent: {}", fs::equivalent("testdir/foo.txt/x", "testdir/../testdir/foo.txt/y")); Output:
filesystem error: cannot check file equivalence: No such file or directory [testdir/foo.txt/x] [testdir/../testdir/foo.txt/y] path1: testdir/foo.txt/x path2: testdir/../testdir/foo.txt/y This is the output on Debian with GCC.
The filesystem_error class provides additional detail through its path1() and path2() methods. These methods return path objects.
std::error_code with some of the filesystem functions: fs::path p {fp}; std::error_code e; println("canonical: {}", fs::canonical(p /= "foo", e)); println("error: {}", e.message()); Output:
canonical: error: Not a directory p: testdir/foo.txt current_path: C:\Users\billw\chap10 absolute(p): C:\Users\billw\chap10\testdir\foo.txt concatenate: testdirfoo.txt append: testdir\foo.txt canonical: C:\Users\billw\chap10\testdir equivalent: true Most of these functions take a path object, an optional std::error_code object, and return a path object:
path absolute(constpath&p); path absolute(constpath&p,std::error_code&ec); The equivalent() function takes two path objects and returns a bool :
bool equivalent( const path& p1, constpath&p2); bool equivalent( const path& p1, const path& p2, std::error_code&ec); The path class has operators for concatenate and append. Both operators are destructive. They modify the path on the left-hand side of the operator:
p1 += source; // concatenate p1 /= source; // append For the right-hand side, these operators take either a path object, a string , a string_view , a C-string, or a pair of iterators.
The concatenate operator adds the string from the right-hand side of the operator to the end of the p1path string.
The append operator adds a separator (e.g., / or \ ), followed by the string from the right-hand side of the operator to the end of the p1path string.
The filesystem library provides a directory_entry class with directory-related information about a given path . We can use this to create useful directory listings.
In this recipe we'll create a directory listing utility using the information in the directory_entry class:
namespace alias and formatter specialization for displaying path objects namespace fs = std::filesystem; template<> struct std::formatter<fs::path>: std::formatter<std::string> { template<typename FormatContext> auto format(const fs::path& p, FormatContext& ctx) const { return std::formatter<std::string, char>::format(p.string(), ctx); } }; directory_iterator class makes it easy to list a directory: int main() { constexpr const char* fn {"."}; const fs::path fp{fn}; for (const auto& de : fs::directory_iterator{fp}) { print("{} ", de.path().filename()); } print("\n"); } Output:
Makefile working working.cpp zedzedtop.txt alpha.txt ls : int main(const int argc, const char** argv) { fs::path fp {argc > 1 ? argv[1] : "."}; if (!fs::exists(fp)) { const auto cmdname {fs::path{argv[0]}.filename()}; println("{}: {} does not exist", cmdname, fp); return 1; } if (is_directory(fp)) { for(const auto& de : fs::directory_iterator{fp}) { print("{} ", de.path().filename()); } } else { print("{} ", fp.filename()); } print("\n"); } If there is a command line argument, we use it to create a path object. Otherwise, we use "." for the current directory.
We check if the path exists with if_exists() . If not, we print an error message and exit. The error message includes cmdname , from argv[0] .
Next, we check is_directory() . If we have a directory, we loop through a directory_iterator for each entry. directory_iterator iterates over directory_entry objects. de.path().filename() gets the path and filename from each directory_entry object.
Output:
$ ./working Makefile working working.cpp textfile.txt include alpha.txt zedzedtop.txt testdir $ ./working working.cpp working.cpp $ ./working foo.bar working: foo.bar does not exist directory_entry objects in a sortable container Let's create an alias for fs::directory_entry . We'll be using this a lot. This goes at the top of the file:
using de = fs::directory_entry; At the top of main() , we declare a vector of de objects:
vector<de> entries{}; Inside the is_directory() block, we load the vector , sort it, and then display it:
if (is_directory(fp)) { for(const auto& de : fs::directory_iterator{fp}) { entries.emplace_back(de); } std::sort(entries.begin(), entries.end()); for (const auto& e : entries) { print("{} ", e.path().filename()); } } else { ... Now our output is sorted:
Makefile alpha.txt include testdir textfile.txt working working.cpp zedzedtop.txt Notice that Makefile is sorted first, apparently out of order. This is because capital letters sort before lowercase in ASCII order.
string strlower(string s) { auto char_lower = [](char c) -> char { if (c >= 'A'&&c<= 'Z') return c + ('a' - 'A'); else return c; }; std::transform(s.begin(), s.end(), s.begin(), char_lower); return s; } directory_entry objects, using strlower() : bool dircmp_lc(const de& lhs, const de& rhs) { const auto lhstr {lhs.path().string()}; const auto rhstr {rhs.path().string()}; return strlower(lhstr) < strlower(rhstr); } dircmp_lc() in our sort: std::sort(entries.begin(), entries.end(), dircmp_lc); Our output is now sorted ignoring case:
alpha.txt include Makefile testdir textfile.txt working working.cpp zedzedtop.txts There's a lot more information available from the filesystem library. Let's create a print_dir() function to gather more information and format it for display in the style of Unix ls :
void print_dir(const de& dir) { using fs::perms; const auto fpath {dir.path()}; const auto fstat {dir.symlink_status()}; const auto fperm {fstat.permissions()}; const uintmax_t fsize {is_regular_file(fstat) ? file_size(fpath) : 0}; const auto fn {fpath.filename()}; string suffix{}; if (is_directory(fstat)) suffix = "/"; else if ((fperm & perms::owner_exec) != perms::none) { suffix = "*"; } println("{}{}", fn, suffix); } The print_dir() function takes a directory_entry argument. We then retrieve some useful objects from the directory_entry .
dir.path() returns a path object.
dir.symlink_status() returns a file_status object, without following symbolic links.
fstat.permissions() returns a perms object. fsize is the size of the file and fn is the filename string. We'll look more closely at each of these as we use them.
Unix ls uses trailing characters, after the filename , to indicate a directory or an executable. We can test the fstat object with is_directory() to see if the file is a directory and add a trailing / to the filename. Likewise, we can find out if a file is executable by testing the fperm object.
print_dir() from main() in the for loop after sort() : std::sort(entries.begin(), entries.end(), dircmp_lc); for (const auto& e : entries) { print_dir(e); } Our output now looks like this:
alpha.txt include* Makefile testdir/ textfile.txt working* working.cpp zedzedtop.txt Notice the include* entry. That's really a symbolic link, not an executable file. Let's notate that properly by following the link to get the target path:
string suffix{}; if (is_symlink(fstat)) { suffix = " -> "; suffix += fs::read_symlink(fpath).string(); } else if (is_directory(fstat)) suffix = "/"; else if((fperm&perms::owner_exec)!=perms::none)suffix="*"; The read_symlink() function returns a path object. We take the string() representation of the returned path object and add it to the suffix for this output:
alpha.txt include -> /home/billw/include Makefile testdir/ textfile.txt working* working.cpp zedzedtop.txt ls command also includes a string of characters to indicate a file's permission bits. It looks something like this: drwxr‑xr‑x . The first character indicates the type of the file, for example: d for directory, l for symbolic link, ‑ for regular file.
The type_char() function returns the appropriate character:
char type_char(const fs::file_status& st) { using fs::file_type; switch (st.type()) { case file_type::symlink: return 'l'; case file_type::directory: return 'd'; case file_type::character: return 'c'; case file_type::block: return 'b'; case file_type::fifo: return 'p'; case file_type::socket: return 's'; case file_type::regular: return '-'; case file_type::unknown: return 'o'; default: return '?'; } } filesystem::file_type is an enum class that provides constants for testing against the file_status value.
rwx . If a bit is not set, its character is replaced by a dash. There are three triplets for three sets of permissions: owner, user, and other, respectively. string rwx(const fs::perms& p) { using fs::perms; auto bit2char = [&p](perms bit, char c) { return(p&bit)==perms::none?'-':c; }; return { bit2char(perms::owner_read, 'r'), bit2char(perms::owner_write, 'w'), bit2char(perms::owner_exec, 'x'), bit2char(perms::group_read, 'r'), bit2char(perms::group_write, 'w'), bit2char(perms::group_exec, 'x'), bit2char(perms::others_read, 'r'), bit2char(perms::others_write, 'w'), bit2char(perms::others_exec, 'x') }; } std::string rwx(fs::perms p) { using fs::perms; auto bit2char = [](perms p, perms bit, char c) { return(p&bit)!=perms::none?c:'-'; }; return { bit2char(p, perms::owner_read, 'r'), bit2char(p, perms::owner_write, 'w'), bit2char(p, perms::owner_exec, 'x'), bit2char(p, perms::group_read, 'r'), bit2char(p, perms::group_write, 'w'), bit2char(p, perms::group_exec, 'x'), bit2char(p, perms::others_read, 'r'), bit2char(p, perms::others_write, 'w'), bit2char(p, perms::others_exec, 'x') }; } The perms object represents the POSIX permissions bitmap, but it's not necessarily implemented as bits. Each entry must be compared to the perms::none value. Our lambda function fulfills this requirement.
We add this definition to the top of our print_dir() function:
const auto permstr {type_char(fstat) + rwx(fperm)}; We update our format string:
println("{} {}{}", permstr, fn, suffix); And we get this output:
-rw-r--r-- alpha.txt lrwxrwxrwx include -> /home/billw/include -rw-r--r-- Makefile drwxr-xr-x testdir/ -rw-r--r-- textfile.txt -rwxr-xr-x working* -rw-r--r-- working.cpp -rw-r--r-- zedzedtop.txt fsize value is from the file_size() function which returns a std::uintmax_t type. This represents the maximum size natural integer on the target system. uintmax_t is not always the same as size_t and does not always convert easily. Notably, uintmax_t is 32 bits on Windows, where size_t is 64 bits. string size_string(const uintmax_t fsize) { constexpr const uintmax_t kilo {1024}; constexpr const uintmax_t mega {kilo * kilo}; constexpr const uintmax_t giga {mega * kilo}; string s; if (fsize >= giga ) return format("{}{}", (fsize + giga / 2) / giga, 'G'); else if (fsize >= mega) return format("{}{}", (fsize + mega / 2) / mega, 'M'); else if (fsize >= kilo) return format("{}{}", (fsize + kilo / 2) / kilo, 'K'); else return format("{}B", fsize); } I chose to use 1,024 as 1K in this function, as that appears to be the default on both Linux and BSD Unix. In production, this could be a command-line option.
We update our format string in print_dir() :
println("{} {:>6} {}{}", permstr, size_string(fsize), fn, suffix); Now, we get this output:
-rw-r--r-- 78B alpha.txt lrwxrwxrwx 0B include -> /home/billw/include -rw-r--r-- 334B Makefile drwxr-xr-x 0B testdir/ -rw-r--r-- 281B textfile.txt -rwxr-xr-x 146K working* -rw-r--r-- 5K working.cpp -rw-r--r-- 588B zedzedtop.txt This utility is designed for POSIX-compliant systems, such as Linux and macOS. It works on a Windows system but the Windows permissions system is significantly different from that of the POSIX system. On Windows, the permission bits will always appear fully set, e.g., ‑rwxrwxrwx .
The filesystem library carries a rich set of information through its directory_entry and related classes. The major classes we used in this recipe include:
path class represents a file system path, according to the rules of the target system. A path is constructed from a string or another path . It need not represent an existing file path, or even a possible file path. The path is parsed to component parts, including a root name, root directory, and an optional series of filenames and directory separators. directory_entry class carries a path object as a member, and may also store additional attributes including, hard link count, status, symbolic link, file size, and last write time. file_status class carries information about the type and permissions of a file. A perms object may be a member of file_status , representing the permissions structure of a file. perms object from a file_status . The status() function and the symlink_status() function both return a perms object. The difference is in how they handle a symbolic link. The status() function will follow a symbolic link and return the perms from the target file. symlink_status() will return the perms from the symbolic link itself. I also wanted to include the last-write time of each file in the directory listing.
The directory_entry class has a member function, last_write_time() , which returns a file_time_type object representing the time stamp of the last time the file was written.
It's recommended that user code should use std::chrono::clock_cast to convert time points between clocks. As of the previous edition of this book, none of the implementations available had a working std::chrono::clock_cast specialization for this purpose. As of late 2025, GCC and MSVC have working clock_cast specializations but Apple's clang does not.
Here's a solution that works by casting the time stamp with chrono::clock_cast :
std::string time_string(const fs::directory_entry& dir) { using std::chrono::clock_cast; using std::chrono::system_clock; auto tp = clock_cast<system_clock>(dir.last_write_time()); return std::format("{:%F %R}", tp); } Using this time_string() function, we can add to print_dir() :
const string timestr {time_string(dir)}; We can then change the format string:
print("{} {:>6} {} {}{}\n", permstr, sizestr, timestr, fn, suffix); And we get this output:
-rw-r--r-- 78B 2026-01-06 20:07 alpha.txt lrwxrwxrwx 0B 2024-02-10 23:44 include -> /home/billw/include -rw-r--r-- 334B 2025-12-19 20:46 Makefile drwxr-xr-x 0B 2026-01-06 19:36 testdir/ -rw-r--r-- 281B 2026-01-06 20:08 textfile.txt -rwxr-xr-x 188K 2026-01-06 20:27 working* -rw-r--r-- 5K 2026-01-06 20:26 working.cpp -rw-r--r-- 588B 2026-01-06 20:08 zedzedtop.txt This works on Debian with GCC-15 and Windows 11 with MSVC 14 or later. Do not expect it to work without modification on other systems.
To demonstrate traversing and searching directory structures, we create a simple utility that works like Unix grep . This utility uses recursive_directory_iterator to traverse nested directories and searches files for matches with a regular expression.
In this recipe we will write a simple grep utility that traverses directories to search files with a regular expression.
namespace fs = std::filesystem; using de = fs::directory_entry; using rdit = fs::recursive_directory_iterator; using match_v = vector<std::pair<size_t, std::string>>; match_v is a vector of regular expression match results.
formatter specialization for path objects: template<> struct std::formatter<std::filesystem::path>: std::formatter<std::string> { template<typename FormatContext> auto format(const std::filesystem::path& p, FormatContext& ctx) const { return std::formatter<std::string, char>::format(p.string(), ctx); } }; match_v matches(const fs::path& fpath, const regex& re) { match_v matches {}; std::ifstream instrm(fpath.string(), std::ios_base::in); string s {}; for (size_t lineno {1}; getline(instrm, s); ++lineno) { if (std::regex_search(s.begin(), s.end(), re)) { matches.emplace_back(lineno, move(s)); } } return matches; } In this function, we open the file with ifstream , read lines from the file with getline() , and match the regular expression with regex_search() . Results are collected in the vector and returned.
main() : int main() { constexpr const char * fn {"working.cpp"}; constexpr const char * pattern {"path"}; fs::path fpath{ fn }; regex re {pattern}; auto regmatches{ matches(fpath, re) }; for (const auto& [lineno, line] : regmatches) { cout << format("{}: {}\n", lineno, line); } println("found {} matches", regmatches.size()); } In this example, we use constants for the filename and the regular expression pattern. We create path and regex objects, call the matches() function, and print the results.
Our output has line numbers and strings for the matching lines:
25: struct std::formatter<fs::path>: std::formatter<std::string> { 27: auto format(const fs::path& p, FormatContext& ctx) { 32: match_v matches(const fs::path& fpath, const regex& re) { 34: std::ifstream instrm(fpath.string(), std::ios_base::in); 62: constexpr const char * pattern{ "path" }; 64: fs::path fpath{ fn }; 66: auto regmatches{ matches(fpath, re) }; regex pattern and filenames. It should be able to traverse directories or take a list of filenames (which may be the result of command-line wildcard expansion). This requires a bit of logic in the main() function. First, we need one more helper function:
size_t pmatches(const regex& re, const fs::path& epath, const fs::path& search_path) { fs::path target {epath}; auto regmatches {matches(epath, re)}; auto matchcount {regmatches.size()}; if (!matchcount) return 0; if (!(search_path == epath)) { target = epath.lexically_relative(search_path); } for (const auto& [lineno, line] : regmatches) { println("{} {}: {}", target, lineno, line); } return regmatches.size(); } This function calls our matches() function and prints the results. It takes a regex object and two path objects. epath is the result of a directory search, and search_path is the search directory itself. We'll set these in main() .
main() , we use the argc and argv command-line arguments and we declare a few variables: int main(const int argc, const char** argv) { const char * arg_pat {}; regex re {}; fs::path search_path {}; size_t matchcount {}; ... arg_pat is for the regular expression pattern from the command line; re is the regex object; search_path is the command line search path argument; and matchcount is for counting the matched lines.
main() , if we have no arguments, then we print a short usage string: if(argc < 2) { auto cmdname {fs::path(argv[0]).filename()}; println("usage: {} pattern [path/file]", cmdname); return 1; } argv[1] is always the invoking command from the command line. cmdname uses the filename() method to return a path with just the filename part of the invoking command path.
Next, we parse the regular expression. We use a try - catch block to capture any error from the regex parser:
arg_pat = argv[1]; try { re = regex(arg_pat, std::regex_constants::icase); } catch(const std::regex_error& e) { println("{}: {}\n", e.what(), arg_pat); return 1; } We use the icase flag to tell the regex parser to ignore case.
If argc == 2 , we have just one argument, which we treat as the regular expression pattern, and we use the current directory for the search path:
if(argc == 2) { search_path = "."; for (const auto& entry : rdit{ search_path }) { const auto epath {entry.path()}; matchcount += pmatches(re, epath, search_path); } } rdit is an alias for the recursive_directory_iterator class, which traverses the directory tree from the starting path, returning a directory_entry object for each file it encounters. We then use create a path object and call pmatches() to go through the file and print any regular expression matches.
main() , we know that argc is >=2 . Now, we handle cases where we have one or more file paths on the command line. int count{ argc - 2 }; while(count-- > 0) { fs::path p {argv[count + 2]}; if(!exists(p)) { println("not found: {}", p); continue; } if(is_directory(p)) { for (const auto& entry : rdit {p}) { const auto epath {entry.path()}; matchcount += pmatches(re, epath, p); } } else { matchcount += pmatches(re, p, p); } } The while loop handles one or more arguments past the search pattern on the command line. It checks each filename to ensure it exists. Then, if it's a directory, it uses the rdit alias for the recursive_directory_iterator class to traverse the directory and call pmatches() to print any pattern matches in the files.
If it's a single file, it calls pmatches() on that file.
grep clone with one argument as the search pattern: $ ./bwgrep using dir.cpp 12: using std::format; dir.cpp 13: using std::cout; dir.cpp 14: using std::string; ... formatter.cpp 10: using std::cout; formatter.cpp 11: using std::string; formatter.cpp 13: using namespace std::filesystem; found 33 matches We can run it with a second argument as a directory to search:
$ ./bwgrep using .. chap04/iterator-adapters.cpp 12: using std::format; chap04/iterator-adapters.cpp 13: using std::cout; chap04/iterator-adapters.cpp 14: using std::cin; ... chap01/hello-version.cpp 24: using std::print; chap01/chrono.cpp 8: using namespace std::chrono_literals; chap01/working.cpp 15: using std::cout; chap01/working.cpp 34: using std::vector; found 529 matches Notice that it traverses the directory tree to find files in sub-directories.
Or we can run it with a single file argument:
$ ./bwgrep using bwgrep.cpp bwgrep.cpp 13: using std::format; bwgrep.cpp 14: using std::cout; bwgrep.cpp 15: using std::string; ... bwgrep.cpp 22: using rdit = fs::recursive_directory_iterator; bwgrep.cpp 23: using match_v = vector<std::pair<size_t, std::string>>; found 9 matches While the main task of this utility is regular expression matching, we're concentrating on the technique of recursively processing directories of files.
The recursive_directory_iterator object is interchangeable with directory_iterator , except recursive_directory_iterator operates recursively over all the entries of each subdirectory.
For more about regular expressions, see the recipe: Parse strings with regular expressions in .
This is a useful utility that renames files using regular expressions. It uses directory_iterator to find the files in a directory and fs::rename() to rename them.
In this recipe we create a file rename utility that uses regular expressions:
namespace fs = std::filesystem; using dit = fs::directory_iterator; using pat_v = vector<std::pair<regex, string>>; The pat_v alias is a vector for use with our regular expressions.
formatter specialization for path objects: template<> struct std::formatter<std::filesystem::path>: std::formatter<std::string> { template<typename FormatContext> auto format(const std::filesystem::path& p, FormatContext& ctx) const { return std::formatter<std::string, char>::format(p.string(), ctx); } }; string replace_str(string s, const pat_v& replacements) { for (const auto& [pattern, repl] : replacements) { s = regex_replace(s, pattern, repl); } return s; } Notice that we loop through a vector of pattern/replacement pairs, applying the regular expressions successively. This allows us to stack our replacements.
main() , we first check the command-line arguments: int main(const int argc, const char** argv) { pat_v patterns {}; if (argc < 3 || argc % 2 != 1) { fs::path cmdname {fs::path{argv[0]}.filename()}; println("usage: {} [regex replacement] ...", cmdname); return 1; } ... The command line accepts one or more pairs of strings . Each pair of strings includes a regex (regular expression) followed by a replacement .
vector with regex and string objects: for (int i {1}; i < argc; i += 2) { patterns.emplace_back(argv[i], argv[i + 1]); } The pair constructor constructs the regex and string objects in place, from the C-strings passed on the command line. These are added to the vector with the emplace_back() method.
We search the current directory using a directory_iterator object:
for (const auto& entry : dit {fs::current_path()}) { fs::path fpath {entry.path()}; string rname{ replace_str(fpath.filename().string(), patterns) }; if (fpath.filename().string() != rname) { fs::path rpath{ fpath }; rpath.replace_filename(rname); if (exists(rpath)) { println("Error: cannot rename – destination file exists."); } else { fs::rename(fpath, rpath); println("{} -> {}", fpath.filename(), rpath.filename()); } } } In this for loop, we call replace_str() to get the replacement filename and then check that the new name is not a duplicate of a file in the directory. We use the replace_filename() method on a path object to create a path with the new filename and use fs::rename() to rename the file.
$ ls bwfoo.txt cpp-version.cpp jump.cpp numword-test.cpp numword.cpp working.cpp We can do something simple like change .cpp to .Cpp :
$ ../rerename .cpp .Cpp numword-test.cpp -> numword-test.Cpp numword.cpp -> numword.Cpp working.cpp -> working.Cpp cpp-version.cpp -> cpp-version.Cpp jump.cpp -> jump.Cpp Let's change them back again:
$ ../rerename .Cpp .cpp cpp-version.Cpp -> cpp-version.cpp numword.Cpp -> numword.cpp jump.Cpp -> jump.cpp working.Cpp -> working.cpp numword-test.Cpp -> numword-test.cpp Using standard regular expression syntax, I can add " bw " to the beginning of each of the filenames:
$ ../rerename '^' bw numword-test.cpp -> bwnumword-test.cpp numword.cpp -> bwnumword.cpp working.cpp -> bwworking.cpp cpp-version.cpp -> bwcpp-version.cpp bwfoo.txt -> bwbwfoo.txt jump.cpp -> bwjump.cpp Notice that it even renamed the files that already had " bw " at the beginning. Let's have it not do that. First, we restore the filenames:
$ ../rerename '^bw' '' bwnumword.cpp -> numword.cpp bwjump.cpp -> jump.cpp bwnumword-test.cpp -> numword-test.cpp bwbwfoo.txt -> bwfoo.txt bwworking.cpp -> working.cpp bwcpp-version.cpp -> cpp-version.cpp Now we use a regex that checks if the filename already begins with " bw ":
$ ../rerename '^(?!bw)' bw numword-test.cpp -> bwnumword-test.cpp numword.cpp -> bwnumword.cpp working.cpp -> bwworking.cpp cpp-version.cpp -> bwcpp-version.cpp jump.cpp -> bwjump.cpp Because we use a vector of regex/replacement strings, we can stack several replacements:
$ ../rerename foo bar '\.cpp$' '.xpp' grep grok bwnumword.cpp -> bwnumword.xpp bwjump.cpp -> bwjump.xpp bwnumword-test.cpp -> bwnumword-test.xpp bwfoo.txt -> bwbar.txt bwworking.cpp -> bwworking.xpp bwcpp-version.cpp -> bwcpp-version.xpp The filesystem part of this recipe uses directory_iterator to return a directory_entry object for each file in the current directory:
for (const auto& entry : dit {fs::current_path()}) { fs::path fpath {entry.path()}; ... } We then construct a path object from the directory_entry object to process the file.
We use the replace_filename() method on a path object to create the destination for the rename operation:
fs::path rpath {fpath}; rpath.replace_filename(rname); Here we create a duplicate path and change its name, giving us both sides for the rename operation:
fs::rename(fpath, rpath); On the regular expression side of the recipe, we use regex_replace() , which uses regular expression syntax to perform substitutions in a string:
s = regex_replace(s, pattern, repl); Regular expression syntax is extremely powerful. It even allows replacements to include sections of the search string:
$ ../rerename '(bw)(.*\.)(.*)$' '$3$2$1' bwgrep.cpp -> cppgrep.bw bwfoo.txt -> txtfoo.bw By using parentheses in the search pattern, I can easily rearrange parts of a filename.
For more about regular expressions, see the recipe: Parse strings with regular expressions in .
This is a simple utility that totals the size of every file in a directory and its subdirectories. It runs on both POSIX/Unix and Windows filesystems.
This recipe is a utility to report the size of every file in a directory and its sub-directories, along with a total. We'll re-use some of the functions we've used elsewhere in this chapter:
namespace fs = std::filesystem; using dit = fs::directory_iterator; using de = fs::directory_entry; formatter specialization for fs::path objects: template<> struct std::formatter<std::filesystem::path>: std::formatter<std::string> { template<typename FormatContext> auto format(const std::filesystem::path& p, FormatContext& ctx) const { return std::formatter<std::string, char>::format(p.string(), ctx); } }; make_commas() function: string make_commas(const uintmax_t& num) { string s {std::to_string(num)}; for (long l = s.length() - 3; l > 0; l -= 3) { s.insert(l, ","); } return s; } We've used this before. It inserts a comma before every third character from the end.
string strlower(string s) { auto char_lower = [](char c) -> char { if (c >= 'A'&&c<= 'Z') return c + ('a' - 'A'); else return c; }; std::transform(s.begin(), s.end(), s.begin(), char_lower); return s; } directory_entry objects by lowercase path name: bool dircmp_lc(const de& lhs, const de& rhs) { const auto lhstr {lhs.path().string()}; const auto rhstr {rhs.path().string()}; return strlower(lhstr) < strlower(rhstr); } size_string() returns abbreviated values for reporting file size in gigabytes, megabytes, kilobytes, or bytes: string size_string(const uintmax_t fsize) { constexpr const uintmax_t kilo {1024}; constexpr const uintmax_t mega {kilo * kilo}; constexpr const uintmax_t giga {mega * kilo}; if (fsize >= giga ) return format("{}{}", (fsize + giga / 2) / giga, 'G'); else if (fsize >= mega) return format("{}{}", (fsize + mega / 2) / mega, 'M'); else if (fsize >= kilo) return format("{}{}", (fsize + kilo / 2) / kilo, 'K'); else return format("{}B", fsize); } entry_size() returns the size of a file, or if it's a directory, the recursive size of the directory uintmax_t entry_size(const fs::path& p) { if (fs::is_regular_file(p)) return fs::file_size(p); uintmax_t accum {}; if (fs::is_directory(p) && !fs::is_symlink(p)) { for (auto& e : dit {p}) { accum += entry_size(e.path()); } } return accum; } main() , we start with declarations and test if we have a valid directory to search: int main(const int argc, const char** argv) { auto dir {argc > 1 ? fs::path(argv[1]) : fs::current_path()}; vector<de> entries {}; uintmax_t accum {}; if (!exists(dir)) { println("path {} does not exist", dir); return 1; } if (!is_directory(dir)) { println("{} is not a directory", dir); return 1; } println("{}:", absolute(dir)); For our directory path dir , we use argv[1] if we have an argument, otherwise current_path() for the current directory.
The vector of directory_entry objects is used for sorting our response.
accum is used to accumulate values for our final size total.
We make sure dir exists and is a directory before proceeding to examine the directory.
vector . Once populated, we sort entries using our dircmp_lc() function as a comparison predicate: for (const auto& e : dit {dir}) { entries.emplace_back(e.path()); } std::sort(entries.begin(), entries.end(), dircmp_lc); Now that everything is set up, we can accumulate results from the sorted vector of directory_entry objects:
for (const auto& e : entries) { fs::path p {e}; uintmax_t esize {entry_size(p)}; string dir_flag {}; accum += esize; if (is_directory(p) && !is_symlink(p)) dir_flag = "▽"; println("{:>5} {}{}", size_string(esize), p.filename(), dir_flag); } println("{:->25}", ""); println("total bytes: {} ({})", make_commas(accum), size_string(accum)); The call to entry_size() returns the size of the file or directory represented in the directory_entry object.
If the current entry is a directory (and not a symbolic link ) we add a symbol to indicate it's a directory. I chose an inverted triangle, like a disclosure symbol. You may use anything here.
After the loop is complete, we display the accumulated size in both bytes with commas, and the abbreviated notation from size_string() .
Our output:
/home/billw/working/testdir/: 133M backup ▽ 935B cpp-version.cpp 20B five-words.txt 2K jump.cpp 2K numword-test.cpp 2K numword.cpp 6K the-raven.txt 154K working 3K working.cpp ------------------------- total bytes: 139,535,298 (133M) The output is a list of the size of every file and directory in the specified directory. Sub-directories are indicated with the inverted triangle symbol ▽ .
The fs::file_size() function returns a uintmax_t value, which is the largest standard unsigned integer defined in the C++ standard. While size_t is normally a 64-bit integer on most 64-bit systems, it remains 32 bits on 32-bit builds, even where uintmax_t may be 64 bits, notably when using the Windows 32-bit compiler. This can cause compilation to fail due to a type mismatch.
The entry_size() function takes a path object and returns a uintmax_t value:
uintmax_t entry_size(const fs::path& p) { if (fs::is_regular_file(p)) return fs::file_size(p); uintmax_t accum {}; if (fs::is_directory(p) && !fs::is_symlink(p)) { for(auto& e : dit {p}) { accum += entry_size(e.path()); } } return accum; } The entry_size() function checks for a regular file and returns the size of the file. Otherwise, it checks for a directory that is not also a symbolic link. We just want the size of the files in a directory, so we don't want to follow symbolic links. (Symbolic links may also cause reference loops, leading to a runaway condition.)
If we find a directory, we loop through it calling entry_size() for each file we encounter. This is a recursive loop, so we eventually end up with the size of the directory.
Scan the QR code (or go to ). Search for this book by name, confirm the edition, and then follow the steps on the page.
Note: Keep your invoice handy. Purchases made directly from Packt don't require an invoice.