3

When I for example read some uninstall string from the registry like "C:\Program Files (x86)\Opera\Launcher.exe" /uninstall I can copy it to the Powershell command line, prefix it with the call operator and execute it.

& "C:\Program Files (x86)\Opera\Launcher.exe" /uninstall

But

$var = '"C:\Program Files (x86)\Opera\Launcher.exe" /uninstall'
& $var

does not work. Of course I can just say

cmd /c $var

But is there really no way to do this without an additional cmd process?

TNT
  • 3,392
  • 1
  • 24
  • 27

3 Answers3

3

In order to use & (or direct invocation if the command name/path is an unquoted literal), the command name/path must be passed separately from its arguments.
When invoking an external program, you may pass these arguments as an array.

The solution below leverages PowerShell's own Write-Output cmdlet in combination with a - safe - invocation of Invoke-Expression[1] in order to parse the string into its constituent arguments.

# Gets the arguments embedded in the specified string as an array of literal tokens 
# (applies no interpolation). 
# Note the assumption is that the input string contains no NUL characters 
# (characters whose code point is `0x0`) - which should be a safe assumption
# Example: 
#   get-EmbeddedArguments '"C:\Program Files (x86)\Opera\Launcher.exe" /uninstall'
# yields array @( 'C:\Program Files (x86)\Opera\Launcher.exe', '/uninstall' )
function get-EmbeddedArguments ([string] $String) {
  (Invoke-Expression "Write-Output -- $($String -replace '\$', "`0")") -replace "`0", '$$'            
}

# Input string.
$var = '"C:\Program Files (x86)\Opera\Launcher.exe" /uninstall'

# Extract the command name (program path) and arguments.
# Note the destructuring assignment that stores the return array's 1st element
# in $exe, and collects the remaining elements, if any, in $exeArgs.
$exe, $exeArgs = get-EmbeddedArguments $var

# Use & to invoke the command (name / program path) ($exe) and pass
# it all arguments as an array.
& $exe $exeArgs

[1] Invoke-Expression should generally be avoided, as Bill points out, because it presents a security risk and there are typically safer and more robust options available. Here, however, there is no simple alternative, and the security risk is avoided by temporarily replacing all $ instances in the input string so as to prevent unintended string interpolation.

mklement0
  • 382,024
  • 64
  • 607
  • 775
1

You wrote:

$var = '"C:\Program Files (x86)\Opera\Launcher.exe" /uninstall'
& $var

You've noticed that this doesn't work. That's because the argument to the & operator is a string representing the command to execute (but not parameters). The correct way to write it would be:

$var = "C:\Program Files (x86)\Opera\Launcher.exe"
& $var /uninstall

If you want to read a command line string from the registry (or somewhere else) and execute it as-is, one way to avoid Invoke-Expression or invoking cmd.exe is to tokenize the string. Here's a function that does that:

function Split-String {
  param(
    [String] $inputString
  )
  [Regex]::Matches($inputString, '("[^"]+")|(\S+)') | ForEach-Object {
    if ( $_.Groups[1].Success ) {
      $_.Groups[1].Value
    }
    else {
      $_.Groups[2].Value
    }
  }
}

The first element in the returned array is the executable's name, and the remaining elements (if any) are the executable's parameters. The quote marks around the array elements are preserved.

You could use the function to run an executable using the & operator as follows:

$var = '"C:\Program Files (x86)\Opera\Launcher.exe" /uninstall'
$tokens = @(Split-String $var)
$command = $tokens[0] -replace '^"?([^"]+)"?$', '$1'
if ( $tokens.Count -eq 1 ) {
  & $command
}
else {
  & $command $tokens[1..$($tokens.Count - 1)]
}

The third line in the code removes leading and trailing " characters from the command name.

If you want to run the executable asynchronously, you can use Start-Process. Example:

$var = '"C:\Program Files (x86)\Opera\Launcher.exe" /uninstall'
$tokens = @(Split-String $var)
$startArgs = @{
  "FilePath" = $tokens[0] -replace '^"?([^"]+)"?$', '$1'
}
if ( $tokens.Count -gt 1 ) {
  $startArgs.ArgumentList = $tokens[1..($tokens.Count - 1)]
}
Start-Process @startArgs
Bill_Stewart
  • 22,916
  • 4
  • 51
  • 62
1

I think Bill Stewart's answer is the right way to do it. If for some reason you can't get Bill's answer to work you could use Invoke-Expression to do it though.

$var = '"C:\Program Files (x86)\Opera\Launcher.exe" /uninstall'
Invoke-Expression "& $var"
TheMadTechnician
  • 34,906
  • 3
  • 42
  • 56
  • 3
    I don't recommend this, because [`Invoke-Expression` can be considered harmful](https://blogs.msdn.microsoft.com/powershell/2011/06/03/invoke-expression-considered-harmful/). – Bill_Stewart May 22 '18 at 18:15