存储可选管道多行字符串和可选参数的批处理脚本

Posted

技术标签:

【中文标题】存储可选管道多行字符串和可选参数的批处理脚本【英文标题】:A batch script that stores optional piped multiline string and optional arguments 【发布时间】:2021-02-05 21:26:59 【问题描述】:

考虑 cmd/batch 脚本program.bat,它接受可选的管道和命令行参数。该脚本将以四种不同的方式执行:

    program.bat: 没有管道和参数 program.bat <arguments>: 带参数 <command> | program.bat: 使用管道多行字符串 <command> | program.bat <arguments>:同时使用管道多行字符串和参数

需要注意的是,<command> 的输出不一定是单行(例如,dir)。该脚本应该在有或没有任何管道信息和参数的情况下工作。使用%* 以列表/数组(reference)的形式获取参数,可以轻松实现案例 2。如果我确定总会有一条单线的管道,我可以使用:(reference)

SET /p "piped="

但是,这将期望用户在情况 1 和 2 中输入某些内容,此时没有任何内容被管道传输,这是不可取的。另外,这仅将<command> 输出的第一行存储到piped 变量中。如果您能帮助我了解如何编写一个脚本,我将不胜感激:

适用于上述所有 4 个场景 将可选的管道多行字符串存储到变量中

P.S.为了测试answer by @aschipfl,我创建了一个文件program.bat,内容如下:

@echo %CmdCmdLine%

我以 3 种不同的方式运行它:

program.bat 结果

命令

<someRandomCommand> | program.bat 结果

C:\WINDOWS\system32\cmd.exe /S /D /c"program.bat"

program.bat | FIND /v ""

C:\WINDOWS\system32\cmd.exe /S /D /c"program.bat"

虽然检测管道本身是一种绝妙的解决方法,但它无法区分program.bat 是管道还是被管道连接的那个。这不是我想过的问题。

【问题讨论】:

您可以指定< nul program.bat 来提供空输入而不是停止。要接受多个输入行,您可以先输入:Label,然后输入set /P "piped=" || goto :EOF(跳过空输入),然后使用%piped% 执行某些操作,然后通过goto :Label 循环返回。使用"%~1"shift(比解析%* 更健壮)时具有参数的相同循环结构,由if "%~1"=="" goto :EOF 离开... 我删除了评论,我想我得到了你想要的。我正在努力做出更好的回应。 我认为你应该参考@DBenham 的关于dostips 的this 主题 @Gerhard 谢谢。会读的。不过,我们将非常感谢 Eli5 博士的总结。 :) 看看Dostips: How to detect if input comes from pipe or redirected file 【参考方案1】:

Windows Command Processor cmd.exe 具有一个 built-in pseudo-variable called CmdCmdLine,其中包含调用它的原始命令行。

鉴于命令提示符仅由cmd.exe(不带任何参数!)打开,CmdCmdLine 的(引用)值为ComSpec,通常为"C:\Windows\system32\cmd.exe"

当通过键入路径/名称从此类命令提示符窗口运行批处理文件时,CmdCmdLine 的值不会改变(因为已经打开的 cmd.exe 实例保持不变)。

但是,当脚本涉及到管道时,它会为任一侧启动新的 cmd.exe 实例,CmdCmdLine 会更改为 C:\Windows\system32\cmd.exe /S /D /c" …",其中 代表批处理文件及其参数按照规定。

我们现在可以利用它并检查 CmdCmdLine 它是否只包含一个项目(ComSpec 值)或多个项目(ComSpec 加上一些参数),以及第二个项目是否等于 /S .

这是一个示例脚本(名为 program.bat)来演示我的意思(尽管当原始命令提示符窗口被%ComSpec% /S … 之类的东西调用时,这很遗憾会失败):

@echo off
setlocal EnableExtensions DisableDelayedExpansion

rem // Enable delayed expansion to safely return `CmdCmdLine`:
setlocal EnableDelayedExpansion
>&2 setlocal DisableDelayedExpansion
>&2 echo/
>&2 echo BatFile = ^<%0^>
>&2 echo BatArgs = ^<%*^>
>&2 endlocal
>&2 echo/
>&2 echo ComSpec = ^<!ComSpec!^>
>&2 echo CmdLine = ^<!CmdCmdLine!^>
for %%L in (!CmdCmdLine!) do (
    rem /* `!!` are consumed by delayed expansion, hence the following condition
    rem    is only true for the first iteration because of the `endlocal`: */
    if "!!"=="" (
        endlocal
    ) else (
        rem /* This point is only reached when there are more than one items in
        rem    `CmdCmdLine`, which is the case when the script was involved in
        rem    a pipe (though it does not matter on which side it stood);
        rem    for this to work we assume that the second item, hence the first
        rem    argument, is `/S` as usual for pipes; the hosting Console Window
        rem    must not have been invoked by `cmd.exe /S ...` then though: */
        >&2 echo CmdSwit = ^<%%~L^>
        >&2 set "CmdSwit=%%~L"
        if /I "%%~L"=="/S" (
            >&2 echo/
            >&2 set "CmdSwit="
           rem /* `timeout` does not accept redirected input, which can be used
           rem    to distinguish between the sides of the pipe: */
           > nul 2>&1 timeout /T 0 || call :PIPE
        )
        goto :ARGS
    )
)
:ARGS
>&2 echo/
rem // Parsing each argument individually is more robust as parsing `%*`:
:ARGS_LOOP
if "%~1"=="" goto :NEXT
rem // Do something with `%1` here...
>&2 echo BatArg# = ^<%1^>
shift /1
goto :ARGS_LOOP

:NEXT
>&2 (if defined CmdSwit echo/& pause)
rem // ...

endlocal
exit /B


:PIPE
    setlocal DisableDelayedExpansion
    rem /* The `for /F` loop awaits the whole input text before iterating!
    rem    `more` hangs when there are more than about 64K lines and it converts
    rem    tabs to spaces (a tab becomes one space here due to switch `/T1`);
    rem    use `findstr "^"` instead when you expect more than about 64K lines;
    rem    regard that line lengths are limited to about 8K characters: */
    for /F delims^=^ eol^= %%I in ('more /S /T1') do (
        rem // Do something with `%%I` here...
        >&2 echo PipLin# = ^<%%I^>
    )
    endlocal
    exit /B

请注意,以&gt;&amp;2 开头的行只是用于说明的调试行。


以下是一些使用示例(调试输出):

    命令行program.bat调用的脚本(无管道,无参数):

    BatFile = <program.bat>
    BatArgs = <>
    
    ComSpec = <C:\Windows\system32\cmd.exe>
    CmdLine = <"C:\Windows\system32\cmd.exe" >
    

    命令行program.bat arg1 arg2调用的脚本(无管道,一些参数):

    BatFile = <program.bat>
    BatArgs = <arg1 arg2>
    
    ComSpec = <C:\Windows\system32\cmd.exe>
    CmdLine = <"C:\Windows\system32\cmd.exe" >
    
    BatArg# = <arg1>
    BatArg# = <arg2>
    

    命令行(echo pip1^&amp;echo pip2^&amp;rem/) | program.bat调用的脚本(管道有一些行,没有参数):

    BatFile = <program.bat>
    BatArgs = <>
    
    ComSpec = <C:\Windows\system32\cmd.exe>
    CmdLine = <C:\Windows\system32\cmd.exe  /S /D /c" program.bat">
    CmdSwit = </S>
    
    PipLin# = <pip1>
    PipLin# = <pip2>
    

    由命令行(echo pip1^&amp;echo pip2^&amp;rem/) | program.bat arg1 arg2 调用的脚本(带有一些行、一些参数的管道):

    BatFile = <program.bat>
    BatArgs = <arg1 arg2>
    
    ComSpec = <C:\Windows\system32\cmd.exe>
    CmdLine = <C:\Windows\system32\cmd.exe  /S /D /c" program.bat arg1 arg2">
    CmdSwit = </S>
    
    PipLin# = <pip1>
    PipLin# = <pip2>
    
    BatArg# = <arg1>
    BatArg# = <arg2>
    

【讨论】:

非常感谢。我将对此进行测试并返回此处。 您能检查一下P.S吗?基本上,使用这种方法,脚本无法检测到它是被管道传输还是它是一个管道。 是的,很遗憾不是,因为CmdCmdLine 在管道的任一侧都是相同的。我不知道区分双方的方法,对此感到抱歉…… 我目前的想法是使用tasklist(或wmic Process)用管道获取整个命令行并解析它以找出批处理文件在哪一侧,但没有成功,不幸的是,因为该命令行似乎没有反映在任何可用字段(如窗口标题或命令行)中。另外,我还不知道如何实施您对choice 的建议…… 最终我找到了一种方法来检测脚本涉及到管道的哪一侧:timeout 命令不接受重定向输入并返回错误,可以检测到(通过@987654356 @operator) 以防万一,当它在右侧时发生……

以上是关于存储可选管道多行字符串和可选参数的批处理脚本的主要内容,如果未能解决你的问题,请参考以下文章

C# 4 中的重载分辨率和可选参数

ORACLE PL/SQL:函数和可选参数,如何?

Javascript 函数和可选参数

Rust语言教程 - 错误处理和可选值

Dart值得注意的知识点(一)

我需要一种策略来处理 CDK 中的可选 SSM 参数存储参数