Build a File Duplicate Finder
Build a C++ CLI tool that scans folders and finds exact duplicate files.
Introduction
30 Second Summary
Everyone's hard drive has duplicate files hiding in forgotten folders. Photos copied twice, downloads grabbed more than once, backup files scattered across your PC.
In this project, you will build a command-line tool in C++ that scans any folder on your computer and identifies exact duplicate files. It compares file content byte-by-byte and prints a clear report showing what is duplicated and how much disk space you can reclaim.
What You'll Build
You run dupfinder.exe in your terminal, point it at a folder full of files, and within seconds see a report revealing exactly which files are duplicates and how much space they waste.
By the end of this project, you'll have:
- A working CLI tool that recursively scans any folder on your PC and finds files with identical content.
- A formatted duplicate report with numbered groups, file paths, and a summary showing total reclaimable disk space.
- Refreshed modern C++ skills including filesystem traversal, STL containers, and binary file I/O through building something real from scratch.
- Secret Mission: Add command-line argument parsing so the tool accepts any folder path as an argument.
Are there any prerequisites?
This project assumes you have basic C++ knowledge from a university course or similar. You should be comfortable with variables, functions, loops, and classes.
Step 1 walks you through setting up a C++ compiler on Windows, so no prior toolchain setup is needed.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
Scan a Folder and List Files
Your toolchain is ready and you have a working "Hello, World!" program. Now it is time to make this program do something useful.
Before you can find duplicate files, you need to read what files exist inside a folder. C++17 includes a standard library called std::filesystem that lets you traverse directories, check file types, and read file sizes without any platform-specific code. In this step, you will use it to scan a folder and print every file it contains.
In this step, get ready to:
- Write a program that recursively lists every file in a folder.
- Print each file's path and size in bytes to the terminal.
- Add error handling so the program does not crash on inaccessible folders.
Replace the hello world with filesystem scanning code
The recursive_directory_iterator from std::filesystem walks through every file and subfolder starting from a given path. You will pair it with a range-based for loop to visit each entry one by one.
- In VS Code, open main.cpp (it should already be open from Step 1).
- Select all the existing hello world code and replace it with the following:
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
// Use the current working directory as the scan target
fs::path targetPath = fs::current_path();
std::cout << "Scanning: " << targetPath.string() << "\n\n";
// Walk through every file and subfolder recursively
for (const auto& entry : fs::recursive_directory_iterator(targetPath)) {
if (entry.is_regular_file()) {
std::cout << entry.path().string() << " " << entry.file_size() << " bytes\n";
}
}
return 0;
}
What does this code do?
namespace fs = std::filesystem; creates a short alias so you can write fs::path instead of std::filesystem::path everywhere.
fs::current_path() returns the folder from which you ran the program. This means your tool scans whichever folder you are in when you execute it.
The range-based for loop with fs::recursive_directory_iterator visits every entry (files and folders) beneath the target path. The is_regular_file() check filters out directories so you only print actual files.
- Save main.cpp.
- Compile the program by running this command in the VS Code terminal:
g++ -std=c++17 main.cpp -o dupfinder.exe
Before you run it, what do you think you will see printed in the terminal?
- Run the program:
.\dupfinder.exe
You should see a list of every file inside your dupfinder folder printed with its full path and size in bytes. At minimum you will see main.cpp and dupfinder.exe listed.
Seeing an error about the filesystem header?
Make sure your compile command includes -std=c++17. The <filesystem> library is only available in C++17 and later.
If you see g++: command not found, close and reopen your terminal so it picks up the PATH change from Step 1.
help me fix a compile error with filesystem in my C++ project
Add error handling for filesystem access
The recursive_directory_iterator can throw an exception if it hits a folder it cannot access (for example, a system-protected directory). Wrapping the iteration in a try-catch prevents your program from crashing unexpectedly.
- In main.cpp, find the for loop and the return 0; line beneath it:
for (const auto& entry : fs::recursive_directory_iterator(targetPath)) {
if (entry.is_regular_file()) {
std::cout << entry.path().string() << " " << entry.file_size() << " bytes\n";
}
}
return 0;
- Replace that section with the following, which wraps the loop in a try-catch block:
try {
for (const auto& entry : fs::recursive_directory_iterator(targetPath)) {
if (entry.is_regular_file()) {
std::cout << entry.path().string() << " " << entry.file_size() << " bytes\n";
}
}
} catch (const fs::filesystem_error& e) {
std::cerr << "Error scanning directory: " << e.what() << "\n";
return 1;
}
return 0;
What does the try-catch do here?
If the iterator encounters a folder it cannot read (permissions issue or a corrupted path), the program catches the fs::filesystem_error exception and prints a helpful message instead of crashing with an unreadable stack trace.
Returning 1 signals to the operating system that the program exited with an error, which is the standard convention for CLI tools.
- Save main.cpp.
- Compile and run the program again:
g++ -std=c++17 main.cpp -o dupfinder.exe
.\dupfinder.exe
You should see the same file listing as before. The output looks identical because no error occurred, but your program is now protected against inaccessible folders.
Seeing a compile error after adding try-catch?
Check that the try { line sits directly above the for loop, and that the } catch closing brace comes after the loop's closing brace. Mismatched braces are the most common cause of errors here.
help me fix a brace mismatch error in my C++ try-catch block
✔️ Awesome, I've got everything!
Great. Double check you have saved main.cpp and that your program compiles without warnings.
ⓧ I'd like to double check the full code
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::path targetPath = fs::current_path();
std::cout << "Scanning: " << targetPath.string() << "\n\n";
try {
for (const auto& entry : fs::recursive_directory_iterator(targetPath)) {
if (entry.is_regular_file()) {
std::cout << entry.path().string() << " " << entry.file_size() << " bytes\n";
}
}
} catch (const fs::filesystem_error& e) {
std::cerr << "Error scanning directory: " << e.what() << "\n";
return 1;
}
return 0;
}
Your scanner can now read every file in a folder and tell you its size. Next up, you will use this information to group files by size and start hunting for potential duplicates.
Group Files by Size
Your tool can list every file in a folder, but it treats each one independently. To find duplicates, you need a way to compare files. Comparing every file against every other file would be painfully slow for large folders.
A smarter approach starts with a quick filter. Files with different sizes cannot possibly be duplicates. Grouping files by size first eliminates most comparisons upfront.
In this step, get ready to:
- Store file paths in an std::unordered_map grouped by file size.
- Print groups of files that share the same size as potential duplicates.
- Add counters to track total files scanned and potential duplicate groups found.
Store files in a size-based map
Instead of printing each file immediately, you will collect file paths into groups based on their byte size. An std::unordered_map provides fast key-value lookup for this.
- In main.cpp, add two new includes below #include <iostream>:
#include <unordered_map>
#include <vector>
- Find the std::cout << "Scanning:..." line. Below it, before the try block, add these declarations:
// Group file paths by their size in bytes
std::unordered_map<std::uintmax_t, std::vector<fs::path>> sizeGroups;
int totalFiles = 0;
- Inside the if (entry.is_regular_file()) block, find this line:
std::cout << entry.path().string() << " " << entry.file_size() << " bytes\n";
- Replace that entire line with this code:
// Get file size safely using error code
std::error_code ec;
std::uintmax_t fileSize = entry.file_size(ec);
if (!ec) {
sizeGroups[fileSize].push_back(entry.path());
totalFiles++;
}
- After the closing } of the catch block and before return 0;, add this line:
std::cout << "Scanned " << totalFiles << " files.\n\n";
What does this code do?
- std::unordered_map<std::uintmax_t, std::vector<fs::path>> creates a hash map where each key is a file size (in bytes) and the value is a list of file paths that share that size.
- std::error_code ec captures errors without throwing exceptions. This is safer than calling file_size() directly, because some files (like system files) may refuse to report their size.
- sizeGroups[fileSize].push_back(entry.path()) adds the file's path to the vector for its size group. If no vector exists yet for that size, the map creates one automatically.
- Save main.cpp.
- Compile and run to confirm it scans without errors:
g++ -std=c++17 main.cpp -o dupfinder.exe
.\dupfinder.exe
You should see "Scanning: ..." followed by "Scanned X files." with no groups printed yet.
Seeing a compile error?
- If you see "unordered_map: No such file or directory", check that #include <unordered_map> is at the top of your file with the other includes.
- If you see "ec was not declared", make sure the std::error_code ec; line is inside the if (entry.is_regular_file()) block, not outside it.
- help me fix a compile error in my duplicate finder
Report potential duplicate groups
The map now holds every file grouped by size. Any group with two or more files could contain duplicates.
- In main.cpp, below the std::cout << "Scanned "... line you just added, insert this reporting loop before return 0;:
// Print groups where 2+ files share the same size
for (const auto& [size, paths] : sizeGroups) {
if (paths.size() >= 2) {
std::cout << "Size " << size << " bytes: " << paths.size() << " files\n";
for (const auto& p : paths) {
std::cout << " " << p.string() << "\n";
}
}
}
What is this structured binding syntax?
const auto& [size, paths] is a C++17 structured binding. It unpacks each map entry into two named variables: size gets the key (file size) and paths gets the value (vector of file paths).
Without structured bindings, you would write it->first and it->second. The modern syntax makes the code much easier to read.
- Save main.cpp.
- Before running, create some test files in your dupfinder folder so there are files with matching sizes. Run these commands:
echo "hello world" > test1.txt
copy test1.txt test2.txt
echo "same size!" > test3.txt
Why create these test files?
test1.txt and test2.txt are true duplicates (identical content). test3.txt might share the same byte size as the others but has different content. This lets you see both real duplicates and false positives in the output.
Before you compile and run, think about this: will the size-only grouping correctly identify which files are true duplicates and which just happen to share a size?
- Compile and run the program:
g++ -std=c++17 main.cpp -o dupfinder.exe
.\dupfinder.exe
You will see groups of files listed by matching size. Some groups contain files that are genuinely identical copies. Others contain completely unrelated files that just happen to share the same byte count.
This is the designed shortfall
Size alone is not enough to confirm duplicates. Two files can be the exact same size but contain entirely different data. This is the flaw with the naive approach.
The next step fixes this by comparing actual file content byte-by-byte. Size grouping is still valuable as a fast first filter that eliminates most files from expensive comparisons.
Not seeing any groups in the output?
- Make sure you created the test files in the same folder where you run dupfinder.exe. The program scans the current directory.
- Check that the condition is paths.size() >= 2 (greater than or equal to 2, not greater than 2).
- help me figure out why no duplicate groups are printing
Add a summary counter
The raw grouped output works, but a summary at the end makes it easier to see the big picture at a glance.
- In main.cpp, find the reporting loop you just added. Add a counter variable on the line directly above the for loop and increment it inside the if block:
int potentialGroups = 0;
for (const auto& [size, paths] : sizeGroups) {
if (paths.size() >= 2) {
potentialGroups++;
std::cout << "Size " << size << " bytes: " << paths.size() << " files\n";
for (const auto& p : paths) {
std::cout << " " << p.string() << "\n";
}
}
}
- After the closing } of the reporting loop and before return 0;, add this summary line:
std::cout << "\nPotential duplicate groups (by size): " << potentialGroups << "\n";
What does this add?
potentialGroups counts how many size groups have two or more files. The summary line at the end gives a quick overview without the learner needing to scroll through the full grouped output.
- Save main.cpp.
- Compile and run the final version:
g++ -std=c++17 main.cpp -o dupfinder.exe
.\dupfinder.exe
You should see the scan count, the grouped file lists, and a final summary line reporting the number of potential duplicate groups.
Seeing a compile error about potentialGroups?
- Make sure int potentialGroups = 0; is declared before the for loop, not inside it. Variables must be declared before they are used.
- Check that potentialGroups++ is inside the if (paths.size() >= 2) block, not outside it.
- help me fix this compile error related to my potentialGroups counter
✔️ Awesome, I've got everything!
Great. Double check you have saved main.cpp and your output shows scanned files, size groups, and the summary line.
ⓧ I'd like to double check the full code
Here is the complete main.cpp at this point. Compare yours against it:
#include <filesystem>
#include <iostream>
#include <unordered_map>
#include <vector>
namespace fs = std::filesystem;
int main() {
fs::path targetPath = fs::current_path();
std::cout << "Scanning: " << targetPath.string() << "\n\n";
std::unordered_map<std::uintmax_t, std::vector<fs::path>> sizeGroups;
int totalFiles = 0;
try {
for (const auto& entry : fs::recursive_directory_iterator(targetPath)) {
if (entry.is_regular_file()) {
std::error_code ec;
std::uintmax_t fileSize = entry.file_size(ec);
if (!ec) {
sizeGroups[fileSize].push_back(entry.path());
totalFiles++;
}
}
}
} catch (const fs::filesystem_error& e) {
std::cerr << "Error scanning directory: " << e.what() << "\n";
return 1;
}
std::cout << "Scanned " << totalFiles << " files.\n\n";
int potentialGroups = 0;
for (const auto& [size, paths] : sizeGroups) {
if (paths.size() >= 2) {
potentialGroups++;
std::cout << "Size " << size << " bytes: " << paths.size() << " files\n";
for (const auto& p : paths) {
std::cout << " " << p.string() << "\n";
}
}
}
std::cout << "\nPotential duplicate groups (by size): " << potentialGroups << "\n";
return 0;
}
Your tool now identifies potential duplicates by size, but it still reports false matches. Next up, you will compare actual file content byte-by-byte to eliminate those false positives and find true duplicates.
Compare File Content for True Duplicates
In the last step, you saw the flaw in size-based grouping. Files with the same byte count appeared as "duplicates" even when their content was completely different.
To fix this, you need to compare what is actually inside each file. In this step, you will write a function that reads two files byte by byte and confirms whether they are truly identical.
In this step, get ready to:
- Write a binary file comparison function using std::ifstream.
- Replace the size-only grouping with true content comparison.
- Verify that false positives are eliminated from the output.
Write the file comparison function
Binary file comparison works by opening two files simultaneously and reading them in chunks. If any chunk differs, you know immediately the files are not identical.
- In main.cpp, find your existing include block at the top of the file:
#include <filesystem>
#include <iostream>
#include <unordered_map>
#include <vector>
- Add <fstream> and <cstring> so the block looks like this:
#include <filesystem>
#include <fstream>
#include <iostream>
#include <unordered_map>
#include <vector>
#include <cstring>
Why these two headers?
<fstream> gives you std::ifstream for reading file data in binary mode. <cstring> gives you std::memcmp for fast byte-by-byte buffer comparison.
- Now add the filesAreIdentical function between the namespace fs = std::filesystem; line and the int main() line by copying this code:
// Compare two files byte-by-byte to check if they are identical
bool filesAreIdentical(const fs::path& a, const fs::path& b) {
std::ifstream file1(a, std::ios::binary);
std::ifstream file2(b, std::ios::binary);
if (!file1.is_open() || !file2.is_open()) {
return false;
}
const std::size_t bufferSize = 4096;
char buffer1[4096];
char buffer2[4096];
while (true) {
file1.read(buffer1, bufferSize);
file2.read(buffer2, bufferSize);
std::streamsize bytesRead1 = file1.gcount();
std::streamsize bytesRead2 = file2.gcount();
if (bytesRead1 != bytesRead2) {
return false;
}
if (bytesRead1 == 0) {
break;
}
if (std::memcmp(buffer1, buffer2, static_cast<std::size_t>(bytesRead1)) != 0) {
return false;
}
}
return true;
}
What does this function do?
- std::ios::binary opens the files in binary mode so no character translation happens. You see the raw bytes.
- The function reads 4096 bytes at a time into two buffers. Reading in chunks keeps memory usage low even for large files.
- std::memcmp compares the two buffers. If any chunk differs, the function returns false immediately without reading the rest of the file.
- If every chunk matches until both files are exhausted, the files are identical and it returns true.
- Save main.cpp.
- Verify the new function compiles without errors by running:
g++ -std=c++17 main.cpp -o dupfinder.exe
You should see no output. No output from the compiler means success.
Seeing a compile error?
- Check that the function is placed between namespace fs = std::filesystem; and int main(). Functions must be defined before they are called.
- Make sure #include <fstream> and #include <cstring> are both present at the top of the file.
Help me fix my compile error in filesAreIdentical
Update the duplicate detection logic
Now that you can compare file content, you need to replace the old size-only printing logic with real duplicate detection that calls your new function.
- In main.cpp, find this block of code inside main() (everything after the "Scanned X files" output):
int potentialGroups = 0;
for (const auto& [size, paths] : sizeGroups) {
if (paths.size() >= 2) {
potentialGroups++;
std::cout << "Size " << size << " bytes: " << paths.size() << " files\n";
for (const auto& p : paths) {
std::cout << " " << p.string() << "\n";
}
}
}
std::cout << "\nPotential duplicate groups (by size): " << potentialGroups << "\n";
return 0;
- Delete that entire block and replace it with the following content comparison logic:
// For each size group with 2+ files, compare content
std::vector<std::vector<fs::path>> duplicateGroups;
for (const auto& [size, paths] : sizeGroups) {
if (paths.size() < 2) {
continue;
}
// Track which files have already been assigned to a group
std::vector<bool> matched(paths.size(), false);
for (std::size_t i = 0; i < paths.size(); i++) {
if (matched[i]) continue;
std::vector<fs::path> group;
group.push_back(paths[i]);
for (std::size_t j = i + 1; j < paths.size(); j++) {
if (matched[j]) continue;
if (filesAreIdentical(paths[i], paths[j])) {
group.push_back(paths[j]);
matched[j] = true;
}
}
if (group.size() > 1) {
duplicateGroups.push_back(group);
}
}
}
What does this logic do?
- duplicateGroups is a vector of vectors. Each inner vector holds the paths of files that are truly identical.
- The matched vector prevents a file from being counted in multiple groups. Once a file is matched, it is skipped in future iterations.
- For each unmatched file i, the code compares it against every later file j using your filesAreIdentical function. Only confirmed matches join the group.
- Directly below the detection logic you just added (and above the closing return 0;), add the output code that prints the confirmed duplicates:
// Print results
if (duplicateGroups.empty()) {
std::cout << "No duplicate files found.\n";
return 0;
}
for (std::size_t g = 0; g < duplicateGroups.size(); g++) {
const auto& group = duplicateGroups[g];
std::cout << "Duplicate group " << (g + 1) << " (" << group.size() << " files):\n";
for (const auto& path : group) {
std::cout << " " << path.string() << "\n";
}
std::cout << "\n";
}
return 0;
What does this output code do?
If no true duplicates were found, the program prints a clean message and exits. Otherwise, it numbers each duplicate group and lists the file paths underneath.
- Save main.cpp.
Compile and test with real duplicates
To prove the tool works, you need files that are truly identical in content. You will create test duplicates by copying a file.
- Create two duplicate test files by copying main.cpp with different names. Run these commands:
copy main.cpp copy1.cpp
copy main.cpp copy2.cpp
- Confirm the files exist by running:
ls
You should see main.cpp, copy1.cpp, copy2.cpp, and dupfinder.exe in the output.
- Compile the updated program by running:
g++ -std=c++17 main.cpp -o dupfinder.exe
Before you run the program, think about what you expect to see. You have three files with identical content (main.cpp, copy1.cpp, copy2.cpp). Do you think they will appear in one group or separate groups?
- Run the program:
.\dupfinder.exe
You should see one duplicate group containing all three files. The false positives from the size-only approach are gone. Only files confirmed identical by content comparison appear.
Seeing "No duplicate files found" instead?
- Make sure you ran the copy commands in the same folder where dupfinder.exe lives. The program scans its current directory.
- Check that your detection logic replaced ALL of the old size-printing code. If both the old and new code exist, they may conflict.
- Verify that the filesAreIdentical function is defined above main() so the compiler knows about it when it is called.
Help me debug why dupfinder reports no duplicates
✔️ Awesome, I've got everything!
Great. Double check you have saved main.cpp and that your duplicate group appeared in the output.
ⓧ I'd like to double check the full code
Scroll through your main.cpp and confirm each section is present in this order:
- Six includes at the top: <filesystem>, <fstream>, <iostream>, <unordered_map>, <vector>, <cstring>.
- The namespace fs = std::filesystem; line.
- The filesAreIdentical function (opens two files in binary mode, reads 4096-byte chunks, compares with memcmp).
- int main() with the directory scanning and sizeGroups map (unchanged from the previous step).
- The new detection logic: duplicateGroups vector, matched tracker, pairwise comparison loop calling filesAreIdentical.
- The output section: checks if duplicateGroups is empty, then prints numbered groups with file paths.
Make sure the old potentialGroups code is completely gone. There should be no reference to it anywhere in the file.
Your duplicate finder now produces accurate results. No more false positives. Next up, you will format this raw output into a polished report with human-readable sizes and a summary of reclaimable disk space.
Format the Final Report
Your duplicate finder now correctly identifies files with identical content. The output works, but raw file paths and missing context make it hard to quickly see what matters.
In this step, you will format the output into a polished report. Each duplicate group will show the number of files, the wasted space in human-readable units, and a summary line at the bottom telling you exactly how much space you could reclaim.
In this step, get ready to:
- Write a helper function that converts byte counts to human-readable sizes (KB, MB, GB).
- Replace the basic output with a numbered, formatted report showing wasted space per group.
- Add a total summary line and test on a real folder.
Add the humanReadableSize helper function
Printing file sizes in raw bytes (like 15728640) is not useful. You need a function that converts bytes into readable units like "15.0 MB".
- In main.cpp, find the include block at the top of the file. Add these two new includes after #include <fstream>:
#include <fstream>
#include <iostream>
#include <string>
- Then add this include after #include <vector>:
#include <vector>
#include <cstring>
#include <cmath>
- Now add the humanReadableSize function between the namespace fs = std::filesystem; line and the filesAreIdentical function. Copy the following code below the namespace line:
// Convert bytes to a human-readable string (KB, MB, GB)
std::string humanReadableSize(std::uintmax_t bytes) {
const char* units[] = {"B", "KB", "MB", "GB", "TB"};
int unitIndex = 0;
double size = static_cast<double>(bytes);
while (size >= 1024.0 && unitIndex < 4) {
size /= 1024.0;
unitIndex++;
}
// Round to 1 decimal place
size = std::ceil(size * 10.0) / 10.0;
std::string result = std::to_string(size);
// Trim to 1 decimal place
size_t dotPos = result.find('.');
if (dotPos != std::string::npos && result.size() > dotPos + 2) {
result = result.substr(0, dotPos + 2);
}
return result + " " + units[unitIndex];
}
What does this function do?
This function takes a raw byte count and divides it by 1024 repeatedly until the number is below 1024. Each division moves up one unit (B to KB to MB to GB to TB).
The std::ceil call rounds to 1 decimal place so you get clean output like "15.0 MB" instead of "14.999999 MB".
- Save main.cpp.
Replace the report output with formatted groups
The current report loop just prints file paths. You will replace it with a version that calculates wasted space per group and prints a numbered, formatted report with a total summary.
- In main.cpp, find this block near the end of the file (the existing report output):
if (duplicateGroups.empty()) {
std::cout << "No duplicate files found.\n";
return 0;
}
for (std::size_t g = 0; g < duplicateGroups.size(); g++) {
const auto& group = duplicateGroups[g];
std::cout << "Duplicate group " << (g + 1) << " (" << group.size() << " files):\n";
for (const auto& path : group) {
std::cout << " " << path.string() << "\n";
}
std::cout << "\n";
}
return 0;
- Replace that entire block (from if (duplicateGroups.empty()) through return 0;) with this formatted report code:
// Step 3: Print the report
if (duplicateGroups.empty()) {
std::cout << "No duplicate files found.\n";
return 0;
}
std::cout << "=== Duplicate File Report ===\n\n";
std::uintmax_t totalWasted = 0;
for (std::size_t g = 0; g < duplicateGroups.size(); g++) {
const auto& group = duplicateGroups[g];
std::error_code ec;
std::uintmax_t fileSize = fs::file_size(group[0], ec);
std::uintmax_t wasted = fileSize * (group.size() - 1);
totalWasted += wasted;
std::cout << "Duplicate group " << (g + 1)
<< " (" << group.size() << " files, "
<< humanReadableSize(wasted) << " wasted):\n";
for (const auto& path : group) {
std::cout << " " << path.string() << "\n";
}
std::cout << "\n";
}
What does this code do?
For each duplicate group, the code calls fs::file_size on the first file to get the shared size. Wasted space is calculated as fileSize * (group.size() - 1) because you only need to keep one copy of each duplicate.
The totalWasted accumulator tracks the running total across all groups for the summary at the end.
- Immediately after the closing } of that for loop, add the summary line and return statement:
std::cout << "Total: " << duplicateGroups.size() << " duplicate groups found, "
<< humanReadableSize(totalWasted) << " could be reclaimed\n";
return 0;
}
What does the summary line show?
This prints the total number of duplicate groups found and the total disk space that could be reclaimed if you deleted the extra copies. It gives you a quick answer to "how much space am I wasting?" at a glance.
- Save main.cpp.
Add comments to existing sections
While you have the file open, add a comment above the scanning loop that says // Step 1: Scan all files and group by size and one above the duplicate detection that says // Step 2: For each size group with 2+ files, compare content. These make the code easier to navigate as it grows.
Compile and test on a real folder
Before you run this, think about what you expect to see. The output should now have a header, numbered groups with human-readable wasted sizes, and a summary total at the bottom.
- Compile the updated program by running this command in the VS Code terminal:
g++ -std=c++17 main.cpp -o dupfinder.exe
Seeing a compile error?
If you see an error about humanReadableSize not being declared, make sure the function appears ABOVE filesAreIdentical in the file. C++ reads top to bottom, so functions must be defined before they are called.
If you see an error about ceil, confirm you added #include <cmath> at the top of the file.
Still stuck? Help me fix my compile error in dupfinder.
- Run the tool by executing:
.\dupfinder.exe
You should see a formatted report starting with === Duplicate File Report ===, followed by numbered groups showing file count and wasted space (like "2 files, 4.1 KB wasted"), indented file paths, and a summary line at the bottom showing total groups and total reclaimable space.
What if it says no duplicates found?
If you cleaned up your test files from the previous step, you might not have any duplicates in the dupfinder folder. Create a quick test duplicate by copying any file in the folder and giving the copy a different name. Then run again.
✔️ Awesome, I've got everything!
Great. Double check you have saved main.cpp and your compiled dupfinder.exe is producing the formatted report.
ⓧ I'd like to double check the full code
Your main.cpp should contain the following sections in this order. Scroll up and confirm each one is present:
- Includes: <filesystem>, <fstream>, <iostream>, <string>, <unordered_map>, <vector>, <cstring>, <cmath>.
- Namespace alias: namespace fs = std::filesystem;.
- humanReadableSize function: converts bytes to B/KB/MB/GB/TB with 1 decimal place using std::ceil.
- filesAreIdentical function: byte-by-byte binary comparison with 4096-byte buffers.
- main() Step 1: scan files, group by size into sizeGroups.
- main() Step 2: compare content within size groups, build duplicateGroups.
- main() Step 3: print the formatted report with === Duplicate File Report === header, numbered groups with wasted space, and a total summary line.
Your duplicate finder is complete. You have a working CLI tool that scans any folder, finds true duplicates, and tells you exactly how much space is wasted. In the Secret Mission, you will make it even more useful by accepting any folder path as a command-line argument.
Secret mission
Add Command-Line Argument Parsing
Right now your tool only scans the current folder. Real CLI tools let you point them at any directory without editing code. Can you make dupfinder accept a folder path as a command-line argument?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs or background processes. Decide whether to keep your resources for future C++ projects, or delete them to free up disk space.
Resources you used:
- The dupfinder folder on your Desktop containing main.cpp and dupfinder.exe.
- MSYS2 installation at C:\msys64 (includes the MinGW-w64 GCC toolchain).
- The C/C++ extension by Microsoft installed in VS Code.
- A PATH environment variable entry pointing to C:\msys64\ucrt64\bin.
Keep everything running
No action needed. Choose this if you plan to continue writing C++ projects or want to keep dupfinder.exe available for everyday use.
- Your MSYS2 toolchain stays ready for compiling future C++ programs.
- The dupfinder.exe tool is yours to run anytime with no cost.
- Nothing is running in the background or consuming resources while idle.
Pause - I'll come back to this later
Since this project is entirely local with nothing running in the background, pausing just means closing your editor.
- Close VS Code to free up memory.
- Your dupfinder folder, MSYS2 installation, and PATH configuration will all be waiting for you when you come back.
- To resume, open VS Code and open the dupfinder folder again.
Delete - I don't want to use this again
Remove all project files and optionally uninstall the C++ toolchain to start fresh.
- Delete the project folder by opening a Command Prompt and running:
rmdir /s /q "%USERPROFILE%\Desktop\dupfinder"
- To remove MSYS2 and the entire GCC toolchain, run:
rmdir /s /q C:\msys64
- Remove C:\msys64\ucrt64\bin from your PATH. Open Settings, search for environment variables, select Edit environment variables for your account, select Path, click Edit, find the C:\msys64\ucrt64\bin entry, and click Delete.
- To uninstall the C/C++ extension, open VS Code, go to the Extensions sidebar (Ctrl+Shift+X), find C/C++ by Microsoft, and click Uninstall.
Nice Work!
Nice Work!
You just built a real, working command-line tool from scratch in modern C++. Your dupfinder scans folders, identifies exact duplicate files by comparing their actual content, and reports how much disk space you could reclaim.
You've learned how to:
- Set up a complete C++ development environment on Windows using MSYS2, MinGW-w64, and VS Code.
- Traverse the filesystem recursively with std::filesystem and organize data using STL containers like unordered_map and vector.
- Compare files byte-by-byte using binary file I/O with std::ifstream to confirm true duplicates and eliminate false positives.
- Secret Mission: Added command-line argument parsing so the tool accepts any folder path as input.
Ready to quiz yourself?