Windows批处理文件输出指定行号

6

针对我当前困境的第二部分,我有一个文件夹列表在c:\file_list.txt中。 我需要能够基于行号提取它们(或者带一些修改地打印它们),因为这个批处理脚本是由迭代宏过程调用的。 我正在将行号作为参数传递。

@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

这段代码给我输出了%a。糟糕!所以,我尝试回显!a!(结果:ECHO is off.);我还尝试回显%a(结果:a)。

我想简单的做法是修改此处找到的head.bat代码: Windows批处理命令从文本文件读取第一行
除了回显每一行 - 我只会回显找到的最后一行。这并不像想象中的那么简单。我注意到我的计数器出于某种原因保持为零;我想知道set /a counter+=1是否正在执行我认为它正在执行的操作。


1
请注意,for /f 命令会跳过空行。这可能会在某些情况下导致计数错误。 - Joey
4个回答

13

我知道这是一个老问题,但对于有类似问题的人,这里有一些额外的信息...

Lee,你关于为什么在for循环之外"%%a"无法工作的推理是正确的。%a-z和%A-Z变量(在批处理文件中为%%a-z)是for循环的构造,不存在于它之外。

我想推荐一个替代解决方案,它匹配正确的行号(不会跳过空行),不需要延迟扩展、计数器或goto语句。看一下下面的代码:

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

以下是导致我进行上述更改的原因。假设您有以下文件内容:

Some text on line 1
Blah blah blah
More text

我做的第一件事是将(c:\file_list.txt)更改为('findstr /n .* "c:\file_list.txt"')

  • 'findstr /n .* "PATH\FILENAME"'读取文件并在每一行开头添加行号('/n')('.*'是匹配任何字符的正则表达式,"0或多个")。由于现在每行开头都有行号(即使是空行),因此for循环不会跳过任何行。

现在,在for循环中,每一行都将如下所示:

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

接下来,我们使用"tokens=1* delims=:"来分割行号和内容。

  • 'tokens=1*'将第一个标记(存储在%%a中)设置为分隔符之前的所有内容,将第二个标记(存储在%%b中)设置为分隔符之后的所有内容。
  • 'delims=:'将":"设置为用于分割字符串的分隔符字符。

现在,当我们循环遍历文件时,%%a将返回当前行号,%%b将返回该行的内容。

现在唯一要做的就是比较%1参数与%%a(而不是计数器变量),并使用%%b来存储当前行内容:if "%%a" == "%1" set line=%%b

额外的好处是,由于上述代码消除了在for循环中读取计数器变量,因此'enabledelayedexpansion'不再必要。

编辑:将'echo %line%'更改为'echo.%line%'。现在,这将正确显示空白行,而不是"ECHO is off."。将'type c:\file_list.txt ^| findstr /n .*'更改为'findstr /n .* "c:\file_list.txt"',因为findstr命令已经可以直接读取文件了。

Jeb,我认为我已经解决了所有特殊字符问题。试一试:

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
  )
)

我测试了一下,但是它在我的文件中失败了。首先我尝试使用::::Colon(所有冒号都被删除),然后我用\..\..\..\windows\system32\calc.exe进行了测试,但它没有打印出来 :-) - jeb
啊...不确定为什么calc.exe那一行没有执行成功(对我来说似乎是好的),但你确实让我在所有冒号都被折叠的问题上遇到了困难。我需要重新考虑这种方法。我会使用find /n /v ""而不是findstr,但是我最终会遇到相同的问题:]]]]Bracket[[[[Bracket都只会显示为Bracket。似乎没有非常简单的方法来避免这些定界符折叠。 - Seth McCauley
我开始觉得你使用"more +LineNumber"的方法可能是唯一可靠的做法(链接)。现在我可能需要编辑一些帖子:/ - Seth McCauley
不要失去希望 :-) 想想 "delims="set "str=!str:*:=!"。另外,试试这一行 ::caret^ Me&myself said "caret ^ soft&hard" - jeb
哇,你的代码块和示例数据真是让我大开眼界。我现在差不多已经弄明白了(请参见上面编辑过的帖子),但我有几个问题。在你的字符串替换示例中,星号的目的是什么?我知道它在做什么,但我不明白为什么或如何使用它。其次,有没有办法将最终数据放入可在endlocal语句之外使用的变量中?此外,您有没有进一步简化此代码的建议?我花了很长时间才将其缩小到现在这样。 - Seth McCauley
现在看起来很好,*<search>匹配到<search>文本之前的任何文本,也可以在set /?中找到相关描述。是的,在endlocal屏障之后获取数据是可能的。SO:使变量在ENDLOCAL后继续存在 - jeb

3
噫,它把我的格式搞乱了。
@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%

好的。首先,set /a counter=1 然后将一个环境变量设置为%%a并回显它 - 因为我猜测%%a在for语句之外不存在。for /f "usebackq delims=" %%a in (c:\file_list.txt) do (if "!counter!"=="%1" set line=%%a & goto :printme set /a counter+=1):printmeecho %line% - Lee

2
您可以使用以下批处理函数:

如下:

@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: () <> %! ^| "&

输出

一个包含特殊shell字符的行:() <> %! ^| "&

无效的行号

上述函数还可以打印空行或包含特殊字符的行,这对于大多数情况已经足够了。但是,为了处理向该函数提供的无效行号,请按照以下方式向函数添加错误检查代码:

: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

  • 特殊字符 -- 打印输出

  • 空行 -- 打印输出

  • 不存在的行 -- 显示错误消息


1

有一个技巧可以提取没有行号前缀(如果需要也可以带上)且无需使用批处理迭代(“for /F”加计数)遍历所有文件行的行字符串。

为此,您必须始终在管道中使用带有/N标志的findstr.exe,并通过第二个findstr.exe在管道中通过/B /C:“<N1>:” /C:“<N2>:” ... /C:“<NX>:”参数进行回过滤行。

这里是我用来解析文本和二进制文件的print_file_string.bat脚本:

@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

优点:

  • 比使用“for /F”迭代文件中所有行的速度更快。
  • 可以处理特殊字符,如& | % " ` ' ? 甚至是!字符(在真实的dll资源上进行了测试)。
  • 处理来自PE文件(例如dll和exe)的资源字符串(从https://technet.microsoft.com/en-us/sysinternals/strings.aspx下载strings.exe并将其放在脚本附近)。 例如,您可以从内置于exe/dll文件中的字符串中提取版本字符串。

已知问题:

  • 如果使用行过滤器或设置了-f1标志,则字符串开头的冒号字符(重复)将被裁剪。
  • findstr对内部字符串缓冲区有限制-8191个字符(包括换行符终止符)。在大多数情况下,所有大于此数字的字符串都将被截断为零长度。

示例:

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

    按行号排序打印example.txt文件的第1、10、20、30行,并带有行号前缀:

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

    打印example.txt文件的第100行并去除行号前缀。

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

    从c:\Application\res.dll二进制文件中打印所有以"VERSION="开头的字符串,并去除行号前缀。

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

    从c:\Application\res.dll二进制文件中打印第1、10、20、30行包含任意字符的字符串资源,并去除行号前缀。


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