Windows批处理中的heredoc?

46

有没有一种类似于Unix shell中的heredoc的方式,在批处理中指定多行字符串。类似于:

cat <<EOF > out.txt
bla
bla
..
EOF

这个想法是从模板文件创建一个定制的文件。

20个回答

37

据我所知,没有。

我所知道的最接近的是

> out.txt (
    @echo.bla
    @echo.bla
    ...
)

(@ 防止命令行本身打印它正在运行的命令,而 echo. 允许您在一行中以空格开头。)


你可以将输出导入到另一个命令中,详见我下面的帖子。不过最佳技巧归功于“ephemient”。 - Michael Erickson

35

这里有另一种方法。

@echo off

:: ######################################################
:: ## Heredoc syntax:                                  ##
:: ## call :heredoc uniqueIDX [>outfile] && goto label ##
:: ## contents                                         ##
:: ## contents                                         ##
:: ## contents                                         ##
:: ## etc.                                             ##
:: ## :label                                           ##
:: ##                                                  ##
:: ## Notes:                                           ##
:: ## Variables to be evaluated within the heredoc     ##
:: ## should be called in the delayed expansion style  ##
:: ## (!var! rather than %var%, for instance).         ##
:: ##                                                  ##
:: ## Literal exclamation marks (!) and carats (^)     ##
:: ## must be escaped with a carat (^).                ##
:: ######################################################



:--------------------------------------------
: calling heredoc with results sent to stdout
:--------------------------------------------

call :heredoc stickman && goto next1

\o/
 | This is the "stickman" heredoc, echoed to stdout.
/ \
:next1



:-----------------------------------------------------------------
: calling heredoc containing vars with results sent to a text file
:-----------------------------------------------------------------

set bodyText=Hello world!
set lipsum=Lorem ipsum dolor sit amet, consectetur adipiscing elit.

call :heredoc html >out.txt && goto next2
<html lang="en">
    <body>
        <h3>!bodyText!</h3>
        <p>!lipsum!</p>
    </body>
</html>

Thus endeth the heredoc.  :)
:next2



echo;
echo Does the redirect to a file work?  Press any key to type out.txt and find out.
echo;

pause>NUL
type out.txt
del out.txt

:: End of main script
goto :EOF

:: ########################################
:: ## Here's the heredoc processing code ##
:: ########################################
:heredoc <uniqueIDX>
setlocal enabledelayedexpansion
set go=
for /f "delims=" %%A in ('findstr /n "^" "%~f0"') do (
    set "line=%%A" && set "line=!line:*:=!"
    if defined go (if #!line:~1!==#!go::=! (goto :EOF) else echo(!line!)
    if "!line:~0,13!"=="call :heredoc" (
        for /f "tokens=3 delims=>^ " %%i in ("!line!") do (
            if #%%i==#%1 (
                for /f "tokens=2 delims=&" %%I in ("!line!") do (
                    for /f "tokens=2" %%x in ("%%I") do set "go=%%x"
                )
            )
        )
    )
)
goto :EOF

示例输出:

C:\Users\oithelp\Desktop>heredoc

\o/
 | This is the "stickman" heredoc, echoed to stdout.
/ \

Does the redirect to a file work?  Press any key to type out.txt and find out.

<html lang="en">
    <body>
        <h3>Hello world!</h3>
        <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
    </body>
</html>

Thus endeth the heredoc.  :)

3
两个小改进:使用echo(代替echo;,否则会在包含/?的行中失败。使用set "line=!line:*:=!"去掉行号,因为使用delims=:也会删除所有前导冒号。 - jeb
毫无疑问,这是批处理文件中heredoc的最佳解决方案(在我看来)。我想添加一些注释并建议一些代码改进,但注释模式不允许这样做。如果您对我的建议感兴趣,请查看我的帖子https://dev59.com/L3NA5IYBdhLWcg3wUL9Y#29329912。 - jsxt
1
请查看更加优雅的实现,使用PrintHere.bat在https://dev59.com/L3NA5IYBdhLWcg3wUL9Y#31216893。 - dbenham

31

非常可能。^ 是转义字符,只需在换行符之前加上它即可。在此示例中,我还添加了额外的换行符,以便将其正确地打印到文件中:

@echo off
echo foo ^

this is ^

a multiline ^

echo > out.txt

输出:

E:\>type out.txt
foo
 this is
 a multiline
 echo

E:\>

11
@echo off
 for /f "delims=:" %%a in (
     'findstr -n "^___" %0') do set "Line=%%a"

 (for /f "skip=%Line% tokens=* eol=_" %%a in (
       'type %0') do echo(%%a) > out.html
:: out.html
pause
goto: EOF



___DATA___
<!Doctype html>
<html>
  <head>
   title></title>
  </head>
  <body>
    <svg width="900" height="600">
        <text x="230" 
              y="150"
              font-size="100"
              fill="blue"
              stroke="gray"
              stroke-width="1">
                  Hello World              
        </text>
    </svg>
  </body>
</html>

1
这是一个很好的开始。请查看https://dev59.com/0FzUa4cB1Zd3GeqP4Y0q以获取更多有关如何完成此操作的详细信息。 - Jared
1
太棒了,这对我非常有效!唯一需要调整的是在for循环之前添加SETLOCAL DISABLEDELAYEDEXPANSION(因为之前启用了延迟扩展,我的输出文件缺少感叹号)。它仍然会删除输出文件中行开头的空格,但我可以接受。 - Shoelaced
1
(1) 这个答案中的代码解释比例是无穷大(这很糟糕)。这太过神秘,不能被认为是显而易见的、自我记录的或自我解释的。 (2) 我没有尝试运行整个批处理文件(我不会从不可信的来源运行不可信的代码),但第5行上不平衡的 ( 让我怀疑它是否能正常工作。 (3) 我确实尝试了 goto: EOF,并且如我所料,它失败了。它需要是 goto :EOFgoto:EOF - Scott - Слава Україні

10

在DosTips网站上,siberia-man发布了一个演示文稿,展示了一个错误的GOTO语句的惊人行为,形式为(goto) 2>nul。Aacini和jeb随后记录了一些有关奇怪行为的其他有趣发现。它基本上的行为类似于EXIT /B,但它允许在调用的例程中连接的命令在父调用者的上下文中执行。

这里是一个简短的脚本,演示了大部分要点:

@echo off
setlocal enableDelayedExpansion
set "var=Parent Value"
(
  call :test
  echo This and the following line are not executed
  exit /b
)
:break
echo How did I get here^^!^^!^^!^^!
exit /b

:test
setlocal disableDelayedExpansion
set "var=Child Value"
(goto) 2>nul & echo var=!var! & goto :break
echo This line is not executed

:break
echo This line is not executed

-- 输出 --

var=Parent Value
How did I get here!!!!

这种神奇的行为使我能够编写一个优雅的批处理模拟程序,具有许多类Unix可用的选项。我将PrintHere.bat实现为一个独立的实用程序,应该放置在你的PATH中列出的文件夹中。然后,任何批处理脚本都可以轻松调用该实用程序来获取"here doc"功能。
以下是使用的一般语法:
call PrintHere :Label
Here doc text goes here
:Label

我如何才能实现这个?...我的PrintHere实用程序两次使用(GOTO) 2>nul技巧。
第一次,我使用(GOTO) 2>nul返回到调用者,以便我可以获取调用脚本的完整路径,以便PrintHere知道要从哪个文件读取。然后我再次CALL PrintHere!
第二次,我使用(GOTO) 2>nul返回到调用者,并跳转到终止标签,以使here doc文本不被执行。
注意-下面的脚本在:start标签直接下方的tab定义中包含一个制表符(0x09)。一些浏览器可能难以显示和复制该制表符。作为替代方案,您可以从我的dropbox下载PrintHere.bat.txt,然后将其重命名为PrintHere.bat。

我最初在DosTips上发布了PrintHere.bat,您可以在那里跟踪未来的开发。

PrintHere.bat

@echo off & setlocal disableDelayedExpansion & goto :start
::PrintHere.bat version 1.1 by Dave Benham
:::
:::call PrintHere [/E] [/- "TrimList"] :Label ["%~f0"]
:::call PrintHere [/E] [/- "TrimList"] :Label "%~f0" | someCommand & goto :Label
:::PrintHere /?
:::PrintHere /V
:::
:::  PrintHere.bat provides functionality similar to the unix here doc feature.
:::  It prints all content between the CALL PrintHere :Label line and the
:::  terminating :Label. The :Label must be a valid label supported by GOTO, with
:::  the additional constraint that it not contain *. Lines are printed verbatim,
:::  with the following exceptions and limitations:
:::
:::    - Lines are lmited to 1021 bytes long
:::    - Trailing control characters are stripped from each line
:::
:::  The code should look something like the following:
:::
:::     call PrintHere :Label
:::         Spacing    and blank lines are preserved
:::
:::     Special characters like & < > | ^ ! % are printed normally
:::     :Label
:::
:::  If the /E option is used, then variables between exclamation points are
:::  expanded, and ! and ^ literals must be escaped as ^! and ^^. The limitations
:::  are different when /E is used:
:::
:::    - Lines are limited to ~8191 bytes long
:::    - All characters are preserved, except !variables! are expanded and ^! and
:::      ^^ are transformed into ! and ^
:::
:::  Here is an example using /E:
:::
:::     call PrintHere /E :SubstituteExample
:::       Hello !username!^!
:::     :SubstituteExample
:::
:::  If the /- "TrimList" option is used, then leading "TrimList" characters
:::  are trimmed from the output. The trim characters are case sensitive, and
:::  cannot include a quote. If "TrimList" includes a space, then it must
:::  be the last character in the list.
:::
:::  Multiple PrintHere blocks may be defined within one script, but each
:::  :Label must be unique within the file.
:::
:::  PrintHere must not be used within a parenthesized code block.
:::
:::  Scripts that use PrintHere must use \r\n for line termination, and all lines
:::  output by PrintHere will be terminated by \r\n.
:::
:::  All redirection associated with a PrintHere must appear at the end of the
:::  command. Also, the CALL can include path information:
:::
:::     call "c:\utilities\PrintHere.bat" :MyBlock>test.txt
:::       This line is written to test.txt
:::     :MyBlock
:::
:::  PrintHere may be used with a pipe, but only on the left side, and only
:::  if the source script is included as a 2nd argument, and the right side must
:::  explicitly and unconditionally GOTO the terminating :Label.
:::
:::     call PrintHere :PipedBlock "%~f0" | more & goto :PipedBlock
:::       text goes here
:::     :PipedBlock
:::
:::  Commands concatenated after PrintHere are ignored. For example:
:::
:::     call PrintHere :ignoreConcatenatedCommands & echo This ECHO is ignored
:::       text goes here
:::     :ignoreConcatenatedCommands
:::
:::  PrintHere uses FINDSTR to locate the text block by looking for the
:::  CALL PRINTHERE :LABEL line. The search string length is severely limited
:::  on XP. To minimize the risk of PrintHere failure when running on XP, it is
:::  recommended that PrintHere.bat be placed in a folder included within PATH
:::  so that the utility can be called without path information.
:::
:::  PrintHere /? prints out this documentation.
:::
:::  PrintHere /V prints out the version information
:::
:::  PrintHere.bat was written by Dave Benham. Devlopment history may be traced at:
:::    http://www.dostips.com/forum/viewtopic.php?f=3&t=6537
:::

:start
set "tab=   "   NOTE: This value must be a single tab (0x09), not one or more spaces
set "sp=[ %tab%=,;]"
set "sp+=%sp%%sp%*"
set "opt="
set "/E="
set "/-="

:getOptions
if "%~1" equ "" call :exitErr Invalid call to PrintHere - Missing :Label argument
if "%~1" equ "/?" (
  for /f "tokens=* delims=:" %%L in ('findstr "^:::" "%~f0"') do echo(%%L
  exit /b 0
)
if /i "%~1" equ "/V" (
  for /f "tokens=* delims=:" %%L in ('findstr /rc:"^::PrintHere\.bat version" "%~f0"') do echo(%%L
  exit /b 0
)
if /i %1 equ /E (
  set "/E=1"
  set "opt=%sp+%.*"
  shift /1
  goto :getOptions
)
if /i %1 equ /- (
  set "/-=%~2"
  set "opt=%sp+%.*"
  shift /1
  shift /1
  goto :getOptions
)
echo %1|findstr "^:[^:]" >nul || call :exitErr Invalid PrintHere :Label
if "%~2" equ "" (
  (goto) 2>nul
  setlocal enableDelayedExpansion
  if "!!" equ "" (
    endlocal
    call %0 %* "%%~f0"
  ) else (
    >&2 echo ERROR: PrintHere must be used within a batch script.
    (call)
  )
)
set ^"call=%0^"
set ^"label=%1^"
set "src=%~2"
setlocal enableDelayedExpansion
set "call=!call:\=[\\]!"
set "label=!label:\=[\\]!"
for %%C in (. [ $ ^^ ^") do (
  set "call=!call:%%C=\%%C!"
  set "label=!label:%%C=\%%C!"
)
set "search=!sp!*call!sp+!!call!!opt!!sp+!!label!"
set "cnt="
for /f "delims=:" %%N in ('findstr /brinc:"!search!$" /c:"!search![<>|&!sp:~1!" "!src!"') do if not defined skip set "skip=%%N"
if not defined skip call :exitErr Unable to locate CALL PrintHere %1
for /f "delims=:" %%N in ('findstr /brinc:"!sp!*!label!$" /c:"!sp!*!label!!sp!" "!src!"') do if %%N gtr %skip% if not defined cnt set /a cnt=%%N-skip-1
if not defined cnt call :exitErr PrintHere end label %1 not found
if defined /E (
  for /f "skip=%skip% delims=" %%L in ('findstr /n "^^" "!src!"') do (
    if !cnt! leq 0 goto :break
    set "ln=%%L"
    if not defined /- (echo(!ln:*:=!) else for /f "tokens=1* delims=%/-%" %%A in (^""%/-%!ln:*:=!") do (
      setlocal disableDelayedExpansion
      echo(%%B
      endlocal
    )
    set /a cnt-=1
  )
) else (
  for /l %%N in (1 1 %skip%) do set /p "ln="
  for /l %%N in (1 1 %cnt%) do (
    set "ln="
    set /p "ln="
    if not defined /- (echo(!ln!) else for /f "tokens=1* delims=%/-%" %%A in (^""%/-%!ln!") do (
      setlocal disableDelayedExpansion
      echo(%%B
      endlocal
    )
  )
) <"!src!"
:break
(goto) 2>nul & goto %~1


:exitErr
>&2 echo ERROR: %*
(goto) 2>nul & exit /b 1

Full documentation is embedded within the script. Below are some demonstrations of usage:
直接输出
@echo off
call PrintHere :verbatim
    Hello !username!^!
    It is !time! on !date!.
:verbatim

-- 输出 --

    Hello !username!^!
    It is !time! on !date!.


扩展变量(无需启用延迟扩展)

@echo off
call PrintHere /E :Expand
    Hello !username!^!
    It is !time! on !date!.
:Expand

--输出--

    Hello Dave!
    It is 20:08:15.35 on Fri 07/03/2015.


扩展变量并修剪前导空格

@echo off
call PrintHere /E /- " " :Expand
    Hello !username!^!
    It is !time! on !date!.
:Expand

--输出--

Hello Dave!
It is 20:10:46.09 on Fri 07/03/2015.


输出可以重定向到文件

@echo off
call PrintHere :label >helloWorld.bat
  @echo Hello world!
:label

输出无法作为输入重定向,但可以使用管道!不幸的是,语法不够优雅,因为管道的两端在一个新的CMD.EXE进程中执行,所以(GOTO) 2>nul返回到子cmd进程,而不是主脚本。
@echo off
call PrintHere :label "%~f0" | findstr "^" & goto :label
  Text content goes here
:label

非常有趣,谢谢。我不是批处理专家,所以我承认大部分内容都超出了我的理解范围 ;) - Amro
@Amro - 或许很难理解它是如何工作的,但你不需要理解它就能使用它;-) 我设计了这个实用程序以方便使用。 - dbenham
如果您想将包含PrintHere的单个批处理文件与主脚本一起交付,是否可以使用标记调用来实现,即call :PrintHere :endLabel - Sam Hasler
1
@SamHasler - 是的,它按照编写的方式可以正常工作。您可能希望消除文档,然后就不需要 GOTO :START 了。您还可以摆脱 @ECHO OFF - dbenham

5
使用带参数的可以更简单地编写“heredoc”:
@echo off

rem Definition of heredoc macro
setlocal DisableDelayedExpansion
set LF=^


::Above 2 blank lines are required - do not remove
set ^"\n=^^^%LF%%LF%^%LF%%LF%^^"
set heredoc=for %%n in (1 2) do if %%n==2 (%\n%
       for /F "tokens=1,2" %%a in ("!argv!") do (%\n%
          if "%%b" equ "" (call :heredoc %%a) else call :heredoc %%a^>%%b%\n%
          endlocal ^& goto %%a%\n%
       )%\n%
    ) else setlocal EnableDelayedExpansion ^& set argv=


rem Heredoc syntax:
rem
rem %%heredoc%% :uniqueLabel [outfile]
rem contents
rem contents
rem ...
rem :uniqueLabel
rem
rem Same notes of rojo's answer apply

rem Example borrowed from rojo's answer:

set bodyText=Hello world!
set lipsum=Lorem ipsum dolor sit amet, consectetur adipiscing elit.

%heredoc% :endHtml out.txt
<html lang="en">
    <body>
        <h3>!bodyText!</h3>
        <p>!lipsum!</p>
    </body>
</html>
:endHtml

echo File created:
type out.txt
del out.txt
goto :EOF


rem Definition of heredoc subroutine

:heredoc label
set "skip="
for /F "delims=:" %%a in ('findstr /N "%1" "%~F0"') do (
   if not defined skip (set skip=%%a) else set /A lines=%%a-skip-1
)
for /F "skip=%skip% delims=" %%a in ('findstr /N "^" "%~F0"') do (
   set "line=%%a"
   echo(!line:*:=!
   set /A lines-=1
   if !lines! == 0 exit /B
)
exit /B

3

参考rojo在https://dev59.com/L3NA5IYBdhLWcg3wUL9Y#15032476上的帖子。

毫无疑问,他的方案是我寻找已久的(当然,我可以尝试实现类似于这样的东西,但懒惰阻碍了进步 :))。我想添加的一件小事是对原始代码的微小改进。我认为如果重定向到文件是在行末写的话会更好。在这种情况下,heredoc起始行可能会更严格,其分析会更简单。

@echo off

set "hello=Hello world!"
set "lorem=Lorem ipsum dolor sit amet, consectetur adipiscing elit."

call :heredoc HTML & goto :HTML
<html>
<title>!hello!</title>
<body>
<p>Variables in heredoc should be surrounded by the exclamation mark (^!).</p>
<p>!lorem!</p>
<p>Exclamation mark (^!) and caret (^^) MUST be escaped with a caret (^^).</p>
</body>
</html>
:HTML

goto :EOF

:: https://dev59.com/L3NA5IYBdhLWcg3wUL9Y#15032476
:heredoc LABEL
setlocal enabledelayedexpansion
set go=
for /f "delims=" %%A in ( '
    findstr /n "^" "%~f0"
' ) do (
    set "line=%%A"
    set "line=!line:*:=!"

    if defined go (
        if /i "!line!" == "!go!" goto :EOF
        echo:!line!
    ) else (
        rem delims are ( ) > & | TAB , ; = SPACE
        for /f "tokens=1-3 delims=()>&| ,;= " %%i in ( "!line!" ) do (
            if /i "%%i %%j %%k" == "call :heredoc %1" (
                set "go=%%k"
                if not "!go:~0,1!" == ":" set "go=:!go!"
            )
        )
    )
)
goto :EOF

这段代码想表达什么意思呢?我们来看一下。

Rojo的代码非常严格:

  • call:heredoc之间,字符串中不允许有多于一个空格字符。
  • call :heredoc必须紧贴着行的边缘(开头不能有任何空格)。
  • 允许在行内某个地方将输出重定向到文件(但并没有太大用处)。

我所建议的是:

  • 更加宽松(分隔符可以是多个空格)
  • 只允许在行尾将输出重定向到文件(需要使用圆括号)
  • 不要强制让代码紧贴着行的边缘

更新1:对查找和执行heredoc开始部分进行了改进:

  • 重要的命令只有 call :heredoc LABEL 或者 call :heredoc :LABEL。因此在打印heredoc内容后可以跳转到另一个标签,脚本结束或运行 exit /b 命令。
  • 移除了无用和不必要的 goto :next2 命令。

更新2:

  • 内部 for 的分隔符为 ( ) > & | TAB , ; = SPACE
  • if 命令添加了开关 /I

更新3:

通过以下链接可以找到该脚本的完整版本(可嵌入您自己的脚本):https://github.com/ildar-shaimordanov/cmd.scripts/blob/master/heredoc.bat


3

@jeb

setlocal EnableDelayedExpansion
set LF=^


REM Two empty lines are required

另一种变体:

@echo off

:)
setlocal enabledelayedexpansion
>nul,(pause&set /p LF=&pause&set /p LF=)<%0
set LF=!LF:~0,1!

echo 1!LF!2!LF!3

pause

+1,很高兴看到创建LF的另一种方法以及暂停命令的效果。 - jeb

2

您可以使用 FOR /F 循环创建一个带引号的文本块,因此您不需要转义特殊字符,例如 <>|&,只有 % 需要转义。
这在创建 HTML 输出等情况下有时非常有用。

@echo off
setlocal EnableDelayedExpansion
set LF=^


REM Two empty lines are required
set ^"NL=^^^%LF%%LF%^%LF%%LF%^^"

for /F "tokens=* delims=_" %%a in (^"%NL%
___"One<>&|"%NL%
___"two 100%%"%NL%
___%NL%
___"three "quoted" "%NL%
___"four"%NL%
") DO (
   @echo(%%~a
)

输出

One<>&|
two 100%

three "quoted"
four

我尝试解释这段代码。变量LF包含一个换行符,变量NL包含^<LF><LF>^
可以使用百分号扩展将一个换行符和一个插入符放在行末。
通常,FOR /F会将引用的文本拆分为多个标记,但仅拆分一次。
当我插入换行符时,FOR循环也会拆分为多行。
第一行和最后一行的引号仅用于创建FOR循环的正确语法。
任何行的开头都是_作为第一个字符将从前一行的多行插入符中转义,如果引号是第一个字符,则失去了转义能力。
使用_定界符,因为空格或逗号会导致XP出现问题(否则XP-Bug会尝试访问垃圾文件名)。
行末处的插入符也仅针对XP-Bug。
XP-Bug在引用文本包含未引用的,;=<space>字符时生效。
for /f "tokens=*" %%a in ("a","b","c") do echo %%a

+1 谢谢。我很难理解这个,进一步的解释将不胜感激。 - Amro

2

这里是ephemient优秀解决方案的变体。它允许您将多行内容传送到另一个程序,而无需创建文本文件并将其输入重定向到您的程序:

(@echo.bla
@echo.bla
) | yourprog.exe

为了快速演示,您可以将yourprog.exe替换为more

(@echo.bla
@echo.bla
) | more

输出:

bla
bla

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