9

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:

devuxer
  • 41,681
  • 47
  • 180
  • 292
  • Is there any reason why you are passing $command into it specifically and not just specifying the location in an argument variable like so `$arguments = "-NoExit Set-Location 'C:\inetpub\wwwroot'" Start-Process powershell -Verb RunAs -ArgumentList $arguments` – Owain Esau Mar 05 '19 at 01:47
  • @OwainEsau, This is a stripped down example. My real script is too large and complex to be easily put into a string. – devuxer Mar 05 '19 at 02:04

2 Answers2

8

$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 script block at hand, any script block that has embedded " chars. wouldn't be parsed correctly as part of the overall "..." string by the target process; to prevent this, they must be escaped as \", which is what external programs expect - including PowerShell via its CLI:

# 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.

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

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
howdoicode
  • 779
  • 8
  • 16