74

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 directory 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?

mklement0
  • 382,024
  • 64
  • 607
  • 775
Jürgen Steinblock
  • 30,746
  • 24
  • 119
  • 189
  • While such a predefined class would be convenient (or is convenient, if it exists) isn't it only one more line to get the path then check exists()? You could have written it more quickly than asking the question. Special reason/need? Just wondering. – MickeyfAgain_BeforeExitOfSO Oct 04 '10 at 14:10
  • 4
    Yepp, should be very easy. But my conviction is that, if a task can be done with the existing library of a probramming language, I favor this way over reinventing the weel again and again. If there isn't smth available, I do it my own. – Jürgen Steinblock Oct 04 '10 at 16:10
  • @MickeyfAgain_BeforeExitOfSO Parsing `PATH` is platform-specific (e.g. `/` vs ``\`` for subdirs, and `:` vs `;` for entry separators), as well as wanting to consistently handle ambiguous results - so it isn't as simple as "get the path then check exists()". Also, if there's dozens or paths to check, or if any are remote/network paths, then there might be significant performance implications of checking in program-code instead of the OS having a pre-cached result, for example. Etc. etc. – Dai Mar 24 '22 at 10:25

9 Answers9

75

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;
}
Can Baycay
  • 875
  • 6
  • 11
digEmAll
  • 56,430
  • 9
  • 115
  • 140
  • 3
    If you are going to do this, I suggest turning these into Extension Methods...http://msdn.microsoft.com/en-us/library/bb383977.aspx – Aaron McIver Oct 04 '10 at 14:42
  • 8
    @Aaron: Are you sure you would see `GetFullPath` as extension method for `string` ? It would sound odd to me... Maybe could have sense for `FileInfo`... – digEmAll Oct 04 '10 at 14:57
  • Yes it would be strange when using a string. However I think it would make sense to wrap the above functionality from both methods into a single extension method titled ExistsOnPath which hangs off FileInfo as you mentioned. – Aaron McIver Oct 04 '10 at 15:04
  • 6
    @Aaron: For several reasons (e.g. why I have to pass through FileInfo if I only need a string...), I still prefer them as static methods, perhaps wrapped in Utilities static class, but I understand that could be arguable... Anyway, for the questioner, is easy to trasform the above code in Extension method ;) – digEmAll Oct 04 '10 at 15:28
  • 5
    This code is not portable to other platforms, on Unix you need to use `Path.PathSeparator` instead of hardcoding the semi colon. – Grzegorz Adam Hankiewicz Jul 03 '16 at 08:50
  • @GrzegorzAdamHankiewicz: the question specifically states "windows path", so yes, it's not portable... anyway thanks for pointing out that with a minor change it can be used on unix-like OS :) – digEmAll Jul 03 '16 at 11:40
  • @EugeneMala: yes, but you need a p/invoke to call that API (e.g. https://www.pinvoke.net/default.aspx/shlwapi.PathFindOnPath) – digEmAll Jun 20 '18 at 16:36
  • @digEmAll This answer doesn't include all possible places for the system executable. For example, it doesn't take into account "C:\Program Files\Windows NT\Accessories" directory path. – lol lol Jun 08 '22 at 12:16
  • I would argue against using extension methods that rely on external IO dependencies, as this will cause problems for unit testing. – Etienne Charland Jul 12 '22 at 18:03
32

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.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • the application that I want to query registers itself to the path (mysqldump.exe). If not, or if not installed, I want to disable the option to use mysqlbackup from a windows forms application. I just don't want to hard code the path to the file. – Jürgen Steinblock Oct 04 '10 at 17:07
  • It is *very* rare these days for installers to modify the PATH. Especially for a utility, check that first. I would just use a setting with Application scope and default to "" here. – Hans Passant Oct 04 '10 at 17:11
  • 7
    This was the subject of a recent Raymond Chen post. Hard to beat his blogging skills, other than I was first. Enjoy: http://blogs.msdn.com/b/oldnewthing/archive/2011/07/25/10189298.aspx – Hans Passant Aug 01 '11 at 00:52
  • 2
    Updated link to Raymond Chen's post: https://devblogs.microsoft.com/oldnewthing/20110725-00/?p=10073 – kojo Mar 09 '20 at 10:25
23

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");
    }
}
Jos van Egmond
  • 2,370
  • 15
  • 19
Dunc
  • 18,404
  • 6
  • 86
  • 103
13

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://learn.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;
Martin Prikryl
  • 188,800
  • 56
  • 490
  • 992
Ron
  • 1,888
  • 20
  • 25
12

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

mklement0
  • 382,024
  • 64
  • 607
  • 775
Eugene Mala
  • 1,081
  • 12
  • 25
3

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);
MattR
  • 85
  • 3
  • 4
    Could you update this answer to provide additional explanation of what you're doing and why it differs from the accepted? That's _always_ a best practice, but it's _especially_ important when responding to old questions with established answers. Often, there are good reasons for new answers as libraries and capabilities change—that's certainly true when a question is a decade old!—but without any explanation, the community isn't going to understand why your answer might be preferable to the existing answers. Thanks for your consideration. – Jeremy Caney Jun 04 '20 at 00:19
  • Code only answers are allowed, but it's encouraged to explain the answer as well. Consider adding some explanation. – zonksoft Jun 04 '20 at 16:20
  • 3
    Thanks, but the question has a c# and .net tag. – Jürgen Steinblock Jun 05 '20 at 05:01
  • You still could do the same in C#, doesn't seem too bad to me. I think, I will use that approach as it is not depending on string splitting of an environment variable. – Romout Jul 15 '20 at 14:51
2

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.

alleey
  • 21
  • 1
  • There could be some portions of code that will be executed even if you create suspended process. If you want to test if some malware or virus exists in system path - this method is very dangerous! – Eugene Mala Feb 07 '19 at 05:38
1

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);
}
ezolotko
  • 1,723
  • 1
  • 21
  • 21
0

Why don't you try / catch the Process.Start() method and handle any problems in the catch?

The only problem might be that Process.Start() will return a rather unspecific Win32Exception when the required executable is not found. So something like catch (FileNotFoundException ex) is not possible.

But you can solve that using the Win32Exception.NativeErrorCode property for further analysis:

try
{
    Process proc = new Process();
    proc.StartInfo.FileName = "...";
    proc.Start();
    proc.WaitForExit();
}
// check into Win32Exceptions and their error codes!
catch (Win32Exception winEx)  
{
    if (winEx.NativeErrorCode == 2 || winEx.NativeErrorCode == 3) {
        // 2 => "The system cannot find the FILE specified."
        // 3 => "The system cannot find the PATH specified."
        throw new Exception($"Executable not found in path");
    }
    else
    {
        // unknown Win32Exception, re-throw to show the raw error msg
        throw;
    }
}

For a list of Win32Exception.NativeErrorCodes see https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/18d8fbe8-a967-4f1c-ae50-99ca8e491d2d?redirectedfrom=MSDN

Jpsy
  • 20,077
  • 7
  • 118
  • 115