How can I get PowerShell to understand this type of thing:
Robocopy.exe | Find.exe "Started"
The old command processor gave a result, but I'm confused about how to do this in PowerShell:
&robocopy | find.exe "Started" #error
&robocopy | find.exe #("Started") #error
&robocopy #("|", "find.exe","Started") #error
&robocopy | &find #("Started") #error
&(robocopy | find "Started") #error
Essentially I want to pipe the output of one external command into another external command. In reality I'll be calling flac.exe and piping it into lame.exe to convert FLAC to MP3.
Cheers
tl;dr
# Note the nested quoting. CAVEAT: May break in the future.
robocopy.exe | find.exe '"Started"'
# Alternative. CAVEAT: doesn't support *variable references* after --%
robocopy.exe | find.exe --% "Started"
# *If available*, use PowerShell's equivalent of an external program.
# In lieu of `findstr.exe`, you can use Select-String (whose built-in alias is scs):
# Note: Outputs are *objects* describing the matching lines.
# To get just the lines, pipe to | % ToString
# or - in PowerShell 7+ _ use -Raw
robocopy.exe | sls Started
For an explanation, read on.
PowerShell does support piping to and from external programs.
The problem here is one of parameter parsing and passing: find.exe has the curious requirement that its search term must be enclosed in literal double quotes.
In cmd.exe, simple double-quoting is sufficient: find.exe "Started"
By contrast, PowerShell by default pre-parses parameters before passing them on and strips enclosing quotes if the verbatim argument value doesn't contain spaces, so that find.exe sees only Started, without the double quotes, resulting in an error.
There are three ways to solve this:
PS v3+ (only an option if your parameters are only literals and/or environment variables): --%, the stop-parsing symbol, tells PowerShell to pass the rest of the command line as-is to the target program (reference environment variables, if any, cmd-style (%<var>%)):
robocopy.exe | find.exe --% "Started"
This answer details the limitations of --%.
PS v2 too, or if you need to use PowerShell variables in the parameters: apply an outer layer of PowerShell quoting (PowerShell will strip the single quotes and pass the contents of the string as-is to find.exe, with enclosing double quotes intact):
robocopy.exe | find.exe '"Started"'
Caveat: It is only due to broken behavior that this technique works. If this behavior gets fixed (the fix may require opt-in), the above won't work anymore, because PowerShell would then pass ""Started"" behind the scenes, which breaks the call - see this answer for more information.
If an analogous PowerShell command is available, use it, which avoids all quoting problems. In this case, the Select-String cmdlet, PowerShell's more powershell analog to findstr.exe can be used, as shown above.
#Jobbo: cmd and PowerShell are two different shells. Mixing them is sometimes possible but as you realized from Shay's answer, it won't get you too far. However, you may be asking the wrong question here.
Most of the time, the problem you are trying to solve like piping to find.exe are not even necessary.
You do have equivalent of find.exe, in fact more powerful version, in Powershell: select-string
You can always run a command and assign results to a variable.
$results = Robocopy c:\temp\a1 c:\temp\a2 /MIR
Results are going to be STRING type, and you have many tools to slice and dice it.
PS > $results |select-string "started"
Started : Monday, October 07, 2013 8:15:50 PM
Invoke it via cmd:
PS> cmd /c 'Robocopy.exe | Find.exe "Started"'
Related
In a PowerShell script I'm trying to filter the output of the exiftool(-k).exe command below, using Select-String.
I've tried numerous permutations, but none work, and I always see the unfiltered output. What do I need to do to filter the output of this command?
Start-Process -FilePath "C:\PowerShell\exiftool(-k).exe" -ArgumentList test.jpg |
Select-String -pattern 'GPS' -SimpleMatch
You cannot directly receive output from a Start-Process call[1], so using it in a pipeline is pointless.
In fact, on Windows your program launched with Start-Process runs in a different, new window, which is where you saw the unfiltered output (given that no Select-String was applied there); in your calling window, Start-Process produced no output at all, and therefore nothing was sent to Select-String, and the pipeline as a whole produced no output.
Never use Start-Process to synchronously invoke a console application whose output you want to capture or redirect - simply call the application directly:
& "C:\PowerShell\exiftool(-k).exe" test.jpg | Select-String GPS -SimpleMatch
Note that &, the call operator, is needed for this invocation, because your executable path is (double-)quoted (of necessity here, because the file name contains ( and )); & is only needed for executable paths that are quoted and/or contain variable references; you wouldn't need it to call git ..., for instance.
[1] While you would see the program's output in the caller's window if you added -NoNewWindow -Wait to a Start-Process call, you still wouldn't be able to capture, pass on or redirect it.
The naming is inconvenient. Rename it to exiftool.exe and run it without start-process.
rename-item 'exiftool(-k).exe' exiftool.exe
C:\Users\bfbarton\PowerShell\exiftool.exe test-jpg | Select-String GPS
Or
$env:path += ';C:\Users\bfbarton\PowerShell'
exiftool test.jpg | Select-String GPS
The website recommends to 'rename to "exiftool.exe" for command-line use'. https://exiftool.org . Even in unix, it wouldn't work without escaping the parentheses.
There's also the option of using the call operator. Using tab completion actually does this:
& '.\exiftool(-k).exe' test.jpg | select-string gps
I'd like to run an external process and capture it's command output to a variable in PowerShell. I'm currently using this:
$params = "/verify $pc /domain:hosp.uhhg.org"
start-process "netdom.exe" $params -WindowStyle Hidden -Wait
I've confirmed the command is executing but I need to capture the output into a variable. This means I can't use the -RedirectOutput because this only redirects to a file.
Note: The command in the question uses Start-Process, which prevents direct capturing of the target program's output. Generally, do not use Start-Process to execute console applications synchronously - just invoke them directly, as in any shell. Doing so keeps the application's output streams connected to PowerShell's streams, allowing their output to be captured by simple assignment $output = netdom ... (and with 2> for stderr output), as detailed below.
Fundamentally, capturing output from external programs works the same as with PowerShell-native commands (you may want a refresher on how to execute external programs; <command> is a placeholder for any valid command below):
# IMPORTANT:
# <command> is a *placeholder* for any valid command; e.g.:
# $cmdOutput = Get-Date
# $cmdOutput = attrib.exe +R readonly.txt
$cmdOutput = <command> # captures the command's success stream / stdout output
Note that $cmdOutput receives an array of objects if <command> produces more than 1 output object, which in the case of an external program means a string[1] array containing the program's output lines.
If you want to make sure that the result is always an array - even if only one object is output, type-constrain the variable as an array ([object[]]), or enclose the command in #(...), the array-subexpression operator:[2]
[array] $cmdOutput = <command>
$cmdOutput = #(<command>) # alternative
By contrast, if you want $cmdOutput to always receive a single - potentially multi-line - string, use Out-String, though note that a trailing newline is invariably added (GitHub issue #14444 discusses this problematic behavior):
# Note: Adds a trailing newline.
$cmdOutput = <command> | Out-String
With calls to external programs - which by definition only ever return strings in PowerShell[1] - you can avoid that by using the -join operator instead:
# NO trailing newline.
$cmdOutput = (<command>) -join "`n"
Note: For simplicity, the above uses "`n" to create Unix-style LF-only newlines, which PowerShell happily accepts on all platforms; if you need platform-appropriate newlines (CRLF on Windows, LF on Unix), use [Environment]::NewLine instead.
To capture output in a variable and print to the screen:
<command> | Tee-Object -Variable cmdOutput # Note how the var name is NOT $-prefixed
Or, if <command> is a cmdlet or advanced function, you can use common parameter
-OutVariable / -ov:
<command> -OutVariable cmdOutput # cmdlets and advanced functions only
Note that with -OutVariable, unlike in the other scenarios, $cmdOutput is always a collection, even if only one object is output. Specifically, an instance of the array-like [System.Collections.ArrayList] type is returned.
See this GitHub issue for a discussion of this discrepancy.
To capture the output from multiple commands, use either a subexpression ($(...)) or call a script block ({ ... }) with & or .:
$cmdOutput = $(<command>; ...) # subexpression
$cmdOutput = & {<command>; ...} # script block with & - creates child scope for vars.
$cmdOutput = . {<command>; ...} # script block with . - no child scope
Note that the general need to prefix with & (the call operator) an individual command whose name/path is quoted - e.g., $cmdOutput = & 'netdom.exe' ... - is not related to external programs per se (it equally applies to PowerShell scripts), but is a syntax requirement: PowerShell parses a statement that starts with a quoted string in expression mode by default, whereas argument mode is needed to invoke commands (cmdlets, external programs, functions, aliases), which is what & ensures.
The key difference between $(...) and & { ... } / . { ... } is that the former collects all input in memory before returning it as a whole, whereas the latter stream the output, suitable for one-by-one pipeline processing.
Redirections also work the same, fundamentally (but see caveats below):
$cmdOutput = <command> 2>&1 # redirect error stream (2) to success stream (1)
However, for external commands the following is more likely to work as expected:
$cmdOutput = cmd /c <command> '2>&1' # Let cmd.exe handle redirection - see below.
Considerations specific to external programs:
External programs, because they operate outside PowerShell's type system, only ever return strings via their success stream (stdout); similarly, PowerShell only ever sends strings to external programs via the pipeline.[1]
Character-encoding issues can therefore come into play:
On sending data via the pipeline to external programs, PowerShell uses the encoding stored in the $OutVariable preference variable; which in Windows PowerShell defaults to ASCII(!) and in PowerShell [Core] to UTF-8.
On receiving data from an external program, PowerShell uses the encoding stored in [Console]::OutputEncoding to decode the data, which in both PowerShell editions defaults to the system's active OEM code page.
See this answer for more information; this answer discusses the still-in-beta (as of this writing) Windows 10 feature that allows you to set UTF-8 as both the ANSI and the OEM code page system-wide.
If the output contains more than 1 line, PowerShell by default splits it into an array of strings. More accurately, the output lines are streamed one by one, and, when captured, stored in an array of type [System.Object[]] whose elements are strings ([System.String]).
If you want the output to be a single, potentially multi-line string, use the -join operator (you can alternatively pipe to Out-String, but that invariably adds a trailing newline):
$cmdOutput = (<command>) -join [Environment]::NewLine
Merging stderr into stdout with 2>&1, so as to also capture it as part of the success stream, comes with caveats:
To do this at the source, let cmd.exe handle the redirection, using the following idioms (works analogously with sh on Unix-like platforms):
$cmdOutput = cmd /c <command> '2>&1' # *array* of strings (typically)
$cmdOutput = (cmd /c <command> '2>&1') -join "`r`n" # single string
cmd /c invokes cmd.exe with command <command> and exits after <command> has finished.
Note the single quotes around 2>&1, which ensures that the redirection is passed to cmd.exe rather than being interpreted by PowerShell.
Note that involving cmd.exe means that its rules for escaping characters and expanding environment variables come into play, by default in addition to PowerShell's own requirements; in PS v3+ you can use special parameter --% (the so-called stop-parsing symbol) to turn off interpretation of the remaining parameters by PowerShell, except for cmd.exe-style environment-variable references such as %PATH%.
Note that since you're merging stdout and stderr at the source with this approach, you won't be able to distinguish between stdout-originated and stderr-originated lines in PowerShell; if you do need this distinction, use PowerShell's own 2>&1 redirection - see below.
Use PowerShell's 2>&1 redirection to know which lines came from what stream:
Stderr output is captured as error records ([System.Management.Automation.ErrorRecord]), not strings, so the output array may contain a mix of strings (each string representing a stdout line) and error records (each record representing a stderr line). Note that, as requested by 2>&1, both the strings and the error records are received through PowerShell's success output stream).
Note: The following only applies to Windows PowerShell - these problems have been corrected in PowerShell [Core] v6+, though the filtering technique by object type shown below ($_ -is [System.Management.Automation.ErrorRecord]) can also be useful there.
In the console, the error records print in red, and the 1st one by default produces multi-line display, in the same format that a cmdlet's non-terminating error would display; subsequent error records print in red as well, but only print their error message, on a single line.
When outputting to the console, the strings typically come first in the output array, followed by the error records (at least among a batch of stdout/stderr lines output "at the same time"), but, fortunately, when you capture the output, it is properly interleaved, using the same output order you would get without 2>&1; in other words: when outputting to the console, the captured output does NOT reflect the order in which stdout and stderr lines were generated by the external command.
If you capture the entire output in a single string with Out-String, PowerShell will add extra lines, because the string representation of an error record contains extra information such as location (At line:...) and category (+ CategoryInfo ...); curiously, this only applies to the first error record.
To work around this problem, apply the .ToString() method to each output object instead of piping to Out-String:
$cmdOutput = <command> 2>&1 | % { $_.ToString() };
in PS v3+ you can simplify to:
$cmdOutput = <command> 2>&1 | % ToString
(As a bonus, if the output isn't captured, this produces properly interleaved output even when printing to the console.)
Alternatively, filter the error records out and send them to PowerShell's error stream with Write-Error (as a bonus, if the output isn't captured, this produces properly interleaved output even when printing to the console):
$cmdOutput = <command> 2>&1 | ForEach-Object {
if ($_ -is [System.Management.Automation.ErrorRecord]) {
Write-Error $_
} else {
$_
}
}
An aside re argument-passing, as of PowerShell 7.2.x:
Passing arguments to external programs is broken with respect to empty-string arguments and arguments that contain embedded " characters.
Additionally, the (nonstandard) quoting needs of executables such as msiexec.exe and batch files aren't accommodated.
For the former problem only, a fix may be coming (though the fix would be complete on Unix-like platforms), as discussed in this answer, which also details all the current problems and workarounds.
If installing a third-party module is an option, the ie function from the Native module (Install-Module Native) offers a comprehensive solution.
[1] As of PowerShell 7.1, PowerShell knows only strings when communicating with external programs. There is generally no concept of raw byte data in a PowerShell pipeline. If you want raw byte data returned from an external program, you must shell out to cmd.exe /c (Windows) or sh -c (Unix), save to a file there, then read that file in PowerShell. See this answer for more information.
[2] There are subtle differences between the two approaches (which you may combine), though they usually won't matter: If the command has no output, the [array] type-constraint approach results in $null getting stored in the target variable, whereas it is an empty ([object[]) array in the case of #(...). Additionally, the [array] type constraint means that future (non-empty) assignments to the same variable are coerced to an array too.
Have you tried:
$OutputVariable = (Shell command) | Out-String
If you want to redirect the error output as well, you have to do:
$cmdOutput = command 2>&1
Or, if the program name has spaces in it:
$cmdOutput = & "command with spaces" 2>&1
Or try this. It will capture output into variable $scriptOutput:
& "netdom.exe" $params | Tee-Object -Variable scriptOutput | Out-Null
$scriptOutput
Another real-life example:
$result = & "$env:cust_tls_store\Tools\WDK\x64\devcon.exe" enable $strHwid 2>&1 | Out-String
Notice that this example includes a path (which begins with an environment variable). Notice that the quotes must surround the path and the EXE file, but not the parameters!
Note: Don't forget the & character in front of the command, but outside of the quotes.
The error output is also collected.
It took me a while to get this combination working, so I thought that I would share it.
I tried the answers, but in my case I did not get the raw output. Instead it was converted to a PowerShell exception.
The raw result I got with:
$rawOutput = (cmd /c <command> 2`>`&1)
I got the following to work:
$Command1="C:\\ProgramData\Amazon\Tools\ebsnvme-id.exe"
$result = & invoke-Expression $Command1 | Out-String
$result gives you the needful
I use the following:
Function GetProgramOutput([string]$exe, [string]$arguments)
{
$process = New-Object -TypeName System.Diagnostics.Process
$process.StartInfo.FileName = $exe
$process.StartInfo.Arguments = $arguments
$process.StartInfo.UseShellExecute = $false
$process.StartInfo.RedirectStandardOutput = $true
$process.StartInfo.RedirectStandardError = $true
$process.Start()
$output = $process.StandardOutput.ReadToEnd()
$err = $process.StandardError.ReadToEnd()
$process.WaitForExit()
$output
$err
}
$exe = "C:\Program Files\7-Zip\7z.exe"
$arguments = "i"
$runResult = (GetProgramOutput $exe $arguments)
$stdout = $runResult[-2]
$stderr = $runResult[-1]
[System.Console]::WriteLine("Standard out: " + $stdout)
[System.Console]::WriteLine("Standard error: " + $stderr)
This thing worked for me:
$scriptOutput = (cmd /s /c $FilePath $ArgumentList)
If all you are trying to do is capture the output from a command, then this will work well.
I use it for changing system time, as [timezoneinfo]::local always produces the same information, even after you have made changes to the system. This is the only way I can validate and log the change in time zone:
$NewTime = (powershell.exe -command [timezoneinfo]::local)
$NewTime | Tee-Object -FilePath $strLFpath\$strLFName -Append
Meaning that I have to open a new PowerShell session to reload the system variables.
What did the trick for me, and would work when using external commands and also when both standard error and standard output streams could be the result of running the command (or a mix of them), was the following:
$output = (command 2>&1)
I have this command that works ok on powershell
Compare-Object (Get-Content "tex1.txt") (Get-Content "tex2.txt") | Where-Object{$_.SideIndicator -eq "<="} | select inputobject | ft -hidetableheaders
I'm trying to running in cmd by doing this:
powershell -Command " & {Compare-Object (Get-Content "tex1.txt") (Get-Content "tex2.txt") | Where-Object{$_.SideIndicator -eq "<="} | select inputobject | ft -hidetableheaders}"
but it says something like: the name, the directory or the volume syntax is incorrect (is in spanish so i dont know the exact translation)
I think the problem is the pipes, since running everything before the pipe: Compare-Object (Get-Content "tex1.txt") (Get-Content "tex2.txt") works
PD: I also tried to write ^ before the pipes but I haven't succeeded.
tl;dr
When calling the PowerShell CLI (powershell.exe for Windows PowerShell, pwsh for the cross-platform PowerShell [Core] 6+ edition):
Using embedded " in an overall "..." string comes with escaping challenges.
If feasible for a given command, formulating it without embedded " is the easiest solution:
powershell -Command "Compare-Object (Get-Content tex1.txt) (Get-Content tex2.txt) | Where-Object {$_.SideIndicator -eq '<='} | select inputobject | ft -hidetableheaders"
Read on, if you do need to use embedded ".
eryksun points out that your problem is your lack of escaping of embedded " chars. inside the overall "..." string, which causes cmd.exe to see multiple strings, including parts it considers unquoted, which causes problems with special characters such as | and < - they never reach PowerShell.
Nesting double-quote strings from cmd.exe is tricky business:
To make cmd.exe happy, you need to double the embedded " chars. ("")
Separately, to make powershell.exe happy, you need to \-escape " chars.
Note: PowerShell [Core] 6+, the cross-platform edition, on Windows now also accepts "" by itself, which is the most robust choice.
Generally, dealing with quoting and escaping arguments when calling from cmd.exe is a frustrating experience with no universal solutions, unlike in the Unix world. Sadly, PowerShell has its own challenges, even in the Unix world.[1]
In short: Escape embedded " chars. when calling the Windows PowerShell CLI, powershell.exe, from cmd.exe as follows (for the PowerShell (Core) 7+ CLI, pwsh.exe, "" is the robust choice):
Use "^"" (sic) when using powershell.exe -Command, which works robustly.
Caveat: "^"" does not work for calling other programs.
This saves you from additional escaping, as would be necessary if you used \"
Example: powershell -command " Write-Output "^""a & b"^"" " yields a & b, as expected, and the & didn't need escaping.
If you use the simpler - and customary - \", you may need to perform additional escaping: Specifically, you must individually ^-escape the following cmd.exe metacharacters with ^ inside \"...\" runs: & | < > ^Thanks, LotPings.
Example: powershell -command " Write-Output \"a ^& b\" " yields a & b; that is, the & needed escaping with ^.
Additionally, to treat % (and, with enabledelayedexpansion , !) verbatim, the escaping syntax unfortunately depends on whether you're calling from the command line or a batch file: use %^USERNAME% (!^USERNAME) from the former, and %%USERNAME%% (^!USERNAME^! / ^^!USERNAME^^! inside \"...\" runs) from the latter - see this answer for the gory details.
It is unfortunate that cmd.exe makes use of \" treacherous, given that it is supported by virtually all programs (except batch files), and if it weren't for these extra escaping requirements, command lines that use it have the potential to work across different platforms and shells - with the notable exception of calling from PowerShell, where, sadly, an additional layer of escaping is needed and " inside "..." must be escaped as \`" (sic); see this answer.
See the bottom section for ways to ease the escaping pain by avoiding use of nested ".
Other programs, including PowerShell Core:
Use just "" for programs compiled with Microsoft compilers and, on Windows, also Python and Node.js as well as PowerShell Core (pwsh.exe).
Regrettably, this robust option does not work with powershell.exe, i.e. Windows PowerShell.
Use \" for programs with Unix heritage, such as Perl and Ruby - which comes with the escaping headaches discussed above.
Avoiding embedded ":
When you call PowerShell's CLI, you can often get away without needing to embed double quotes:
There may be arguments in your string that don't require quoting at all, such as text1.txt and text2.txt
You can alternatively use single-quoting ('...') inside the overall command string, which require no escaping; note that such strings, from PowerShell's perspective, are string literals.
To put it all together:
powershell -Command "Compare-Object (Get-Content tex1.txt) (Get-Content tex2.txt) | Where-Object {$_.SideIndicator -eq '<='} | select inputobject | ft -hidetableheaders"
Note that I've also removed the & { ... } around your command, as it isn't necessary.
[1] eryksun puts it as follows: "This is the inescapable frustration of the Windows command line. Every program parses its own command line, using whatever rules it wants. So the syntax of a command line has to work with not only the shell (CMD) but also all programs invoked in the pipeline. In the Unix world the shell parses the command line into argv arrays, so typically you only have to get the syntax right to make the shell happy."
The problems with PowerShell Core, even on Unix, stem from how it re-quotes arguments behind the scenes before passing them on - see this GitHub docs issue.
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.
I am trying to execute the following powershell commands from CMD, for example:
powershell Get-WmiObject Win32_PnPSignedDriver
powershell Get-WmiObject Win32_PnPSignedDriver > test.txt
which both work correctly.
But when I do a query, for example:
powershell (Get-WmiObject Win32_PnPSignedDriver | where {$_.location -like "*PCI bus 0, device 22, function 0*"}).DeviceName
I am getting this message the cause of which I can not pin down:
INFO: Could not find files for the given pattern(s)
This seems to work for me (I was seeing the same error as you originally):
powershell -command "(Get-WmiObject Win32_PnPSignedDriver | where {$_.location -like '*PCI bus 0, device 22, function 0*'}).DeviceName"
To complement the existing answers with general guidelines for passing commands to powershell.exe from cmd.exe (a Command Prompt):
Enclose the entire PowerShell command in "..." (double quotes).
This protects its contents from unwanted up-front interpretation by cmd.exe - as happened with | in this case, as explained in aschipfl's answer.
However, cmd.exe-style environment-variable references (e.g., %USERNAME%) are still expanded.
For quoting that is embedded in the PowerShell command:
Use '...' (single quotes) where feasible; this is what Mark Wragg's helpful answer does, but it is only an option if the quoted string is meant to be a literal (doesn't contain variable references or subexpressions).
If you do need embedded "...", escape it as \"...\"
Note that while inside PowerShell it is the ` (backtick) that serves as the escape character, when passing strings from the outside PowerShell requires \.
Simplified examples (run from cmd.exe):
Note that passing a command without a parameter name implies the -Command parameter; run powershell -? to see the command-line syntax.
You may also want to use -NoProfile so that the user profile isn't loaded every time.
Commands that don't need embedded quoting - simply double-quote:
powershell -noprofile "get-date"
powershell -noprofile "(get-date).Date"
powershell -noprofile "get-date | get-member"
Commands with embedded quoting - use '...' or \"...\":
powershell -noprofile "(get-date).Year -like '*17'"
powershell -noprofile "$v=17; (get-date).Year -like \"*$v\""
Commands that incorporate the value of a cmd.exe environment variable:
:: # Nothing special to do: cmd.exe expands the reference irrespective of quoting.
set "v=17"
powershell -noprofile "(get-date).Year -like '*%v%'"
:: # More robust variant that passes the value as an argument to a script block.
set "v=17"
powershell -noprofile ". { param($v) (get-date).Year -like \"*$v\" }" "%v%"
Optional reading: calling powershell from POSIX-like shells such as bash:
The above rules apply analogously, except that you'll typically use '...' (single quotes) to enclose the entire PowerShell command, which categorically prevents up-front interpretation by the shell.
Using "..." is an option if up-front expansions of shell-variable references and command substitutions are explicitly desired, but the potential for confusion is great, because both POSIX-like shells and PowerShell use sigil $ to refer to variables - it may not be obvious what is expanded when.
POSIX-like shells categorically do not support embedding ' instances in side '...' strings, which necessitates a somewhat awkward workaround (see below).
Simplified examples (run from a POSIX-like shell such as bash):
Commands that don't need embedded quoting - simply single-quote:
powershell -noprofile 'get-date'
powershell -noprofile '(get-date).Date'
powershell -noprofile 'get-date | get-member'
Commands with embedded quoting - replace embedded ' instances with '\'' (sic) and use embedded " as-is:
powershell -noprofile '(get-date).Year -like '\''*17'\'''
powershell -noprofile '$v=17; (get-date).Year -like "*$v"'
Commands that incorporate the value of a shell/environment variable:
# By using a double-quoted string, the shell expands $v up front.
v=17
powershell -noprofile "(get-date).Year -like '*$v'"
# It gets trickier if you want to reference PS variables too.
# Note how the PS variable's $ is \-escaped so that the shell doesn't interpret it.
v=$HOME
powershell -noprofile "\$HOME -eq '$v'"
# More robust variant that passes the value as an argument to a script block.
v=17
powershell -noprofile '. { param($v) (get-date).Year -like "*$v" }' "$v"
I am pretty sure that the pipe character | is the problem here, because cmd tries to process it.
Simply escape it by preceding with ^:
powershell (Get-WmiObject Win32_PnPSignedDriver ^| where {$_.location -like "*PCI bus 0, device 22, function 0*"}).DeviceName
If this code is used within a parenthesised block of code in cmd, you may need to escape the closing ) as well (the opening ( can be escaped too, but there is no need):
powershell ^(Get-WmiObject Win32_PnPSignedDriver ^| where {$_.location -like "*PCI bus 0, device 22, function 0*"}^).DeviceName