Heads‑up: We focus on safe, repeatable file operations and production‑style error handling. The examples work on Windows, macOS, and Linux with a C++17‑capable compiler.
What You’ll Learn
By the end of this lesson, you’ll be able to:
- Use
std::filesystemto create single and nested directories. - Check existence, type (file vs directory), and permissions.
- Implement idempotent create/remove logic with
std::error_code(no exceptions path). - List and recursively traverse subfolders with filtering.
- Provide a neat CLI with dry‑run and verbose output.
Project Structure
subfolders/
├─ src/
│ ├─ main.cpp # CLI: parse args, call ops, print results
│ ├─ ops.hpp # declarations for fs ops
│ └─ ops.cpp # implementations using <filesystem>
├─ tests/
│ └─ smoke.cpp # minimal smoke tests (no framework)
├─ CMakeLists.txt # build config
└─ README.md
Minimal CMakeLists.txt:
cmake_minimum_required(VERSION 3.16)
project(subfolders LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(subfolders src/main.cpp src/ops.cpp)
Core API Design (ops.hpp)
#pragma once
#include <filesystem>
#include <string>
#include <vector>
namespace sf {
namespace fs = std::filesystem;
struct CreateOpts {
bool recursive = true; // create nested folders
bool dry_run = false; // print actions but don’t touch disk
bool verbose = true; // log what happens
fs::perms perms = fs::perms::owner_all | fs::perms::group_read | fs::perms::others_read;
};
struct RemoveOpts {
bool recursive = true;
bool dry_run = false;
bool verbose = true;
};
// Returns true on success (idempotent). On error, returns false and sets ec.
bool ensure_dir(const fs::path& p, const CreateOpts& opts, std::error_code& ec);
// Returns number of removed entries. On error returns (size_t)-1 and sets ec.
std::uintmax_t remove_dir(const fs::path& p, const RemoveOpts& opts, std::error_code& ec);
// Lists immediate children matching an optional predicate.
using PathList = std::vector<fs::path>;
PathList list_children(const fs::path& p, bool include_files, bool include_dirs,
const std::string& name_contains, std::error_code& ec);
// Recursively collects directories (depth‑first). If max_depth==0 => unlimited.
PathList list_dirs_recursive(const fs::path& root, std::size_t max_depth,
const std::string& name_contains, std::error_code& ec);
}
Implementation (ops.cpp)
#include "ops.hpp"
#include <iostream>
namespace sf {
namespace fs = std::filesystem;
static void vlog(bool verbose, const std::string& msg){ if(verbose) std::cout << msg << '\n'; }
bool ensure_dir(const fs::path& p, const CreateOpts& opts, std::error_code& ec) {
ec.clear();
if (opts.dry_run) { vlog(opts.verbose, "[dry-run] mkdir: " + p.string()); return true; }
if (fs::exists(p, ec)) {
if (ec) return false;
if (fs::is_directory(p, ec)) { vlog(opts.verbose, "exists: " + p.string()); return !ec; }
// A non-directory exists at path: error
vlog(opts.verbose, "error: path exists but is not a directory: " + p.string());
ec = std::make_error_code(std::errc::file_exists);
return false;
}
bool ok = false;
if (opts.recursive) ok = fs::create_directories(p, ec);
else ok = fs::create_directory(p, ec);
if (!ec) {
// try to set permissions (best-effort; not all platforms honor this fully)
std::error_code perm_ec;
fs::permissions(p, opts.perms, perm_ec);
vlog(opts.verbose, std::string("mkdir: ") + p.string());
return true; // idempotent: treat existing as success
}
return false;
}
std::uintmax_t remove_dir(const fs::path& p, const RemoveOpts& opts, std::error_code& ec) {
ec.clear();
if (opts.dry_run) { vlog(opts.verbose, "[dry-run] rm" + std::string(opts.recursive?" -r":"") + ": " + p.string()); return 0; }
if (!fs::exists(p, ec)) { return 0; }
if (ec) return static_cast<std::uintmax_t>(-1);
std::uintmax_t count = 0;
if (opts.recursive) count = fs::remove_all(p, ec);
else {
bool ok = fs::remove(p, ec);
count = ok ? 1 : 0;
}
if (!ec) vlog(opts.verbose, std::string("removed entries: ") + std::to_string(count));
return ec ? static_cast<std::uintmax_t>(-1) : count;
}
sf::PathList list_children(const fs::path& p, bool include_files, bool include_dirs,
const std::string& name_contains, std::error_code& ec) {
PathList out; ec.clear();
if (!fs::exists(p, ec) || ec) return out;
for (auto it = fs::directory_iterator(p, ec); !ec && it != fs::directory_iterator(); ++it) {
const auto& entry = *it;
bool is_file = entry.is_regular_file(ec);
bool is_dir = entry.is_directory(ec);
const auto name = entry.path().filename().string();
if (!name_contains.empty() && name.find(name_contains) == std::string::npos) continue;
if ((include_files && is_file) || (include_dirs && is_dir)) out.push_back(entry.path());
}
return out;
}
sf::PathList list_dirs_recursive(const fs::path& root, std::size_t max_depth,
const std::string& name_contains, std::error_code& ec) {
PathList out; ec.clear();
if (!fs::exists(root, ec) || ec) return out;
std::size_t root_depth = std::distance(root.begin(), root.end());
for (auto it = fs::recursive_directory_iterator(root, ec); !ec && it != fs::recursive_directory_iterator(); ++it) {
const auto& entry = *it;
if (!entry.is_directory(ec)) continue;
auto path = entry.path();
if (!name_contains.empty() && path.filename().string().find(name_contains) == std::string::npos) continue;
std::size_t d = std::distance(path.begin(), path.end()) - root_depth;
if (max_depth && d > max_depth) { it.disable_recursion_pending(); continue; }
out.push_back(path);
}
return out;
}
}
CLI Wiring (src/main.cpp)
#include "ops.hpp"
#include <iostream>
#include <string>
using namespace sf;
static void usage(){
std::cout << "Usage:\n"
<< " subfolders create <path> [--dry] [--no-rec] [--quiet]\n"
<< " subfolders remove <path> [--dry] [--no-rec] [--quiet]\n"
<< " subfolders ls <path> [--files] [--dirs] [--name <substr>]\n"
<< " subfolders finddirs <root> [--max-depth N] [--name <substr>]\n";
}
int main(int argc, char** argv) {
if (argc < 3) { usage(); return 1; }
std::string cmd = argv[1];
std::filesystem::path arg = argv[2];
std::error_code ec;
if (cmd == "create") {
CreateOpts o{};
for (int i=3; i<argc; ++i) {
std::string f = argv[i];
if (f=="--dry") o.dry_run=true; else if (f=="--no-rec") o.recursive=false; else if (f=="--quiet") o.verbose=false; }
bool ok = ensure_dir(arg, o, ec);
if (!ok || ec) { std::cerr << "create error: " << ec.message() << "\n"; return 2; }
}
else if (cmd == "remove") {
RemoveOpts o{};
for (int i=3; i<argc; ++i) { std::string f = argv[i]; if (f=="--dry") o.dry_run=true; else if (f=="--no-rec") o.recursive=false; else if (f=="--quiet") o.verbose=false; }
auto n = remove_dir(arg, o, ec);
if (n==(std::uintmax_t)-1 || ec) { std::cerr << "remove error: " << ec.message() << "\n"; return 3; }
}
else if (cmd == "ls") {
bool inc_files=false, inc_dirs=true; std::string name;
for (int i=3; i<argc; ++i) { std::string f = argv[i]; if (f=="--files") inc_files=true; else if (f=="--dirs") inc_dirs=true; else if (f=="--name" && i+1<argc) name=argv[++i]; }
auto xs = list_children(arg, inc_files, inc_dirs, name, ec);
if (ec) { std::cerr << "ls error: " << ec.message() << "\n"; return 4; }
for (auto& p: xs) std::cout << p.string() << "\n";
}
else if (cmd == "finddirs") {
std::size_t depth=0; std::string name;
for (int i=3; i<argc; ++i) { std::string f = argv[i]; if (f=="--max-depth" && i+1<argc) depth=static_cast<std::size_t>(std::stoul(argv[++i])); else if (f=="--name" && i+1<argc) name=argv[++i]; }
auto xs = list_dirs_recursive(arg, depth, name, ec);
if (ec) { std::cerr << "finddirs error: " << ec.message() << "\n"; return 5; }
for (auto& p: xs) std::cout << p.string() << "\n";
}
else {
usage();
return 1;
}
return 0;
}
Minimal Smoke Tests (tests/smoke.cpp)
#include "../src/ops.hpp"
#include <cassert>
#include <iostream>
int main(){
using namespace sf;
std::error_code ec;
auto tmp = std::filesystem::temp_directory_path() / "sf_demo";
// ensure
CreateOpts c{}; c.verbose=false; c.dry_run=false; c.recursive=true;
assert(ensure_dir(tmp/"a"/"b"/"c", c, ec));
assert(!ec);
// list children
auto kids = list_children(tmp/"a"/"b", /*files*/false, /*dirs*/true, "", ec);
assert(!ec && !kids.empty());
// remove
RemoveOpts r{}; r.verbose=false; r.recursive=true;
auto n = remove_dir(tmp, r, ec);
assert(!ec && n>0);
std::cout << "OK\n";
}
Build & run (example):
cmake -S . -B build && cmake --build build
./build/subfolders create ./demo/a/b/c
./build/subfolders ls ./demo --dirs
./build/subfolders remove ./demo --no-rec # fails if non‑empty (by design)
./build/subfolders remove ./demo # recursive
Safety & Idempotency Notes
- Use the
error_codeoverloads to avoid exceptions in operational code. - Treat “already exists and is a directory” as success.
- Fail if a non‑directory exists at the target path.
- Log clearly in dry‑run mode to preview actions.
Nice‑to‑Haves
- Add
--mode 755style permissions parsing on POSIX. - Add a
--patternglob (e.g.,**/*.log) using a tiny matcher. - Add a
copy_tree(src, dst)with collision strategies: skip/overwrite/rename. - Add colored output for actions and results.
Learning Checklist
- I can create nested directories portably with
<filesystem>. - I can check existence/type and handle collisions safely.
- I can list and traverse subfolders (flat and recursive).
- I can remove directories with and without recursion.
- I can design a small, testable filesystem API.
