批量设置命令的输出和错误以分隔变量

Batch set output and error of a command to separate variables

在 windows 7 批处理(cmd.exe 命令行)中,我试图将命令的标准输出 (stdout) 和标准错误 (stderr) 重定向到分隔变量(因此第一个变量设置为输出,第二个变量设置为错误(如果有))而不使用任何临时文件。我试了又试都没有成功。

那么,将命令的输出和错误设置为分隔变量的有效方法是什么?

您可以使用两个嵌套的 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 的嵌套管道,只要实际需要其管道功能,就需要多次转义来隐藏 | 字符。

首先,批处理没有像 unix shell 脚本那样捕获多行输出的简单方法。您可以使用 FOR /F 逐行构建多行值,但总长度限制为 < 8191 字节,并且语法笨拙。或者您可以使用 FOR /F 来捕获模拟变量数组中的多行。

关于您的问题,如果不使用至少一个临时文件,则无法独立捕获 stdout 和 stderr。 编辑: 错误,。但是,临时文件更快,也更简单。

这里是一个使用文件捕获stderr的简单演示。我假设你最多想捕获一行标准输出 and/or stderr.

for /f "delims=" %%A in ('yourCommand 2^>err.log`) do set "out=%%A"
<err.log set /p "err="
del err.log

这是一个更复杂的示例,它为 stdout 和 stderr 捕获了一组行。这里我假设 none 的输出行以 : 开头。 FINDSTR 在每行前加上行号前缀 :,FOR /F 解析出要用作 "array" 索引的行号,以及 [=17 之后的值=].

@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

仍然更喜欢临时文件解决方案;-)

只要发送到 stdout 的行不是以冒号分隔的行号本身开头,此解决方案就可以正常工作。

@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