I've written a function that takes a list of extensions and recursively finds files of those types, and logs their paths to a text file.

Usage example (finding image files in a home directory):

// set up filestream for unicode
const std::locale utf8_locale = std::locale(std::locale(), new std::codecvt_utf8<wchar_t>());
std::wofstream log("image_paths.txt", std::ios::app); // append mode

const std::set<std::wstring> image_extensions = {L".jpeg", L".jpg", L".tiff", L".gif", L".bmp", L".png"};
get_files(L"C:\\Users\\username", image_extensions, log);

Output to image_paths.txt:

C:\Users\username\directory\subdirectory\other file.tiff
C:\Users\username\directory with spaces\file with spaces.jpeg

Function Code:

// return 0 -- all good
// return 1 -- root doesn't exist
// return 2 -- root isn't a directory
// return 3 -- no matching files found or error opening first file
// return 4 -- hit recursion limit
auto get_files(_In_ const std::wstring root, // root dir of search
        _In_  const std::set<std::wstring> &ext, // extensions to search for
        _Out_ std::wofstream &log, // file to write paths to
        _In_  unsigned limit = 10 /* default recursion limit */) -> int
    if(limit == 0) return 4;

    // check root path
        DWORD root_attrib = GetFileAttributesW(root.c_str());

        if(root_attrib == INVALID_FILE_ATTRIBUTES) return 1;    // root doesn't exist
        if(!(root_attrib & FILE_ATTRIBUTE_DIRECTORY)) return 2; // root isn't a directory

    LPCWSTR dir; // root directory + "\*"
    HANDLE file = INVALID_HANDLE_VALUE; // handle to found file
    WIN32_FIND_DATAW file_info; // attributes of found file

    // prepare path for use with FindFile functions
    std::wstring root_slash = root;
    dir = root_slash.c_str();

    file = FindFirstFileW(dir, &file_info);

    if(file == INVALID_HANDLE_VALUE) return 3; // no matching files found or error opening first file

    do { // for each file in directory

        // for some reason
        // file_info != L"." && file_info != L".."
        // won't work unless file_info.cFileName is assigned to a var
        std::wstring name = file_info.cFileName;

        std::wstring path = root; // full path to current file

        if(!(file_info.dwFileAttributes & FILE_ATTRIBUTE_READONLY)        // not read-only
                && !(file_info.dwFileAttributes & FILE_ATTRIBUTE_OFFLINE) // not physically moved to offline storage
                && !(file_info.dwFileAttributes & FILE_ATTRIBUTE_SYSTEM)  // not a system file
                && file_info.dwFileAttributes != INVALID_FILE_ATTRIBUTES  // not invalid
                && (name != L"." && name != L"..")) { // not "." or ".."
            if(file_info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) // file is a directory
                get_files(path, ext, log, --limit);
            else // file is not a directory
                if(ext.find(PathFindExtensionW(path.c_str())) != ext.end()) // extension matches
                    log << path << '\n' << std::flush; // log path to file
    } while(FindNextFileW(file, &file_info) != 0);


    return 0;
  • \$\begingroup\$ Welcome to Code Review! You can take the tour for an overview of our site. \$\endgroup\$
    – L. F.
    Oct 26 '19 at 13:55

The declarations for dir and file can be moved lower, where they are first assigned values. However, since dir is only used in one place, it can be eliminated and the value used directly.

HANDLE file = FindFirstFileW(root_slash.c_str(), &file_info);

When testing file attributes, the check for invalid attributes should be first, and you can combine several individual tests into one:

if (file_info.dwFileAttributes != INVALID_FILE_ATTRIBUTES && 
    (name != L"." && name != L".."))

Since there are three attributes you want to ignore, you can define a constant to hold them rather than list them out in your if statement.

Incidentally, since cFileName is a C array, you need to assign it to a string variable to be able to use the equality comparisons with it. Or you could leave it in cFileName and use basic comparisons like strcmp. Since there are two similar strings that are very short, you could also do direct character comparison but that makes the code larger and harder to understand and should only be done when absolutely necessary.

Since you decrement limit with each recursive call to get_files, you reduce the limit for the current directory as well. If your initial directory has 16 subdirectories, your search will skip the 10th, may skip some of the subdirectories of the first 9, and will search all of the directories under the 11th and later subdirectories. You should use

get_files(path, ext, log, limit - 1);

instead. If any of the recursive calls fail in some way (return nonzero) you ignore the error and keep going. This is reasonable in this instance, but does make the "recursion limit reached" error somewhat pointless since it will never be returned to the caller. This one value should probably be handled differently, so that if any search reaches the recursion limit, this value is returned to the original caller to indicate that the results are incomplete.

Potentially more serious is that your extension comparison is case sensitive. A file called "IMAGE.JPG" will not be listed, because the extension ins in uppercase and you're looking for a lowercase one.

Unless there's an absolute need for it, you should omit the std::flush from the log outputs. This will reduce the performance as every filename will be written one at a time, instead of in larger chunks.


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.