Check if an executable exists in the Windows path

C#.NetFile

C# Problem Overview


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?

C# Solutions


Solution 1 - C#

I think there's nothing built-in, but you could do something like this with System.IO.File.Exists:

public static bool ExistsOnPath(string fileName)
{
    return GetFullPath(fileName) != null;
}

public static string GetFullPath(string fileName)
{
    if (File.Exists(fileName))
        return Path.GetFullPath(fileName);

    var values = Environment.GetEnvironmentVariable("PATH");
    foreach (var path in values.Split(Path.PathSeparator))
    {
        var fullPath = Path.Combine(path, fileName);
        if (File.Exists(fullPath))
            return fullPath;
    }
    return null;
}

Solution 2 - C#

This is risky, there's a lot more to it than just searching the directories in the PATH. Try this:

 Process.Start("wordpad.exe");

The executable is stored in c:\Program Files\Windows NT\Accessories on my machine, that directory is not on the path.

The HKCR\Applications and HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths keys also play a role in finding executables. I'm fairly sure there are additional land-mines like this around, directory virtualization in 64-bit versions of Windows could trip you up for example.

To make this more reliable I think you need to pinvoke AssocQueryString(). Not sure, never had the need. The better approach is certainly to not have to ask the question.

Solution 3 - C#

Ok, a better way I think...

This uses the where command, which is available at least on Windows 7/Server 2003:

public static bool ExistsOnPath(string exeName)
{
	try
	{
		using (Process p = new Process())
		{
			p.StartInfo.UseShellExecute = false;
			p.StartInfo.FileName = "where";
			p.StartInfo.Arguments = exeName;
			p.Start();
			p.WaitForExit();
			return p.ExitCode == 0;
		}
	}
	catch(Win32Exception)
	{
		throw new Exception("'where' command is not on path");
	}
}

public static string GetFullPath(string exeName)
{
	try
	{
		using (Process p = new Process())
		{
			p.StartInfo.UseShellExecute = false;
			p.StartInfo.FileName = "where";
			p.StartInfo.Arguments = exeName;
    	    p.StartInfo.RedirectStandardOutput = true;
			p.Start();
    	    string output = p.StandardOutput.ReadToEnd();
			p.WaitForExit();
		
			if (p.ExitCode != 0)
				return null;
		
			// just return first match
			return output.Substring(0, output.IndexOf(Environment.NewLine));
		}
	}
	catch(Win32Exception)
	{
		throw new Exception("'where' command is not on path");
	}
}

Solution 4 - C#

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;

Solution 5 - C#

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.

Solution 6 - C#

I'm after the same thing and I think the best option that I have right now is to use native call to CreateProcess to create a process suspended and watch for success; terminating the process immediately afterward. Terminating a suspended process should not incur any resource bleeding [citation needed :)]

I may not be able to figure out the path that actually got used but for a simple requirement as ExistsOnPath() it should do - till there's a better solution.

Solution 7 - C#

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);

Solution 8 - C#

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);
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJ&#252;rgen SteinblockView Question on Stackoverflow
Solution 1 - C#digEmAllView Answer on Stackoverflow
Solution 2 - C#Hans PassantView Answer on Stackoverflow
Solution 3 - C#DuncView Answer on Stackoverflow
Solution 4 - C#RonView Answer on Stackoverflow
Solution 5 - C#Eugene MalaView Answer on Stackoverflow
Solution 6 - C#alleeyView Answer on Stackoverflow
Solution 7 - C#MattRView Answer on Stackoverflow
Solution 8 - C#ezolotkoView Answer on Stackoverflow