Utils/which.ps1

Register-GnuUtil @{
    Name        = 'which'
    Category    = 'Searching and comparing'
    Synopsis    = 'Show the full path of (shell) commands.'
    Usage       = @('which [options] [--] COMMAND [...]')
    Description = @('Write the full path of COMMAND(s) to standard output.')
    Options     = @(
        @{ Short = 'a'; Long = 'all'; Help = 'Print all matches in PATH, not just the first' }
        @{ Long = 'skip-dot'; Help = 'Skip directories in PATH that start with a dot.' }
        @{ Long = 'skip-tilde'; Help = 'Skip directories in PATH that start with a tilde (or your home directory).' }
        @{ Long = 'show-dot'; Help = "Don't expand a dot to current directory in output." }
        @{ Long = 'show-tilde'; Help = 'Output a tilde for HOME directory for non-root.' }
    )
    Notes       = @(
        'PowerShell notes: only programs and scripts in PATH are found (not aliases or functions; use',
        'Get-Command for those). On Windows a COMMAND without extension is looked up with every PATHEXT extension.'
    )
}



function script:Test-GnuExecutable {
    <#
    .SYNOPSIS
    $true if Path is an existing file that can be run: on Windows by extension, on Unix by an execute bit.
    #>

    param([Parameter(Mandatory)][string]$Path, [AllowEmptyCollection()][string[]]$Extensions = @())

    if (-not [System.IO.File]::Exists($Path)) { return $false }
    if (Test-GnuWindows) {
        $extension = [System.IO.Path]::GetExtension($Path)
        foreach ($candidate in $Extensions) { if ([string]::Equals($extension, $candidate, [System.StringComparison]::OrdinalIgnoreCase)) { return $true } }
        return $false
    }
    $mode = Get-GnuUnixFileMode -Path $Path
    if ($null -eq $mode) { return $true }   # runtime cannot read modes: trust the file system
    ($mode -band 73) -ne 0                  # 0111: any execute bit
}

function script:Invoke-GnuWhichSearch {
    <#
    .SYNOPSIS
    Implementation of which.
    #>

    param([Parameter(Mandatory)][hashtable]$Context)

    $commands = @($Context.Operands)
    if ($commands.Count -eq 0) {
        Write-GnuUsageError -Context $Context -Message 'missing operand'
        return
    }

    $all = Test-GnuOption -Context $Context -Key 'all'
    $pathText = [string]$env:PATH
    $directories = @($pathText.Split([System.IO.Path]::PathSeparator) | Where-Object { -not [string]::IsNullOrEmpty($_) })
    $homeDirectory = [string]$env:HOME
    if (-not $homeDirectory) { $homeDirectory = [string]$env:USERPROFILE }
    if (Test-GnuOption -Context $Context -Key 'skip-dot') { $directories = @($directories | Where-Object { -not $_.StartsWith('.', [System.StringComparison]::Ordinal) }) }
    if (Test-GnuOption -Context $Context -Key 'skip-tilde') {
        $directories = @($directories | Where-Object { -not $_.StartsWith('~', [System.StringComparison]::Ordinal) -and -not ($homeDirectory -and $_.StartsWith($homeDirectory, [System.StringComparison]::OrdinalIgnoreCase)) })
    }

    $extensions = @()
    if (Test-GnuWindows) {
        $pathExt = [string]$env:PATHEXT
        if (-not $pathExt) { $pathExt = '.COM;.EXE;.BAT;.CMD' }
        $extensions = @($pathExt.Split(';') | Where-Object { -not [string]::IsNullOrEmpty($_) })
    }

    foreach ($command in $commands) {
        $names = @($command)
        if ((Test-GnuWindows) -and [string]::IsNullOrEmpty([System.IO.Path]::GetExtension($command))) { $names = @(foreach ($extension in $extensions) { $command + $extension }) }

        $found = New-Object 'System.Collections.Generic.List[string]'
        if ($command.IndexOfAny((Get-GnuPathSeparator)) -ge 0) {
            foreach ($name in $names) {
                $path = Resolve-GnuPath -Path $name
                if (Test-GnuExecutable -Path $path -Extensions $extensions) { $found.Add($name); break }
            }
        }
        else {
            foreach ($directory in $directories) {
                $expanded = $directory
                if ($directory -eq '.' -or $directory.StartsWith('.' + [System.IO.Path]::DirectorySeparatorChar, [System.StringComparison]::Ordinal)) {
                    $expanded = Resolve-GnuPath -Path $directory
                }
                foreach ($name in $names) {
                    $candidate = [System.IO.Path]::Combine($expanded, $name)
                    if (-not (Test-GnuExecutable -Path $candidate -Extensions $extensions)) { continue }
                    $shown = $candidate
                    if ((Test-GnuOption -Context $Context -Key 'show-dot') -and $directory -ne $expanded) { $shown = [System.IO.Path]::Combine($directory, $name) }
                    if ((Test-GnuOption -Context $Context -Key 'show-tilde') -and $homeDirectory -and $shown.StartsWith($homeDirectory, [System.StringComparison]::OrdinalIgnoreCase)) {
                        $shown = '~' + $shown.Substring($homeDirectory.Length)
                    }
                    if (-not $found.Contains($shown)) { $found.Add($shown) }
                    if (-not $all) { break }
                }
                if ($found.Count -gt 0 -and -not $all) { break }
            }
        }

        if ($found.Count -eq 0) {
            Write-GnuError -Context $Context -Message "no $command in ($pathText)"
            continue
        }
        foreach ($path in $found) { $path }
    }
}