Page 1 of 1

PrintHere.bat - an emulation of the unix here doc feature

Posted: 03 Jul 2015 18:30
by dbenham
Unix has a nifty feature called a here document where you can include a portion of the source script as stdin to a command. One frequent use is to simply print out a portion of the source script via the cat command. There are a number of options to here documents.

Print out the content verbatim if the label is quoted:

Code: Select all

cat << 'EOF'
        The current working directory may be accessed via "$pwd"
EOF
--OUTPUT--

Code: Select all

        The current working directory may be accessed via "$pwd"

Expand variables and interpret backticks normally if the label is not quoted:

Code: Select all

cat << EOF
        The current working directory is "$pwd"
EOF
--OUTPUT--

Code: Select all

        The current working directory is "/home/user"

Trim leading tab characters if - follows << (assume there are leading tabs in the file, not spaces)

Code: Select all

cat <<- EOF
        The current working directory is "$pwd"
EOF
--OUTPUT--

Code: Select all

The current working directory is "/home/user"


Woudn't it be nice if batch had a similar feature that allowed you to print out a portion of the source script :?:

People have been doing similar things within batch using FOR loops coupled with FINDSTR, but the syntax is not very elegant.

:idea: But the amazing behavior of the erroneous (GOTO) 2>nul has enabled me to create a new PrintHere.bat utility that offers downright sexy syntax :D
And I have provided similar options as the unix here doc, except you get to choose what leading characters are stripped :!:

Print content verbatim

Code: Select all

@echo off
call PrintHere :verbatim
    Hello !username!^!
    It is !time! on !date!.
:verbatim
--OUTPUT--

Code: Select all

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

Expand variables

Code: Select all

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

Code: Select all

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

And also trim leading spaces

Code: Select all

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

Code: Select all

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

You can write the output to a file with redirection:

Code: Select all

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

You can even pipe the output to a command, but the syntax isn't as sexy :|

Code: Select all

@echo off
call PrintHere :text "%~f0" | findstr "^" & goto :text
  Some text goes here
:text


So how is this done :?:
I use the (GOTO) 2>nul trick twice :!:

- The first time I use it to return to the parent script so I can retrieve the full path to the script, and then I CALL PrintHere a 2nd time.

- The second time through I use it to return permanently to the parent script and then GOTO the terminating :Label.

I actually used it a 3rd time for error processing, though it wasn't really necessary. I have multiple places where I detect errors, and I wanted an error handling routine that would take arguments, do certain processing, and then abort the utility. So I created :exitErr which uses (GOTO) to return to the root of PrintHere so that EXIT /B returns to the parent script.

Below is the magic code. Full documentation is embedded within the script.

Note that the code includes a tab character that does not post properly to this site. It appears where I define the tab variable, just below the :start label. You will have to edit that character when you copy the script. Alternatively, you can download PrintHere.bat.txt from my dropbox, and then simply rename it to PrintHere.bat.

PrintHere.bat
Edit version 1.1 - Fixed bug where empty lines were not printed properly if /E option not used

Code: Select all

@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


Dave Benham

Re: PrintHere.bat - an emulation of the unix here doc feature

Posted: 18 Jul 2016 16:38
by mirrormirror
Hi Dave, I know this is an older thread but I was trying out this utility and found a couple of issues in case you are interested:
Issue #1: Empty lines are not echoed correctly if trimming spaces/tabs (or maybe all characters)

Code: Select all

@ECHO OFF
@ECHO BEGIN ---------------------------------------------
CALL printhere.bat /- "    " :testlabel1

      DELETE FROM Users;
      DROP TABLE IF EXISTS tmpDataInsert;
      
      CREATE TEMP TABLE IF NOT EXISTS [tmpDataInsert] ( [uName] CHAR );
      .import SQLImport_UsernNm.txt tmpDataInsert
      
      INSERT INTO Users(UserNm) SELECT * FROM tmpDataInsert;
      DROP TABLE IF EXISTS tmpDataInsert;


:testlabel1

@ECHO END ---------------------------------------------

Output:

Code: Select all

T:\>test.cmd
BEGIN ---------------------------------------------
*:=
DELETE FROM Users;
DROP TABLE IF EXISTS tmpDataInsert;
*:=
CREATE TEMP TABLE IF NOT EXISTS [tmpDataInsert] ( [uName] CHAR );
.import SQLImport_UsernNm.txt tmpDataInsert
*:=
INSERT INTO Users(UserNm) SELECT * FROM tmpDataInsert;
DROP TABLE IF EXISTS tmpDataInsert;
*:=
*:=
END ---------------------------------------------
T:\>

Notice the *:= where the empty lines should be.

Issue #2: If the CALL line to "printhere.bat" has preceding whitespace AND the [trim] option is not being used then failure results:

Code: Select all

@ECHO OFF
@ECHO BEGIN ---------------------------------------------
   CALL printhere.bat :testlabel1

      DELETE FROM Users;
      DROP TABLE IF EXISTS tmpDataInsert;
      
      CREATE TEMP TABLE IF NOT EXISTS [tmpDataInsert] ( [uName] CHAR );
      .import SQLImport_UsernNm.txt tmpDataInsert
      
      INSERT INTO Users(UserNm) SELECT * FROM tmpDataInsert;
      DROP TABLE IF EXISTS tmpDataInsert;


:testlabel1

@ECHO END ---------------------------------------------

Notice the <TAB> before CALL printhere.bat.
Output:

Code: Select all

T:\>test.cmd
BEGIN ---------------------------------------------
ERROR: Unable to locate CALL PrintHere :testlabel1
'DELETE' is not recognized as an internal or external command,
operable program or batch file.
'DROP' is not recognized as an internal or external command,
operable program or batch file.
'CREATE' is not recognized as an internal or external command,
operable program or batch file.
'.import' is not recognized as an internal or external command,
operable program or batch file.
'INSERT' is not recognized as an internal or external command,
operable program or batch file.
'DROP' is not recognized as an internal or external command,
operable program or batch file.
END ---------------------------------------------
T:\>

Re: PrintHere.bat - an emulation of the unix here doc feature

Posted: 18 Jul 2016 22:10
by dbenham
mirrormirror wrote:Issue #1: Empty lines are not echoed correctly if trimming spaces/tabs (or maybe all characters)
Ugh :oops: I had a stupid copy/paste bug for the section of code that uses SET /P to read the file. I copied code from the section that uses FOR /F and forgot to edit a small piece of code. I updated the code in my first post to version 1.1.

mirrormirror wrote:Issue #2: If the CALL line to "printhere.bat" has preceding whitespace AND the [trim] option is not being used then failure results:
...
Notice the <TAB> before CALL printhere.bat.
I cannot reproduce your problem - it works fine for me. Make sure that the tab variable definition contains a <TAB> character, and not a <SPACE>. It is defined immediately after the :start label at the end of the documentation.
[code]

Dave Benham