Search code examples
powershellvariablesparameters

start powershell wont accept variable as parameter


issue

the called powershell script will accept parameters but not all of them:
enter image description here

Current Set-Up and code:

I have a common folder where two .ps1 scripts are located:

  • DoWork.ps1
  • Workmanager.ps1

Workmanager.ps1 calls the Dowork.ps1:

$targetPath="M:\target"
echo "target path: $targetPath"

start powershell {.\DoWork.ps1 -target $targetPath -tempdrive D:\}

output (as expected):

target path: M:\target

DoWork.ps1 contains some start code:

param 
(
    [string]$tempdrive, 
    [string]$target, 
    [int] $threads = 8,
    [int] $queuelength = -1
)
echo "variables:"
echo "temp drive: $tempdrive"
echo "target path: $target"

Unexpectedly, the $target is not beeing assigned. Previously I had the variable named $targetpath, which did not work either.

variables:
temp drive: D:\
target path:

Findings

It appears that the issue relies in Workmanager.ps1. Spcifying the parameter as fixed string rather than as variable will load the parameter. Any solution for this?

start powershell {.\DoWork.ps1 -target "foo" -tempdrive D:\}

Solution

  • When you use a ScriptBlock as an argument to powershell.exe, variables aren't going to be evaluated until after the new session starts. $targetPath has not been set in the child PowerShell process called by Workmanager.ps1 and so it has no value. This is actually an expected behavior of a ScriptBlock in general and behaves this way in other contexts too.

    The solution is mentioned in the help text for powershell -?:

    [-Command { - | <script-block> [-args <arg-array>] <========== THIS GUY
                  | <string> [<CommandParameters>] } ]
    

    You must provide the -args parameter which will be passed to the ScriptBlock on execution (separate multiple arguments with a ,). Passed arguments are passed positionally, and must be referenced as though you were processing the arguments to a function manually using the $args array. For example:

    $name = 'Bender'
    & powershell { Write-Output "Hello, $($args[0])" } -args $name
    

    However, especially with more complicated ScriptBlock bodies, having to remember which index of $args[i] contains the value you want at a given time is a pain in the butt. Luckily, we can use a little trick with defining parameters within the ScriptBlock to help:

    $name = 'Bender'
    & powershell { param($name) Write-Output "Hello, $name" } -args $name
    

    This will print Hello, Bender as expected.


    Some additional pointers:

    • The ScriptBlock can be multiline as though you were defining a function. way. The examples above are single line due to their simplicity.

    • A ScriptBlock is just an unnamed function, which is why defining parameters and referencing arguments within one works the same way.

    • To exemplify this behavior outside of powershell.exe -Command, Invoke-Command requires you to pass variables to its ScriptBlock in a similar fashion. Note however that answer uses an already-defined function body as the ScriptBlock (which is totally valid to do)

    • You don't need to use Start-Process here (start is its alias), at least as demonstrated in your example. You can simply use the call operator & unless you need to do something more complex than "run the program and wait for it to finish". See this answer of mine for more information.

    • If you opt to pass a string to powershell.exe instead, you don't need to provide arguments and your variables will get rendered in the current PowerShell process. However, so will any other unescaped variables that might be intended to set within the child process, so be careful with this approach. Personally, I prefer using ScriptBlock regardless, and just deal with the extra parameter definition and arguments.

    • Using the call & operator is optional when you are not executing a path rendered as a string. It can be omitted in the examples above, but is more useful like so:

      & "C:\The\Program Path\Contains\spaces.exe"
      
      & $programPathAsAVariable