File: CommandPathResolver.cs
Web Access
Project: src\src\Aspire.Hosting.Tasks\Aspire.Hosting.Tasks.csproj (Aspire.Hosting.Tasks)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
namespace Aspire.Hosting.Tasks;
 
internal static class CommandPathResolver
{
    public static string? ResolveFromPath(string command, string? path = null)
    {
        return EnumerateFromPath(command, path).FirstOrDefault();
    }
 
    public static IEnumerable<string> EnumerateFromPath(string command, string? path = null)
    {
        path ??= Environment.GetEnvironmentVariable("PATH");
        if (string.IsNullOrWhiteSpace(path))
        {
            yield break;
        }
 
        var executableNames = GetExecutableNames(command);
        var seenPaths = new HashSet<string>(IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal);
 
        // PATH entries can be quoted, for example:
        //   C:\Tools;"C:\Program Files\dotnet";C:\Users\user\.dotnet\tools
        foreach (var pathEntry in path.Split(Path.PathSeparator))
        {
            var directory = TryGetFullPath(pathEntry.Trim().Trim('"'));
            if (directory is null)
            {
                continue;
            }
 
            foreach (var executableName in executableNames)
            {
                var candidate = Path.Combine(directory, executableName);
                if (seenPaths.Add(candidate) && FileExistsAndIsExecutable(candidate))
                {
                    yield return candidate;
                }
            }
        }
    }
 
    private static string? TryGetFullPath(string path)
    {
        if (string.IsNullOrWhiteSpace(path))
        {
            return null;
        }
 
        try
        {
            return Path.GetFullPath(path);
        }
        catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException or System.Security.SecurityException)
        {
            return null;
        }
    }
 
    private static string[] GetExecutableNames(string command)
    {
        return IsWindows()
            ? [$"{command}.exe", $"{command}.cmd", $"{command}.bat", command]
            : [command];
    }
 
    private static bool FileExistsAndIsExecutable(string path)
    {
        if (!File.Exists(path))
        {
            return false;
        }
 
#if NET
        if (OperatingSystem.IsWindows())
        {
            return true;
        }
 
        try
        {
            const UnixFileMode ExecuteBits = UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute;
 
            return (File.GetUnixFileMode(path) & ExecuteBits) != 0;
        }
        catch (IOException)
        {
            return false;
        }
        catch (UnauthorizedAccessException)
        {
            return false;
        }
#else
        // .NET Framework only runs on Windows, where file existence is sufficient.
        return true;
#endif
    }
 
    private static bool IsWindows() => Path.DirectorySeparatorChar == '\\';
}