Execute a PowerShell script within RunAs in a script - powershell

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.

Related

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.

I am trying to run several PowerShell commands from a batch script, however the "%" symbol does not get transferred

I am trying to run several PowerShell commands from a batch script, however the "%" symbol does not get transferred to PowerShell.
For example, writing the following in a command prompt window:
powershell -Command "& {echo 'per%entage'}"
Will print:
per%entage
which is what I want, however if I save the same command into a .bat or .cmd file, it instead prints:
perentage
Why is it ignoring the "%" symbol? Is there a way to make it transfer properly? I'm especially confused that it works in a command prompt window, but not in a batch script. You'd think both would either work or not work.
Very unfortunately, cmd.exe's behavior differs with respect to command invocations from an interactive prompt vs. from a batch file with respect to how % characters are interpreted.
See this answer for background information.
Therefore, when calling from a batch file, a % char. that is to interpreted verbatim, must be escaped as %%:
:: From a *batch file*.
powershell -Command "'per%%entage'"
Note:
echo is a built-in alias for the Write-Output cmdlet, whose explicit use is rarely needed - see this answer for more information.
Invocation of commands (symbolized as ... here) in the form & { ... } is virtually never needed when using the PowerShell CLI - just use ... as-is.
Generally, for predictable invocation, it's worth using the -NoProfile switch as well - before the -Command parameter - so as to bypass loading of PowerShell's profile files, which are primarily meant for interactive sessions.

Passing newline character to PowerShell via Cmd

I'm trying to run a PowerShell script from Windows cmd.exe. The input to the PowerShell script is a string, which contains newline characters using PowerShell backtick escaping - i.e:
`r`n
For demonstration purposes, the input string is then written to the console, and also dumped to a file.
The issue I have is that when the script is run from cmd.exe using the syntax
powershell.exe script.ps1 "TEST`r`nTEST"
The newline characters in the string are not treated as newline, and are included literally in both the console output and the output text file.
TEST`r`nTEST
However, if I run this from a PowerShell environment, I get the expected result (i.e. the newline characters are parsed correctly, and a newline is inserted in the appropriate location).
TEST
TEST
Similarly, if I pass in \r\n instead of the escaped newline characters through Windows cmd.exe, and do a .replace in the PowerShell script
$date = $data.replace("\r\n","`r`n")
I get the expected output:
TEST
TEST
Is anyone able to shed some light on why this happens?
The test script is as follows:
param([string]$data) # data to send
Write-Host $data
[IO.File]::WriteAllText('d:\temp.txt', $data)
return 0
And the file is called from the command line as:
powershell.exe script.ps1 "TEST`r`nTEST"
The script is running on Windows Server 2012 R2, using PowerShell v4.0
tl;dr
Use -Command and pass the entire PowerShell command as a single string; e.g.:
C:\> powershell -NoProfile -Command "script.ps1 \"TEST`r`nTEST\""
TEST
TEST
Note how the internal " instances are escaped as \", which PowerShell requires when called from the outside (alternatively, for full robustness, use "^"" (sic) in Windows PowerShell and "" in PowerShell (Core) v6+).
In your specific case,
powershell -NoProfile -Command script.ps1 "TEST`r`nTEST" would have worked too, but generally that only works as intended if the string has no embedded spaces.
Given that -Command is the default up to PSv5.1, your command - as currently posted - should work as-is.
As of PowerShell v5.1, arguments passed to powershell.exe from the outside:
ARE subject to interpretation by PowerShell, including string interpolation, by default and when you use -Command (i.e., specifying neither -File nor -Command currently defaults to -Command).
Caveat: The default behavior will change in v6: -File will be the default then - see the relevant change on GitHub.
are NOT subject to interpretation if you use -File to invoke a script - (after potential interpretation by cmd.exe) PowerShell treats all arguments as literal strings.
Caveat: This behavior is currently being discussed with respect to v6, given that it is overtly problematic in at least one case: trying to pass Boolean values.
Optional reading: Why you should pass the entire PowerShell command as a single argument when using -Command:
When you use -Command with multiple arguments, PowerShell essentially assembles them into a single command line behind the scenes before executing it.
Any "..."-quoting around the individual arguments is lost in the process, which can have unexpected results; e.g.:
C:\> powershell -NoProfile -Command "& { $args.count }" "line 1`r`nline 2"
3 # !! "line 1`r`nline 2" was broken into 3 arguments
Given that the outer "..." quoting was removed in the process of parsing the command line, the actual command line that PowerShell ended up executing was:
C:\ PS> & { $args.Count } line 1`r`nline 2
3
To illustrate why, let's look at an equivalent command that uses explicit quoting:
C:\ PS> & { $args.Count } "line" "1`r`nline" "2"
In other words: After the enclosing " were removed, the resulting token was broken into multiple arguments by spaces, as usual.
The parameter will need to be reinterpreted as a PowerShell string. Will this get you down the road?
The reason your -replace did not work is that the original string actually contains a backtick. It needs to be escaped in the search string.
C:\src\t>type p1.ps1
Param([string]$s)
Write-Host $s
$p = Invoke-Expression `"$s`"
Write-Host $p
$p2 = $s -replace "``r``n","`r`n"
Write-Host $p2
C:\src\t>powershell -noprofile -file .\p1.ps1 "TEST`r`nTEST"
TEST`r`nTEST
TEST
TEST
TEST
TEST
Carriage return and Linefeed are bytes with values 13 and 10, you can't write them, you can't see them.
As a convenience, when writing PowerShell code, the language will let you write:
"`r`n"
in a double quoted string, and when processing PowerShell source code (and at no other time), it will read those and replace them with bytes value 13 and 10.
It is this line of code in the PowerShell tokenizer which does it.
There is nothing special about backtick-n to the cmd.exe interpreter, and nothing special about having it in a string - you can put it there in a single quoted string
'`n'
or replacing it in a string - except that you have to note when the replacement happens. e.g. in your comment:
For example, if you pass in 'r'n and then replace 'r'n with 'r'n, the 'r'n is still output literally
Because your code
-replace "`r`n"
becomes
-replace "[char]13[char]10"
and your string passed in from outside contains
`r`n
and they don't match. Backtick-n in a string isn't magic, strings are not all interpreted by the PowerShell engine as PowerShell code, nor are parameters, or anything. And it's only in that context - when you write your -replace code, that is when the swap for actual newline characters happens.

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).