如何在Windows CMD中使用管道(|)功能与CALL:Label命令选项?

20

当我想在Windows CMD shell中使用CALL :Label选项的管道(|)功能时,我遇到了一个令人沮丧的问题。以下是一个非常简单的示例:call-test.cmd以及样本输出。

问题的核心是要将CMD脚本的输出导入到另一个程序中,例如tee实用程序或find命令。例如:

    @call   :Label-02  param  | tee call-test.log

如何在带有标签Label-02的当前命令文件中启动命令并将输出导入tee。但是,在具有"call :label"选项的行上使用管道字符(|)会导致错误:

Invalid attempt to call batch label outside of batch script.

虽然"call example.cmd | tee example.log"可以正常工作。

另一种IO重定向>也可以正常工作,只有在使用"call :label pipe(|)"时才会出现问题。在我看来,这似乎是Windows的一个错误。

有人知道解决方法和/或解释吗?

谢谢, Will


  • 调用测试输出

c:\> call-test
    [start]
    label 03 :: p1
Invalid attempt to call batch label outside of batch script.
Invalid attempt to call batch label outside of batch script.
    [done]
Press any key to continue . . .
  • 调用测试

  • @echo off 
    @rem   call-test.cmd
    @rem  _________________________________________________
    @rem    Test :label call option for .cmd files.
    @rem
    @echo   ^  [start]
    @call   :Label-03  p1
    @call   :Label-02  second  | find " "
    @call   :Label-02  second  | tee call-test.log
    @goto   Done
    @rem  _________________________________________________
    :Label-01 
    @echo   ^  label 01 :: %1
    @goto Exit
    @rem  _________________________________________________
    :Label-02 
    @echo   ^  label 02 :: %1
    @goto Exit
    @rem  _________________________________________________
    :Label-03 
    @echo   ^  label 03 :: %1
    @goto Exit
    @rem  _________________________________________________
    :Done 
    @echo   ^  [done]
    @pause
    @rem  _________________________________________________
    :Exit 
    @exit /b
    

    6
    如果你使用了 @echo off,你就不必在每一行前面加上 @ 符号。 - Dennis Williamson
    1
    实际上我想要@,因为当我关闭调试的echo时,我只能看到重要的命令。 - will
    6个回答

    16

    问题的原因是,在cmd上下文中,管道符号同时启动了两个命令(它们在一个cmd窗口中同时运行),而每个命令都被解释为真实的命令行参数,在cmd命令行中不允许使用标签。

    但是,如果重新启动批处理文件,则可以调用您的函数。

    if not "%1"=="" goto %1
    @call "%~0" :Label-02  param  | tee call-test.log
    

    编辑:完整示例

    @echo off
    if not "%~1"=="START" goto :normalStart
    shift 
    shift 
    call %0 %1 %2 %3 %4 %5 %6 %7 %8
    exit /b
    
    :normalStart
    rem   call-test.cmd
    rem  _________________________________________________
    rem    Test :label call option for .cmd files.
    rem
    echo   ^  [start]
    rem call   :Label-03  p1
    rem call   :Label-02  second  | find " "
    call "%~dpf0" "START" :Label-02  second  |  tee call-test.log
    goto   Done
    rem  _________________________________________________
    :Label-01 
    echo   ^  label 01 :: %1
    goto Exit
    rem  _________________________________________________
    :Label-02 
    echo   ^  label 02 :: %1
    goto Exit
    rem  _________________________________________________
    :Label-03 
    echo   ^  label 03 :: %1
    goto Exit
    rem  _________________________________________________
    :Done 
    echo   ^  [done]
    pause
    rem  _________________________________________________
    :Exit 
    exit /b
    

    1
    为了公平起见,我需要指出这个建议是错误的。有两件事情出了问题。第一,原始问题是管道(|)不能与调用一起使用。第二,当你尝试使用以下语法时:call "%~0" :Label-02 param它会失败。虽然我可以并且确实尝试手动跳转到一个标签,但这是一个hack,最好还是有第二个.CMD脚本。(这也是我想要避免的事情)。 - will
    公正的评论 - 所寻求的“解决方案”是将输出管道传输到“tee”命令。我知道如果我们对“@call”和参数进行修改,它们可以起作用。 - will
    好的,| tee call-test.log 是否硬编码到批处理文件中了,能否避免这种情况? - n611x007
    使用 shift /1 可以避免更改脚本中使用的 %0。 - Sam Hasler
    @n611x007 -- 我不明白你的问题。这个用例是为了内部使用的 call :someLabel | tee someFile。但是它失败了。这就是整个问题——如何使其工作。这意味着 ... | tee fileName 是必需的。它可以是一个变量,例如 %TEE_TO_LOG%。我很确定我尝试过这样做,但没有成功。 - will
    显示剩余2条评论

    1

    显而易见的解决方法是将调用的输出重定向到临时文件,将其用作find/tee的输入,然后删除文件:

    @call :Label-02 second > tmp
    tee call-test.log < tmp
    delete tmp
    

    这更有帮助。tee的价值在于捕捉工作流程中的问题。这些是长时间的工作。以前我有日志文件,但在我知道有问题之前,工作已经完成了。 - will
    1
    更不用说...如果标签或任何东西要求用户输入,它将会对大多数用户造成“挂起”的情况。 - Brent Rittenhouse

    1
    这是jebs答案的更简洁版本。它使用相同的goto技术,但在重新进入时不传递唯一的“START”参数,而是使用子字符串提取测试第一个参数的第一个字符是否为“:”,并仅在其为标签时调用goto。这简化了调用,但是您无法对%1变量或空/不存在的变量使用子字符串提取,因此必须使用始终包含值的临时变量。它需要临时变量来记住标签,因为SHIFT /1将删除第一个:LABEL参数,但它只需使用SHIFT一次,并且不需要在调用站点添加额外的参数。

    [更新:必须执行shift /1以避免更改%0(如果脚本中使用)]

    set "LABEL=%~1_"
    if "%LABEL:~0,1%"==":" SHIFT /1 & goto %LABEL:~0,-1%
    

    以下脚本展示了如何使用传递给原始脚本的参数,以及重新进入以处理标签:
    @echo off
    
    set "LABEL=%~1_"
    if "%LABEL:~0,1%"==":" SHIFT /1 & goto %LABEL:~0,-1%
    
    call "%~f0" :LABEL_TEST param1 p2 | findstr foo
    
    echo param 1 is %1
    
    exit /b
    
    :LABEL_TEST
    echo (foo) called label with PARAMS: %1 %2 %3
    echo (bar) called label with PARAMS: %1 %2 %3
    exit /b
    

    将输出:
    C:\>call-test-with-params TEST
    (foo) called label with PARAMS: param1 p2
    param 1 is TEST
    

    通过管道到findstr的过程中,echo (bar)行被剥离。


    问题的解决方案:

    这个脚本:

    @echo off 
    
    set "LABEL=%~1_"
    if "%LABEL:~0,1%"==":" SHIFT /1 & goto %LABEL:~0,-1%
    
    @rem   call-test.cmd
    @rem  _________________________________________________
    @rem    Test :label call option for .cmd files.
    @rem
    @echo   ^  [start]
    @call "%~f0" :Label-03  p1
    @call "%~f0" :Label-02  second  | find " "
    @call "%~f0" :Label-02  second  | tee call-test.log
    @goto   Done
    @rem  _________________________________________________
    :Label-01
    @echo   ^  label 01 :: %1
    @goto Exit
    @rem  _________________________________________________
    :Label-02
    @echo   ^  label 02 :: %1
    @goto Exit
    @rem  _________________________________________________
    :Label-03
    @echo   ^  label 03 :: %1
    @goto Exit
    @rem  _________________________________________________
    :Done
    @echo   ^  [done]
    @pause
    @rem  _________________________________________________
    :Exit
    @exit /b
    

    将会输出:

    C:\>call-test
        [start]
        label 03 :: p1
        label 02 :: second
        label 02 :: second
        [done]
    Press any key to continue . . .
    

    而且 call-test.log 的内容是正确的:

    C:\>more call-test.log
        label 02 :: second
    

    1
    如果您将当前脚本的名称"%~dpf0"作为第一个参数传递给call,则我发布的两行代码将解决您的问题。请参考我添加的示例脚本。 - Sam Hasler
    这是一个非常好的“解决方法”。在调用中加入“%~dpf0”的结果与不使用CALL命令相同;回到旧版DOS 3.3之前;并直接将call-test作为命令文件调用(但标签作为参数)。顶部的“修复”处理了正确指出的传递的标签。 - will
    在顶部有什么固定的方法吗?我只能看到jebs答案中相同的解决方法。正如jeb所说,因为管道在新的cmd上下文中同时启动两侧,所以你不能做你想要的事情。那里没有“修复”,只有重新启动批处理并跳转到所需的标签。 - Sam Hasler
    如果您不喜欢输入 @call "%~f0" 或者认为在脚本中看起来凌乱/冗长,您可以将其放入变量中:set "CALL=@call ""%~f0"",然后您就可以使用 %CALL% :LABEL_1 param1 - Sam Hasler
    嗨 - 我想我没有表达清楚我的观点。在Window/MS-DOS中,当您使用call命令时:"CALL call-test :string",这不使用命令文件"Call:label"机制。它所做的是递归调用命令文件:"call-test.cmd";如果不使用SHIFT /1来删除":string",则结果是命令shell的stackoverflow。在DOS v3.3添加call命令之前,每个BAT和CMD文件都是如此。"CALL <file> [...]"不执行CALL命令,而是打开并执行文件。因此失去了本地环境等。 - will
    显示剩余2条评论

    1

    我意识到这有点晚了,但可能对其他人有帮助。这不是一个很好的黑客技巧,而是一种解决方法,或者如果你必须要用“很好的黑客技巧”来形容的话,那也可以。

    我正在使用以下解决方案来解决类似的问题:

    @echo off
    
    SET CURRENT_SCRIPT_IS=%~dpnx0
    IF NOT "%RUN_TO_LABEL%" == "" (
      call :%RUN_TO_LABEL% %1 %2 %3 %4 %5 %6 %7 %8 %9
      goto:eof
    )
    
    goto over_debug_stuff
    
    :debugstr
      echo %~1
      echo %~1>>%2
    goto:eof
    
    :debuglbl
      SET RUN_TO_LABEL=%1
      for /f "tokens=*" %%L in ('%CURRENT_SCRIPT_IS% %3 %4 %5 %6 %7 %8 %9') do (
        echo %%L
        echo %%L>>%2
      )
      SET RUN_TO_LABEL=
    goto:eof
    
    :over_debug_stuff
    
    call :debugstr "this is a string" "test_str.txt"
    call :debuglbl tst "test_lbl.txt"
    
    goto:eof
    
    :tst
      echo label line 1
      echo label line 2
      echo label line 3
    goto:eof
    

    它的好处是我可以将“header”复制粘贴到任何需要运行它的批处理脚本中。由于我不需要递归安全,所以我没有费心去制作它,因此在放入之前请确保您测试了该场景。 显然,我在这些debug*调用上有包装函数,以便我不必每次调用都携带日志文件。此外,在我的debug日志调用中,我还测试了debug标志,因此包装器本身也具有一些更多的逻辑,这主要取决于所使用的脚本。


    1
    我猜这个解决方法是针对 call :label ... | tee ... 而不是 call :label ... | any_command ...,我说的对吗? - Andriy M
    我已经编写了特定的日志记录和显示相同内容的代码,而不需要执行两次(就像tee解决方案一样),但您可以在“debuglbl”函数中执行任何操作。因此,您基本上会将any_command移动到debuglbl主体中(显然要相应地命名标签),并且您可以拥有任何所需的功能(也许“any”在这里有点过于强大)。错误基本上是很明显的,网络上也充满了相同的解释:call:label不是命令行功能,而是批处理脚本功能,因此您不能在那里使用它。 - ciuly
    这个 "命令行" 和 "批处理脚本" 的区别非常类似于 SQL 和 PL/SQL 的差异。就像在批处理脚本中使用 %%I 一样,但是当你在命令行中时,你必须写成 %I in。此外还有其他的区别,其中之一是你不能调用标签。这很明显,因为标签没有在命令解释器环境中定义,而是在脚本中定义。就像在 Oracle 中,你可以在 PL/SQL 中使用 BOOLEAN 类型,但不能在 SQL 中使用。我想你已经明白了。 - ciuly
    多或少。 :) 我的意思是,考虑到你划出的对比并且我了解批处理文件和命令行之间的区别,我现在更好地理解了SQL和PL / SQL之间的差异性质。(关于SQL,我一直都是个SQL Server人员,你知道的。)不过,最重要的是,无论如何,我已经学到了东西,谢谢。 :) - Andriy M
    你有一只可爱的猫,Ciuly。 - n611x007

    -1

    我认为你可以使用“|”字符,然后它就会被视为普通字符。


    2
    你说得没错,但那不是问题所在。楼主不想处理管道字符,函数的输出应该被管道传递。 - jeb

    -3
    将符号^放在管道命令前面....例如:

    @call :Label-02 second ^| find " "


    8
    那行不通。 建议人们在发布前“测试”或“尝试”一个想法,是否不公平? - will
    2
    请检查您的答案,然后发布。 - Ghost Answer

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接