简体   繁体   中英

Can't pass a script block as a parameter to powershell.exe via -Command

I'm trying this

$Global:commandBlock={
Start-Transcript -path $projectFolder\gruntLog.txt;
grunt $argList;
Stop-Transcript
}

$cmdProc=start-process powershell -ArgumentList ('-command `$Global:commandBlock') -WorkingDirectory $fwd -PassThru -NoNewWindow:$NoNewWindow

And keep getting $commandBlock : The term '$Global:commandBlock' is not recognized as the name of a cmdlet, function, script file, or operable program.

My guess was it has to do with scope. But making variable global didn't help. Adding -args $commandBlock like that:

-ArgumentList ('-command `$Global:commandBlock -args "-commandBlock:$commandBlock"') 
-ArgumentList ('-command `$Global:commandBlock -args $commandBlock"') 

didn't help

And I'm not sure that I escape variables correctly in the block, read this , but not sure how to apply to my script.

There's a few things which I think are keeping this from working. First, when you're using single quotes, ' you're instructing PowerShell to operate literally. This means that it won't expand variables. Not what you're looking for.

A better way to do this is to do it with an subexpression like this.

$Global:commandBlock={
'ham' >> C:\temp\test.txt
}

$cmdProc=start-process powershell -ArgumentList ("-command $($Global:commandBlock)") -PassThru -NoNewWindow:$NoNewWindow

This will give you the desired results.

Subexpressions are pretty sweet. It lets you embed a mini-scriptblock within a string, and it's then expanded out in the parent string.

"today's date is $(get-date), on system: $($env:COMPUTERNAME)"

today's date is 02/14/2017 11:50:49, on system: BEHEMOTH

There are two major issues (leaving the obvious mistake of attempting to reference a variable inside a single-quoted string aside):

  • Any argument you want to pass to a new powershell instance via -Command must be escaped in non-obvious ways if it contains " and/or \\ chars , which is especially likely if you're passing a piece of PowerShell source code .

    • The escaping issue can generally be solved by Base64-encoding the source-code string and passing it via the -EncodedCommand parameter - see this answer of mine to a related question for how to do that, but a more concise alternative is presented below.
  • If the source code being passed references any variables that only exist in the calling session, the new instance won't see them .

    • The solution is not to reference session-specific variables in the source code being passed, but to pass their values as parameter values instead.

To solve the local-variable-not-seen-by-the-new-instance problem, we must rewrite the script block to accept parameters :

$scriptBlock={
  param($projectFolder, $argList)
  # For demonstration, simply *output* the parameter values.
  "folder: [$projectFolder]; arguments: [$argList]"
}

Now we can apply the necessary escaping, using PetSerAl 's sophisticated -replace expression from his comment on the question.
We can then invoke the resulting string with & {...} while passing it parameter values (I'm omitting the -WorkingDirectory and -PassThru parameters for brevity):

# Parameter values to pass.
$projectFolder = 'c:\temp'
$argList='-v -f'

Start-Process -NoNewWindow powershell -ArgumentList '-noprofile', '-command', 
  (('& {' + $scriptBlock.ToString() + '}') -replace '\"|\\(?=\\*("|$))', '\$&'), 
    "'$projectFolder'", 
    "'$argList'"

For an explanation of the regular expression, again see this answer .

Note how the variable values passed as parameters to the script block are enclosed in '...' inside a "..." -enclosed string in order to:

  • pass the values as a single parameter value.
  • protect them from another round of interpretation by PowerShell.

Note: If your variable values have embedded ' instances, you'll have to escape them as '' .

The above yields:

folder: [c:\temp]; arguments: [-v -f]

Alternative with a temporary, self-deleting script file:

Using -File with a script file has the advantage of being able to pass parameter values as literals , with no concern over additional interpretation of their contents.

Caveat : As of PowerShell Core v6-beta.3, there is a problem when passing parameter values that start with - : they are not bound as expected; see this GitHub issue .
To work around this problem, the sample script block below accesses only the first parameter by name, and relies on all remaining ones binding via the automatic $Args variable.

# Define the script block to be executed by the new PowerShell instance.
$scriptBlock={
  param($projectFolder)
  # For demonstration, simply *output* the parameter values.
  "folder: [$projectFolder]; arguments: [$Args]"
}

# Parameter values to pass.
$projectFolder = 'c:\temp'
$argList='-v -f'

# Determine the temporary script path.
$tempScript = "$env:TEMP\temp-$PID.ps1"

# Create the script from the script block and append the self-removal command.
# Note that simply referencing the script-block variable inside `"..."`
# expands to the script block's *literal* content (excluding the enclosing {...})
"$scriptBlock; Remove-Item `$PSCommandPath" > $tempScript

# Now invoke the temporary script file, passing the arguments as literals.
Start-Process -NoNewWindow powershell -ArgumentList '-NoProfile', '-File', $tempScript,
  $projectFolder,
  $argList

Again, the above yields:

folder: [c:\temp]; arguments: [-v -f]

I've messed around with the syntax for passing args to a new powershell instance and have found the following works. So many variations fail without a good error message. Maybe it would work in your case?

$arg = "HAM"
$command = {param($ham) write-host $ham}
 #please not its important to wrap your command 
 #in a further script block to stop it being processed to a string at execution
 #The following would normally suffice "& $command $arg"

Start-Process powershell -ArgumentList "-noexit -command & {$command}  $arg"

Also simply using the Invoke-Command gives you the -ArgumentList parameter to opperate against the given Command that you are missing with the standard powershell.exe parameters. This is probably a bit cleaner looking.

Start-Process powershell -ArgumentList "-noexit -command invoke-command -scriptblock {$command} -argumentlist $arg"

No need for any extra complex escaping or unwanted persisted variables. Just keep the script block in curly braces so it remains a script block on arrival in the new session. At least in this simple case...

If you have several string parameters that contain spaces. I found popping the string in a single parenthesis and separating with commas works well. You could also probably pass a predefined array as a single argument.

Start-Process powershell -ArgumentList "-noexit -command invoke-command -scriptblock {$command} -argumentlist '$arg1', '$arg2', '$arg3'"

Will this work:

$Global:commandBlock={
Start-Transcript -path $projectFolder\gruntLog.txt;
grunt $argList;
Stop-Transcript
}

& $Global:commandBlock

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM