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.
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.
2 Answers
Reset to default 2You 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;
}
(live example)
You are very close. The problem is, you can't have your option_R()
function return a compile-time template type that is determined at runtime. You need to have it return the std::variant
itself, and then the caller can decide what to do with the value. You can use std::visit()
to help you with that, for example:
#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 isDirIter = is_same_v<T, directory_iterator> || is_same_v<T, recursive_directory_iterator>;
using DirIterVariant = variant<directory_iterator, recursive_directory_iterator>;
DirIterVariant option_R(std::string_view options, const path &my_path) {
if (options.find('R') != string::npos) {
cout << "Will scan subdirectories recursively" << endl;
return recursive_directory_iterator(my_path, directory_options::skip_permission_denied);
} else {
cout << "Will scan directory only" << endl;
return directory_iterator(my_path);
}
}
template <isDirIter dirIterator>
void traverse(dirIterator &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)) {
DirIterVariant file_iterator_v = option_R(options, sys_path);
visit([&](auto&& file_iterator) {
traverse(file_iterator, files);
},
file_iterator_v);
}
cout << "after this the program will parse other options" << endl;
} else {
cout << "simply print all files" << endl;
}
}
option_R
won't work the way you call it – UnholySheep Commented yesterdayoption_R
could just return thestd::variant
but then you need to adjust how you usetraverse
to pass in the proper template argument – UnholySheep Commented yesterdayoption_R()
entirely, and just do anif-else
according to presence or absence of flag -R, insidetraverse()
– Giogre Commented yesterday