How to list files in a directory using the Windows API?
Asked Answered
P

4

15

I have this code and it displays the folder with the directory itself and not its contents. I want to display its contents. I don't want to use boost::filesystem.

How can I resolve this?

Code:

#include <windows.h>
#include <iostream>

int main()
{
    WIN32_FIND_DATA data;
    HANDLE hFind = FindFirstFile("C:\\semester2", &data);      // DIRECTORY

    if ( hFind != INVALID_HANDLE_VALUE ) {
        do {
            std::cout << data.cFileName << std::endl;
        } while (FindNextFile(hFind, &data));
        FindClose(hFind);
    }
}

Output:

semester2
Petitionary answered 31/12, 2016 at 1:42 Comment(3)
Possible duplicate of How to get Current Directory?Amphiarthrosis
@RawN Well, not really, but it gave me an idea! Now it's working! Thanks! :)Petitionary
Side-note: You're using a narrow character literal with compile-time chosen character width APIs. Modern build environments tend to default to Unicode builds, which will cause this to break. I'd strongly suggest either using explicitly Unicode types and APIs everywhere (add W suffix to WIN32_FIND_DATA, FindFirstFile, & FindNextFile, L prefix to path literal, replace std::cout with std::wcout) or using TCHARs consistently (add #include <tchar.h>, make the string literal _T("C:\\semester2"), and conditionally alias std::tcout to std::cout/std::wcout as appropriate).Ramah
C
17
HANDLE hFind = FindFirstFile("C:\\semester2", &data);       // DIRECTORY

You got the directory because that's what you asked for. If you want the files, ask for them:

HANDLE hFind = FindFirstFile("C:\\semester2\\*", &data);  // FILES

(You can instead use *.* if you prefer, but apparently this only works because of a backwards compatibility hack so should probably be avoided. See comments and RbMm's answer.)

Culinary answered 31/12, 2016 at 1:51 Comment(8)
Awesome! Thanks! One question, why are there dots (.) before it displays the files? I've edited the output at my question with your change :)Petitionary
Every directory (except the root directory) contains a link to itself . and a link to its parent .., if you don't want to include these it is easy to skip them.Culinary
Understood. How can it be skipped?Petitionary
Nothing sophisticated. Typically just a couple of string comparisons and an if statement.Culinary
Ah, okay. Gotcha. Thanks :)Petitionary
*.* is just a throwback to DOS. It is clearer in my view to use * instead.Unskilled
I know that I may be a little late but is there an optimized way to avoid those string compariosns? Thanks in advance!Shipentine
@BahramGozalov, not that I know of, but don't worry about it. Calling into the kernel is expensive enough that the cost of the string comparisons is negligible.Culinary
D
11

Let me take some notes about "*.*" vs "*". These filers are not equal.

2 different files can exist in our folder: somefile and somefile..

If we used the low level api ZwQueryDirectoryFile with "*.*" as a search expression (this is the 10th parameter - FileName [in, optional] ) - we would get somefile. only. But if we used "*" we'd get both files - somefile and somefile.

If we try FindFirstFile("C:\\semester2\\*.*", &data); we can note than both files somefile and somefile. are returned. So here "*.*" vs "*" have the same effect - no difference in usage.

Why does this happen? Because inside FindFirstFileEx in kernelbase (kernel32 ) do special check for "*.*" mask and if it true - replace to "" (An empty name which have the same effect as "*" ).

I think this is done to fix a very common error when users pass "*.*" instead of the correct "*" and for backward compatability with legacy code.

. and .. aren't actually part of the directory as it is stored on disk, but are added by the Win32 API.

This is not true.

  • for FAT-style file system this is really stored on FAT directory as 2 first entry.
  • in NTFS there are no such entries, but NTFS.sys artificially add this 2 entries if they in mask.

So this is done not at Win32 API level, but in kernel - on driver level.

In conclusion, "*.*" will work correct with Win32 API how minimum now - but the correct and clean way is to use "*" here.
"*.*" will be mistake with ZwQueryDirectoryFile api.

Dorsum answered 31/12, 2016 at 12:53 Comment(5)
The *.* support is a backwards compatibility feature, short filenames always had extensions. (The extension might be empty, but it was there.) You're right about the dot and double-dot being part of the kernel rather than Win32, I had observed that long paths that used them didn't work at the command line (e.g., tab completion doesn't work) but looking more closely that seems to be something to do with the way cmd.exe works. A long path with a dot works if passed to notepad, for example.Culinary
@HarryJohnston - yes, i fast looking FindFirstFileEx and note that it do special check for "*.*" and fix this to "" - so faster of all for backwards compatibility hack. i however was not familiar with this before, because always use only ZwQueryDirectoryFile in own code, and here "*.*" not return files like somefile (but return somefile.)Dorsum
I hope you'll forgive me if I point out that that's one of the reasons why people recommend against using the native API unnecessarily. :-)Culinary
@HarryJohnston - it you'll excuse me that i have mentioned NT api here. it seems like obscene swearword for 99%+ of usermode programmers. but here only for show at which level system do hack for "*.*". about NT api i have another opinion (i think it is right that it does not make such hacks), i like it for direct return error code (at not convert it with loss of accuracy), strict uniformed signatures (compare win32), much more power and effective compare win32, for the possibility wich not exist at all in win32.. but all this already is off-topic here. i not advice to anybode use this :)Dorsum
@HarryJohnston - anyway thank you. excuse me my english and native api :)Dorsum
M
1

Here is an example implementation:

#include <iostream>
#include <vector>
#include <string>
#include <Windows.h>

std::vector<std::string>
list_directory(
    const std::string &directory)
{
    WIN32_FIND_DATAA findData;
    HANDLE hFind = INVALID_HANDLE_VALUE;
    std::string full_path = directory + "\\*";
    std::vector<std::string> dir_list;

    hFind = FindFirstFileA(full_path.c_str(), &findData);

    if (hFind == INVALID_HANDLE_VALUE)
        throw std::runtime_error("Invalid handle value! Please check your path...");

    while (FindNextFileA(hFind, &findData) != 0)
    {
        dir_list.push_back(std::string(findData.cFileName));
    }

    FindClose(hFind);

    return dir_list;
}

Note: It would be much better to use something like boost::filesystem if your using C++ 11 or std::filesystem if your using C++ 17. Also the input path has to be like C:\path and not C:\path\ otherwise this wont work!!

Malo answered 19/1, 2021 at 13:49 Comment(2)
std::filesystem::path is available since C++ 17 which might be preferable over boost.Carapace
I know, but some people still use C++ 11Malo
J
0

Harrys answer will actually yield files and folders having an extension in your desired folder "C:\\semester2".

So for example if you have a folder named "C:\\semester2\\math.course" it will also be found by the above example. Moreover if you will have a file named "C:\\semester2\\math_scores" (notice it not having an extension) it will not be found.

Taking the above into consideration I would suggest the following solution:

HANDLE hFind = FindFirstFile("C:\\semester2\\*", &data); 

This will list the entire listing of items under the directory. Filtering the directories can be done in the following way:

if (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
// directory
}
else
{
// file
}

The following can be used for references: FileAttributes constants, FIND_DATA struct, FindFirstFile API

Jewish answered 31/12, 2016 at 9:44 Comment(1)
That's not true. *.* will find files without extensions. See RbMm's answer for more detail.Culinary

© 2022 - 2024 — McMap. All rights reserved.