0

I'm trying to output part of a file (the text enclosed between :begin(...) and :end(...) markers) using a BATCH script. The target is pure ASCII text but it may contain blank lines and symbols; the few answers I could read in SO don't handle arbitrary text correctly...

The code below is supposed to scan itself for the block in question and output its content:

@echo off

call :SelfExtractBlock "README.TXT"
exit /b %ErrorLevel%

:SelfExtractBlock
    setlocal DisableDelayedExpansion
    set enter=:begin(%~1)
    set leave=:end(%~1)
    set found=
    for /f "delims=" %%a in ('findstr /n "^" "%~f0"') do (
        set line=%%a
        setlocal EnableDelayedExpansion
        set text=!line:*:=!
        if defined found (
            if "!text!" == "!leave!" exit /b 0
            echo(!text!
        ) else if "!text!" == "!enter!" (
            set found=1
        )
        endlocal & set found=%found%
    )
    exit /b 1

rem ################# EMBEDDED FILES #################

:begin(README.TXT)
symbols: % ^ & < > | ' ` , ; = ( ) ! " \ [ ] " . * ?  
escapes: %% ^^ ^& ^< ^> ^| ^' ^` ^, ^; ^= ^( ^) ^^! "" \\ \[ \] \" \. \* \?

variables: %var% !var!
:end(README.TXT)

But I don't get any output, while I'm expecting:

symbols: % ^ & < > | ' ` , ; = ( ) ! " \ [ ] " . * ?  
escapes: %% ^^ ^& ^< ^> ^| ^' ^` ^, ^; ^= ^( ^) ^^! "" \\ \[ \] \" \. \* \?

variables: %var% !var!

What's going on? How do I fix the code?

5
  • 2
    1. You can easily transfer the value of found to the "parent" environment by changing endlocal & set found=%found% by for %%f in ("!found!") do endlocal & set found=%%~f 2. It is not necessary that "!leave!" and "!enter!" use Delayed Expansion because their values don't change inside the for Commented Sep 26 at 6:21
  • The first change above solve your problem, but I invite you to see my answer Commented Sep 26 at 6:30
  • Wow, using a for to define a variable that won't disappear because of an endlocal; nice trick! Commented Sep 26 at 6:52
  • For what I know, enter and leave could contain something like :begin(<h1 class="">The rise of</h1>), won't that break the if condition if you use %enter% instead of !enter!? Commented Sep 26 at 7:00
  • Yes... If enter or leave contain a quote, they must use delayed expansion... Commented Sep 26 at 17:18

2 Answers 2

3

You can entirely avoid all these problems if you read the file via set /P instead of for /F. For example:

@echo off

call :SelfExtractBlock "README.TXT"
exit /b %ErrorLevel%

:SelfExtractBlock
    setlocal EnableDelayedExpansion

    rem Get the number of lines to skip and to copy
    set "skip="
    set "copy="
    for /F "delims=:" %%n in ('findstr /N ":begin(%~1) :end(%~1)" "%~F0"') do (
        if not defined skip (
            set "skip=%%n"
        ) else (
            set /A "copy=%%n-skip-1"
        )
    )
    if not defined copy exit /B 1

    rem Into this block read the file
    < "%~F0" (

        rem Skip the lines before copy
        for /L %%s in (1,1,%skip%) do set /P "="

        rem Copy the lines
        for /L %%c in (1,1,%copy%) do (
            set "line="
            set /P "line="
            echo(!line!
        )

    )

    exit /b 0

rem ################# EMBEDDED FILES #################

:begin(README.TXT)
symbols: % ^ & < > | ' ` , ; = ( ) ! " \ [ ] " . * ?  
escapes: %% ^^ ^& ^< ^> ^| ^' ^` ^, ^; ^= ^( ^) ^^! "" \\ \[ \] \" \. \* \?

variables: %var% !var!
:end(README.TXT)

The code is simpler and run faster because it don't duplicate the entire environment in each line. The only drawback is that the lines can have a max lenght of 1024 characters instead of 8K with for /F.

Sign up to request clarification or add additional context in comments.

5 Comments

I'm not sure that you can call it "simpler" but your method is nice: finding the No. of the lines containing the markers; though I would add a /L to findstr, and maybe call it twice (once for each marker) in order to be able to handle spaces in the markers. +1
Well, the other code assign a line, enable delayed expansion, process the line, and then endlocal with a trick in order to preserve a value for the next iteration... That is a lot of code that have no relation to solve the problem, but to circumvent technicall limitations of Batch files. Of course that my code is simpler! ;)
You can get both lines from the same findstr (with spaces in the markers) by using findstr /N /C":begin(%~1)" /C:":end(%~1)" "%~F0". May I ask you the select my answer as Best Answer?
I'll do it in time, don't worry. Now I'm trying to adapt your code to make it handle any literal input string, but it doesn't seam possible because of some horrible parser rules of findstr (see stackoverflow.com/a/8844873/3387716 : Escaping Backslash within command line literal search strings)
On the other hand, my initial method doesn't have any pitfall, it works with any tag the user may want to use.
0

The problem lays in the passing of found=1 from the EnableDelayedExpansion environment to the parent environment. The %found% in the line endlocal & set found=%found% is only expanded once (when the loop is parsed), so the "outer" found is never updated with the "inner" found value, leading to no output.

Unfortunately, you cannot drop DisableDelayedExpansion nor EnableDelayedExpansion as you need the former to safely expand %%a in the for loop and the latter to expand the other variables safely...

To fix the issue and keep the RAW text verbatim you'll have to set found=1 after endlocal; as the EnableDelayedExpansion block contains a few branches you'll have to add an endlocal in each one of them:

:SelfExtractBlock
    setlocal DisableDelayedExpansion
    set "enter=:begin(%~1)"
    set "leave=:end(%~1)"
    set "found="
    for /f "delims=" %%a in ('findstr /n "^" "%~f0"') do (
        set "line=%%a"
        setlocal EnableDelayedExpansion
        set "text=!line:*:=!"
        if defined found (
            if "!text!" == "!leave!" exit /b 0
            echo(!text!
            endlocal
        ) else if "!text!" == "!enter!" ( 
            endlocal
            set "found=1"
        ) else (
            endlocal
        )
    )
    exit /b 1

Now the code should work as expected, in the sense that it doesn't remove any empty line nor trim (nor add) any character.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.