批量设置命令的输出和错误以分隔变量
Posted
技术标签:
【中文标题】批量设置命令的输出和错误以分隔变量【英文标题】:Batch set output and error of a command to separate variables 【发布时间】:2016-04-01 22:01:12 【问题描述】:在 Windows 7 批处理(cmd.exe 命令行)中,我试图将命令的标准输出(stdout)和标准错误(stderr)重定向到分隔变量(因此第一个变量设置为输出,并且第二个变量设置为错误(如果有))而不使用任何临时文件。我已经尝试过,但没有成功。
那么,将命令的输出和错误设置为分隔变量的有效方法是什么?
【问题讨论】:
【参考方案1】:您可以使用两个嵌套的for /F
循环,其中内部的一个捕获标准输出,而外部的一个捕获重定向的错误。由于内部实例一个新的cmd
进程,捕获的文本不能只分配给一个变量,因为它会在执行完成后丢失。相反,我在每一行前面加上|
,然后将其回显到标准输出。外部循环检测前导 |
并相应地分隔行:
@echo off
setlocal EnableExtensions EnableDelayedExpansion
set "STDOUT="
set "STDERR="
(set LF=^
%=empty line=%
)
for /F "delims=" %%E in ('
2^>^&1 ^(^
for /F "delims=" %%O in ^('^
command_line^
'^) do @^(^
echo ^^^|%%O^
^)^
^)
') do (
set "LINE=%%E"
if "!LINE:~,1!"=="|" (
set "STDOUT=!STDOUT!!LINE:~1!!LF!"
) else (
set "STDERR=!STDERR!!LINE!!LF!"
)
)
echo ** STDOUT **!LF!!STDOUT!
echo ** STDERR **!LF!!STDERR!
endlocal
exit /B
以下限制适用于代码:
空行被忽略; 以分号;
开头的行将被忽略;
感叹号!
丢失,因为启用了延迟的环境变量扩展;
以竖线字符|
开头的行可能分配错误;
数据总大小不得超过8190字节;
所有这些限制都适用于标准输出和标准错误。
编辑:
这是上述代码的改进变体。空行和以分号;
开头的行的问题已解决,其他限制仍然存在:
@echo off
setlocal EnableExtensions EnableDelayedExpansion
set "STDOUT="
set "STDERR="
(set LF=^
%=empty line=%
)
for /F "delims=" %%E in ('
2^>^&1 ^(^
for /F "delims=" %%O in ^('^
command_line ^^^^^^^| findstr /N /R "^"^
'^) do @^(^
echo ^^^^^^^|%%O^
^)^
^) ^| findstr /N /R "^"
') do (
set "LINE=%%E"
set "LINE=!LINE:*:=!"
if "!LINE:~,1!"=="|" (
set "STDOUT=!STDOUT!!LINE:*:=!!LF!"
) else (
set "STDERR=!STDERR!!LINE!!LF!"
)
)
echo ** STDOUT **!LF!!STDOUT!
echo ** STDERR **!LF!!STDERR!
endlocal
exit /B
findstr
命令用于在每一行前面加上行号加上:
,因此for /F
不会出现空行;当然,这个前缀稍后会被删除。此更改还隐式解决了;
问题。
由于findstr
中嵌套了管道,只要实际需要其管道功能,就需要多次转义才能隐藏|
字符。
【讨论】:
这不可能。除了你的每一行 IN() 必须在最后使用行继续^
之外,基本思想是有缺陷的。每个 IN() 命令都在一个新的 cmd.exe 进程中执行。一旦流程终止,您在流程中定义的任何变量都将消失。所以你的父脚本看不到 STDOUT 变量 - 它不会存在。
我也想到了这一点,@dbenham,当然我的第一次测试失败了;所以我稍微改变了策略并相应地更新了我的答案......我想我会再次修改它以克服所有列出的限制(除了大小限制),因为它应该很容易(!
问题除外) ...无论如何感谢您的提示!
+1 哦——非常聪明!通过使用“数组”,您可以支持每行近 8190 个字节。在我的回答中使用 FINDSTR 以避免;
EOL 问题,并获取数组索引,并解决空行问题。您可以通过在 stdout 和 stderr 前加上唯一的标志字符来处理任何前导字符。我会使用+
用于标准输出,-
用于标准错误。您可以通过仔细切换延迟扩展来保留!
。但是,尽管您的解决方案很有创意,但我相信临时文件解决方案更快,而且实施起来肯定要简单得多。
非常好。保存!
的方法见my edited answer的结尾。它很丑陋,但它有效。
Eureka - 除大小限制外,所有内容均已消除。再次查看我的更新答案。【参考方案2】:
首先,批处理没有像 unix shell 脚本那样捕获多行输出的简单方法。可以使用 FOR /F 逐行构建多行值,但总长度限制在
关于您的问题,如果不使用至少一个临时文件,就无法独立捕获 stdout 和 stderr 。 编辑: 错误,aschipfl found a way。但是,临时文件更快,而且更简单。
这是一个简单的演示,它使用一个文件来捕获标准错误。我假设您最多要捕获一行标准输出和/或标准错误。
for /f "delims=" %%A in ('yourCommand 2^>err.log`) do set "out=%%A"
<err.log set /p "err="
del err.log
这是一个更复杂的示例,它为 stdout 和 stderr 捕获一组行。这里我假设没有任何输出行以:
开头。 FINDSTR 以行号为前缀,后跟 :
,FOR /F 解析出用作“数组”索引的行号,以及 :
之后的值。
@echo off
setlocal disableDelayedExpansion
set /a out.cnt=err.cnt=0
for /f "delims=: tokens=1*" %%A in ('yourCommand 2^>err.log ^| findstr /n "^"') do (
set "out.%%A=%%B"
set "out.cnt=%%A"
)
for /f "delims=: tokens=1*" %%A in ('findstr /n "^" err.log') do (
set "err.%%A=%%B"
set "err.cnt=%%A"
)
:: Display the results
setlocal enableDelayedExpansion
echo ** STDOUT **
for /l %%N in (1 1 %out.cnt%) do echo(!out.%%N!
echo ** STDERR **
for /l %%N in (1 1 %err.cnt%) do echo(!err.%%N!
第二次编辑
如果您想正确处理以:
开头的输出,则需要额外的代码。
@echo off
setlocal disableDelayedExpansion
set /a out.cnt=err.cnt=0
for /f "delims=" %%A in ('yourCommand 2^>err.log ^| findstr /n "^"') do for /f "delims=:" %%N in ("%%A") do (
set "ln=%%A"
setlocal enableDelayedExpansion
for /f "delims=" %%B in (^""!ln:*:=!"^") do (
endlocal
set "out.%%N=%%~B"
set "out.cnt=%%N"
)
)
for /f "delims=" %%A in ('findstr /n "^" err.log') do for /f "delims=:" %%N in ("%%A") do (
set "ln=%%A"
setlocal enableDelayedExpansion
for /f "delims=" %%B in (^""!ln:*:=!"^") do (
endlocal
set "err.%%N=%%~B"
set "err.cnt=%%N"
)
)
:: Display the results
setlocal enableDelayedExpansion
echo ** STDOUT **
for /l %%N in (1 1 %out.cnt%) do echo(!out.%%N!
echo ** STDERR **
for /l %%N in (1 1 %err.cnt%) do echo(!err.%%N!
下面我改编了 aschipfl 的第二个代码,该代码避免使用临时文件,以便保留 !
字符。代码变得越来越丑陋;-)
@echo off
setlocal disableDelayedExpansion
set "STDOUT="
SET "STDERR="
for /f "delims=" %%E in (
'2^>^&1 (for /f "delims=" %%O in ('^
yourCommand^
^^^^^^^| findstr /n /r "^"'^) do @(echo ^^^^^^^|%%O^)^) ^| findstr /n /r "^"'
) do (
set "ln=%%E"
setlocal enableDelayedExpansion
set "ln=x!ln:*:=!"
set "ln=!ln:\=\s!"
if "!ln:~0,2!"=="x|" (
set "ln=!ln:~0,-1!"
for /f "delims=" %%A in (^""!STDOUT!"^") do for /f "delims=" %%B in (^""!ln:*:=!"^") do (
endlocal
set "STDOUT=%%~A%%~B\n"
)
) else (
for /f "delims=" %%A in (^""!STDERR!"^") do for /f "delims=" %%B in (^""!ln:~1!"^") do (
endlocal
set "STDERR=%%~A%%~B\n"
)
)
)
setlocal enableDelayedExpansion
for %%L in (^"^
%= empty line =%
^") do (
if defined STDOUT (
set "STDOUT=!STDOUT:\n=%%~L!"
set "STDOUT=!STDOUT:\s=\!"
set "STDOUT=!STDOUT:~0,-1!"
)
if defined stderr (
set "STDERR=!STDERR:\n=%%~L!"
set "STDERR=!STDERR:\s=\!"
set "STDERR=!STDERR:~0,-1!"
)
)
echo ** STDOUT **
echo(!STDOUT!
echo ** STDERR **
echo(!STDERR!
exit /b
如果结果存储在数组中而不是一对字符串中会更简单一些。
@echo off
setlocal disableDelayedExpansion
set /a out.cnt=err.cnt=1
for /f "delims=" %%E in (
'2^>^&1 (for /f "delims=" %%O in ('^
yourCommand^
^^^^^^^| findstr /n /r "^"'^) do @(echo ^^^^^^^|%%O^)^) ^| findstr /n /r "^"'
) do (
set "ln=%%E"
setlocal enableDelayedExpansion
set "ln=x!ln:*:=!"
if "!ln:~0,2!"=="x|" (
set "ln=!ln:~0,-1!"
for %%N in (!out.cnt!) do for /f "delims=" %%A in (^""!ln:*:=!"^") do (
endlocal
set "out.%%N=%%~A"
set /a out.cnt+=1
)
) else (
for %%N in (!err.cnt!) do for /f "delims=" %%A in (^""!ln:~1!"^") do (
endlocal
set "err.%%N=%%~A"
set /a err.cnt+=1
)
)
)
set /a out.cnt-=1, err.cnt-=1
setlocal enableDelayedExpansion
echo ** STDOUT **
for /l %%N in (1 1 %out.cnt%) do echo(!out.%%N!
echo ** STDERR **
for /l %%N in (1 1 %err.cnt%) do echo(!err.%%N!
exit /b
我知道很多人试图避免使用临时文件,但在这种情况下,我认为它会适得其反。测试表明,当输出非常大时,临时文件可以比使用 FOR /F 循环处理命令的结果快得多。临时文件解决方案要简单得多。所以我肯定会使用临时文件解决方案。
但是找到一个非临时文件解决方案是一个有趣的挑战。感谢 aschipfl 解决了复杂的转义序列。
第三次(最终?)编辑
最后,这是一个消除所有限制的解决方案,除了每个捕获的输出行必须小于大约 8180 字节。
我本可以将整个代码放在一个大循环中,但是转义序列将是一场噩梦。当我将代码分解为更小的子例程时,找出转义序列要简单得多。
我为在底部的 :test 例程中找到的一堆 ECHO 命令捕获了标准输出和标准错误。
::
:: Script to demonstrate how to run one or more commands
:: and capture stdout in one array and stderr in another array,
:: without using a temporary file.
::
:: The command(s) to run should be placed in the :test routine at the bottom.
::
@echo off
setlocal disableDelayedExpansion
if "%~1" equ ":out" goto :out
if "%~1" equ ":err" goto :err
if "%~1" equ ":test" goto :test
set /a out.cnt=err.cnt=0
:: Runs :err, which runs :out, which runs :test
:: stdout is captured in out array, and stderr in err array.
for /f "delims=. tokens=1*" %%A in ('^""%~f0" :err^"') do (
for /f "delims=:" %%N in ("%%B") do (
set "ln=%%B"
setlocal enableDelayedExpansion
for /f "delims=" %%L in (^""!ln:*:=!"^") do (
endlocal
set "%%A.%%N=%%~L"
set "%%A.cnt=%%N"
)
)
)
:: Show results
setlocal enableDelayedExpansion
echo ** STDOUT **
for /l %%N in (1 1 %out.cnt%) do echo(!out.%%N!
echo(
echo ** STDERR **
for /l %%N in (1 1 %err.cnt%) do echo(!err.%%N!
exit /b
:err :: 1) Run the :out code, which swaps stdout with stderr
:: 2) Prefix stream 1 (stderr) output with err.###: where ### = line number
:: 3) Rredirect stream 2 (stdout) to combine with stream 1 (stderr)
2>&1 (for /f "delims=" %%A in ('^""%~f0" :out^|findstr /n "^"^"') do echo err.%%A)
exit /b
:out :: 1) Run the :test code.
:: 2) Prefix stream 1 (stdout) output with out.###: where ### = line number
:: 3) Swap stream 1 (stdout) with stream 2 (stderr)
3>&2 2>&1 1>&3 (for /f "delims=" %%A in ('^""%~f0" :test^|findstr /n "^"^"') do echo out.%%A)
exit /b
:test :: Place the command(s) to run in this routine
echo STDOUT line 1 with empty line following
echo(
>&2 echo STDERR line 1 with empty line following
>&2 echo(
echo STDOUT line 3 with poison characters "(<^&|!%%>)" (^<^^^&^|!%%^>)
>&2 echo STDERR line 3 with poison characters "(<^&|!%%>)" (^<^^^&^|!%%^>)
echo err.4:STDOUT line 4 spoofed as stderr - No problem!
>&2 echo out.4:STDERR line 4 spoofed as stdout - No problem!
echo :STDOUT line 5 leading colon preserved
>&2 echo :STDERR line 5 leading colon preserved
echo ;STDOUT line 6 default EOL of ; not a problem
>&2 echo ;STDERR line 6 default EOL of ; not a problem
exit /b
-- 输出--
** STDOUT **
STDOUT line 1 with empty line following
STDOUT line 3 with poison characters "(<^&|!%>)" (<^&|!%>)
err.4:STDOUT line 4 spoofed as stderr - No problem!
:STDOUT line 5 leading colon preserved
;STDOUT line 6 default EOL of ; not a problem
** STDERR **
STDERR line 1 with empty line following
STDERR line 3 with poison characters "(<^&|!%>)" (<^&|!%>)
out.4:STDERR line 4 spoofed as stdout - No problem!
:STDERR line 5 leading colon preserved
;STDERR line 6 default EOL of ; not a problem
我仍然更喜欢临时文件解决方案;-)
【讨论】:
【参考方案3】:只要发送到标准输出的行不以行号本身以冒号分隔开始,此解决方案就可以正常工作。
@echo off
setlocal EnableDelayedExpansion
set /A out=0, err=1
for /F "tokens=1* delims=:" %%a in ('(theCommand 1^>^&2 2^>^&3 ^| findstr /N "^"^) 2^>^&1') do (
if "%%a" equ "!err!" (
set "stderr[!err!]=%%b"
set /A err+=1
) else (
set /A out+=1
if "%%b" equ "" (
set "stdout[!out!]=%%a"
) else (
set "stdout[!out!]=%%a:%%b"
)
)
)
set /A err-=1
echo Lines sent to Stdout:
for /L %%i in (1,1,%out%) do echo !stdout[%%i]!
echo/
echo Lines sent to Stderr:
for /L %%i in (1,1,%err%) do echo !stderr[%%i]!
例如,如果命令是这个 .bat 文件:
@echo off
echo Line one to stdout
echo Line one to stderr >&2
echo Line two to stderr >&2
echo Line two to stdout
...那么这是输出:
Lines sent to Stdout:
Line one to stdout
Line two to stdout
Lines sent to Stderr:
Line one to stderr
Line two to stderr
【讨论】:
这是如何工作的?findstr
是否处理流 3 中的数据?如果是,这是findstr
的另一个“隐藏功能”吗??
@aschipfl - 不,FINDSTR 仅处理流 1。1>&2 2>&3
(或 2>&3, 1>&2
)有效地交换 stdout 和 stderr,但前提是流 3 之前未定义。如果流 3 可能已经存在,那么您必须使用3>&1 1>&2 2>&3
,(或3>&2 2>&1 1>&3
)。请参阅***.com/a/12274145/1012053 了解更多信息。以上是关于批量设置命令的输出和错误以分隔变量的主要内容,如果未能解决你的问题,请参考以下文章