检查可执行文件是否存在于Windows路径中。

77

如果我使用ShellExecute(或在.NET中使用System.Diagnostics.Process.Start())运行一个进程,要启动的文件名不需要是完整路径。

如果我想启动记事本,可以使用:

Process.Start("notepad.exe");

替代

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

因为目录 c:\windows\system32 是 PATH 环境变量的一部分。 如何在不执行过程和不解析 PATH 变量的情况下检查文件是否存在于 PATH 中?
System.IO.File.Exists("notepad.exe"); // returns false
(new System.IO.FileInfo("notepad.exe")).Exists; // returns false

但我需要像这样的东西:

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

并且

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

在BCL中有没有预定义的类来完成这个任务?

虽然这样一个预定义类会很方便(如果存在的话),但只需再加一行代码获取路径并检查 exists(),不是更快吗?你写代码的时间比提问还要短。有特殊原因或需要吗?只是好奇。 - MickeyfAgain_BeforeExitOfSO
4
没问题,应该很容易。但是我坚信,如果一个任务可以用编程语言现有的库来完成,我更倾向于这种方式,而不是一遍又一遍地重新发明轮子。如果没有可用的东西,我就自己动手实现。 - Jürgen Steinblock
解析PATH是特定于平台的(例如,子目录的/\,以及条目分隔符的:;),并且希望一致地处理模糊结果 - 因此它不像“获取路径然后检查exists()”那样简单。此外,如果有数十个要检查的路径,或者任何一个是远程/网络路径,则在程序代码中进行检查可能会对性能产生重大影响,例如,而不是操作系统具有预缓存结果。等等。 - Dai
9个回答

76

我认为没有内置的方法,但是你可以使用 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;
}

3
如果您想这样做,我建议将它们转换为扩展方法... http://msdn.microsoft.com/en-us/library/bb383977.aspx - Aaron McIver
8
你确定你会把 GetFullPath 视为 string 的扩展方法吗?这对我来说听起来有些奇怪...也许对于 FileInfo 来说会更有意义... - digEmAll
6
@Aaron:由于种种原因(例如,如果我只需要一个字符串,为什么我还要通过FileInfo),我仍然喜欢将它们作为静态方法,或许包装在Utilities静态类中,但我明白这可能是有争议的......不管怎样,对于提问者来说,将上面的代码转换为扩展方法很容易 ;) - digEmAll
5
这段代码不适用于其他平台,在Unix上,您需要使用Path.PathSeparator代替硬编码的分号。 - Grzegorz Adam Hankiewicz
我认为不应该使用依赖于外部IO的扩展方法,因为这会给单元测试带来问题。 - Etienne Charland
显示剩余4条评论

32

这是有风险的,它不仅仅是在PATH中搜索目录那么简单。请尝试以下操作:

 Process.Start("wordpad.exe");

可执行文件存储在我的机器上的c:\Program Files\Windows NT\Accessories目录中,但该目录在路径中。

HKCR\Applications和HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths键也在查找可执行文件时起着作用。我相当确定还有其他类似的“地雷”,例如64位版本的Windows中的目录虚拟化可能会导致问题。

为了使此更加可靠,我认为您需要使用pinvoke AssocQueryString()。 不确定,从未有过这样的需求。 更好的方法当然是不必提出问题。


我想查询的应用程序将自己注册到路径(mysqldump.exe)。如果没有注册,或者未安装,则希望在Windows窗体应用程序中禁用使用mysqlbackup的选项。我只是不想硬编码文件路径。 - Jürgen Steinblock
现在安装程序很少修改PATH,特别是对于实用工具,首先要检查一下。我会在这里使用应用程序范围的设置,并将其默认为“”。 - Hans Passant
7
这是雷蒙德·陈最近发表的一篇博客文章的主题。他的博客技巧难以超越,除了我比他更早之外。请享受:http://blogs.msdn.com/b/oldnewthing/archive/2011/07/25/10189298.aspx。 - Hans Passant
2
更新了雷蒙德·陈的帖子链接:https://devblogs.microsoft.com/oldnewthing/20110725-00/?p=10073 - kojo

23

好的,我认为有一个更好的方法...

这个方法使用了where命令,在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");
    }
}

14
接受的答案声称没有内置的方法,但这是不正确的。 自从Windows 2000以来,有一个标准的WinAPI PathFindOnPath 可以做到这一点。

13

我试用了Dunc的where进程,它可以正常工作,但速度较慢且资源占用较多,存在进程孤立的微小风险。

我喜欢Eugene Mala关于PathFindOnPath的技巧,因此我将其完整地补充为一个答案。这就是我们自定义内部工具所使用的内容。

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

3
更加简短直接,符合海报的要求。
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);

4
请您更新这个回答,增加解释您所做的事情及其与被接受答案的不同之处。这总是最佳实践,但尤其是在回答已有确定答案的老问题时更为重要。通常,随着库和功能的变化,新增答案可能有很好的理由--当问题已经存在十年以上时就绝对是如此!--但如果没有任何解释,社区将无法理解您的答案为何优于现有答案。谢谢您的考虑。 - Jeremy Caney
只允许回答代码,但最好也解释一下答案。考虑添加一些说明。 - zonksoft
3
谢谢,但是这个问题有C#和.NET标签。 - Jürgen Steinblock
你仍然可以在C#中做同样的事情,对我来说似乎并不太糟。我认为我会采用这种方法,因为它不依赖于环境变量的字符串拆分。 - Romout

2

我也想做同样的事情,目前我认为最好的选择是使用本机调用CreateProcess来创建一个被挂起的进程,并观察成功;立即终止该进程。终止一个挂起的进程不应该导致任何资源流失 [需要引证 :)]

我可能无法弄清楚实际使用的路径,但对于如ExistsOnPath()这样简单的要求,它应该可行-直到有更好的解决方案出现。


即使您创建了挂起的进程,仍可能执行某些代码部分。如果您想测试系统路径中是否存在某些恶意软件或病毒,则此方法非常危险! - Eugene Mala

2

我结合了@Ron和@Hans Passant的答案,创建了一个类来检查文件路径,它会在App Path注册表键和PATH中调用PathFindOnPath。它还允许省略文件扩展名。在这种情况下,它会从PATHEXT中探测几个可能的“可执行”文件扩展名。

如何使用:

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

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

这里是代码:

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

0
为什么不尝试使用try/catch捕获Process.Start()方法,并在catch中处理任何问题?
唯一的问题可能是当所需的可执行文件未找到时,Process.Start()会返回一个相当不具体的Win32Exception。因此,像catch (FileNotFoundException ex)这样的写法是不可能的。
但是你可以使用Win32Exception.NativeErrorCode属性来进一步分析解决这个问题:
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;
    }
}

请参阅https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/18d8fbe8-a967-4f1c-ae50-99ca8e491d2d?redirectedfrom=MSDN以获取Win32Exception.NativeErrorCode列表。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接