How do I call the PowerShell CLI robustly, with respect to character encoding, input and output streams, quoting and escaping? - powershell

This self-answered question aims to give a systematic overview of the PowerShell CLI (command-line interface), both for Windows PowerShell (powershell.exe) and PowerShell (Core) v6+ (pwsh.exe on Windows, pwsh on Unix).
While official help topics exist (see the links in the answer), they do not paint the full picture and lack systematic treatment (as of this writing).
Among others, the following questions are answered:
How do the edition-specific CLIs differ?
How do I pass PowerShell code to be executed to the CLIs? How do -Command (-c) and -File (-f) differ?
How do the arguments passed to these parameters need to be quoted and escaped?
What character-encoding issues come into play?
How do the PowerShell CLIs handle stdin input, and what stdout / stderr do they produce, and in what format?

PowerShell CLI fundamentals:
PowerShell editions: The CLI of the legacy, bundled-with-Windows Windows PowerShell edition is powershell.exe, whereas that of the cross-platform, install-on-demand PowerShell (Core) 7+ edition is pwsh.exe (just pwsh on Unix-like platforms).
Interactive use:
By default, unless code to execute is specified (via -Command (-c) or -File (-f, see below), an interactive session is entered. However, unlike in POSIX-compatible shells such as bash, you can use -NoExit to still enter an interactive session after executing code. This is especially handy for troubleshooting command lines when the CLI is called without a preexisting console window.
Use -NoLogo to suppress the startup text that is shown when entering an interactive session (not needed if code to execute is passed). GitHub issue #15644 suggest not showing this startup text by default.
To opt out of telemetry / update notifications, define the following environment variables before entering an interactive session: POWERSHELL_TELEMETRY_OPTOUT=1 / POWERSHELL_UPDATECHECK=Off
Parameters and defaults:
All parameter names are case-insensitive (as PowerShell generally is); most parameters have short aliases, such as -h and -? for -Help, which shows command-line help, which with pwsh (but not powershell.exe) also lists these short aliases.
Caveat: For long-term stability of your code, you should either use the full parameter names or their official aliases. Note that PowerShell's "elastic syntax" also allows you to use prefixes of parameter names ad hoc, as long as such a prefix unambiguously identifies the target parameter; e.g., -ver unambiguously targets -version currently, but - at least hypothetically - such a call could break in the future if a new parameter whose name also starts with ver were to be introduced.
pwsh supports more parameters than powershell.exe, such as -WorkingDirectory (-wd).
There are two (mutually exclusive) ways to pass code to execute, in which case the PowerShell process exits automatically when execution ends; pass -NonInteractive to prevent use of interactive commands in the code or -NoExit to keep the session open after execution:
-Command (-c) is for passing arbitrary PowerShell commands, which may be passed either as a single string or as individual arguments, which, after removing (unescaped) double-quotes, are later joined with spaces and then interpreted as PowerShell code.
-File (-f) is for invoking script files (.ps1) with pass-through arguments, which are treated as verbatim values.
These parameters must come last on the command line, because all subsequent arguments are interpreted as part of the command being passed / the script-file call.
See this answer for guidance on when to use -Command vs. -File, and the bottom section for quoting / escaping considerations.
It is advisable to use -Command (-c) or -File (-f) explicitly, because the two editions have different defaults:
powershell.exe defaults to -Command (-c)
pwsh defaults to -File (-f), a change that was necessary for supporting shebang lines on Unix-like platforms.
Unfortunately, even with -Command (-c) or -File (-f), profiles (initialization files) are loaded by default (unlike POSIX-compatible shells such as bash, which only do so when starting interactive shells).
Therefore, it is advisable to routinely precede -Command (-c) or -File (-f) with -NoProfile (-nop), which suppresses profile loading for the sake of both avoiding extra overhead and a more predictable execution environment (given that profiles can make changes that affect all code executed in a session).
GitHub proposal #8072 discusses introducing a separate CLI (executable) that does not load profiles in combination with these parameters and could also improve other legacy behaviors that the existing executables cannot change for the sake of backward-compatibility.
Character encoding (applies to both in- and output streams):
Note: The PowerShell CLIs only ever process text[1], both on input and output, never raw byte data; what the CLIs output by default is the same text you would see in a PowerShell session, which for complex objects (objects with properties) means human-friendly formatting not designed for programmatic processing, so to output complex objects it's better to emit them in a structured text-based format, such as JSON.
Note what while you can use -OutputFormat xml (-of xml) to get CLIXML output, which uses XML for object serialization, this particular format is of little use outside of PowerShell; ditto for accepting CLIXML input via stdin (-InputFormat xml / -if xml).
On Windows, the PowerShell CLIs respect the console's code page, as reflected in the output from chcp and, inside PowerShell, in [Console]::InputEncoding. A console's code page defaults to the system's active OEM code page.
Caveat: OEM code pages such as 437 on US-English systems are fixed, single-byte character encodings limited to 256 characters in total. To get full Unicode support, you must switch to code page 65001 before calling a PowerShell CLI (from cmd.exe, call chcp 65001); while this works in both PowerShell editions, powershell.exe unfortunately switches the console to a raster font in this case, which causes many Unicode characters not to display properly; however, the actual data is not affected.
On Windows 10, you may switch to UTF-8 system-wide, which sets both the OEM and the ANSI code page to 65001; note, however, that this has far-reaching consequences, and that the feature is still in beta as of this writing - see this answer.
On Unix-like platforms (pwsh), UTF-8 is invariably used (even if the active locale (as reported by locale) is not UTF-8-based, but that is very rare these days).
Input-stream (stdin) handling (received via stdin, either piped to a CLI call or provided via input redirection <):
To process stdin input as data:
Explicit use of the automatic $input variable is required.
This in turn means that in order to pass stdin input to a script file (.ps1), -Command (-c) rather than -File (-f) must be used. Note that this makes any arguments passed to the script (symbolized with ... below) subject to interpretation by PowerShell (whereas with -File they would be used verbatim):
-c "$Input | ./script.ps1 ..."
To process stdin input as code (pwsh only, seems to be broken in powershell.exe):
While passing PowerShell code to execute via stdin works in principle (by default, which implies -File -, and also with -Command -), it exhibits undesirable pseudo-interactive behavior and prevents passing of arguments: see GitHub issue #3223; e.g.:
echo "Get-Date; 'hello'" | pwsh -nologo -nop
Output-stream (stdout, stderr) handling:
(Unless you use a script block ({ ... }), which only works from inside PowerShell, see below), all 6 PowerShell's output streams are sent to stdout, including errors(!) (the latter are normally sent to stderr).
However, when you apply an - external - stderr redirection you can selectively suppress error-stream output (2>NUL from cmd.exe, 2>/dev/null on Unix) or send it to a file (2>errs.txt).
See the bottom section of this answer for more information.
Quoting and escaping of the -Command (-c) and -File (-f) arguments:
When calling from PowerShell (rarely needed):
There is rarely a need to call the PowerShell CLI from PowerShell, as as any command or script can simply be called directly and, conversely, calling the CLI introduces overhead due to creating a child process and results in loss of type fidelity.
If you still need to, the most robust approach is to use a script block ({ ... }), which avoids all quoting headaches, because you can use PowerShell's own syntax, as usual. Note that using script blocks only works from inside PowerShell, and that you cannot refer to the caller's variables in the script block; however, you can use the -args parameter to pass arguments (based on the caller's variables) to the script block, e.g., pwsh -c { "args passed: $args" } -args foo, $PID; using script blocks has additional benefits with respect to output streams and supporting data types other than strings; see this answer.
# From PowerShell ONLY
PS> pwsh -nop -c { "Caller PID: $($args[0]); Callee PID: $PID" } -args $PID
When calling from outside PowerShell (the typical case):
Note:
-File (-f) arguments must be passed as individual arguments: the script-file path, followed by arguments to pass to the script, if any. Both the script-file path and the pass-through arguments are used verbatim by PowerShell, after having stripping (unescaped) double quotes on Window[2].
-Command (-c) arguments may be passed as multiple arguments, but in the end PowerShell simply joins them together with spaces, after having stripped (unescaped) double quotes on Windows, before interpreting the resulting string as PowerShell code (as if you had submitted it in a PowerShell session).
For robustness and conceptual clarity, it is best to pass the command(s) as a single argument to -Command (-c), which on Windows requires a double-quoted string ("...") (although the overall "..." enclosure isn't strictly necessary for robustness in no-shell invocation environments such as Task Scheduler and some CI/CD and configuration-management environments, i.e. in cases where it isn't cmd.exe that processes the command line first).
Again, see this answer for guidance on when to use -File (-f) vs. when to use -Command (-c).
To test-drive a command line, call it from a cmd.exe console window, or, in order to simulate a no-shell invocation, use WinKey-R (the Run dialog) and use -NoExit as the first parameter in order to keep the resulting console window open.
Do not test from inside PowerShell, because PowerShell's own parsing rules will result in different interpretation of the call, notably with respect to recognizing '...' (single-quoting) and potential up-front expansion of $-prefixed tokens.
On Unix, no special considerations apply (this includes Unix-on-Windows environments such as WSL and Git Bash):
You only need to satisfy the calling shell's syntax requirements. Typically, programmatic invocation of the PowerShell CLI uses the POSIX-compatible system default shell on Unix, /bin/sh), which means that inside "..." strings, embedded " must be escaped as \", and $ characters that should be passed through to PowerShell as \$; the same applies to interactive calls from POSIX-compatible shells such as bash; e.g.:
# From Bash: $$ is interpreted by Bash, (escaped) $PID by PowerShell.
$ pwsh -nop -c " Write-Output \"Caller PID: $$; PowerShell PID: \$PID \" "
# Use single-quoting if the command string need not include values from the caller:
$ pwsh -nop -c ' Write-Output "PowerShell PID: $PID" '
On Windows, things are more complicated:
'...' (single-quoting) can only be used with -Command (-c) and never has syntactic function on the PowerShell CLI command line; that is, single quotes are always preserved and interpreted as verbatim string literals when the parsed-from-the-command-line argument(s) are later interpreted as PowerShell code; see this answer for more information.
"..." (double-quoting) does have syntactic command-line function, and unescaped double quotes are stripped, which in the case of -Command (-c) means that they are not seen as part of the code that PowerShell ultimate executes. " characters you want to retain must be escaped - even if you pass your command as individual arguments rather than as part of a single string.
powershell.exe requires " to be escaped as \"[3] (sic) - even though inside PowerShell it is ` (backtick) that acts as the escape character; however \" is the most widely established convention for escaping " chars. on Windows command lines.
Unfortunately, from cmd.exe this can break calls, if the characters between two \" instances happen to contain cmd.exe metacharacters such as & and |; the robust - but cumbersome and obscure - choice is "^""; \" will typically work, however.
:: powershell.exe: from cmd.exe, use "^"" for full robustness (\" often, but not always works)
powershell.exe -nop -c " Write-Output "^""Rock & Roll"^"" "
:: With double nesting (note the ` (backticks) needed for PowerShell's syntax).
powershell.exe -nop -c " Write-Output "^""The king of `"^""Rock & Roll`"^""."^"" "
:: \" is OK here, because there's no & or similar char. involved.
powershell.exe -nop -c " Write-Output \"Rock and Roll\" "
pwsh.exe accepts \" or "".
"" is the robust choice when calling from cmd.exe ("^"" does not work robustly, because it normalizes whitespace; again, \" will typically, but not always work).
:: pwsh.exe: from cmd.exe, use "" for full robustness
pwsh.exe -nop -c " Write-Output ""Rock & Roll"" "
:: With double nesting (note the ` (backticks)).
pwsh.exe -nop -c " Write-Output ""The king of `""Rock & Roll`""."" "
:: \" is OK here, because there's no & or similar char. involved.
pwsh.exe -nop -c " Write-Output \"Rock and Roll\" "
In no-shell invocation scenarios, \" can safely be used in both editions; e.g., from the Windows Run dialog (WinKey-R); note that the first command would break from cmd.exe (& would be interpreted as cmd.exe's statement separator, and it would attempt to execute a program named Roll on exiting the PowerShell session; try without -noexit to see the problem instantly):
pwsh.exe -noexit -nop -c " Write-Output \"Rock & Roll\" "
pwsh.exe -noexit -nop -c " Write-Output \"The king of `\"Rock & Roll`\".\" "
See also:
Quoting headaches also apply in the inverse scenario: calling external programs from a PowerShell session: see this answer.
When calling from cmd.exe, %...%-enclosed tokens such as %USERNAME% are interpreted as (environment) variable references by cmd.exe itself, up front, both when used unquoted and inside "..." strings (and cmd.exe has no concept of '...' strings to begin with). While typically desired, sometimes this needs to be prevented, and, unfortunately, the solution depends on whether a command is being invoked interactively or from a batch file (.cmd, .bat): see this answer.
[1] This also applies to PowerShell's in-session communication with external programs.
[2] On Unix, where no process-level command lines exist, PowerShell only ever receives an array of verbatim arguments, which are the result of the calling shell's parsing of its command line.
[3] Use of "" is half broken; try powershell.exe -nop -c "Write-Output 'Nat ""King"" Cole'" from cmd.exe.

Related

On WSL2 how do I get a powershell command's output to print to stdout

In WSL2 I want to run a powershell command and get the output of the command to print to stdout on my WSL2.
I have tried many variants but it seems I always just get the command given back and not its output:
$ pwsh.exe -Command {Get-Date}
Get-Date
$ pwsh.exe -Command '{Write-Output "hello"}'
Write-Output "hello"
You should not use curly braces here :
pwsh.exe -Command 'Get-Date'
Philippe's helpful answer provides the crucial pointer:
When calling pwsh, the PowerShell (Core) CLI, from outside PowerShell:
Do not use { ... } to enclose your command(s) - that only works from inside PowerShell.
Inside PowerShell, { ... } creates a script block literal, which is a reusable piece of PowerShell code that can be invoked on demand.
If you pass { ... } to pwsh from inside PowerShell, special processing happens behind the scenes, which notably preserves type fidelity in the output as much as possible - see this answer for details.
It should be noted that calling the PowerShell CLI from PowerShell is rarely necessary, except, for instance, on Windows, if you want to call the respective other PowerShell edition's CLI.
Instead, pass your command(s) as-is, inside a string.
If you pass a string to -Command from outside PowerShell with { ... } embedded, what happens is that PowerShell creates a script block and - in the absence of invoking it, with & or . - its string representation is output, which is the block's verbatim content (excluding { and }) - that is what you saw.
As an aside: There are many examples out there that use "& { ... }" as a -Command argument; while that technically works, it is unnecessary, and passing just "..." (or '...', from POSIX-compatible shells) is sufficient.
Thus:
# Simple case: no quoting necessary
$ pwsh.exe -Command Get-Date
# Command string that requires quoting:
$ pwsh.exe -Command 'Write-Output "hello"'

Powershell 7: using ampersand (&) in string literals

I'm trying to execute the following command in PowerShell, but I have no idea how to escape the ampersand character which is part of the URL
az rest `
--method GET `
--uri ("https://graph.microsoft.com/v1.0/groups?`$count=true&`$filter=startsWith(displayName,'some+filter+text')&`$select=id,displayName") `
--headers 'Content-Type=application/json'
As the & character is used to start a new command, it breaks the url and want to execute the remainder.
Is there a way to tell powershell not to do that?
Olaf's answer provides an effective solution; let me add an explanation:
The source of the problem is a confluence of two behaviors:
When calling external programs, PowerShell performs on-demand double-quoting of each argument solely based on whether a given argument value contains spaces - otherwise, the argument is passed unquoted - irrespective of whether or not the value was originally quoted in the PowerShell command (e.g., cmd /c echo ab, cmd /c echo 'ab', and cmd /c echo "ab" all result in unquoted ab getting passed as the last token on the command line PowerShell rebuilds behind the scenes to ultimately use for execution).
The Azure az CLI is implemented as a batch file (az.cmd) and when a batch file is called, it is cmd.exe that parses the arguments given; surprisingly - and arguably inappropriately - it parses them as if the command had been submitted from inside a cmd.exe session.
As a result, if an argument is passed from PowerShell to a batch file that (a) contains no spaces, yet (b) contains cmd.exe metacharacters such as &, the call breaks.
A simple demonstration, using a cmd /c echo call as a stand-in for a call to a batch file:
# !! Breaks, because PowerShell (justifiably) passes *unquoted* a&b
# !! when it rebuilds the command line to invoke behind the scenes.
PS> cmd /c echo 'a&b'
a
'b' is not recognized as an internal or external command,
operable program or batch file.
There are three workarounds:
Use embedded "..." quoting:
# OK, but with a CAVEAT:
# Works as of PowerShell 7.2, but arguably *shouldn't*, because
# PowerShell should automatically *escape* the embedded " chars. as ""
PS> cmd /c echo '"a&b"'
"a&b"
# Ditto, using an *expandable* (interpolating) PowerShell string:
PS> cmd /c echo "`"$HOME & Family; can't put a `$ value on that.`""
"C:\Users\jdoe & Family; can't put a $ value on that." # e.g.
Use --%, the stop-parsing token - but see the bottom section of this answer for the limitations of --% and its associated pitfalls.
# OK, but with a CAVEAT:
# Requires "..." quoting, but doesn't recognize *PowerShell* variables,
# also doesn't support single-quoting and line continuation.
PS> cmd /c echo --% "a&b"
"a&b"
Call via cmd /c and pass a single string encompassing the batch-file call and all its arguments, (ultimately) using cmd.exe's syntax.
# OK (remember, cmd /c echo stands for a call to a batch file, such as az.cmd)
# Inside the single string passed to the outer cmd /c call,
# be sure to use "...", as that is the only quoting cmd.exe understands.
PS> cmd /c 'cmd /c echo "a&b"'
"a&b"
# Ditto, using an *expandable* (interpolating) PowerShell string:
PS> cmd /c "cmd /c echo `"$HOME & Family; can't put a `$ value on that.`""
"C:\Users\jdoe & Family; can't put a $ value on that." # e.g.
Taking a step back:
Now, wouldn't it be nice if you didn't have to worry about all these things?
Especially since you may not know or care if a given CLI - such as az - just so happens to be implemented as a batch file?
As a shell, PowerShell should do its best to relay arguments faithfully behind the scenes, and allow the caller to focus exclusively on satisfying only PowerShell's syntax rules:
Unfortunately, PowerShell has to date (PowerShell 7.2) generally done a very poor job in this regard, irrespective of cmd.exe's quirks - see this answer for a summary.
With respect to cmd.exe's (batch-file call) quirks, PowerShell could predictably compensate for them in a future version - but it looks like that isn't going to happen, unfortunately; see GitHub issue #15143.
I don't have access to an Azure tennant right now to test and I actually don't have experiences with the Azure CLI in general but I'd expect this to work:
az rest `
--method GET `
--uri 'https://graph.microsoft.com/v1.0/groups?$count=true&$filter=startsWith(displayName,some+filter+text)&$select=id,displayName' `
--headers 'Content-Type=application/json'
or this:
az rest --method GET --headers "Content-Type=application/json" `
--% --uri "https://graph.microsoft.com/v1.0/groups?$count=true&$filter=startsWith(displayName,some+filter+text)&$select=id,displayName"
I only added the backticks for better readability - you may remove them in your actual code.

Execute a PowerShell script within RunAs in a script

I have a need to run a PowerShell script as another user (the users will actually be doing the auth) based on detected environment. The script leverages a smartcard for login. The problem I have is when the PowerShell.exe instance launches from the runas, it simply prints my command and doesn't actually run it.
Note: The filepaths have spaces that get escaped with double-`, just not shown here hence the escaped ``' in the actual command. Have also used ```".
Example:
Start-Process runas.exe "/netonly /smartcard /user:domain\$env:USERNAME ""powershell.exe -noexit -Command `'&$filePath -arg1 -arg2`' "" "
or
Start-Process runas.exe "/netonly /smartcard /user:domain\$env:USERNAME ""powershell.exe -noexit -File `'$filePath -arg1 -arg2`' "" "
File path points to the same .ps1 file. The dir is similar to: C:\Users\Username\My Documents\script.ps1
When this runs I simply get a script window that doesn't actually run, it just prints the File name. I have also tried using -File but that simply crashes (regardless of -noexit). The runas bit works fine, I get my smartcard prompt etc its just the actual PowerShell launch that I am struggling with.
These work just fine calling them directly from PowerShell Cli but when in a .ps1 it just won't work.
Any help would be greatly appreciate.
There's generally no reason to use Start-Process to run a console application such as runas.exe - unless you explicitly want the application to run in a new window, asynchronously (by default) - see this answer for more information.
Eliminating Start-Process simplifies the quoting:
runas.exe /netonly /smartcard /user:domain\$env:USERNAME "powershell.exe -noexit -File \`"$filePath\`" -arg1 -arg2"
Note the - unfortunate - need to manually \-escape the embedded " chars. (`"), which is required up to at least v7.1 - see this answer.
As for what you tried:
On Windows, passing a '...'-enclosed string to the PowerShell CLI's -Command (-c) parameter from outside PowerShell (such as via runas.exe) causes it to be interpreted as a verbatim PowerShell string, printing its content as-is (except for whitespace normalization).
You can verify this by running the following from cmd.exe, for instance:
C:\>powershell -c 'Get-Date; whatever I type here is used as-is - almost'
Get-Date; whatever I type here is used as-is - almost
Note how the multiple spaces before the word almost were normalized to a single one.
The reason is that on Windows only double-quoting ("...") has syntactic function on the command line - ' characters are interpreted verbatim and therefore passed through.
Therefore, when the -Command (-c) argument sees its argument(s) -- after command-line parsing - and then interprets the resulting space-joined, possibly double-quote-stripped arguments as PowerShell source code, a span of '...' is interpreted as a verbatim PowerShell string, as usual (see the conceptual about_Quoting_Rules help topic, which discusses the types of PowerShell string literals).
In concrete terms:
'Get-Date; whatever I type here is used as-is - almost' is parsed by the PowerShell CLI as the following arguments(!):
'Get-Date;, whatever, I, type, here, is, used, as-is, -, almost' - note how any information about the amount of whitespace that originally separated these argument is lost in the process.
These arguments are then joined with a single space to form the string that PowerShell then interprets as PowerShell source code, which yields:
'Get-Date; whatever I type here is used as-is - almost'
This amounts to a verbatim (single-quoted) PowerShell string literal, whose content is then output as-is.

Command line arguments for msiexec break on PowerShell if they contain space

I'm trying to set a public property in an InstallShield installer with a value containing space. While running the MSI installer, I'm using below command on PowerShell prompt. Since the value contains a space so I used double quotes to pass the value
msiexec -i "myinstaller.msi" MYDIRPATH="C:\new folder\data.txt"
It breaks the command as the argument value C:\new folder\data.txt has a space in the string new folder. It results in showing up below error prompt of msiexec:
It suggests that arguments passed to the msiexec command has some problem.
But if I execute the same command on Windows default command prompt then it runs fine:
Few other options that I've tried to make things work on PowerShell prompt are as below:
Using single quote in place of double quotes
Using a back tick (`) character before space in the argument as per this answer.
Try with this
msiexec -i "myinstaller.msi" MYDIRPATH=`"C:\new folder\data.txt`"
The escape character in PowerShell is the grave-accent(`).
Note:
This answer addresses direct, but asynchronous invocation of msiexec from PowerShell, as in the question. If you want synchronous invocation, use Start-Process with the -Wait switch, as shown in Kyle 74's helpful answer, which also avoids the quoting problems by passing the arguments as a single string with embedded quoting.
Additionally, if you add the -PassThru switch, you can obtain a process-information object that allows you to query msiexec's exit code later:
# Invoke msiexec and wait for its completion, then
# return a process-info object that contains msiexec's exit code.
$process = Start-Process -Wait -PassThru msiexec '-i "myinstaller.msi" MYDIRPATH="C:\new folder\data.txt"'
$process.ExitCode
Note: There's a simple trick that can make even direct invocation of msiexec synchronous: pipe the call to a cmdlet, such as Wait-Process
(msiexec ... | Wait-Process) - see this answer for more information.
To complement Marko Tica's helpful answer:
Calling external programs in PowerShell is notoriously difficult, because PowerShell, after having done its own parsing first, of necessity rebuilds the command line that is actually invoked behind the scenes in terms of quoting, and it's far from obvious what rules are applied.
Note:
While the re-quoting PowerShell performs behind the scenes in this particular case is defensible (see bottom section), it isn't what msiexec.exe requires.
Up to at least PowerShell 7.1, some of the re-quoting is downright broken, and the problems, along with a potential upcoming (partial) fix, are summarized in this answer.
Marko Tica's workaround relies on this broken behavior, and with the for now experimental feature that attempts to fix the broken behavior (PSNativeCommandArgumentPassing, available since Core 7.2.0-preview.5), the workaround would break. Sadly, it looks like then simply omitting the workaround won't work either, because it was decided not to include accommodations for the special quoting requirements of high-profile CLIs such as msiexec - see GitHub issue #15143.
To help with this problem, PSv3+ offers --%, the stop-parsing symbol, which is the perfect fit here, given that the command line contains no references to PowerShell variables or expressions: --% passes the rest of the command line as-is to the external utility, save for potential expansion of %...%-style environment variables:
# Everything after --% is passed as-is.
msiexec --% -i "myinstaller.msi" MYDIRPATH="C:\new folder\data.txt"
If you do need to include the values of PowerShell variables or expressions in your msiexec call, the safest option is to call via cmd /c with a single argument containing the entire command line; for quoting convenience, the following example uses an expandable here-string (see the bottom section of this answer for an overview of PowerShell's string literals).
$myPath = 'C:\new folder\data.txt'
# Let cmd.exe invoke msiexec, with the quoting as specified.
cmd /c #"
msiexec --% -i "myinstaller.msi" MYDIRPATH="$myPath"
"#
If you don't mind installing a third-party module, the ie function from the Native module (Install-Module Native) obviates the need for any workarounds: it fixes problems with arguments that have embedded " chars. as well as empty-string arguments and contains important accommodations for high-profile CLIs such as msiexec on Windows, and will continue to work as expected even with the PSNativeCommandArgumentPassing feature in effect:
# `ie` takes care of all necessary behind-the-scenes re-quoting.
ie msiexec -i "myinstaller.msi" MYDIRPATH="C:\new folder\data.txt"
As for what you tried:
PowerShell translated
MYDIRPATH="C:\new folder\data.txt" into
"MYDIRPATH=C:\new folder\data.txt" behind the scenes - note how the entire token is now enclosed in "...".
Arguably, these two forms should be considered equivalent by msiexec, but all bets are off in the anarchic world of Windows command-line argument parsing.
This is the best way to install a program in general with Powershell.
Here's an example from my own script:
start-process "c:\temp\SQLClient\sqlncli (x64).msi" -argumentlist "/qn IACCEPTSQLNCLILICENSETERMS=YES" -wait
Use Start-Process "Path\to\file\file.msi or .exe" -argumentlist (Parameters) "-qn or whatever" -wait.
Now -wait is important, if you have a script with a slew of programs being installed, the wait command, like piping to Out-Null, will force Powershell to wait until the program finishes installing before continuing forward.

How to download a file to $env:temp directory using powershell from cmd console?

I can download a file to a constant location from cmd console using powershell like this
C:\Users\Weijun>powershell -command "(new-object System.Net.WebClient).DownloadFile('https://example.com/example.zip', 'D:\example.zip')"
but i need to download it to system temp directory,using the powershell variable $env:temp,the following doesn't work.(variable not expanded in single-quote)
powershell -command "(new-object System.Net.WebClient).DownloadFile('https://example.com/example.zip', '$env:temp\example.zip')"
how to pass variable as parameter in method invoke?
UPDATE: it is a typo for $env:temp:,I amend it.
tl;dr:
From outside of PowerShell (cmd.exe - Command Prompt or batch file), use escaped double-quoted strings - \"...\" - inside the overall command string - "..." - passed to powershell -command for strings that need interpolation (e.g., expansion of variable references such as $env:temp):
powershell -command "(new-object System.Net.WebClient).DownloadFile('https://example.com/example.zip', \"$env:temp\example.zip\")"
You not only need to fix the incidental problem that Nkosi points out - removing the extraneous : at the end of $env:temp: - but you must generally either not quote the path at all (argument syntax), or use escaped double quotes (expression syntax or values with shell metacharacters) - single-quoted strings don't work, because PowerShell treats their content as a literal and won't expand the environment-variable reference.[1]
For a description of PowerShell's two fundamental parsing modes - argument syntax vs. expression syntax - see Get-Help about_Parsing.
Simplified examples:
Without quoting (argument syntax):
c:\>powershell.exe -noprofile -command "write-output $env:temp\example.zip"
C:\Users\jdoe\AppData\Local\Temp\example.zip
With double-quoting (expression syntax or if the value contains shell metacharacters such as , or |):
c:\>powershell.exe -noprofile -command "write-output \"$env:temp\example.zip\""
C:\Users\jdoe\AppData\Local\Temp\example.zip
Note: When called from the outside, PowerShell requires embedded " chars. to be escaped as \" - unlike inside PowerShell, where `" must be used.
Applied to your command: with (escaped) double-quoting, which is needed, because you're using .NET method syntax, which is parsed with expression syntax:
powershell -command "(new-object System.Net.WebClient).DownloadFile('https://example.com/example.zip', \"$env:temp\example.zip\")"
[1] If you run the same command from PowerShell, you won't see the problem, because the command as a whole is enclosed in "...", which means that the current PowerShell session will expand $env:temp - before it is passed to powershell.exe.
However, when called from cmd.exe, this interpolation is NOT applied - hence the need to either omit quoting or use embedded (escaped) double-quoting.
I would use Invoke-WebRequest with -OutFile:
powershell -command "Invoke-WebRequest -Uri 'http://example.com/file-to-get.txt' -OutFile $env:temp\file-to-get.txt"
NB this requires PowerShell 3.0 (if I remember correctly).