I am at a very early stage in writing a C++ program replicating the most basic functions of the ls
bash command.
I use the <filesystem>
header.
The std::filesystem::directory_iterator
and std::filesystem::recursive_directory_iterator
from that library help me traverse the input file path in a shallow or recursive manner, respectively.
The input receives an argv[1]
, which should be composed of an initial hyphen -
, followed by letters standing for option flags.
Flag R
stands for recursive traversal of all subdirectories, otherwise, look only into the given path.
argv[2]
is the file path to explore.
My question however is about how to run this in a templated manner, or if it is even at all possible.
As a test, I am using std::variant
in a function option_R()
which selects the correct directory_iterator
depending on whether the -R
flag is present on input or not.
Since this would be runtime polymorphism, I am skeptical I can implement something like the below:
#include <iostream>
#include <vector>
#include <string>
#include <filesystem>
#include <variant>
#include <type_traits>
#include <concepts>
using namespace std;
using namespace std::filesystem;
template<typename T> concept isIter =
is_same<T, directory_iterator>::value || is_same<T, recursive_directory_iterator>::value;
template <isIter dirIterator>
dirIterator option_R(std::string_view options, const path &my_path) {
variant<directory_iterator, recursive_directory_iterator> my_iter;
if (options.find('R') != string::npos) {
my_iter.emplace<recursive_directory_iterator>(my_path, directory_options::skip_permission_denied);
return get<recursive_directory_iterator>(my_iter);
} else {
my_iter.emplace<directory_iterator>(my_path);
return get<directory_iterator>(my_iter);
}
}
template <isIter dirIterator>
void traverse(isIter &my_iter, vector<string> &my_vec) {
cout << "This function traverses the given dir path, "
<< "stores all filenames found inside a vector." << endl;
for (const auto &entry : my_iter)
my_vec.push_back(entry.path().string());
}
int main(int argc, const char **argv) {
const path sys_path{argv[2]};
const string options{argv[1]};
vector<string> files;
if (options[0] == '-') {
if (is_directory(sys_path)) {
isIter file_iterator = option_R(options, sys_path);
traverse(file_iterator, files);
}
cout << "after this the program will parse other options" << endl;
} else {
cout << "simply print all files" << endl;
}
}
The above does not compile, among other reasons because I leisurely use the concept isIter
to invoke option_R()
in main
.
Would there be a way to write option_R()
, so that it just does one thing: select the correct directory_iterator
according to whether flag -R
is given or not - and pass it back to main()
?
Would be easy to pack option_R()
and traverse()
together in only one function, but then this would do multiple things, and the code would be more difficult to understand.
You can achieve what you are trying to do with std::variant
and std::visit
.
A std::variant
is a safer union, which you can use to store both a std::filesystem::directory_iterator
and a std::filesystem::recursive_directory_iterator
, without having the need to allocate memory for both iterators separately.
With std::visit
you can then have your std::variant
passed to a templated (or overloaded) function which can work with all types the std::variant
has.
Using this, we can have your option_R()
function return a std::variant
of both iterators mentioned above and then the traverse()
function takes one such std::variant
as argument and then uses it with std::visit
to iterate over all files in the expected way.
#include <iostream>
#include <vector>
#include <string>
#include <filesystem>
#include <variant>
using IterVariant = std::variant<std::filesystem::directory_iterator, std::filesystem::recursive_directory_iterator>;
IterVariant option_R(std::string_view options, const std::filesystem::path& my_path) {
if (options.find('R') != std::string_view::npos) {
return std::filesystem::recursive_directory_iterator(my_path, std::filesystem::directory_options::skip_permission_denied);
}
return std::filesystem::directory_iterator(my_path);
}
void traverse(IterVariant& my_iter, std::vector<std::string>& my_vec)
{
std::cout << "This function traverses the given dir path, "
<< "stores all filenames found inside a vector.\n";
std::visit([&my_vec](auto&& it) {
for (const auto& entry : it) {
my_vec.push_back(entry.path().string());
}
}, my_iter);
}
int main(int argc, const char** argv) {
if (argc < 3) {
return 1;
}
const std::filesystem::path sys_path{ argv[2] };
const std::string options{ argv[1] };
std::vector<std::string> files;
if (options[0]=='-') {
if (std::filesystem::is_directory(sys_path)) {
IterVariant file_iterator = option_R(options, sys_path);
traverse(file_iterator, files);
}
std::cout << "after this the program will parse other options\n";
} else {
std::cout << "simply print all files\n";
}
for (std::string& file_name : files) {
std::cout << file_name << '\n';
}
return 0;
}