In this section, we will implement a little tool that lists all files in any user provided directory. It will not only list the filenames, but also their type, size, and access permissions.
- First, we need to include some headers and declare that we use the namespaces std and filesystem by default.
#include <iostream>
#include <sstream>
#include <iomanip>
#include <numeric>
#include <algorithm>
#include <vector>
#include <filesystem>
using namespace std;
using namespace filesystem;
- One helper function that we are going to need is file_info. It accepts a directory_entry object reference and extracts the path from it, as well as a file_status object (using the status function), which contains file type and permission information. Finally, it also extracts the size of the entry if it is a regular file. For directories or other special files, we plainly return a size of 0. All this information is bundled into a tuple.
static tuple<path, file_status, size_t>
file_info(const directory_entry &entry)
{
const auto fs (status(entry));
return {entry.path(),
fs,
is_regular_file(fs) ? file_size(entry.path()) : 0u};
}
- Another helper function that we will need is type_char. A path cannot only represent directories and simple text/binary files. Operating systems provide a variety of other types that abstract something else, such as hardware device interfaces in the form of so-called character/block files. The STL filesystem library provides a lot of predicate functions for them. This way we can return the letter 'd' for directories, the letter 'f' for regular files, and so on.
static char type_char(file_status fs)
{
if (is_directory(fs)) { return 'd'; }
else if (is_symlink(fs)) { return 'l'; }
else if (is_character_file(fs)) { return 'c'; }
else if (is_block_file(fs)) { return 'b'; }
else if (is_fifo(fs)) { return 'p'; }
else if (is_socket(fs)) { return 's'; }
else if (is_other(fs)) { return 'o'; }
else if (is_regular_file(fs)) { return 'f'; }
return '?';
}
- Yet another helper we will need is the rwx function. It accepts a perms variable (which is just an enum class type from the filesystem library) and returns a string such as "rwxrwxrwx" that describes the file's permission settings. The first group of "rwx" characters describes the read, write, and execution permissions for the owner of the file. The next group describes the same rights for all users that are part of the user group the file belongs to. The last character group describes which rights everyone else has for accessing the file. A string such as "rwxrwxrwx" means that everyone can access the object in any way. "rw-r--r--" means that only the owner can read and modify the file, while anyone else can only read it.
We just compose a string from such read/write/execute character values, permission bit by permission bit. A lambda expression helps us with the repetitive work of checking if the perms variable p contains a specific owner bit and then returns '-' or the right character.
static string rwx(perms p)
{
auto check ([p](perms bit, char c) {
return (p & bit) == perms::none ? '-' : c;
});
return {check(perms::owner_read, 'r'),
check(perms::owner_write, 'w'),
check(perms::owner_exec, 'x'),
check(perms::group_read, 'r'),
check(perms::group_write, 'w'),
check(perms::group_exec, 'x'),
check(perms::others_read, 'r'),
check(perms::others_write, 'w'),
check(perms::others_exec, 'x')};
}
- Finally, the last helper function accepts an integral file size and converts it to a better to read form. We just ignore the period while dividing numbers down and floor them to the nearest kilo, mega, or giga boundary.
static string size_string(size_t size)
{
stringstream ss;
if (size >= 1000000000) {
ss << (size / 1000000000) << 'G';
} else if (size >= 1000000) {
ss << (size / 1000000) << 'M';
} else if (size >= 1000) {
ss << (size / 1000) << 'K';
} else { ss << size << 'B'; }
return ss.str();
}
- Now we can finally implement the main function. We begin with checking if the user provided a path in the command line. If he didn't, we just take the current directory ".". Then, we check if the directory exists. If it doesn't, we can't possibly list any files.
int main(int argc, char *argv[])
{
path dir {argc > 1 ? argv[1] : "."};
if (!exists(dir)) {
cout << "Path " << dir << " does not exist.n";
return 1;
}
- Now, we will fill a vector with file information tuples just like our first helper function file_info returns from directory_entry objects. We instantiate a directory_iterator and give its constructor the path object, which we created in the last step. While iterating with the directory iterator, we transform the directory_entry objects to file information tuples and insert them into the vector.
vector<tuple<path, file_status, size_t>> items;
transform(directory_iterator{dir}, {},
back_inserter(items), file_info);
- Now we have all information saved in the vector items and can simply print it using all the helper functions we wrote.
for (const auto &[path, status, size] : items) {
cout << type_char(status)
<< rwx(status.permissions()) << " "
<< setw(4) << right << size_string(size)
<< " " << path.filename().c_str()
<< 'n';
}
}
- Compiling and running the project with a file path in the offline version of the C++ documentation yields the following output. We see that the folder only contains directories and plain files because there are only 'd' and 'f' entries as first characters of all output lines. These files have different access permissions, and of course different sizes. Note that the files appear in alphabetical order of their names, but we cannot really rely on that because alphabetic ordering is not required by the C++17 standard.
$ ./list ~/Documents/cpp_reference/en/cpp
drwxrwxr-x 0B algorithm
frw-r--r-- 88K algorithm.html
drwxrwxr-x 0B atomic
frw-r--r-- 35K atomic.html
drwxrwxr-x 0B chrono
frw-r--r-- 34K chrono.html
frw-r--r-- 21K comment.html
frw-r--r-- 21K comments.html
frw-r--r-- 220K compiler_support.html
drwxrwxr-x 0B concept
frw-r--r-- 67K concept.html
drwxr-xr-x 0B container
frw-r--r-- 285K container.html
drwxrwxr-x 0B error
frw-r--r-- 52K error.html