functions/Find-WUPath.ps1

<#
    .SYNOPSIS
    Search for paths in all locations.
 
    .DESCRIPTION
    Search the path everywhere using command path or es.exe.
 
    .OUTPUTS
    System.String.
 
    Returns the path found by searching.
 
    .EXAMPLE
    PS C:\> Find-WUPath 'powershell.exe'
 
    In this example, Finds and returns the path where the leaf contains powershell.exe.
 
    .EXAMPLE
    PS C:\> Find-WUPath 'powershell.exe' -Strict
 
    In this example, Finds and returns the path where the leaf exactly matches powershell.exe.
 
    .EXAMPLE
    PS C:\> Find-WUPath 'PowerShell\v1.0\powershell.exe' -Strict
 
    In this example, Searches for and returns a path whose leaf exactly matches powershell.exe and whose parent path contains PowerShell\v1.0.
 
    .EXAMPLE
    PS C:\> Find-WUPath 'powershell.exe' -Strict -Exclude 'C:\Windows\WinSxS'
 
    In this example, Searches for and returns a path that does not contain C:\Windows\WinSxS and leaves exactly match powershell.exe.
 
    .EXAMPLE
    PS C:\> Find-WUPath 'powershell.exe' -Program
 
    In this example, powershell.exe is searched for in the order of command, start menu shortcut file link destination, es.exe, and the path containing powershell.exe in the leaf is returned.
#>


[CmdletBinding()]
param (
  # Specify the character string contained in the leaf of the path to be searched. In addition, the strings contained in its parent directory can be specified before the leaf, separated by / or \. However, if -Strict is specified, the path with exact leaf matches will be searched. Also, wildcards are not supported.
  [Parameter(Mandatory,
    Position = 0,
    ValueFromPipeline,
    ValueFromPipelineByPropertyName)]
  [ValidateNotNullOrEmpty()]
  [string[]]
  $Name,

  # Excludes paths that include the specified string. The following paths are excluded by default.
  <#
    'C:\Windows\SysWOW64'
    'C:\Windows\WinSxS'
    "$env:LOCALAPPDATA\Microsoft\Windows\FileHistory"
  #>

  [string[]]
  $Exclude = @(
    'C:\Windows\SysWOW64'
    'C:\Windows\WinSxS'
    "$env:LOCALAPPDATA\Microsoft\Windows\FileHistory"
  ),

  # Search for a path that has an exact leaf match.
  [switch]
  $Strict,

  # Searches the command from the leaf of the path specified by -Name and returns the path if found. If not found, searches all locations by using es.exe.
  [switch]
  $Program
)

Set-StrictMode -Version 'Latest'

# 結果を処理するスクリプト
$isCompleated = {
  param (
    [string[]]
    $AddPath
  )

  if (!$AddPath) {
    return $false
  }

  $resultPaths.AddRange($AddPath)
  $resultItems = Get-Item -LiteralPath $resultPaths -ErrorAction Ignore

  if ($Exclude) {
    foreach ($aExclude in $Exclude) {
      $resultItems = $resultItems | Where-Object { !(Select-String -InputObject $_.FullName -SimpleMatch $aExclude) }
    }
  }

  $resultItems = $patterns | ForEach-Object {
    $pattern = $_

    $completedItem = $resultItems |
    Where-Object {
      if (!$pattern.Parent) {
        return $true
      }
      return Select-String -InputObject (Split-Path $_ -Parent) -SimpleMatch $pattern.Parent
    } |
    Where-Object {
      if ($Strict) {
        return $_.Name -eq $pattern.Leaf
      }
      return Select-String -InputObject $_.Name -SimpleMatch $pattern.Leaf
    }

    if ($completedItem) {
      $completedItem
      $completedLeafs.add($pattern.Leaf) | Out-Null
    }
  }

  $resultPaths.Clear()
  if (!$resultItems) {
    return $false
  }
  $resultPaths.AddRange([string[]](Convert-Path -LiteralPath $resultItems.FullName | Select-Object -Unique))

  # $leafsを未取得のもので上書き
  [string[]]$leafs = $leafs | Where-Object { $completedLeafs -notcontains $_ }

  $completedLeafs.Clear()

  return !$leafs
}

# パスセパレータを\で統一
$Name = $Name -replace '/', '\'

$patterns = $Name | ForEach-Object {
  @{
    Leaf   = Split-Path $_ -Leaf
    Parent = Split-Path $_ -Parent
  }
}

$leafs = $patterns.Leaf
$resultPaths = New-Object System.Collections.ArrayList
$completedLeafs = New-Object System.Collections.ArrayList

if ($Program) {
  # コマンドから探す
  [string[]]$cmdPaths = Get-Command $leafs -ErrorAction Ignore | Select-Object -ExpandProperty Path

  if ($cmdPaths) {
    $cmdResultPaths = @()
    [string[]]$scoopShimPaths = $cmdPaths |
    Where-Object {
      (Split-Path $_ -Parent) -like '*scoop\shims' -and
      (Split-Path $_ -Leaf) -match '\.exe$'
    }
    $GeneralCmdPaths = $cmdPaths | Where-Object { $scoopShimPaths -notcontains $_ }

    $cmdResultPaths += $GeneralCmdPaths

    if ($scoopShimPaths) {
      if ((Get-Command -Name scoop -ErrorAction Ignore)) {
        $scoopCmdPaths = Split-Path $scoopShimPaths -Leaf | ForEach-Object {
          scoop which ($_ -replace '\.exe$', '')
        }
        $cmdResultPaths += $scoopCmdPaths
      }
      else {
        $cmdResultPaths += $scoopShimPaths
      }
    }

    if ((& $isCompleated -AddPath $cmdResultPaths)) {
      return $resultPaths
    }
  }

  # ショートカットから探す
  $lnkDirs = @(
    "$env:APPDATA\Microsoft\Windows\Start Menu"
    "C:\ProgramData\Microsoft\Windows\Start Menu"
  )

  $lnkResultPaths = Get-WULnkTarget -LiteralPath (Get-ChildItem -LiteralPath $lnkDirs -Recurse | Where-Object { $_.Extension -eq '.lnk' } | Select-Object -ExpandProperty FullName) -WarningAction Ignore

  if ((& $isCompleated -AddPath $lnkResultPaths)) {
    return $resultPaths
  }
}

# es.exeで探す
[string[]]$esResultPaths = $leafs | ForEach-Object {
  es.exe $_
}

if ((& $isCompleated -AddPath $esResultPaths)) {
  return $resultPaths
}

return $resultPaths