devuxer
devuxer

Reputation: 42354

Running script-block command with param using Start-Process

Why does powershell think $dir is null when setting the location but not when writing the output?

$command = {
    param($dir)
    Set-Location $dir
    Write-Output $dir
}

# run the command as administrator
Start-Process powershell -Verb RunAs -ArgumentList "-NoExit -Command $command 'C:\inetpub\wwwroot'"

This results in the following output:

Set-Location : Cannot process argument because the value of argument "path" is null. Change the value of argument
"path" to a non-null value.
At line:3 char:2
+  Set-Location $dir
+  ~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [Set-Location], PSArgumentNullException
    + FullyQualifiedErrorId : ArgumentNull,Microsoft.PowerShell.Commands.SetLocationCommand

C:\inetpub\wwwroot

I also tried:

$command = {
    param($dir)
    Set-Location $dir
    Write-Output $dir
}

$outerCommand = {
    Invoke-Command -ScriptBlock $command -ArgumentList 'C:\inetpub\wwwroot'
}

# run the command as administrator
Start-Process powershell -Verb RunAs -ArgumentList "-NoExit -Command $outerCommand"

But then I got:

Invoke-Command : Cannot validate argument on parameter 'ScriptBlock'. The argument is null. Provide a valid value for
the argument, and then try running the command again.
At line:2 char:30
+  Invoke-Command -ScriptBlock $command 'C:\inetpub\wwwroot'
+                              ~~~~~~~~
    + CategoryInfo          : InvalidData: (:) [Invoke-Command], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationError,Microsoft.PowerShell.Commands.InvokeCommandCommand

Possible clue: if I set a local variable instead of using a param, it works perfectly:

$command = {
    $dir = 'C:\inetpub\wwwroot'
    Set-Location $dir
    Write-Output $dir
}

# run the command as administrator
Start-Process powershell -Verb RunAs -ArgumentList "-NoExit -Command $command"

Similar Q/As that didn't quite answer my question:

Upvotes: 10

Views: 6472

Answers (2)

mklement0
mklement0

Reputation: 437248

$command is a script block ({ ... }) and stringifying a script block results in its literal contents, excluding the enclosing { and }.

Therefore, your expandable string "-NoExit -Command $command 'C:\inetpub\wwwroot'" literally expands to the following string - note the missing { ... } around the original script block:

 -NoExit -Command 
    param($dir)
    Set-Location $dir
    Write-Output $dir
 'C:\inetpub\wwwroot'

Due to the loss of the enclosing { and }, the new powershell process spawned by Start-Process quietly ignored the orphaned param($dir) statement and, given that the new process therefore had no $dir variable (given that it isn't an automatic variable either), the command failed, because Set-Location $dir was tantamount to Set-Location $null, which fails.[1]

Note that you can never pass script blocks as such to Start-Process - all arguments must be strings, because only strings can be passed to external processes.


The simplest solution in your case is to:

  • enclose the $command reference in your expandable string in { ... } to compensate for the loss of this enclosure due to stringification

  • and prepend & to ensure invocation of the resulting script block in the new process.

Here's a working solution:

Start-Process powershell -Verb RunAs -ArgumentList `
  "-NoExit -Command & { $command } 'C:\inetpub\wwwroot'"

Important:

  • While not needed with the specific -Command argument at hand, because of how the PowerShell CLI parses its process command line when -Command is used, embedded " chars. must be escaped as \",[2] as shown in the following example:
# Script block with embedded " chars.
$command = {
    param($dir)
    Set-Location $dir
    "Current dir: $dir"
}

# Embed the script block with " escaped as \"
Start-Process powershell -Verb RunAs -ArgumentList `
  "-NoExit -Command & { $($command -replace '"', '\"') } 'C:\inetpub\wwwroot'"

[1] It fails in Windows PowerShell; in PowerShell Core, it is tantamount to Set-Location without arguments, which changes to the current user's home folder.

[2] See this answer for details.

Upvotes: 9

howdoicode
howdoicode

Reputation: 961

I had a similar issue, but I wanted to place all of the commands needed for Start-Process -ArgumentList into a variable. Thanks to mklement0's answer above, I managed to find the solution.

Here's a slightly alternative answer, which includes powershell.exe's -Command (and any other parameters), in a Here-String [link 1] [link 2]:

$commandString = @'
    -NoExit -Command & {
        Write-Host -ForegroundColor Green \"Hello, new window!\"
        Set-Location -Path \"c:\\\"
        Get-ChildItem
        Start-Sleep -Seconds 3
    }
'@

Start-Process -FilePath "powershell.exe" -ArgumentList $commandString

Upvotes: 1

Related Questions