Powershell 7: using ampersand (&) in string literals - powershell

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.

Related

Powershell script is failing when files with a single quote are passed through script. Alternate batch file is also failing with & and ! characters

This is a deceptively complex issue, but I'll do my best to explain the problem.
I have a simple wrapper script as follows called VSYSCopyPathToClipboard.ps1:
param (
[Parameter(Mandatory,Position = 0)]
[String[]]
$Path,
[Parameter(Mandatory=$false)]
[Switch]
$FilenamesOnly,
[Parameter(Mandatory=$false)]
[Switch]
$Quotes
)
if($FilenamesOnly){
Copy-PathToClipboard -Path $Path -FilenamesOnly
}else{
Copy-PathToClipboard -Path $Path
}
Copy-PathToClipboard is just a function I have available that copies paths/filenames to the clipboard. It's irrelevant to the issue, just assume it does what it says.
The way the wrapper is called is through the Windows right click context menu. This involves creating a key here: HKEY_CLASSES_ROOT\AllFilesystemObjects\shell\.
Mine looks like this:
The command is as follows:
"C:\Tools\scripts\BIN\SingleInstanceAccumulator.exe" -q:' "-c:pwsh -noprofile -windowstyle hidden -Command "C:\Tools\scripts\VSYSCopyPathToClipboard.ps1" -Path $files" "%1"
And similarly for the "Copy as Filename":
"C:\Tools\scripts\BIN\SingleInstanceAccumulator.exe" -q:' "-c:pwsh -noprofile -windowstyle hidden -Command "C:\Tools\scripts\VSYSCopyPathToClipboard.ps1" -FilenamesOnly -Path $files" "%1"
I am using a tool here called SingleInstanceAccumulator. This allows me to pass multiple selected files to a single instance of PowerShell. If I didn't use this program and ran my command with multiple files selected it would launch multiple instances of PowerShell for each file selected. It's the next best thing to creating your own shell extension and implementing IPC etc.
This has been working great until today when I encountered a file with a single quote in its filename (I.E.testing'video.mov) and the entire script failed. It's failing because the delimiter I'm using with SingleInstanceAccumulator is also a single quote and PowerShell sees no matching quote... thus errors out.
I could fix this if my variables were static by just doubling up the offending single quote, but since my parameters are files I have no opportunity to escape the single quote beyond renaming the file itself ... which is a non-solution.
So now I have no clue how to handle this.
My first try at solving the problem was as such:
Create a batch file and redirect my registry command to it.
Change the SingleInstanceAccumulator delimiter to '/' (All files will be separated by a forward slash.)
Replace the offending single quote to two single quotes.
Replace the '/' delimiters with single quotes.
Finally pass the whole argument list back to Powershell.
This image demonstrates how the above process looks:
This is the batch file's code:
#echo off
setlocal EnableDelayedExpansion
:: This script is needed to escape filenames that have
:: a single quote ('). It's replaced with double single
:: quotes so the filenames don't choke powershell
:: echo %cmdcmdline%
set "fullpath=%*"
echo Before
echo !fullpath!
echo ""
echo After
set fullpath=%fullpath:'=''%
set fullpath=%fullpath:/='%
echo !fullpath!
:: pwsh.exe -noprofile -windowstyle hidden -command "%~dpn0.ps1 -Path !fullpath!
pause
Once I got that wired up I started celebrating ... until I hit a file with an ampersand (&) or an exclamation point (!). Everything fell apart again. I did a whole bunch of google-fu with regards to escaping the & and ! characters but nothing suggested worked at all for me.
If I pass 'C:\Users\futur\Desktop\Testing\Video Files\MOV Batch\testing&video.mov' into my batch file, I get 'C:\Users\futur\Desktop\Testing\Video Files\MOV Batch\testing back.
It truncates the string at the exact position of the ampersand.
I feel like there has to be a way to solve this, and that I'm missing something stupid. If I echo %cmdcmdline% it shows the full commandline with the &, so it's available somehow with that variable.
In conclusion: I'm sorry for the novel of a post. There is a lot of nuance in what I'm trying to accomplish that needs to be explained. My questions are as follows:
Can I accomplish this with Powershell only and somehow pre-escape single quotes?
Can I accomplish this with a batch file, and somehow pre-escape & and ! (and any other special characters that would cause failure)?
Any help at all would be hugely appreciated.
Edit1:
So in the most hideous and hackish way possible, I managed to solve my problem. But since it's so horrible and I feel horrible for doing it I am still looking for a proper solution.
Basically, to recap, when I do either of these variable assignments:
set "args=%*"
set "args=!%*!"
echo !args!
& and ! characters still break things, and I don't get a full enumeration of my files. Files with & get truncated, etc.
But I noticed when I do:
set "args=!cmdcmdline!"
echo !args!
I get the full commandline call with all special characters retained:
C:\WINDOWS\system32\cmd.exe /c ""C:\Tools\scripts\VSYSCopyPathToClipboardTest.bat" /C:\Users\futur\Desktop\Testing\Video Files\MOV Batch\KylieCan't.mov/,/C:\Users\futur\Desktop\Testing\Video Files\MOV Batch\The !Rodinians - Future Forest !Fantasy - FFF Trailer.mov/,/C:\Users\futur\Desktop\Testing\Video Files\MOV Batch\Yelle - Je Veu&x Te Voir.mov/,/C:\Users\futur\Desktop\Testing\Video Files\MOV Batch\Erik&Truffaz.mov/,/C:\Users\futur\Desktop\Testing\Video Files\MOV Batch\my_file'name.mov/,/C:\Users\futur\Desktop\Testing\Video Files\MOV Batch\testing&video.mov/"
So what I did was simply strip out the initial C:\WINDOWS\system32\cmd.exe /c ""C:\Tools\scripts\VSYSCopyPathToClipboardTest.bat" part of the string:
#echo off
setlocal enableDelayedExpansion
set "args=!cmdcmdline!"
set args=!args:C:\WINDOWS\system32\cmd.exe=!
set args=!args: /c ""C:\Tools\scripts\VSYSCopyPathToClipboard.bat" =!
set args=!args:'=''!
set args=!args:/='!
set args=!args:~0,-1!
echo !args!
pwsh.exe -noprofile -noexit -command "%~dpn0.ps1 -Path !args!
And... it works flawlessly. It handles any crazy character I throw at it without needing to escape anything. I know It's totally the most degenerate garbage way of approaching this, but not finding a solution anywhere leads me to desperate measures. :)
I am probably going to make the string removal a bit more universal since it literally breaks if I change the filename.
I am still VERY much open to other solutions should anyone know of a way to accomplish the same thing in a more elegant way.
A fully robust solution based on PowerShell's -Command (-c) CLI parameter that can handle ' characters in paths as well as $ and ` ones requires a fairly elaborate workaround, unfortunately:[1]
Use an aux. cmd.exe call that echoes the $files macro as-is and pipe that to pwsh.exe; make SingleInstanceAccumulator.exe double-quote the individual paths (as it does by default), but use no delimiter (d:"") in order to effectively output a string in the form "<path 1>""<path 2>""...
Make pwsh.exe reference the piped input via the automatic $input variable and split it into an array of individual paths by " (removing empty elements that are a side effect of splitting with -ne ''). The necessity for providing the paths via the pipeline (stdin) is discussed in more detail in this related answer.
The resulting array can safely be passed to your scripts.
Also, enclose the entire -Command (-c) argument passed to pwsh.exe in \"...\" inside the "-c:..." argument.
Note: You may get away without doing this; however, this would result in whitespace normalization, which (however unlikely) would alter a file named, say, foo bar.txt to foo bar.txt (the run of multiple spaces was normalized to a single space).
Escaping " characters as \" is necessary for PowerShell's -Command (-c) CLI parameter to treat them verbatim, as part of the PowerShell code to execute that is seen after initial command-line parsing, during which any unescaped " characters are stripped.
Therefore, the first command stored in the registry should be (adapt the second one analogously; note that there must be no space between the echo $files and the subsequent |):
"C:\Tools\scripts\BIN\SingleInstanceAccumulator.exe" -d:"" "-c:cmd /c echo $files| pwsh.exe -noprofile -c \"& 'C:\Tools\scripts\VSYSCopyPathToClipboard.ps1' -Path ($input -split '\\\"' -ne '')\"" "%1"
Note:
If you modified your scripts to accept the paths as individual arguments rather than as an array, a much simpler solution via the -File CLI parameter (rather than -Command (-c)) is possible. This could be as simple as decorating the $Path parameter declaration with [Parameter(ValueFromRemainingArguments)] and then invoking the script without naming the target parameter explicitly (-Path):
"C:\Tools\scripts\BIN\SingleInstanceAccumulator.exe" -d:" " "-c:pwsh.exe -noprofile -File \"C:\Tools\scripts\VSYSCopyPathToClipboard.ps1\" $files" "%1"
Note the use of -d:" " to make SingleInstanceAccumulator.exe space-separate the (double-quoted by default) paths. Since -File passes the pass-through arguments verbatim, there is no concern about what characters the paths are composed of.
Self-contained PowerShell sample code:
The following code defines a Copy Paths to Clipboard shortcut-menu command for all file-system objects (except drives):
No separate .ps1 script is involved; instead, the code passed to -Command / -c directly performs the desired operation (copying the paths passed to the clipboard).
The following helps with troubleshooting:
The full command line with which PowerShell was invoked ([Environment]::CommandLine) is printed, as is the list of paths passed ($file)
-windowstyle hidden is omitted to keep the console window in which the PowerShell commands visible and -noexit is added so as to keep the window open after the command has finished executing.
Prerequisites:
Download and build the SingleInstanceAccumulator project using Visual Studio (using the .NET SDK is possible, but requires extra work).
Place the resulting SingleInstanceAccumulator.exe file in one of the directories listed in your $env:Path environment variable. Alternatively, specify the full path to the executable below.
Note:
reg.exe uses \ as its escape character, which means that \ characters that should become part of the string stored in the registry must be escaped, as \\.
The sad reality as of PowerShell 7.2 is that an extra, manual layer of \-escaping of embedded " characters is required in arguments passed to external programs. This may get fixed in a future version, which may require opt-in. See this answer for details. The code below does this by way of a -replace '"', '\"' operation, which can easily be removed if it should no longer be necessary in a future PowerShell version.
# RUN WITH ELEVATION (AS ADMIN).
# Determine the full path of SingleInstanceAccumulator.exe:
# Note: If it isn't in $env:PATH, specify its full path instead.
$singleInstanceAccumulatorExe = (Get-Command -ErrorAction Stop SingleInstanceAccumulator.exe).Path
# The name of the shortcut-menu command to create for all file-system objects.
$menuCommandName = 'Copy Paths To Clipboard'
# Create the menu command registry key.
$null = reg.exe add "HKEY_CLASSES_ROOT\AllFilesystemObjects\shell\$menuCommandName" /f /v "MultiSelectModel" /d "Player"
if ($LASTEXITCODE) { throw }
# Define the command line for it.
# To use *Windows PowerShell* instead, replace "pwsh.exe" with "powershell.exe"
# SEE NOTES ABOVE.
$null = reg.exe add "HKEY_CLASSES_ROOT\AllFilesystemObjects\shell\$menuCommandName\command" /f /ve /t REG_EXPAND_SZ /d (#"
"$singleInstanceAccumulatorExe" -d:"" "-c:cmd /c echo `$files| pwsh.exe -noexit -noprofile -c \\"[Environment]::CommandLine; `$paths = `$input -split [char] 34 -ne ''; `$paths; Set-Clipboard `$paths\\"" "%1"
"# -replace '"', '\"')
if ($LASTEXITCODE) { throw }
Write-Verbose -Verbose "Shortcut menu command '$menuCommandName' successfully set up."
Now you can right-click on multiple files/folders in File Explorer and select Copy Paths to Clipboard in order to copy the full paths of all selected items to the clipboard in a single operation.
[1] An alternative is to use the -f option instead, which causes SingleInstanceAccumulator.exe to write all file paths line by line to an auxiliary text file, and then expands $files to that file's full path. However, this requires the target scripts to be designed accordingly, and it is their responsibility to clean up the auxiliary text file.

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.

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

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.

Passing a cmd-line IF statement through Invoke-Expression breaks on output

If I pass an IF statement through PowerShell's Invoke-Expression, the command appears to be running and completing, but then it appears that the output is being evaluated as a new command instead of being returned to PowerShell. Three examples:
Invoke-Expression 'echo "hi"' (No IF statement)
Normal Output: hi
Invoke-Expression 'cmd /c IF exist C:\Windows (echo "hi")'
Error on Output: 'hi' is not recognized as an internal or external command, operable program or batch file.
Invoke-Expression 'cmd /c IF exist C:\Windows (query user)'
Error on Output:
'" USERNAME SESSIONNAME ID STATE IDLE TIME LOGON TIME"' is not recognized as an internal or external command, operable program or batch file.
What's the best way to run a command-line IF statement from PowerShell and be able to read its output? I tried Start-Process but cannot figure out for the life of me how to read its output. Tried a System.Diagnostics.ProcessStartInfo object copied from another StackOverflow post, but no luck there either.
Because people are bound to ask: The reason why I'm passing this through cmd in the first place is because this entire code block needs to be passed through Invoke-Command to a remote machine and cmd has folder/file access to computers on its network while PowerShell does not.
Your immediate problem is unrelated to the use of Invoke-Expression, which should generally be avoided:
cmd /c IF exist C:\Windows (echo "hi") # WRONG
is interpreted by PowerShell first, up front, and (echo "hi") is the same as (Write-Output "hi"), which PowerShell expands (interpolates) to the command's output, a string with content hi.
The - broken - command line that cmd exe ends up seeing is the following, which explains the error message:
cmd /c IF exist C:\Windows hi
For an overview of how PowerShell parses unquoted command-line arguments, see this answer.
There are several ways to fix that problem, appropriate in different scenarios:
# Single-quoting - passed as-is.
cmd /c 'IF exist C:\Windows (echo "hi")'
# Double-quoting - PowerShell would still expand $-prefixed tokens up front.
cmd /c "IF exist C:\Windows (echo `"hi`")"
#`# The stop-parsing symbol, --%, prevents PowerShell from parsing subsequent arguments,
# with the exception of cmd-style environment-variable references (%FOO%)
cmd /c --% IF exist C:\Windows (echo "hi")
Now, with Invoke-Expression you'd have to add escape those quotes due to having to specify them as part of a string, but, as mentioned in the comments, there is rarely a need for Invoke-Expression, and it is neither needed here, nor would I expect it to help with the "double hop" authentication problem you describe in a comment.
To address the latter, try this answer, which uses explicitly passed credentials to establish an auxiliary drive mapping on the remote machine.

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.