Note: It is the involvement of Start-Process
that complicates the solution significantly - see below. If powershell
were invoked directly from PowerShell, you could safely pass a script block as follows:
powershell ${function:Check-PC} # !! Does NOT work with Start-Process
${function:Check-PC}
is an instance of variable namespace notation: it returns the function's body as a script block ([scriptblock]
instance); it is the more concise and faster equivalent of
Get-Content Function:Check-PC
.
If you needed to pass (positional-only) arguments to the script block, you'd have to append -Args
, followed by the arguments as an array (,
-separated).
Start-Process
solution with an auxiliary self-deleting temporary file:
See the bottom half of this answer to a related question.
Start-Process
solution with -EncodedCommand
and Base64 encoding:
Start-Process powershell -args '-noprofile', '-noexit', '-EncodedCommand',
([Convert]::ToBase64String(
[Text.Encoding]::Unicode.GetBytes(
(Get-Command -Type Function Check-PC).Definition
)
))
The new powershell
instance will not see your current session's definitions (unless they're defined in your profiles), so you must pass the body of your function to it (the source code to execute).
(Get-Command -Type Function Check-PC).Definition
returns the body of your function definition as a string.
The string needs escaping, however, in order to be passed to the new Powershell process unmodified.
"
instances embedded in the string are stripped, unless they are either represented as \"
or tripled ("""
).
(\"
rather than the usual `"
is needed to escape double quotes in this case, because PowerShell expects \"
when passing a command to the powershell.exe
executable.)
Similarly, if the string as a whole or a double-quoted string inside the function body ends in (a non-empty run of) \
, that \
would be interpreted as an escape character, so the \
must be doubled.Thanks, PetSerAl.
The most robust way to bypass these quoting (escaping) headaches is to use a Base64-encoded string in combination with the -EncodedCommand
parameter:
Note: To also pass arguments, you have two options:
You can "bake" them into the -EncodedCommand
argument, assuming you can call a command to pass them to there - see below, which shows how to define your function as such in the new session, so you can call it by name with arguments.Thanks, Abraham Zinala
- The advantage of this approach is that you can pass named arguments this way. The disadvantage is that you are limited to arguments that have string-literal representations.
You can use the (currently undocumented) -EncodedArguments
parameter, to which you must similarly pass a Base64-encoded string, albeit based on the CLIXML representation of the array of arguments to pass
The advantage of this approach is that you can pass a wider array of data types, within the limits of the type fidelity that CLIXML serialization can provide - see this answer; the disadvantage is that only positional arguments are supported this way (although you could work around that by passing a hashtable that the target code then uses for splatting with the ultimate target command).
Here's a simplified, self-contained example, which uses Write-Output
to echo the (invariably positional) arguments received:
$command = 'Write-Output $args'
$argList = 'foo', 42
Start-Process powershell -args '-noprofile', '-noexit',
'-EncodedCommand',
([Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($command))),
'-EncodedArguments',
([Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes(
[System.Management.Automation.PSSerializer]::Serialize($argList)
)))
In case you want to pass the complete function, so it can be called by name in order to pass arguments as part of the command string, a little more work is needed.
# Simply wrapping the body in `function <name> { ... }` is enough.
$func = (Get-Command -Type Function Check-PC)
$wholeFuncDef = 'Function ' + $func.Name + " {`n" + $func.Definition + "`n}"
Start-Process powershell -args '-noprofile', '-noexit', '-EncodedCommand', `
([Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes("$wholeFuncDef; Check-PC")))
As stated above, you can "bake" any arguments to pass to your function - assuming they can be represented as string literals - into the -EncodedCommand
argument, simply by appending them inside the "$wholeFuncDef; Check-PC"
string above; e.g.,
"$wholeFuncDef; Check-PC -Foo Bar -Baz Quux"
Start-Process
solution with regex-based escaping of the source code to pass:
PetSerAl suggests the following alternative, which uses a regex to perform the escaping.
The solution is more concise, but somewhat mind-bending:
Start-Process powershell -args '-noprofile', '-noexit', '-Command', `
('"' +
((Get-Command -Type Function Check-PC).Definition -replace '"|\\(?=\\*("|$))', '\$&') +
'"')
"|\\(?=\\*("|$))
matches every "
instance and every nonempty run of \
chars. - character by character - that directly precedes a "
char. or the very end of the string.
\\
is needed in the context of a regex to escape a single, literal \
.
(?=\\*("|$))
is a positive look-ahead assertion that matches \
only if followed by "
or the end of the string ($
), either directly, or with further \
instances in between (\\*
). Note that since assertions do not contribute to the match, the \
chars., if there are multiple ones, are still matched one by one.
\$&
replaces each matched character with a \
followed by the character itself ($&
) - see this answer for the constructs you can use in the replacement string of a -replace
expression.
Enclosing the result in "..."
('"' + ... + '"'
) is needed to prevent whitespace normalization; without it, any run of more than one space char. and/or tab char. would be normalized to a single space, because the entire string wouldn't be recognized as a single argument.
- Note that if you were to invoke
powershell
directly, PowerShell would generally automatically enclose the string in "..."
behind the scenes, because it does so for arguments that contain whitespace when calling an external utility (a native command-line application), which is what powershell.exe
is - unlike the Start-Process
cmdlet.
- PetSerAl points out that the automatic double-quoting mechanism is not quite that simple, however (the specific content of the string matters with respect to whether automatic double-quoting is applied), and that the specific behavior changed in v5, and again (slightly) in v6.
start-process powershell -argumentlist "`$PC = read-host 'pc name'; write-host 'Checking'; test-connection `$PC"
– Chapple