0

I am experimenting with a single line cmd /c to get an inner loop without branching. (Actually i have the showLines routine which performs the loop.) I know its worst for performance but i want to know if its possible to get it run without quotes. Currently it raises "%G was unexpected at this time" error. So, it needs some correct escaping or expansion of variables.

@echo off

setlocal enableDelayedExpansion

set "param=%~1"

netstat -aonb | findstr /n $ > tmpFile_Content


for /F "tokens=*" %%A in ('type tmpFile_Content ^| findstr /r /c:"%param%" /i') do (

  SET line=%%A

  for /F "tokens=1 delims=:" %%I in ("!line!") DO (
      set /a LineNum=%%I
      rem set /a NextLineNum=LineNum+1

  )

  set /a lineNum=!LineNum!-1 


  if !lineNum!==0 ( set param="tokens=*" ) else ( set param="tokens=* skip=!lineNum!" )
  rem FOLLOWING WORKS FINE in quotes
  cmd /q /v:on /c "@echo off && setlocal enableDelayedExpansion && set cnt=2 && for /F %%param%% %%B in (tmpFile_Content) do ( echo %%B && set /a cnt-=1 >nul && if ^!cnt^!==0 exit /b )"

  rem Following does not work even though cmd should take the rest of arguments after /c
  cmd /q /v:on /c setlocal enableDelayedExpansion && FOR /F "tokens=*" %%C IN ('echo !param!') DO ( for /F %%C %%G in (tmpFile_Content) do (  echo %%G && set /a cnt-^=1 >nul && if ^!cnt^!==0 exit /b ))

  rem call :showLines !LineNum!

 )

del tmpFile_Content

goto :eof

:showLines
  set /a lineNum=%1-1
  set cnt=2
  for /F "tokens=* skip=%lineNum%" %%B in (tmpFile_Content) do (
    echo %%B
    set /a cnt-=1
    if !cnt!==0 goto exitLoop
  )
  :exitLoop
  exit /b



lockedscope
  • 965
  • 17
  • 45
  • 2
    it runs fine in quotes because the quotes protect "poison chars" (like `&<>...`). when outside of quotes, you have to escape them (with a caret): `.. ^&^& ...` – Stephan Jun 10 '20 at 15:04
  • 2
    Btw: `setlocal enabledelayedexpansion` makes no sense, as `cmd /v:on` already enables delayed expansion. – Stephan Jun 10 '20 at 15:05
  • 2
    `('echo !param!')` can be replaced by `("!param!")` which is faster, because it doesn't open an additional `cmd` process. – Stephan Jun 10 '20 at 15:07
  • 2
    and `cmd /q` already replaces `@echo off` – Stephan Jun 10 '20 at 15:09
  • 2
    What are you trying to do with `for /F %%C %%G in …`? **#.** if `%%C` is supposed to specify the option string, I have to disappoint you, this cannot work, because `for /F` tries to parse this value *before* loop meta-variables and delayed expansion occurs, so you can only use normal variables like `%var%` there; **#.** if you are trying to initialise two loop variables, then I am afraid, this will not work, since there simply is no such particular syntax… – aschipfl Jun 10 '20 at 15:36
  • 1
    @Stephan, no, because `for /F` will take `%%C` literally as the option string since it parses that parameter in an early stage (note that `for`, like `if`, is recognised earlier than other commands)… – aschipfl Jun 10 '20 at 16:36
  • @aschipfl you're correct. I simplified the code too much to find out what it's supposed to do. Deleted my false comment. – Stephan Jun 10 '20 at 18:26

2 Answers2

2

to construct for loops with variable parameters, you essentially need to define and execute them as a macro. Eg:

@Echo Off & Setlocal ENABLEdelayedExpasnion
Set param="tokens=* delims="
Set "test=string line"
Set For=For /F %param% %%G in ("^!test^!") Do echo %%G
%For%

Of course you could go even further, and build the entire for loop with another for loop macro on the fly.

UPDATE:

  • Method for defining conditional concatenation of commands now exampled
  • Syntax simplified to allow the same usage form for regular expansion and within codeblocks by having the constructor macro call a subroutine to expand the new for loop once it's constructed.
  • delayed concatenation variable usage simplified to avoid the escaping requirement
@Echo off
::: { Macro Definition
Setlocal DisabledelayedExpansion
    (set \n=^^^
%= This creates an escaped Line Feed - DO NOT ALTER =%
)
::: [ For Loop Constructor macro. ] For advanced programmers who need to use dynamic for loop options during code blocks.
::: - usage: %n.For%{For loop options}{variable set}{For Metavariable}{commands to execute}
::: - use delayed !and! variable to construct concatenated commands in the new for loop.
    Set n.For=For %%n in (1 2) Do If %%n==2 (%\n%
        Set FOR=%\n%
        For /F "tokens=1,2,3,4 Delims={}" %%1 in ("!mac.in!") Do (%\n%
            Set "FOR=For /F %%1 %%3 in ("!%%2!") Do (%%~4)"%\n%
        )%\n%
        Call :Exc.For%\n%
    )Else Set mac.in=
    Set "and.=&&"
    Set "and=!and.!"
::: } End macro definition.
    Setlocal EnableDelayedExpansion& rem // required to expand n.For constructor macro
::: - Usage examples:
    Set "example=is a string line"
    %n.For%{"tokens=* delims="}{example}{%%G}{Echo/%%~G}
    %n.For%{"tokens=1,2,3,4 delims= "}{example}{%%G}{"Echo/%%~J %%~G %%~H %%~I !and! Echo/%%~I %%~G %%~H %%~J"}
    Set "example2=Code block example"
    For %%a in (1 2 3) do (
        %n.For%{"Tokens=%%a Delims= "}{example2}{%%I}{"For /L %%# in (1 1 4) Do (Set %%I[%%#]=%%a%%#) !and! Set %%I[%%#]"}
    )
    Pause > Nul
    Goto :EOF
:Exc.For
    %FOR%
Exit /B

Example output:

is a string line
line is a string 
string is a line
Code[1]=11
Code[2]=12
Code[3]=13
Code[4]=14
block[1]=21
block[2]=22
block[3]=23
block[4]=24
example[1]=31
example[2]=32
example[3]=33
example[4]=34
Community
  • 1
  • 1
T3RR0R
  • 2,747
  • 3
  • 10
  • 25
  • This sample works fine, i tried it ```Set mycmd=for /F ^!param^! ^%^%G in ^(tempFile_Content^) Do ^( echo %%G ^&^& set /a cnt-^=1 ^>nul ^&^& if ^!cnt^!==0 exit /b ^)``` but it raises "for' is not recognized as an internal or external command". – lockedscope Jun 10 '20 at 17:07
  • 1
    you are incorrectly escaping alot of elements in your command. `^%` does nothing beneficial here, nor does escaping the parentheses. I've updated my answer with a macro to update for loops on the fly and a function that can be called to execute the macro during code blocks. The usage should be simple enough – T3RR0R Jun 10 '20 at 17:18
  • 1
    I recommend heading over to dostips and exploring topics relating to [batch macros](https://www.dostips.com/forum/viewtopic.php?f=3&t=2518&hilit=batch+macro+parameters&start=15) to develop an understanding of their construction and application, in addition to reading [this question](https://stackoverflow.com/questions/4094699/how-does-the-windows-command-interpreter-cmd-exe-parse-scripts) that details the rules of parsing for batch scripts (such as they are) to get a better grip on when escaping is required. – T3RR0R Jun 10 '20 at 17:31
  • I tried ```Set mycmd=for /F ^!param^! %%Z in (tempFile_Content^) Do ( echo %%Z ^) >nul !mycmd!``` But for is not recognized... – lockedscope Jun 10 '20 at 17:40
  • 1
    attention to detail here. note how in the above examples, the macro commands are expanded with % expansion? this is a requirement due to the stages involved in parsing the command - hence why the exampled subroutine usage when using the for constructor during code blocks. Again, note in my examples above there is no escaping of parentheses. Invest the time in developing the understanding to utilise these scripting techniques as per the links I provided. – T3RR0R Jun 10 '20 at 17:47
  • 1
    lastly, there is no escaping of the options variable during macro definition - the value of the For macro must contain the correct syntax when it's expanded. – T3RR0R Jun 10 '20 at 17:53
  • So, this -for- macro works in % expansion but does not work in delayed expansion scope like in if/for. Thus, it seems ```cmd /c "for /F %%options%%"...``` is the only ugly and poor performing way to create a -for- loop in a delayed expansion scope. – lockedscope Jun 11 '20 at 16:40
  • 1
    You will find that will not work. The approach for defining macros during a delayed expansion environment (Code Blocks) is exampled in the answer, which is to call a subroutine to perform the action in a new command instance. – T3RR0R Jun 11 '20 at 16:50
  • 1
    Key point to remember is to double %% in metavariables passed via call to the subroutine, due to the aditional level of parsing. Example: `For %%a in (1 2 3) do Call :new.loop "tokens=%%a Delims= " "example" "%%%%H" "For /L %%%%# in (1 1 4) Do Echo %%%%H[%%%%#]"` – T3RR0R Jun 11 '20 at 17:09
  • Oh, I missed it, thats fine. I could also achive same with ```:myloop For /F "%~1" %~3 in ("!%~2!") Do (%~4) exit /b ``` – lockedscope Jun 11 '20 at 17:24
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/215765/discussion-between-t3rr0r-and-lockedscope). – T3RR0R Jun 11 '20 at 17:27
0

Finally, i came up with following to execute code given in string for anyone interested experimentally and may be for some insight about escaping and expansion.

I use macro instead of cmd which will be much more faster i think(not sure because its said that "call" also causes launch of cmd).

So, it is a simple one-liner without a lot of extra code. But things easily become complicated and when extra escaping and special characters used then @T3RR0R's macro routine would be a necessity.

@echo off

setlocal EnableDelayedExpansion

set "param=%~1"

netstat -aonb | findstr /n $ > tmpFile_Content


for /F "tokens=*" %%A in ('type tmpFile_Content ^| findstr /r /c:"%param%" /i') do (

  SET line=%%A

  for /F "tokens=1 delims=:" %%I in ("!line!") DO (
      set /a LineNum=%%I
      rem set /a NextLineNum=LineNum+1

  )

  set /a lineNum=!LineNum!-1 


  rem CORRECT QUOTING
  if !lineNum!==0 ( set "param="tokens=*"" ) else ( set "param="tokens=* skip=!lineNum!"" )
  rem FOLLOWING WORKS FINE in quotes
  rem cmd /q /v:on /c "set cnt=2 && for /F ^!param^! %%B in (tmpFile_Content) do ( echo %%B && set /a cnt-=1 >nul && if ^!cnt^!==0 exit /b )"

  rem For reading !cnt! use !x!cnt!x!.
  rem Only one extra variable used, and in routine its replaced with !(exclamation) for our "cnt" variable.
  set "x=^!x^!"
  call :ExecCode "set cnt=2 && for /F ^!param^! %%%%B in (tmpFile_Content) do (echo %%%%B && set /a cnt=!x!cnt!x!-1 >nul && if !x!cnt!x!==0 (exit /b) )"

  rem call :showLines !LineNum!

 )

del tmpFile_Content

goto :eof

:ExecCode
  setlocal

  rem Replace with exclamation for variable
  set "x=^!"
  set "s=%~1"

  %s%

  endlocal
  exit /b  


:showLines
  set /a lineNum=%1-1
  set cnt=2
  for /F "tokens=* skip=%lineNum%" %%B in (tmpFile_Content) do (
    echo %%B
    set /a cnt-=1
    if !cnt!==0 goto exitLoop
  )
  :exitLoop
  exit /b

lockedscope
  • 965
  • 17
  • 45