File System Paths
A guide to effectively working with file system paths, using the std::filesystem::path
type.
In the previous lessons, we represented our file paths as simple strings, but the standard library provides a dedicated class for this: std::filesystem::path
.
This type provides additional utility specific to working with the file system. For brevity, we'll alias std::filesystem
to fs
in the code examples in this lesson:
namespace fs = std::filesystem;
Creating fs::path
Objects
We can create fs::path
objects using simple strings:
#include <filesystem>
namespace fs = std::filesystem;
int main() {
fs::path Location{R"(c:\test)"};
}
We can get the string representation of a path using the string()
method, which is useful when we want to display it:
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::path Location{R"(c:\test)"};
std::cout << Location.string();
}
c:\test
Using fs::path
Objects with Directory Entries
The fs::directory_entry
constructor we've been using in the previous lesson accepts an fs::path
argument:
#include <filesystem>
namespace fs = std::filesystem;
int main() {
fs::path Location{R"(c:\test)"};
fs::directory_entry File{Location};
}
Since fs::path
can be created from a string, our fs::path
objects were being created implicitly:
#include <filesystem>
namespace fs = std::filesystem;
int main() {
// Implicitly converting raw string to fs::path
fs::directory_entry File{R"(c:\test)"};
}
We can get the fs::path
associated with a fs::directory_entry
using the path()
method:
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::directory_entry Entry{R"(c:\test)"};
std::cout << Entry.path().string();
}
c:\test
Accessing fs::path
Components
A variety of methods give us access to specific parts of the path. These also return fs::path
objects, so in the following example we use the string()
method to display them:
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::path Location{R"(c:\test\hello.txt)"};
std::cout << "File Name: "
<< Location.filename().string();
std::cout << "\nFile Stem: "
<< Location.stem().string();
std::cout << "\nFile Extension: "
<< Location.extension().string();
std::cout << "\nParent Path: "
<< Location.parent_path().string();
std::cout << "\nRoot Path: "
<< Location.root_name().string();
}
File Name: hello.txt
File Stem: hello
File Extension: .txt
Parent Path: c:\test
Root Path: c:
Equivalent boolean methods return true
or false
based on the existence of any of these path components.
We can access these by prepending has_
to the method names. For example, has_filename()
and has_extension()
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::path Directory{R"(c:\test\)"};
if (!Directory.has_filename()) {
std::cout << Directory.string()
<< " has no file name\n";
}
fs::path File{R"(c:\hi.txt)"};
if (File.has_extension()) {
std::cout << File.string()
<< " has a file extension";
}
}
c:\test\ has no file name
c:\hi.txt has a file extension
Note that the result of these functions is based only on the format of the provided string. For example, the has_filename()
method returns true
if it appears that the provided string has a filename.
To access the file system and check whether there really is a file at that path, we need to create a fs::directory_entry
, not just a fs::path
. We can then call a method like is_regular_file()
, as we covered in the previous lesson.
Working with fs::path
File Names
When working with paths, a common requirement is to manipulate the file name. We have some methods to help us there:
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::path File{R"(c:\test\hello.txt)"};
std::cout << File.string() << '\n';
File.replace_filename("world.txt");
std::cout << File.string() << '\n';
File.replace_extension("doc");
std::cout << File.string() << '\n';
File.remove_filename();
std::cout << File.string();
}
c:\test\hello.txt
c:\test\world.txt
c:\test\world.doc
c:\test\
Use cases for these methods typically come up when we're creating reusable functions.
For example, the following function creates a file, but the location of the file it creates is derived from an argument. In this case, it will create the file c:\test\hello.backup
:
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
void CreateBackup(const fs::path& Path) {
fs::path Backup{Path};
Backup.replace_extension("backup");
fs::copy_file(Path, Backup);
}
int main() {
CreateBackup(R"(c:\test\hello.txt)");
}
Appending to an fs::path
The fs::path
type also overrides the /=
operator, which allows us to create paths to subdirectories or files. This is done by automatically appending separators that are appropriate to the underlying operating system:
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::path File{R"(c:\)"};
std::cout << File.string() << '\n';
File /= "test";
std::cout << File.string() << '\n';
File /= "hello.txt";
std::cout << File.string() << '\n';
}
c:\
c:\test
c:\test\hello.txt
This operator, and most of the fs::path
methods, returns a reference to the original object. This allows them to be chained:
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::path File{R"(c:\test\hello.txt)"};
std::cout << File.string() << '\n';
File.remove_filename() /= "subdirectory";
std::cout << File.string() << '\n';
(File /= "nested") /= "directory";
std::cout << File.string();
}
c:\test\hello.txt
c:\test\subdirectory
c:\test\subdirectory\nested\directory
Relative Paths
All the paths we've shown so far have been absolute paths. If we wanted to access files in an exact location, we should use absolute paths.
However, if we want to access files in a location relative to where our program is installed, we don't necessarily know the exact location in advance. For this, we use relative paths.
Relative paths are based on another directory, often referred to as the current path or current working directory.
We can check if a path is relative or absolute using the is_relative()
and is_absolute()
methods:
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::path A{R"(c:\test\hello.txt)"};
if (A.is_absolute()) {
std::cout << "A is Absolute";
}
fs::path B{R"(hello.txt)"};
if (B.is_relative()) {
std::cout << "B is Relative";
}
}
A is Absolute
B is Relative
We can retrieve the current path that relative paths are based on using the current_path()
method. Its default value depends on our settings, but we can pass a new path to that function to set a new current path for our relative paths:
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::directory_entry Entry{R"(hello.txt)"};
std::cout
<< "The default working directory is:\n"
<< fs::current_path().string();
if (!Entry.exists()) {
std::cout << "\nThe file was not found\n\n";
}
// Setting the current path to a new lcoation
fs::current_path(R"(c:\test)");
std::cout << "The current working directory "
"was changed to:\n"
<< fs::current_path().string();
if (Entry.exists()) {
std::cout << "\nThe file was found!";
}
}
The working directory is:
C:\Users\ryan\repos\cpp
The file was not found
The working directory was changed to:
c:\test
The file was found!
Summary
In this lesson, we explored the versatile capabilities of std::filesystem::path
, demonstrating how to create, manipulate, and use file paths effectively. We covered various operations from basic path creation to advanced manipulations.
Key Takeaways
- Learned to create and use
fs::path
objects for representing and manipulating file paths. - Explored methods to access and modify different components of a file path, like filename, extension, and parent path.
- Discovered how to append subdirectories or files to a path using the
/=
operator, with examples demonstrating path chaining. - Understood the distinction between absolute and relative paths and how to determine the type of a given path.
- Gained insights into setting and retrieving the current working directory using
fs::current_path()
.
Directory Iterators
An introduction to iterating through the file system, using directory_iterator
and recursive_directory_iterator
.