Note: This answer addresses common use cases first; for a discussion of what you tried, see the bottom section.
Note: If you just want to execute commands stored in a string from inside a PowerShell session, without needing to run commands in a child process, use Invoke-Expression
, but do note that Invoke-Expression
should generally be avoided:
Note: For illustrative purposes, I'm substituting command Get-Date -UFormat "%s"
for your original command, mkdir "C:\Temp\555"
. This Get-Date
command prints the current point in time as a Unix timestamp, i.e., an integer denoting the seconds since 1 Jan 1970, midnight UTC; e.g., 1565229614
for Wednesday, August 7, 2019 10:00:13 PM ETD
.
$cmd = 'Get-Date -UFormat "%s"'
Invoke-Expression $cmd # Execute the string $cmd as code.
If you control construction of the commands, it's better to store pieces of code as script blocks ({ ... }
) inside a PowerShell session:
$cmd = { Get-Date -UFormat "%s" } # create a script block
& $cmd # execute the script block
How to pass a complex command to a PowerShell child process via its CLI, using the -EncodedCommand
parameter:
- From outside PowerShell, use the
-EncodedCommand
parameter, which requires a Base64 encoding of the bytes of a UTF-16LE-encoded string ("Unicode"-encoded), not of a UTF-8-encoded one:
# Get the Base64-encoding of the bytes that make up the UTF-16LE
# ("Unicode") encoding of string 'Get-Date -UFormat "%s"'
# Assigns the following to $base64Cmd:
# 'RwBlAHQALQBEAGEAdABlACAALQBVAEYAbwByAG0AYQB0ACAAIgAlAHMAIgA='
$base64Cmd =
[System.Convert]::ToBase64String(
[System.Text.Encoding]::Unicode.GetBytes(
'Get-Date -UFormat "%s"'
)
)
powershell -EncodedCommand $base64Cmd # executes: Get-Date -UFormat "%s"
Note: The above assumes Windows PowerShell. If you're using PowerShell Core, use pwsh
instead of powershell
.
Specifying the System.
component in PowerShell type literals explicitly is optional; e.g., [System.Text.Encoding]
can be shortened to [Text.Encoding]
.
- From inside PowerShell, there's no need to pass a Base64-encoded string via
-EncodedCommand
explicitly, because PowerShell does that implicitly when you simply pass the code to execute as a script block({ ... }
):
powershell { Get-Date -UFormat "%s" } # -command (-c) parameter implied
Note:
- You cannot refer to the caller's variables in a script block, but you can pass arguments to it via the
-args
parameter; e.g.:
powershell { Get-Date -UFormat $args[0] } -args '%s'
.
Behind the scenes, the appropriate Base64 encoding is performed, and the encoded command is passed to -EncodedCommand
, as follows:
# The command to execute:
$cmd = 'Get-Date -UFormat "%s"'
# Obtain a Base64-encoded version of the command from the bytes of its
# UTF-16LE encoding.
$base64Cmd = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($cmd))
# Pass the result to the -EncodedCommand parameter:
powershell -EncodedCommand $base64Cmd -inputFormat xml -outputFormat xml
Note:
-inputFormat xml -outputFormat xml
are automatically added when you pass a script block to the (possibly positionally implied) -Command
/ -c
parameter.
They trigger CLIXML serializing of the arguments passed to as well as the output from the CLI call.
This serialization is the same as the one used by PowerShell's remoting / background-job infrastructure and has two benefits:
- PowerShell's output streams are preserved (whereas passing a string
-Command
merges all output into the single standard stdout stream).
- Arguments and output are passed and received as objects (rather than as text only, as happens when you pass a string
-Command
), albeit with the same limitations on type fidelity as in remoting - see this answer for more information.
The equivalent of using -args
to pass arguments to a script block is to pass an explicitly Base64-encoded argument list to -encodedArguments
(-ea
):
- This parameter - undocumented as of this writing - additionally requires serializing the argument list to XML (CLIXML format) before Base64-encoding the result, as demonstrated in this answer.
As for what you tried:
Your 1st command works, because you're passing a decoded plain-text version of the mkdir
command to powershell.exe
, which implicitly binds to the -Command
parameter and is executed as a command in the new PowerShell session.
As an aside: pwsh
, PowerShell Core's CLI, now defaults to -File
, so -Command
(or -c
) would have to be used explicitly.
Your 2nd command does not work, because $code
now contains the plain text of the Base64-decoding command from your 1st command.
That command references variable $4
, which the new PowerShell instance you're creating knows nothing about.
However, instead of trying to defer the decoding of the Base64-encoded mkdir
command to the new PowerShell session, it makes much more sense to pass the Base64-encoded command directly to the new session (if a new session is even needed, see above), via -EncodedCommand
.
However, -EncodedCommand
requires a Base64 encoding based on UTF-16LE, not UTF-8 - see above for how to produce such an encoding explicitly (if needed).
If you're given a UTF-8-based Base64 encoding, you can translate it into a UTF-16LE-based one as follows:
# UTF-8-based Base64-encoding
$4 = "bWtkaXIgQzpcVGVtcFw1NTU="
# Decode to plain text.
$plainTextCode = [Text.Encoding]::Utf8.GetString([Convert]::FromBase64String($4))
# Re-encode to Base64 via UTF-16 ("Unicode"):
$utf16Base64Command = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($plainTextCode))
# Pass to powershell.exe via -EncodedCommand
powershell -EncodedCommand $code