Check if an executable exists in the Windows path

Viewed 38652

If I run a process with ShellExecute (or in .net with System.Diagnostics.Process.Start()) the filename process to start doesn't need to be a full path.

If I want to start notepad, I can use

Process.Start("notepad.exe");

instead of

Process.Start(@"c:\windows\system32\notepad.exe");

because the direcotry c:\windows\system32 is part of the PATH environment variable.

how can I check if a file exists on the PATH without executing the process and without parsing the PATH variable?

System.IO.File.Exists("notepad.exe"); // returns false
(new System.IO.FileInfo("notepad.exe")).Exists; // returns false

but I need something like this:

System.IO.File.ExistsOnPath("notepad.exe"); // should return true

and

System.IO.File.GetFullPath("notepad.exe"); // (like unix which cmd) should return
                                           // c:\windows\system32\notepad.exe

Is there a predefined class to do this task available in the BCL?

8 Answers

I tried out Dunc's where process and it works, but it's slow and resource-heavy and there's the slight danger of having an orphaned process.

I like Eugene Mala's tip about PathFindOnPath, so I fleshed that out as a complete answer. This is what I'm using for our custom in-house tool.

/// <summary>
/// Gets the full path of the given executable filename as if the user had entered this
/// executable in a shell. So, for example, the Windows PATH environment variable will
/// be examined. If the filename can't be found by Windows, null is returned.</summary>
/// <param name="exeName"></param>
/// <returns>The full path if successful, or null otherwise.</returns>
public static string GetFullPathFromWindows(string exeName)
{
    if (exeName.Length >= MAX_PATH)
        throw new ArgumentException($"The executable name '{exeName}' must have less than {MAX_PATH} characters.",
            nameof(exeName));

    StringBuilder sb = new StringBuilder(exeName, MAX_PATH);
    return PathFindOnPath(sb, null) ? sb.ToString() : null;
}

// https://docs.microsoft.com/en-us/windows/desktop/api/shlwapi/nf-shlwapi-pathfindonpathw
// https://www.pinvoke.net/default.aspx/shlwapi.PathFindOnPath
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode, SetLastError = false)]
static extern bool PathFindOnPath([In, Out] StringBuilder pszFile, [In] string[] ppszOtherDirs);

// from MAPIWIN.h :
private const int MAX_PATH = 260;

Accepted answer states that there is nothing build-in, but this is not true. There is a standard WinAPI PathFindOnPath for doing this, it is available since Windows 2000.

Much shorter and direct, which is what the poster wanted.

FILE *fp
char loc_of_notepad[80] = "Not Found";

// Create a pipe to run the build-in where command
// It will return the location of notepad
fp = popen("cmd /C where notepad", "r");
// Read a line from the pipe, if notepad is found 
// this will be the location (followed by a '\n')
fgets(loc_of_notepad, 80, fp);
fclose(fp);

printf("Notepad Location: %s", loc_of_notepad);

I combined the answers by @Ron and @Hans Passant to create a class that checks for the file path in both App Path registry key, and in PATH by calling PathFindOnPath. It also allows to omit the file extension. In such cases, it probes for several possible "executable" file extensions from PATHEXT.

How to use:

CommandLinePathResolver.TryGetFullPathForCommand("calc.exe"); // C:\WINDOWS\system32\calc.exe

CommandLinePathResolver.TryGetFullPathForCommand("wordpad"); // C:\Program Files\Windows NT\Accessories\WORDPAD.EXE

Here is the code:

internal static class CommandLinePathResolver
{
    private const int MAX_PATH = 260;
    private static Lazy<Dictionary<string, string>> appPaths = new Lazy<Dictionary<string, string>>(LoadAppPaths);
    private static Lazy<string[]> executableExtensions = new Lazy<string[]>(LoadExecutableExtensions);

    public static string TryGetFullPathForCommand(string command)
    {
        if (Path.HasExtension(command))
            return TryGetFullPathForFileName(command);

        return TryGetFullPathByProbingExtensions(command);
    }

    private static string[] LoadExecutableExtensions() => Environment.GetEnvironmentVariable("PATHEXT").Split(';');

    private static Dictionary<string, string> LoadAppPaths()
    {
        var appPaths = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

        using var key = Registry.LocalMachine.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\App Paths");
        foreach (var subkeyName in key.GetSubKeyNames())
        {
            using var subkey = key.OpenSubKey(subkeyName);
            appPaths.Add(subkeyName, subkey.GetValue(string.Empty)?.ToString());
        }

        return appPaths;
    }

    private static string TryGetFullPathByProbingExtensions(string command)
    {
        foreach (var extension in executableExtensions.Value)
        {
            var result = TryGetFullPathForFileName(command + extension);
            if (result != null)
                return result;
        }

        return null;
    }

    private static string TryGetFullPathForFileName(string fileName) =>
        TryGetFullPathFromPathEnvironmentVariable(fileName) ?? TryGetFullPathFromAppPaths(fileName);

    private static string TryGetFullPathFromAppPaths(string fileName) =>
        appPaths.Value.TryGetValue(fileName, out var path) ? path : null;

    private static string TryGetFullPathFromPathEnvironmentVariable(string fileName)
    {
        if (fileName.Length >= MAX_PATH)
            throw new ArgumentException($"The executable name '{fileName}' must have less than {MAX_PATH} characters.", nameof(fileName));

        var sb = new StringBuilder(fileName, MAX_PATH);
        return PathFindOnPath(sb, null) ? sb.ToString() : null;
    }

    [DllImport("shlwapi.dll", CharSet = CharSet.Unicode, SetLastError = false)]
    private static extern bool PathFindOnPath([In, Out] StringBuilder pszFile, [In] string[] ppszOtherDirs);
}
Related