开发者

Windows Batch file to echo a specific line number

So for the second part of my current dilemma, I have a list of folders in c:\file_list.txt. I need to be able to extract them (well, echo them with some mods) based on the line number because this batch script is being called by an iterative macro process. I'm passing the line number as a parameter.

@echo off
setlocal enabledelayedexpansion
set /a counter=0
set /a %%a = ""
for /f "usebackq delims=" %%a in (c:\file_list.txt) do (
   if "!counter!"=="%1" goto :printme & set /a counter+=1
)
:printme
echo %%a

which gives me an output of %a. Doh! So, I've tried echoing !a! (result: ECHO is off.); I've tried echoing %a (result: a)

I figured the easy thing to do wou开发者_JAVA技巧ld be to modify the head.bat code found here: Windows batch command(s) to read first line from text file

except rather than echoing every line - I'd just echo the last line found. Not as simple as one might think. I've noticed that my counter is staying at zero for some reason; I'm wondering if the set /a counter+=1 is doing what I think it's doing.


I know this is an old question, but here is some additional info for anyone with a similar issue...

Lee, your reasoning on why "%%a" isn't working outside the for loop is correct. The %a-z and %A-Z variables (%%a-z inside a batch file) are a construct of the for loop, and do not exist outside of it.

I would like to recommend an alternative solution to this problem that matches the correct line numbers (no empty lines skipped) and does not require delayed expansion, counters, or a goto statement. Take a look at the following code:

@echo off
for /f "tokens=1* delims=:" %%a in ('findstr /n .* "c:\file_list.txt"') do if "%%a"=="%1" set line=%%b
echo.%line%

Here is what led me to the above changes. Let's say you had the following file contents:

Some text on line 1
Blah blah blah
More text

The first thing I did was change (c:\file_list.txt).to ('findstr /n .* "c:\file_list.txt"').

  • 'findstr /n .* "PATH\FILENAME"' reads the file and adds a line number ('/n') to every line ('.*' is a regular expression matching "0 or more" of any character). Since every line will now have a line number at the beginning (even the empty ones), no lines will be skipped by the for loop.

Each line will now look like this inside the for loop:

1:Some text on line 1
2:Blah blah blah
3:More text

Next, we use "tokens=1* delims=:" to break up the line number and the content.

  • 'tokens=1*' sets the first token (stored in %%a) to everything before the delimiter, and the second token (stored in %%b) to everything after it.
  • 'delims=:' sets ":" as the delimiter character used to break up the string.

Now as we loop through the file, %%a will return the current line number and %%b will return the content of that line.

All that's left is to compare the %1 parameter to %%a (instead of a counter variable) and use %%b to store the current line content: if "%%a" =="%1" set line=%%b.

An added bonus is that 'enabledelayedexpansion' is no longer necessary, since the above code eliminates reading a counter variable in the middle of a for loop.

Edit: changed 'echo %line%' to 'echo.%line%'. This will correctly display blank lines now, instead of "ECHO is off.". Changed 'type c:\file_list.txt ^| findstr /n .*' to 'findstr /n .* "c:\file_list.txt"', since the findstr command can already read files directly.

Jeb, I think I've solved all the special character issues. Give this a shot:

for /f "tokens=*" %%a in ('findstr /n .* "c:\file_list.txt"') do (
  set "FullLine=%%a"
  for /f "tokens=1* delims=:" %%b in ("%%a") do (
    setlocal enabledelayedexpansion
    set "LineData=!FullLine:*:=!"
    if "%%b" equ "%1" echo(!LineData!
    endlocal
  )
)


Bah, it ate my formatting.

@echo off

setlocal enabledelayedexpansion

set /a counter=0
set %%a = ""

for /f "usebackq delims=" %%a in (c:\file_list.txt) do (if "!counter!"=="%1" goto :printme & set /a counter+=1)

:printme

echo %%a%


You can use a batch function like this:

@ECHO OFF
CALL :ReadNthLine "%~nx0" 10
PAUSE >NUL
GOTO :EOF

:ReadNthLine File nLine
FOR /F "tokens=1* delims=]" %%A IN ('^<"%~1" FIND /N /V "" ^| FINDSTR /B /C:"[%2]"') DO ECHO.%%B
GOTO :EOF

A line containing special shell characters: () <> %! ^| "&

OUTPUT

A line containing special shell characters: () <> %! ^| "&

Invalid Line Numbers

The above function can also print empty lines or lines containing special characters, and this is enough for most cases. However, in order to handle invalid line numbers supplied to this function, please add error checking code to the function like this:

:ReadNthLine File nLine
FOR /F %%A IN ('^<"%~1" FIND /C /V ""') DO IF %2 GTR %%A (ECHO Error: No such line %2. 1>&2 & EXIT /b 1)
FOR /F "tokens=1* delims=]" %%A IN ('^<"%~1" FIND /N /V "" ^| FINDSTR /B /C:"[%2]"') DO ECHO.%%B
EXIT /b

ReadNthLine2

  • special characters -- printed

  • empty line -- printed

  • non-existing line -- an error message shown


There is a trick to extract line strings w/o line numbers prefix (or with if you want to) and w/o need to use batch iterations ("for /F" plus counting) over all file lines.

To do so you must use findstr.exe always with /N flag in pipeline and backfilter lines by second findstr.exe in pipeline through the /B /C:"<N1>:" /C:"<N2>:" ... /C:"<NX>:" arguments.

Here the print_file_string.bat script i am using to parse text and binary files:

@echo off

rem Description:
rem   Script for string lines extraction from a text/binary file by findstr
rem   utility pattern and/or line number.

rem Command arguments:
rem %1 - Optional flags:
rem      -n - prints line number prefix "<N>:" for each found string from file.
rem           By default, the line number prefix does not print.
rem      -f1 - filter by line numbers for strings after %4..%N filter pattern.
rem           By default, filters by line numbers from the file.
rem      -pe - treats input file as a Portable Executable file
rem           (the strings.exe must exist).
rem           By default, the file treated as a text file.
rem %1 - Path to a directory with a file to extract.
rem %2 - Relative path to a text/binary file with strings.
rem %3 - Set of line numbers separated by : character to print strings of.
rem      These line numbers by default are line numbers of strings from the
rem      file, not from filtered output. If you want to point line numbers
rem      after %4..%N filter pattern, then you must use -f1 flag.
rem      If empty, then treated as "all strings".
rem %4..%N - Arguments for findstr command line in first filter.
rem      If empty, then treated as /R /C:".*", which means "any string".

rem CAUTION:
rem   DO NOT use /N flag in %4..%N arguments, instead use script -n flag to
rem   print strings w/ line number prefix.

rem Examples:
rem 1. call print_file_string.bat -n . example.txt 1:20:10:30 /R /C:".*"
rem Prints 1, 10, 20, 30 lines of the example.txt file sorted by line number
rem and prints them w/ line number prefix:
rem
rem 2. call print_file_string.bat . example.txt 100 /R /C:".*"
rem Prints 100'th string of example.txt file and prints it w/o line number
rem prefix.
rem
rem 3. call print_file_string.bat -pe c:\Application res.dll "" /B /C:"VERSION="
rem Prints all strings from the c:\Application\res.dll binary file, where
rem strings beginning by the "VERSION=" string and prints them w/o line number
rem prefix.
rem
rem 4. call print_file_string.bat -pe c:\Application res.dll 1:20:10:30 /R /C:".*"
rem Prints 1, 10, 20, 30 lines of string resources from the
rem c:\Application\res.dll binary file, where strings beginning by the
rem "VERSION=" string and prints them w/o line number prefix.

setlocal EnableDelayedExpansion

set "?~dp0=%~dp0"
set "?~nx0=%~nx0"

rem script flags
set FLAG_PRINT_LINE_NUMBER_PREFIX=0
set FLAG_F1_LINE_NUMBER_FILTER=0
set FLAG_FILE_FORMAT_PE=0

rem flags
set "FLAGS="

:FLAGS_LOOP

rem flags always at first
set "FLAG=%~1"

if not "%FLAG%" == "" ^
if not "%FLAG:~0,1%" == "-" set "FLAG="

if not "%FLAG%" == "" (
  if "%FLAG%" == "-n" set FLAG_PRINT_LINE_NUMBER_PREFIX=1
  if "%FLAG%" == "-f1" set FLAG_F1_LINE_NUMBER_FILTER=1
  if "%FLAG%" == "-pe" set FLAG_FILE_FORMAT_PE=1
  shift

  rem read until no flags
  goto FLAGS_LOOP
)

set "DIR_PATH=%~dpf1"
set "FILE_PATH=%~2"

set "FILE_PATH_PREFIX="
if not "%DIR_PATH%" == "" set "FILE_PATH_PREFIX=%DIR_PATH%\"

if not "%FILE_PATH_PREFIX%" == "" ^
if not exist "%FILE_PATH_PREFIX%" (
  echo.%?~nx0%: error: Directory path does not exist: "%FILE_PATH_PREFIX%"
  exit /b 1
) >&2

if "%FILE_PATH%" == "" (
  echo.%?~nx0%: error: File path does not set.
  exit /b 2
) >&2

if not exist "%FILE_PATH_PREFIX%%FILE_PATH%" (
  echo.%?~nx0%: error: File path does not exist: "%FILE_PATH_PREFIX%%FILE_PATH%"
  exit /b 3
) >&2

set "LINE_NUMBERS=%~3"

set "FINDSTR_LINES_FILTER_CMD_LINE="
if "%LINE_NUMBERS%" == "" goto FINDSTR_LINES_FILTER_END

set LINE_NUMBER_INDEX=1
:FINDSTR_LINES_FILTER_LOOP
set "LINE_NUMBER="
for /F "tokens=%LINE_NUMBER_INDEX% delims=:" %%i in ("%LINE_NUMBERS%") do set "LINE_NUMBER=%%i"
if "%LINE_NUMBER%" == "" goto FINDSTR_LINES_FILTER_END

set FINDSTR_LINES_FILTER_CMD_LINE=!FINDSTR_LINES_FILTER_CMD_LINE! /C:"!LINE_NUMBER!:"
set /A LINE_NUMBER_INDEX+=1
goto FINDSTR_LINES_FILTER_LOOP

:FINDSTR_LINES_FILTER_END

shift
shift
shift

set "FINDSTR_FIRST_FILTER_CMD_LINE="

:FINDSTR_FIRST_FILTER_LOOP
set ARG=%1

if not "!ARG!" == "" (
  set FINDSTR_FIRST_FILTER_CMD_LINE=!FINDSTR_FIRST_FILTER_CMD_LINE! !ARG!
  shift
  goto FINDSTR_FIRST_FILTER_LOOP
)

if "!FINDSTR_FIRST_FILTER_CMD_LINE!" == "" set FINDSTR_FIRST_FILTER_CMD_LINE=/R /C:".*"

set OUTPUT_HAS_NUMBER_PREFIX=0

rem in case if /N at the end
set "FINDSTR_FIRST_FILTER_CMD_LINE=!FINDSTR_FIRST_FILTER_CMD_LINE! "

rem 1. add /N parameter to first filter if must print line prefixes and -f1 flag is not set.
rem 2. flags prefixed output if must print line prefixes.
if %FLAG_PRINT_LINE_NUMBER_PREFIX% NEQ 0 (
  if %FLAG_F1_LINE_NUMBER_FILTER% EQU 0 (
    if "!FINDSTR_FIRST_FILTER_CMD_LINE:/N =!" == "!FINDSTR_FIRST_FILTER_CMD_LINE!" (
      set "FINDSTR_FIRST_FILTER_CMD_LINE=/N !FINDSTR_FIRST_FILTER_CMD_LINE!"
    )
  )
  set OUTPUT_HAS_NUMBER_PREFIX=1
)

rem 1. add /N parameter to first filter and flags prefixed output if lines filter is not empty and -f1 flag is not set.
rem 2. add /B parameter to lines filter if lines filter is not empty
if not "!FINDSTR_LINES_FILTER_CMD_LINE!" == "" (
  if %FLAG_F1_LINE_NUMBER_FILTER% EQU 0 (
    if "!FINDSTR_FIRST_FILTER_CMD_LINE:/N =!" == "!FINDSTR_FIRST_FILTER_CMD_LINE!" (
      set "FINDSTR_FIRST_FILTER_CMD_LINE=/N !FINDSTR_FIRST_FILTER_CMD_LINE!"
      set OUTPUT_HAS_NUMBER_PREFIX=1
    )
  )
  if "!FINDSTR_LINES_FILTER_CMD_LINE:/B =!" == "!FINDSTR_LINES_FILTER_CMD_LINE!" (
    set "FINDSTR_LINES_FILTER_CMD_LINE=/B !FINDSTR_LINES_FILTER_CMD_LINE!"
  )
)

rem 1. remove /N parameter from first filter if -f1 flag is set.
rem 2. flags prefixed output if -f1 flag is set.
if %FLAG_F1_LINE_NUMBER_FILTER% NEQ 0 (
  if not "!FINDSTR_FIRST_FILTER_CMD_LINE:/N =!" == "!FINDSTR_FIRST_FILTER_CMD_LINE!" (
    set "FINDSTR_FIRST_FILTER_CMD_LINE=!FINDSTR_FIRST_FILTER_CMD_LINE:/N =!"
  )
  set OUTPUT_HAS_NUMBER_PREFIX=1
)

if "%TOOLS_PATH%" == "" set "TOOLS_PATH=%?~dp0%"
rem set "TOOLS_PATH=%TOOLS_PATH:\=/%"
if "%TOOLS_PATH:~-1%" == "\" set "TOOLS_PATH=%TOOLS_PATH:~0,-1%"

if %FLAG_FILE_FORMAT_PE% EQU 0 (
  set CMD_LINE=type "%FILE_PATH_PREFIX%%FILE_PATH%" ^| findstr !FINDSTR_FIRST_FILTER_CMD_LINE!
) else (
  rem add EULA acception into registry to avoid EULA acception GUI dialog
  reg add HKCU\Software\Sysinternals\Strings /v EulaAccepted /t REG_DWORD /d 0x00000001 /f >nul 2>nul

  rem @ for bug case workaround
  set CMD_LINE=@"%TOOLS_PATH%\strings.exe" -q "%FILE_PATH_PREFIX%%FILE_PATH%" ^| findstr !FINDSTR_FIRST_FILTER_CMD_LINE!
)

if %FLAG_F1_LINE_NUMBER_FILTER% NEQ 0 set CMD_LINE=!CMD_LINE! ^| findstr /N /R /C:".*"
if not "!FINDSTR_LINES_FILTER_CMD_LINE!" == "" set CMD_LINE=!CMD_LINE! ^| findstr !FINDSTR_LINES_FILTER_CMD_LINE!

rem echo !CMD_LINE! >&2
(
  endlocal
  rem to avoid ! character truncation
  setlocal DisableDelayedExpansion
  if %OUTPUT_HAS_NUMBER_PREFIX% NEQ 0 (
    if %FLAG_PRINT_LINE_NUMBER_PREFIX% NEQ 0 (
      %CMD_LINE% 2>nul
    ) else ( 
      for /F "usebackq eol= tokens=1,* delims=:" %%i in (`^(%CMD_LINE: | findstr = ^| findstr %^) 2^>nul`) do echo.%%j
    )
  ) else (
    %CMD_LINE% 2>nul
  )
)

exit /b 0

Advantages:

  • Faster than "for /F" iteration over all lines in the file.
  • Works with special characters like & | % " ` ' ? and even ! character (tested on a real dll resources).
  • Handles resource strings from PE files like dll and exe (download the strings.exe from https://technet.microsoft.com/en-us/sysinternals/strings.aspx and put it near the script). For example, you can extract version string from strings builtin in exe/dll file.

Known Issues:

  • If a filter by line(s) has used or -f1 flag is set, then : characters (repeated) will be trimmed from the beginning of a string.
  • findstr has a limit to internal string buffer - 8191 characters (including line return characters terminator). All strings greater than this number in most cases will be truncated to zero length.

Examples:

  1. call print_file_string.bat -n . example.txt 1:20:10:30 /R /C:".*"

    Prints 1, 10, 20, 30 lines of the example.txt file sorted by line number and prints them w/ line number prefix:

  2. call print_file_string.bat . example.txt 100 /R /C:".*"

    Prints 100'th string of example.txt file and prints it w/o line number prefix.

  3. call print_file_string.bat -pe c:\Application res.dll "" /B /C:"VERSION="

    Prints all strings from the c:\Application\res.dll binary file, where strings beginning by the "VERSION=" string and prints them w/o line number prefix.

  4. call print_file_string.bat -pe c:\Application res.dll 1:20:10:30 /R /C:".*"

    Prints 1, 10, 20, 30 lines of string resources from the c:\Application\res.dll binary file, where strings contains any character and prints them w/o line number prefix.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜