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