如果我使用
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 中是否有预定义的类来执行此任务?
我认为没有任何内置内容,但你可以使用 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;
}
这是有风险的,它不仅仅是搜索 PATH 中的目录。 试试这个:
Process.Start("wordpad.exe");
可执行文件存储在我的计算机上的 c:\Program Files\Windows NT\Accessories 中,该目录不是在路径上。
HKCR\Applications 和 HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths 键也在查找可执行文件中发挥作用。 我相当确定周围还存在其他类似的地雷,例如 64 位版本的 Windows 中的目录虚拟化可能会让您陷入困境。
为了使其更可靠,我认为您需要 pinvoke AssocQueryString()。 不确定,从来没有这个需要。 更好的方法当然是不必问这个问题。
好吧,我认为更好的方法...
这使用 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");
}
}
接受的答案指出没有任何内置内容,但这不是事实。 有一个标准的 WinAPI PathFindOnPath 用于执行此操作,它自 Windows 2000 起可用。
我尝试了 Dunc 的
where
进程,它可以工作,但速度慢且资源消耗大,并且存在出现孤立进程的轻微危险。
我喜欢尤金·马拉关于
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;
更短、更直接,这正是发帖者想要的。
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);
我也在追求同样的事情,我认为我现在最好的选择是使用对 CreateProcess 的本机调用来创建一个挂起的进程并观察成功;之后立即终止该过程。终止挂起的进程不应导致任何资源流失[需要引用:)]
我可能无法找出实际使用的路径,但对于 ExistsOnPath() 这样的简单要求,它应该这样做 - 直到有更好的解决方案。
我结合了 @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);
}
为什么不尝试/捕获
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;
}
}
有关
Win32Exception.NativeErrorCode
的列表,请参阅 https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/18d8fbe8-a967-4f1c-ae50-99ca8e491d2d?redirectedfrom=MSDN
以下代码片段使用实际的 Windows API,而不是您自己的 API。
有人在这里建议了它,我只是需要它,所以我把它编码了。
[DllImport("shlwapi.dll", CharSet = CharSet.Auto, SetLastError = false)]
static extern bool PathFindOnPath([In, Out] StringBuilder pszFile, [In] String[] ppszOtherDirs);
public static bool PathFindOnPathEx(string fileToFindInPath)
{
return PathFindOnPath(new StringBuilder(fileToFindInPath, 260), null);
}