Reputation: 307
I am making a batch dos code to execute commands and capture code in files, while also keeping the output on screen.
EDIT2: 3 apr 21, 15:36 The commands that I want execute are slow and fast and some commands can only be executed once. I have to execute for example commands like chkdsk, dism, sfc, wmic, dos (internal, external), powershell.
I thought the fastest way was to use the powershell "tee".
ex. (echo foo & echo bar) | powershell "$input | tee -a out.txt"
C:\WINDOWS\system32>(echo foo & echo bar) | powershell "$input | tee -a out.txt"
foo
bar
but when i use it with wmic i have different results:
C:\WINDOWS\system32>wmic recoveros get autoreboot
AutoReboot
FALSE
C:\WINDOWS\system32>wmic recoveros get autoreboot | powershell "$input | tee -a out.txt"
AutoReboot
FALSE
C:\WINDOWS\system32>
EDIT1: 3 apr 21, 14:46
I add some information how suggest JosefZ:
C:\WINDOWS\system32>del out.txt out2.txt
C:\WINDOWS\system32>wmic recoveros get autoreboot > out2.txt
C:\WINDOWS\system32>wmic recoveros get autoreboot | powershell "$input | tee -a out.txt"
AutoReboot
FALSE
C:\WINDOWS\system32>type out.txt
AutoReboot
FALSE
C:\WINDOWS\system32>type out2.txt
AutoReboot
FALSE
C:\WINDOWS\system32>powershell -nopro "(cat out.txt -encoding Byte) -join ' '"
255 254 65 0 117 0 116 0 111 0 82 0 101 0 98 0 111 0 111 0 116 0 32 0 32 0 13 0 10 0 13 0 10 0 70 0 65 0 76 0 83 0 69 0 32 0 32 0 32 0 32 0 32 0 32 0 32 0 13 0 10 0 13 0 10 0 13 0 10 0 13 0 10 0
C:\WINDOWS\system32>powershell -nopro "(cat out2.txt -encoding Byte) -join ' '"
255 254 65 0 117 0 116 0 111 0 82 0 101 0 98 0 111 0 111 0 116 0 32 0 32 0 13 0 10 0 70 0 65 0 76 0 83 0 69 0 32 0 32 0 32 0 32 0 32 0 32 0 32 0 13 0 10 0
C:\WINDOWS\system32>
EDIT3: 3 apr 21, 16:05 This is the code that I developed for the moment (some commands are commented for the moment with "rem"):
for %%C in (
"rem sfc /scannow"
"rem dism /online /cleanup-image /scanhealth"
"rem dism /online /cleanup-image /restorehealth"
"rem sfc /scannow"
"rem"
"rem chkdsk /scan"
"rem"
"wmic recoveros get autoreboot"
"wmic recoveros set autoreboot = false"
"wmic recoveros get autoreboot"
"wmic recoveros get DebugInfoType"
"wmic recoveros set DebugInfoType = 7"
"wmic recoveros get DebugInfoType"
"rem"
"wmic pagefile list /format:list"
"wmic Computersystem where name="%COMPUTERNAME%" get AutomaticManagedPagefile"
"wmic Computersystem where name="%COMPUTERNAME%" set AutomaticManagedPagefile=True"
"wmic Computersystem where name="%COMPUTERNAME%" get AutomaticManagedPagefile"
"rem"
"bcdedit /enum {badmemory}"
) do ( %ComSpec% /c "echo(%CD%^>%%~C & %%~C " | powershell -command " $input | tee -Append test.txt ")
EDIT4: 10 apr 2021, 8:21 I add this code to create a reference output to facilitate the comparison process between the various tests. This output is to be considered both on screen and on file.
@echo off
rem Warning! Run as administrator.
rem go safe place for testing...
cd %temp%
for %%C in (
"rem sfc /scannow"
"rem dism /online /cleanup-image /scanhealth"
"rem dism /online /cleanup-image /restorehealth"
"rem sfc /scannow"
"rem chkdsk /scan"
"wmic recoveros get autoreboot"
"wmic recoveros set autoreboot = false"
"wmic recoveros get autoreboot"
"wmic recoveros get DebugInfoType"
"wmic recoveros set DebugInfoType = 7"
"wmic recoveros get DebugInfoType"
"wmic pagefile list /format:list"
"wmic Computersystem where name="%COMPUTERNAME%" get AutomaticManagedPagefile"
"wmic Computersystem where name="%COMPUTERNAME%" set AutomaticManagedPagefile=True"
"wmic Computersystem where name="%COMPUTERNAME%" get AutomaticManagedPagefile"
"bcdedit /enum {badmemory}"
) do (
rem Partially simulates "echo on" on commands.
echo(%CD%^>%%~C
rem Execute command.
%%~C
rem alternative, but opens another cmd process.
rem %ComSpec% /c "echo(%CD%^>%%~C & %%~C"
)
pause
goto :eof
Upvotes: 1
Views: 947
Reputation: 437353
Bacon Bits' helpful answer contains good background information.
To solve your problem, I suggest you:
Redirect the wmic
commands directly to a temporary file, as that at least partially avoids the CRCRLF problem (i.e., two instead of the usual one CR (Carriage Return, U+000D
) character followed by a LF (Line Feed, U+000A
) that surfaces otherwise.[1]
Then let PowerShell read that temporary file, fix any remaining CRCRLF newlines, and send it to Tee-Object
, preceded by the simulated prompt string, which both prints it and appends it to the overall output file.
Here's a simplified batch-file example:
@echo off & setlocal
:: Delete a previous output file, if present.
del test.txt 2>NUL
for %%C in (
"wmic recoveros get autoreboot"
"wmic recoveros get DebugInfoType"
"wmic Computersystem where name="%COMPUTERNAME%" get AutomaticManagedPagefile"
) do (
%%~C > %TEMP%\tmp.txt
echo "%CD%>%%~C"| powershell -noprofile -c "$($input).Trim([char] 0x22), ((Get-Content -Raw %TEMP%\tmp.txt -Encoding Oem) -replace '\r\r\n', [Environment]::NewLine) | tee -Append test.txt"
del %TEMP%\tmp.txt
)
Note the required "..."
around %COMPUTERNAME%
in the name="%COMPUTERNAME%"
WMI filter expression.
%%~C > %TEMP%\tmp.txt
executes the command at hand and directly saves its output to a temporary file.
echo "%CD%>%%~C"
, as in your question, echoes a simulated prompt showing the current directory followed by the quote-stripped command string (%%~C
); e.g.,
"C:\Users\jdoe>wmic recoveros get autoreboot"
. This string is piped to PowerShell, albeit enclosed in double quotes, to protect it from unwanted interpretation of its content by cmd.exe
itself, such as >
and |
characters.
In the PowerShell command, $($input).Trim([char] 0x22)
reads stdin input via the automatic $input
variable, trims the enclosing "
chars. that echo
included in its output (.Trim([char] 0x22])
, where [char] 0x22
represents a "
char., whose literal use is avoided here to prevent escaping headaches), and (implicitly) outputs the resulting string.
Get-Content
then reads the temporary file and outputs the result. Note the use of -Raw
, which speeds up processing, because the entire file is then read at once, as single, multi-line string, rather than line by line, which is also the prerequisite for replacing the broken CRCRLF newlines with the usual CRLF sequences, using -replace '\r\r\n', [Environment]::NewLine
.
As a side effect, an empty line is effectively inserted between blocks of command-specific output (assuming each >
operation resulted in a file with a trailing newlin); if that is not desired, you can append a second -replace
operation: -replace '\r\n\z'
echo "%CD%>%%~C"| powershell -noprofile -c "$($input).Trim([char] 0x22), ((Get-Content -Raw %TEMP%\tmp.txt -Encoding Oem) -replace '\r\r\n', [Environment]::NewLine -replace '\r\n\z') | tee -Append test.txt"
If you want to normalize empty lines between output blocks, you can trim any leading and trailing newlines first, and then add the desired, fixed number; in the following example, a single newline is ensured between blocks:
echo "%CD%>%%~C"| powershell -noprofile -c "$($input).Trim([char] 0x22), (((Get-Content -Raw %TEMP%\tmp.txt -Encoding Oem) -replace '\r\r\n', [Environment]::NewLine).Trim[Environment]::NewLine) + [Environment]::NewLine) | tee -Append test.txt"
Character-encoding note: As discussed in the bottom section, wmic
, when outputting directly to a file, uses "Unicode" encoding (UTF-16LE, with BOM), which PowerShell interprets correctly. Other, non-wmic
commands may produce OEM code page-encoded output, which, if it contains non-ASCII characters (such as accented characters), will be misinterpreted by PowerShell. For that reason, -Encoding Oem
is used with Get-Content
, which PowerShell fortunately ignores if the input file has a BOM, as in the wmic
output case. That is, -Encoding Oem
is only applied if the input files has no BOM.
Both the simulated prompt string and the content of the temporary file are then piped to PowerShell's Tee-Object
cmdlet (whose built-in alias (on Windows only) is tee
), which passes its input through while also appending it to file test.txt
(-FilePath text.txt -Append
).
Tee-Object
uses "Unicode" encoding (UTF-16LE, with BOM) when writing to a file via -FilePath
; In PowerShell (Core) 7+, it is BOM-less UTF-8 (the encoding used consistently in that edition - see this answer for more information).If you really want to emulate the exact output you'd see on the screen if you ran your commands directly in cmd.exe
only, you can try the following:
@echo off & setlocal
:: Delete a previous output file, if present.
del test.txt 2>NUL
for %%C in (
"wmic recoveros get autoreboot"
"wmic recoveros set autoreboot = true"
"wmic recoveros get DebugInfoType"
"wmic recoveros set DebugInfoType = 7"
"bcdedit /enum {badmemory}"
"wmic Computersystem where name="%COMPUTERNAME%" get AutomaticManagedPagefile"
) do (
%%~C > %TEMP%\tmp.txt
echo "%CD%>%%~C"| powershell -noprofile -c "$o = $($input).Trim([char] 0x22) + [Environment]::NewLine + (Get-Content -Raw %TEMP%\tmp.txt -Encoding Oem) -replace '\r\r\n', [Environment]::NewLine; if ([IO.File]::ReadAllBytes('%TEMP%\tmp.txt')[0..1] -join ' ' -ne '255 254') { $o = $o -replace '\r\n\z' }; $o | tee -Append test.txt"
del %TEMP%\tmp.txt
)
Note:
Optional reading: deprecation of wmic.exe
, character-encoding quirks and limitations:
wmic
(WMIC.exe
) is now officially deprecated, as a warning in red tells you when run
wmic /?
- see this answer for more information.
The CRCRLF newline problem occurs only sometimes if you redirect wmic
output directly to a file, seemingly dependent on whether the command is a get
or a set
command:
With a get
command (e.g., cmd /c "wmic recoveros get DebugInfoType > out.txt"
):
wmic
then uses "Unicode" encoding (UTF-16LE, with BOM[2]), irrespective of the console's code page.>
does not write directly to a file: PowerShell always converts output from external programs to .NET strings first, and then writes the result to a file; therefore, to see the direct-to-file behavior, you must call via cmd /c
, as in the example abpve.With a set
command (e.g., cmd /c "wmic recoveros set DebugInfoType = 7 > out.txt"
):
wmic
then seemingly uses the system's active OEM code page, which in Western languages is a BOM-less single-byte encoding, such as 437
(IBM437) on US-English systems.When not writing to a directly to a file, wmic
respects the current console window's (OEM) code page for its output, which makes PowerShell decode the output correctly, except if it is set to 65001
, i.e. the UTF-8 code page: wmic
then produces invalid-as-UTF-8 output for non-ASCII characters (e.g., é
), which PowerShell decodes to �
(the REPLACEMENT CHARACTER, U+FFFD
).
[1] Inside a PowerShell session you only see the effects of these CRCRLF sequences, not the sequences themselves, due to PowerShell invariably splitting an external program's stdout output into lines, and in this case the first CR in the CRCRLF sequence becomes an empty line of its own - for more information, see this answer (the latter relates to sfc.exe
rather than wmic
, but the problem is the same).
[2] If you use >>
to append to an existing file, the BOM is not written, but note that UTF16-LE encoding is blindly applied, irrespective of the encoding the existing content has.
Upvotes: 3
Reputation: 32155
As others have mentioned in the comments, WMIC is pretty poorly behaved for the pipeline because it outputs inconsistent line endings (CR+CR+LF). The Powershell behavior you're seeing is consistent with the input it's getting, it's just that WMIC is badly behaved. IMO, you might as well just call it twice if you want to write to a file and to the screen. It's not likely to change and isn't a slow command:
wmic recoveros get autoreboot
wmic recoveros get autoreboot > out.txt
However, you should be able to do this to strip any blank or whitespace-only lines:
wmic recoveros get autoreboot | powershell.exe "$input | Where-Object { -not [String]::IsNullOrWhiteSpace($_) } | Tee-Object out.txt"
That works for me but it's also significantly slower because the system has to start Powershell to do it.
Otherwise, is there a reason not to use Powershell entirely for your script? If you're doing lots of similar operations it's probably worthwhile. You can more-or-less replace the wmic call with Get-CimInstance -ClassName Win32_OSRecoveryConfiguration
. If you want to know the actual class names that wmic
uses, you can call wmic alias list brief
.
Upvotes: 2