pf-exe.ps1

function Register-Exe($path) {
    $cachePath = "$env:TEMP\Find-App.cache.json"

    if (-not $global:Exe_Registry) {
        $global:Exe_Registry = @{} 
        if ( Test-Path $cachePath ) {
            $json = Get-Content -Path $cachePath -Raw
           # $global:Exe_Registry = ConvertFrom-Json $json
        }
    }

    $resolvedPath = $Exe_Registry[$path]
    if ( $resolvedPath ) {
        return
    }

    $resolvedPath = Resolve-Exe $path

    if ( -not $resolvedPath ) {
        if ( -not ( Test-Path $path -ErrorAction SilentlyContinue ) ) {
            $resolvedPath = Find-App $path | Get-Path 
            if (-not $resolvedPath) {
                throw "Exe not found : '$path' "
            }
            $path = $resolvedPath
        }
        $resolvedPath = Resolve-Path $path | Get-Path
    }
    $exeName = Split-Path $resolvedPath -Leaf
    $exeName = $exeName.ToLowerInvariant()
        
    $Exe_Registry[$exeName] = $resolvedPath 

    $exeName = [System.IO.Path]::GetFileNameWithoutExtension($exeName)
    $exeName = $exeName.ToLowerInvariant()
    $Exe_Registry[$exeName] = $resolvedPath

    $json = $Exe_Registry | ConvertTo-Json
    try {
        New-Folder_EnsureExists -folder ( Split-Path $cachePath -Parent )
        Set-Content -Path $cachePath -Value $json -ErrorAction SilentlyContinue
    }
    catch {
        Write-Warning "$cachePath not updated for'$path'"
        # ignore changes on the cache
    }
}

function Get-Exe($path) {
    $path = Get-Path $path
    if (-not $path) { return }
    if ( Test-Path $path -ErrorAction SilentlyContinue ) {
        return $path
    }
    $path = $path.ToLowerInvariant()
    
    if ($Exe_Registry) {
        $result = $Exe_Registry[$path]
        if ($result ) {
            return $result
        }
    }
    $result = Resolve-Exe $path
    if ($result ) {
        return $result
    }
    throw "Exe not found : '$path' "
}

function Update-ExeArguments_Encode { 
    Param ($exeArgs, [switch]$sort, [switch]$ignoreEmpty ) 

    $exeProps = $exeArgs.GetEnumerator() | ForEach-Object { 
        [PSCustomObject]@{ Name  = $_.Name.Trim(); Value = $_.Value } }
    if ($sort){
        $exeProps = $exeProps | Sort-Object Name
    }

    if ($ignoreEmpty) {
        $exePropNull = ( $exeProps | Where-Object { [String]::IsNullOrEmpty($_.Value) } ) -join ', '
        if ($exePropNull) {
            Write-Warning "The following properties does not have a value '$exePropNull' "    
        }

        $exeProps = $exeProps | Where-Object { -not [String]::IsNullOrEmpty($_.Value) }
    }

    $exeProps | ForEach-Object { 
        $val = if ($_.Value -is [Uri] ) { $_.Value.OriginalString } else { "$($_.Value)" }
        $val = $val.Replace('"','\""')
        $hasChar = $val.IndexOfAny(@(' ','"',"'"))
        if ( $hasChar -gt -1 ) { 
            $_.Value = $val | Update-String_Enclose '"'
        }
    } 

    $result = $exeProps | ForEach-Object { $_.Name + '=' + $_.Value }
    return $result
}
function Update-ExeArguments_Encode:::Test {
    Update-ExeArguments_Encode -exeArgs @{ A = 'b' }

    Update-ExeArguments_Encode -exeArgs ([Ordered]@{ C = '1'; A = '2' })
    Update-ExeArguments_Encode -exeArgs ([Ordered]@{ C = '1'; A = '2' }) -sort
 
    Update-ExeArguments_Encode -exeArgs @{ A = 'b'; B = $null }
    Update-ExeArguments_Encode -exeArgs @{ A = 'b'; B = $null } -ignoreEmpty

    Update-ExeArguments_Encode -exeArgs @{ A = 'b'; C = 'https://www.abc.com:9090' }

    Update-ExeArguments_Encode -exeArgs @{ A = 'b"c'; D = 'dd' }
}

function New-ProcessExeInfo ($exeToRun, $Arguments, $WorkingDirectory, $NoEcho = $false,
         $logFileName = '', $Timeout = '00:30:00')  {
    if ($logFileName) {
       $logFolder = Split-Path $logFileName -Parent
       if ( $logFolder ) {
           New-Folder_EnsureExists $logFolder
       }
    }
 
    $User = [Security.Principal.WindowsIdentity]::GetCurrent()
    
  [PSCustomObject]@{ 
    Exe = $exeToRun
    Arguments = $Arguments
    WorkingDirectory = $WorkingDirectory
    ExitCode = 0
    StartTime = [DateTime]::Now
    EndTime = $null
    Error = ''
    Output = '' 
    LogFileName = $logFileName
    OutputFileName = $logFileName
    ErrorFileName = $logFileName
    NoEcho = $NoEcho
    IsAdministrator = Test-Administrator
    TimeOut = $Timeout
    UserName = $User.Name  # "$env:USERDOMAIN\$env:USERNAME"
  }
}

function Start-ProcessAsAdmin {
param( 
  $ProcessInfo
)
  $psi = new-object System.Diagnostics.ProcessStartInfo
  $psi.RedirectStandardError = $true
  $psi.RedirectStandardOutput = $true
  $psi.UseShellExecute = $false
  
  $psi.FileName = $ProcessInfo.Exe
  if ($ProcessInfo.Arguments) {
    $psi.Arguments = "$($ProcessInfo.Arguments)"
  }

  if ([Environment]::OSVersion.Version -ge (new-object 'Version' 6,0)){
    $psi.Verb = "runas"
  }

  if (-not $ProcessInfo.WorkingDirectory) {
    $ProcessInfo.WorkingDirectory = get-location | Get-Path
  }

  $psi.WorkingDirectory = Get-Path $ProcessInfo.WorkingDirectory

  if ($minimized) {
    $psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
  }

  function AddOutput ([string[]]$lastOutput) {
    if ($lastOutput) {
        $ProcessInfo.Output += $lastOutput
        Write-Host $lastOutput -NoNewline -BackgroundColor DarkGray -ForegroundColor White
        if ( $ProcessInfo.OutputFileName ) {
            $lastOutput | Out-File -FilePath $ProcessInfo.OutputFileName -Append
        }
    }
  }

  function AddError ($lastError) {
    if ($lastError) {
        $ProcessInfo.Error += $lastError
        Write-Host $lastError -NoNewline -BackgroundColor Red -ForegroundColor White
        if ( $ProcessInfo.ErrorFileName ) {
            $lastError | Out-File -FilePath $ProcessInfo.ErrorFileName -Append
        }
    }
  }

  function Read ($outputReader) { 
    if ( -not $outputReader ) { return }

    $bufferLength = 4096
    $buffer = new-object char[] $bufferLength

    $sb = New-Object 'System.Text.StringBuilder'
    $StartTime = [DateTime]::Now
    $refreshInternal = [TimeSpan]::FromMilliseconds(5000)
    while ( $true ) {
        $readCount = $outputReader.ReadBlock($buffer, 0, $buffer.Length)
        if (-not $readCount ) { break }
        for($i = 0; $i -lt $readCount; $i++) {
            $sb.Append([string]$buffer[$i]) | Out-Null   
        }
        $CurrentTime = [DateTime]::Now
        if ($readCount -lt $bufferLength ) { 
            break 
        }
        $duration = $CurrentTime - $StartTime
        if ( $refreshInternal -lt $duration) {
            break
        } 
    }
    $result = $sb.ToString()
    $result
  }  

  function GetProcessOutput {
    $lastOutput = Read $process.StandardOutput
    AddOutput $lastOutput
    return $true
  }

  try {
    $process = [System.Diagnostics.Process]::Start($psi)
    $Timeout = $ProcessInfo.TimeOut ?? '00:30:00'
    if (-not ( Wait-Exe -while { GetProcessOutput } -timeOut $Timeout -process $process ) ) {
        throw "TIMEOUT: '$($ProcessInfo.Exe)' $($ProcessInfo.Arguments)"
    }

    AddOutput $process.StandardOutput.ReadToEnd()
    AddError $process.StandardError.ReadToEnd()

    $ProcessInfo.EndTime = [DateTime]::Now
    $ProcessInfo.ExitCode = $process.ExitCode
    $global:LASTEXITCODE = $ProcessInfo.ExitCode
  }
  finally {
    Invoke-Dispose ([ref] $process)
  }

  # Removes trailing lines and spaces
  $ProcessInfo.Output = $ProcessInfo.Output.TrimEnd() 
  $ProcessInfo.Error = $ProcessInfo.Error.Trim()
}

function Join-ExeArguments($argList) {
    if ( -not $argList ) {
        return;
         
    }
    $result = [string]::Join(' ', ( $argList | ForEach-Object { 
            $param = [string]$_ 
            if ( $param.StartsWith('-') -and $param.EndsWith(':') ) {
                $param = $param.Substring(0, $param.Length - 1)
            }
            $param
        } ) ) 
    return $result
}

function Invoke-Exe_Scope {
    Param(
        [Parameter()]
        [ScriptBlock]$script,
        [Parameter()]
        [int]$OkExitCode,
        [Parameter()]
        [switch]$ignoredOutput,
        [Parameter()]
        [string]$WorkingFolder,
        [Parameter()]
        [TimeSpan]$Timeout='00:30:00'
    ) 

    . $script

}

function Invoke-Exe () {
<# This function cannot declare explicitly parameters in order to be able to send them to
   the executable.
    
   Use Invoke-Exe_Scope
#>


    $OkExitCode = @() + ( Get-Argument OkExitCode -remove -default 0 )
    $ignoredOutput = Get-Argument NoOutput -remove -switch
    $NoEcho = Get-Argument NoEcho -remove -switch
    $TimeOut = Get-Argument TimeOut -remove
    $WorkingFolder = Get-Argument WorkingFolder -remove -default ( Get-PS_WorkingFolder ) 

    $unboundArgs = $MyInvocation.UnboundArguments

    if ( -not $unboundArgs) {
        throw 'No command line provided'
    }
    [String]$exe = Get-Exe $unboundArgs[0]

    $unboundArgs.RemoveAt(0)
    $commandArgs = ( Join-ExeArguments $unboundArgs )
    $commandLine = if ( $exe.Contains(' ') ) { $exe | Update-String_Enclose '"' } else { $exe } 
    $commandLine = $commandLine + ' ' + $commandArgs 

    if ( $global:SafeExecuteCounter ) { $global:SafeExecuteCounter += 1 } else { $global:SafeExecuteCounter = 1 } 
    $padPid = ([string]$pid).PadLeft(7,'0')
    $padCounter = ([string]$global:SafeExecuteCounter).PadLeft(5,'0')
    $counterPrefix = "$padPid-$padCounter-"
    $logFileName = $counterPrefix + ( Split-Path $exe -Leaf | Update-Suffix '"' | Update-Suffix "'" ) + ".ps1.log"
    $logFileName = Initialize-ExportFile $logFileName -append -silent

    "#" + ( Get-Date ) >> $logFileName
    "cd '$WorkingFolder'" >> $logFileName
    "$commandLine" >> $logFileName

    $global:LastCommandLine = $commandLine
    if (-not $NoEcho) {
        write-host $global:LastCommandLine
    }

    $ProcessInfo = New-ProcessExeInfo -exeToRun $exe -Arguments $commandArgs `
        -WorkingDirectory $WorkingFolder -NoEcho $NoEcho -LogFileName $logFileName -Timeout $Timeout 

    try {
       try {
          Start-ProcessAsAdmin -ProcessInfo $ProcessInfo
       }
       finally {
            #compress option here fixes a bad format error and removes white spaces
          $ProcessOutputJson = ConvertTo-Json $ProcessInfo -Compress
          $ProcessOutputJson = $ProcessOutputJson.Replace('\r\n',"`r`n")
          $ProcessOutputJson >> $logFileName
       }
    }
    catch {
       $StackMessage = Get-ErrorStackMessage
       $StackMessage >> $logFileName
       $DumpLocation = ' Dump Generated at: ' + ( Get-UNC_FileName $logFileName )

       $errorMessage = "Failed to execute : `n$commandLine`n`n$DumpLocation`n`n$StackMessage`n`n$ProcessOutputJson"
       Write-Warning $errorMessage
       throw $errorMessage
    }

    if ( $ProcessInfo.ExitCode -notin $OkExitCode) {
        $output = $ProcessInfo | Format-List  | Out-String
        Write-Warning $output
        throw [System.ApplicationException] "$commandLine`n`Exit code: $($ProcessInfo.ExitCode) but expected [$OkExitCode] "
    }

    if (-not $ignoredOutput) {
        $output = $ProcessInfo.Output.split("`n");
        $output
    }
}
function Invoke-Exe:::Test {
    Invoke-Exe ping localhost
    Invoke-Exe ping -OkExitCode 1 
    Invoke-Exe ping -OkExitCode 1 -NoOutput
    
    $currentFolder = Split-Path ( Get-PSCallStack )[0].ScriptName  -Parent
    Invoke-Exe git remote -v -WorkingFolder $currentFolder
# Invoke-Exe ping localhost -t -timeout '00:01:00'
}

function Wait-Exe ($process, [TimeSpan]$timeOut = '10:00:00', 
        [TimeSpan]$pollingInterval = '00:00:01', [scriptblock]$while) {
    if ( $while ) {
        $checkResult = Invoke-Command -ScriptBlock $while
        [DateTime]$limit = [DateTime]::Now + $timeOut 
        do {
            $exited = $process.WaitForExit($pollingInterval.TotalMilliseconds)
            if ($exited) {
                return $exited
            }
            $checkResult = Invoke-Command -ScriptBlock $while
        } while ( $checkResult -and ( $limit -gt [DateTime]::Now ) )
    }
    else 
    {
        $exited = $process.WaitForExit($timeOut.TotalMilliseconds)
    }

    return $exited
}

function Get-InvokeArguments {
    param($BoundParameters, [switch]$ExcludeNames)

    if (-not $BoundParameters) { return '' }
    $cmdArgs = $BoundParameters.GetEnumerator() | ForEach-Object{ 
            $result = if ($ExcludeNames) { '' } else { '-' + $_.Key }
            if ($_.Value) {
                if ($_.Value -is [Switch]) {
                    if ( $_.Value.IsPresent ) {
                        return $result
                    }
                    return
                }
                if ( $result ) { $result += ' ' }
                $result += $_.Value
                return $result
            }
        }
    $cmdArgs = $cmdArgs -join ' '
    return $cmdArgs
}
function Get-InvokeArguments:::Example {
    Get-InvokeArguments  | Assert -eq ''
    Get-InvokeArguments @{} | Assert -eq ''
    Get-InvokeArguments @{A = 1} | Assert -eq '-A 1'
    Get-InvokeArguments @{A = 1; B = 'BValue'} | Assert -eq '-A 1 -B Bvalue'
    Get-InvokeArguments @{A = 1} -ExcludeNames | Assert -eq '1'
}

function Get-Powershell_Call ($PSConsoleFile, $Version, 
        [Switch]$NoLogo, [Switch]$NoExit, [Switch]$Sta, [Switch]$Mta, [Switch]$NoProfile, [Switch]$NonInteractive,
        $InputFormat, $OutputFormat, $WindowStyle, $EncodedCommand,
        $File, $ExecutionPolicy, $Command ) {
    
    return Get-InvokeArguments $PSBoundParameters
# [-InputFormat {Text | XML}] [-OutputFormat {Text | XML}]
# [-WindowStyle <style>] [-EncodedCommand <Base64EncodedCommand>]
# [-File <filePath> <args>] [-ExecutionPolicy <ExecutionPolicy>]
# [-Command { - | <script-block> [-args <arg-array>]
# | <string> [<CommandParameters>] } ]
}
function Get-Powershell_Call:::Example{
    Get-Powershell_Call -Command { $env:COMPUTERNAME } -NoProfile
}