Question about using PowerShell Select-String, exiftool(-k) - powershell

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

Related

In powershell spawn notepad++ when file to open has spaces in it

$npp = "C:\Program Files\Notepad++\notepad++.exe";
$myfiles = #(
"C:\bad boys\file1.txt",
"C:\bad boys\file2.txt",
"C:\bad boys\file3.txt"
)
foreach ($file in $myfiles) {
Start-Process -FilePath $npp -ArgumentList "$file" -PassThru -NoNewWindow | out-null
}
This almost works... except, It doesn't open in notepad++ because it sees the space in the file name and thinks this is where the file path ends... thus, i am unable to open my file list. Any Ideas how to fix? What i get instead is notepad++ asking many times if I want to create the file "C:\bad"
tl;dr
While Joel Coehoorn's helpful answer provides an effective solution to your Start-Process problem (which stems from the bug detailed below), you can simplify your code to:
foreach ($file in $myfiles) {
# Note: | Out-Null is a trick that makes calling *GUI* applications
# *synchronous* (makes PowerShell wait for them to exit).
& $npp $file | Out-Null
}
You're seeing a long-standing bug in Start-Process that causes it to blindly space-concatenate its -ArgumentList (-Args) arguments without using required embedded double-quoting for arguments with spaces when forming the single string encoding all arguments that is passed to the target executable behind the scenes.
See GitHub issue #5576, which also discusses that a fix will require a new parameter so as not to break backward compatibility.
For that reason, the required embedded double-quoting must be performed manually as shown in Joel's answer.
When passing multiple arguments, it is ultimately easier to pass a single string to -ArgumentList, with embedded double-quoting as necessary - essentially by formulating a string similar to how you would pass multiple arguments from cmd.exe:
E.g., if you were to pass two file paths with spaces to Notepad++ at once, you would do:
Start-Process -Wait -FilePath $npp -ArgumentList "`"C:\bad boys\file1.txt`" `"C:\bad boys\file2.txt`""
Alternatively, since your argument string doesn't require string interpolation, you could use a verbatim (single-quoted) string instead, which avoids the need for escaping the embedded " as `":
Start-Process -Wait -FilePath $npp -ArgumentList '"C:\bad boys\file1.txt`" `"C:\bad boys\file2.txt"'
Using a here-string is yet another option that avoids the need to escape, and can additionally make the call more readable (also works with single quotes (#'<newline>...<newline>'#):
Start-Process -Wait -FilePath $npp -ArgumentList #"
"C:\bad boys\file1.txt" "C:\bad boys\file2.txt"
"#
Also note the overall simplification of the Start-Process call:
Use of -Wait to ensure synchronous execution (waiting for Notepad++ to exit before continuing).
It looks like this is what you tried to do by combining -PassThru with piping to Out-Null, but that doesn't actually work, because that only waits for Start-Process itself to exit (which itself - unlike the launched process - executes synchronously anyway).
The omission of the unnecessary -NoNewWindow parameter, which only applies to starting console applications (in order to prevent opening a new console window); Notepad++ is a GUI application.
Note that the only good reason to use Start-Process here - rather than direct invocation - is the need for synchronous execution: Start-Process -Wait makes launching GUI applications synchronous (too), whereas with direct invocation only console applications execute synchronously.
If you didn't need to wait for Notepad++ to exit, direct invocation would make your quoting headaches would go away, as the required embedded quoting is then automatically performed behind the scenes:[1]
foreach ($file in $myfiles) {
& $npp $file # OK, even with values with spaces
}
However, the | Out-Null trick can be used effectively in direct invocation to make calling GUI applications synchronous[2], which leads us to the solution at the top:
foreach ($file in $myfiles) {
& $npp $file | Out-Null # Wait for Notepad++ to exit.
}
[1] However, up to at least PowerShell 7.2.x, other quoting headaches can still arise, namely with empty-string arguments and arguments whose values contain " chars. - see this answer.
[2] Out-Null automatically makes PowerShell wait for the process in the previous pipeline segment to exit, so as to ensure that all input can be processed - and it does so irrespective of whether the process is a console-subsystem or GUI-subsystem application. Since GUI applications are normally detached from the calling console and therefore produce no output there, Out-Null has no ill effects. In the rare event that a GUI application does explicitly attach to the calling console and produce output there, you can use | Write-Output instead (which also works if there's no output, but is perhaps more confusing).
Try quotes around the file paths within the string data:
$myfiles = #(
"`"C:\bad boys\file.txt`"",
"`"C:\bad boys\file2.txt`"",
"`"C:\bad boys\file3.txt`""
)

How to pass an array to the arguments in Start-Process in Powershell?

I am writing a script to play certain files in a player. I use Get-ChildItem to get an array of file names. Then I want to use Start-Process to play these files. However, how can I add these file names to the arguments of the player program?
I used Start-Process -FilePath "C:\Program Files\DAUM\PotPlayer\PotPlayerMini64.exe" -ArgumentList $selected_items but it seems it doesn't work and the files are not played.
Notice there are spaces in the file names.
Syntax-wise, your approach should work, but doesn't, due to an unfortunate bug, still present in PowerShell 7.2 - see GitHub issue #5576.
While passing an array of arguments to Start-Process's -ArgumentList parameter does cause the array elements to be passed as individual arguments (which is usually how external CLIs expect multiple file arguments), the necessary double-quoting around elements with spaces is not applied when the command line ultimately used for invocation is constructed behind the scenes.
Also, for robustness you should use the .FullName property of the objects stored in $selected_items, so as to ensure that full paths are passed, because - in Windows PowerShell, situationally - Get-ChildItem's output objects may stringify to the file name only - see this answer.
Workaround: Pass a single argument to -ArgumentList, in which you encode all pass-through arguments, using embedded double-quoting.
Start-Process `
-FilePath "C:\Program Files\DAUM\PotPlayer\PotPlayerMini64.exe" `
-ArgumentList ($selected_items.ForEach({ '"{0}"' -f $_.FullName }) -join ' ')
Taking a step back:
If PotPlayerMini64.exe is a Windows GUI(-subsystem) application, you don't need Start-Process at all, because even direct invocation will then act asynchronously (i.e., the program will launch, and control will return to PowerShell right away; conversely, if you wanted to wait for the program to exit, use Start-Process -Wait).
& "C:\Program Files\DAUM\PotPlayer\PotPlayerMini64.exe" $selected_Items.FullName
Note that in direct invocations such as this, PowerShell does perform the necessary double-quoting behind the scenes, on demand.
Note: I'm unclear on whether passing multiple file paths to PotPlayerMini64.exe alone also starts playback - the alternative solution in the next section may ensure that.
Alternative, clipboard-based solution:
Judging by PotPlayerMini64.exe's available command-line options[1], the following may work (I cannot personally verify):
/clipboard :Appends content(s) from clipboard into playlist and starts playback immediately.
# Copy the full names of the files of interest to the clipboard.
Set-Clipboard -Value $selected_items.FullName
# Launch the player and tell it to start playback of the files on the clipboard.
# Parameters -FilePath and -ArgumentList are positionally implied.
Start-Process 'C:\Program Files\DAUM\PotPlayer\PotPlayerMini64.exe' /clipboard
There are file-arguments-related options such as /new, /insert, and /add, but it's unclear to me whether they - or omitting them altogether, as in your attempt - automatically start playback (may depend on the application's persistent configuration).
[1] Note that this is not the official documentation; I couldn't find the latter.
You can ForEach-Object:
Get-ChildItem . | ForEach-Object {Start-Process -FilePath "C:\Program Files\DAUM\PotPlayer\PotPlayerMini64.exe" -ArgumentList $_.FullName}
You don't need start-process (plus, -argumentlist doesn't handle filenames with spaces as easily).
& "C:\Program Files\DAUM\PotPlayer\PotPlayerMini64.exe"
C:\Program` Files\DAUM\PotPlayer\PotPlayerMini64.exe
$env:path += ';C:\Program Files\DAUM\PotPlayer'; PotPlayerMini64
Even using start-process, it varies with the program. For example, Emacs can take multiple file arguments, separated by spaces. If the filename has a space, it would need to be double quoted. External programs don't know what arrays are, and start-process converts them to one string with spaces in between each element.
start emacs file1,file2,'"my file"'
get-wmiobject win32_process | ? name -eq emacs.exe | % commandline
"c:\program files\emacs\bin\emacs.exe" file1 file2 "my file"
ps emacs | % commandline # ps 7
"c:\program files\emacs\bin\emacs.exe" file1 file2 "my file"

Command Line Command Output in start-process from exe file

Here is the program. I am using dell command | configure. The command-line command is as follows:
"C:\Program Files (x86)\Dell\Command Configure\X86_64>cctk.exe" --wakeonlan
In Powershell you can navigate to the folder and run:
./cctk.exe --wakeonlan
I can pipe the above command into a variable and get the information I need. This requires my shell to cd into the folder accordingly and run accordingly.
$test = ./cctk.exe --wakeonlan
This will give you an output. However when you use start-process, you get no output as this is a command-line command. A cmd screen appears and runs the command. So, I added a -nonewwindow and -wait flags. The output now appears on the screen, but I can't seem to capture it.
$test = start-process "C:\Program Files (x86)\Dell\Command Configure\X86_64\cctk.exe" -ArgumentList #("--wakeonlan") -NoNewWindow -Wait
At this point test is empty. I tried using the Out-File to capture the information as well. No success. The command outputs to the screen but nowhere else.
I also tried the cmd method where you pipe the information in using the /C flag.
$test = Start-Process cmd -ArgumentList '/C start "C:\Program Files (x86)\Dell\Command Configure\X86_64\cctk.exe" "--wakeonlan"' -NoNewWindow -Wait
However, I have tried many variations of this command with no luck. Some say C:\Program is not recognized. Some just open command prompt. The above says --wakeonlan is an unknown command.
Any pointers would help greatly.
There are various ways to run this without the added complication of start-process.
Add to the path temporarily:
$env:path += ';C:\Program Files (x86)\Dell\Command Configure\X86_64;'
cctk
Call operator:
& 'C:\Program Files (x86)\Dell\Command Configure\X86_64\cctk'
Backquote all spaces and parentheses:
C:\Program` Files` `(x86`)\Dell\Command` Configure\X86_64\cctk
To elaborate on js2010's helpful answer:
In short: Because your executable path is quoted, direct invocation requires use of &, the call operator, for syntactic reasons - see this answer for details.
To synchronously execute console applications or batch files and capture their output, call them directly ($output = c:\path\to\some.exe ... or $output = & $exePath ...), do not use Start-Process (or the System.Diagnostics.Process API it is based on) - see this answer for more information.
If you do use Start-Process, which may be necessary in special situations, such as needing to run with a different user identity:
The only way to capture output is in text files, via the -RedirectStandardOutput / -RedirectStandardError parameters. Note that the character encoding of the output files is determined by the encoding stored in [Console]::OutputEncoding[1], which reflects the current console output code page, which defaults to the system's active legacy OEM code page.
By contrast, even with -NoNewWindow -Wait, directly capturing output with $output = ... does not work, because the launched process writes directly to the console, bypassing PowerShell's success output stream, which is the one variable assignments capture.
[1] PowerShell uses the same encoding to decode output from external programs in direct invocations - see this answer for details.

Start-Process, Invoke-Command or?

Using the program got your back or GYB. I run the following command
Start-Process -FilePath 'C:\Gyb\gyb.exe' -ArgumentList #("--email <Email Address>", "--action backup", "--local-folder $GYBfolder", "--service-account", "--batch-size 4") -Wait
The issue is that when the process is done my script does not complete.
$GYBfolder = $GYBfolder.Replace('"', "")
$output = [PSCustomObject]#{
Name = $SourceGYB
Folder = $GYBfolder
}
$filename = "C:\reports\" + $SourceGYB.Split("#")[0] + "_Backup.csv"
$output | Export-Csv $filename -NoTypeInformation | Format-Table text-align=left -AutoSize
Return $filename
For some reason the script stops right before the return.
I am curious to know if I should be using a different command to run GYB?
Any thoughts on why the script does not process the return?
There's great information in the comments, but let me attempt a systematic overview:
To synchronously execute external console applications and capture their output, call them directly (C:\Gyb\gyb.exe ... or & 'C:\Gyb\gyb.exe' ...), do not use Start-Process - see this answer.
Only if gyb.exe were a GUI application would you need **Start-Process -Wait in order to execute it synchronously**.
A simple, but non-obvious shortcut is to pipe the invocation to another command, such as Out-Null, which also forces PowerShell to wait (e.g. gyb.exe | Out-Null) - see below.
When Start-Process is appropriate, the most robust way to pass all arguments is as a single string encoding all arguments, with appropriate embedded "..." quoting, as needed; this is unfortunate, but required as a workaround for a long-standing bug: see this answer.
Invoke-Command's primary purpose is to invoke commands remotely; while it can be used locally, there's rarely a good reason to do so, as &, the call operator is both more concise and more efficient - see this answer.
When you use an array to pass arguments to an external application, each element must contain just one argument, where parameter names and their values are considered distinct arguments; e.g., you must use #(--'action', 'backup', ...) rather than
#('--action backup', ...)
Therefore, use the following to run your command synchronously:
If gyb.exe is a console application:
# Note: Enclosing #(...) is optional
$argList = '--email', $emailAddress, '--action', 'backup', '--local-folder', $GYBfolder, '--service-account', '--batch-size', 4
# Note: Stdout and stderr output will print to the current console, unless captured.
& 'C:\Gyb\gyb.exe' $argList
If gyb.exe is a GUI application, which necessitates use of Start-Process -Wait (a here-string is used, because it makes embedded quoting easier):
# Note: A GUI application typically has no stdout or stderr output, and
# Start-Process never returns the application's *output*, though
# you can ask to have a *process object* returned with -PassThru.
Start-Process -Wait 'C:\Gyb\gyb.exe' #"
--email $emailAddress --action backup --local-folder "$GYBfolder" --service-account --batch-size 4
#"
The shortcut mentioned above - piping to another command in order to force waiting for a GUI application to exit - despite being obscure, has two advantages:
Normal argument-passing syntax can be used.
The automatic $LASTEXITCODE variable is set to the external program's process exit code, which does not happen with Start-Process. While GUI applications rarely report meaningful exit codes, some do, notably msiexec.
# Pipe to | Out-Null to force waiting (argument list shortened).
# $LASTEXITCODE will reflect gyb.exe's exit code.
# Note: In the rare event that the target GUI application explicitly
# attaches to the caller's console and produces output there,
# pipe to `Write-Output` instead, and possibly apply 2>&1 to
# the application call so as to also capture std*err* output.
& 'C:\Gyb\gyb.exe' --email $emailAddress --action backup | Out-Null
Note: If the above unexpectedly does not run synchronously, the implication is that gyb.exe itself launches another, asynchronous operation. There is no generic solution for that, and an application-specific one would require you to know the internals of the application and would be nontrivial.
A note re argument passing with direct / &-based invocation:
Passing an array as-is to an external program essentially performs splatting implicitly, without the need to use #argList[1]. That is, it passes each array element as its own argument.
By contrast, if you were to pass $argList to a PowerShell command, it would be passed as a single, array-valued argument, so #argList would indeed be necessary in order to pass the elements as separate, positional arguments. However, the more typical form of splatting used with PowerShell commands is to use a hashtable, which allows named arguments to be passed (parameter name-value pairs; e.g., to pass a value to a PowerShell command's
-LiteralPath parameter:
$argHash = #{ LiteralPath = $somePath; ... }; Set-Content #argHash
[1] $args and #args are largely identical in this context, but, strangely, #argList, honors use of --%, the stop-parsing symbol operator, even though it only makes sense in a literally specified argument list.

Invoke a Perl script from Powershell and stores the script output in a variable [duplicate]

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)