Why is my for loop repeating the last operation? [duplicate] - batch-file

This question already has answers here:
Where does GOTO :EOF return to?
(4 answers)
Closed 8 months ago.
I have a batch file which is performing a series of functions for each item in a file. It's running correctly, however, for some reason, it's performing the operation for the last line in the file twice. Can anyone help me determine why? This is the first for loop I've made on my own, so I'm sure I've made some mistakes.
for /F "tokens=*" %%A in (nations.txt) do (
set "nationname=%%A"
call :ageofdiscovery
)
Point of clarification, what I am trying to do is call each and every line of "nations.txt" one at a time, storing them as a variable, then performing an elaborate series of operations using that variable, before moving on to the next line, and running through the whole of "nations.txt". The idea is to allow the script to work for an arbitrary number of loops, so as to make the script more flexible (It's a text generator, creating histories for fantasy kingdoms).
If there is no problem with the for loop, could someone explain why it's repeating the last output? I have an exit command after the loop, so it shouldn't be executing the script again, and it also has the same random generated output for the repeated last line.
EDIT: As requested, the current contents of nations.txt is:
Nation1
Nation2
Nation3
Nation4
As for the batch script itself, it's 2,134 lines long (and runs perfectly fine with a hard-coded version of the nation selection system. I'm retrofitting code here). I'm unsure of what, or where, any problems could be occurring. I also know that people here do not want me to share the entire script. I will do as requested in relation to the script itself.

The best guess for your observed phenomenon is that your function is defined directly after your loop (you don't show that important detail).
for /F "tokens=*" %%A in (nations.txt) do (
set "nationname=%%A"
call :ageofdiscovery
)
:ageofdiscovery
echo %nationname%
In that case, the function code will be executed after the loop is finished, because batch-files don't have a concept of functions, they only know labels.
It can be easily fixed by an exit /b or goto :eof
for /F "tokens=*" %%A in (nations.txt) do (
set "nationname=%%A"
call :ageofdiscovery
)
exit /b
:ageofdiscovery
echo %nationname%

Related

How to output multiple lines from a FINDSTR to a variable

It's no surprise that the official documentation doesn't really help in the matter of understanding how does the command process the result of a command instead of a filelist neither why is it even called 'FOR'. Yes I already know Stack Overflow is full of similar question but apparently, since batch scripts are influenced by so many
"breaking" factors that, even as a non-batch experienced programmer, it is difficult not to get lost in the thousands exceptions and do-nots which may affect the result.
My objective, aside from learning from the best answer possible, is to formulate a generic enough question to represent the matter which is probably the most common task including the FINDSTR command:
THE QUESTION:
How do I get the output of a FINDSTR in a way that allows me to compute every result line one at the time
possibly INSIDE the loop?
The most 'generic' (batch bs-proof if you know what I mean) example I can make is the following:
Let's say this is secret_file.txt
some not interesting line
A very interesting line = "secret1";
some not interesting line
A very interesting line = "secret2";
some not interesting line
A very interesting line = "secret3";
some not interesting line
Now with the findstr command I can output every "secret" line like this:
findstr /R /C:"secret.\"" secret_file.txt
A very interesting line = "secret1";
A very interesting line = "secret2";
A very interesting line = "secret3";
But this result is just useless without further parsing right? I could have used ctrl-F over any text reader/editor
for this matter, anyway, let's say I now want to output every line ONE AT THE TIME so that I can compute it, for
example, saving every secret to a variable then using that variable somehow
(it doesn't really matter how, we can just echo it to keep things simple).
Now, everybody agrees on the fact that for this kind of task, a FOR loop is needed.
Quoting https://ss64.com/nt/for.html on the syntax, my script.bat should looks like this:
#echo off
FOR /F %%A IN ('findstr /R /C:"secret.\"" secret_file.txt') DO ECHO Batch script language is completely fine, good job Microsoft!
This just doesn't even give any output, can someone explain me why? My hypothesis was that the output from the findstr command
is in a non-compatible format with the FOR command, not like I could check or something since the source is closed and the
documentation doesn't even bother defining the word String.
I'm ready to provide any details and even edit the question to be more visible to the wanna be Microsoft-forsaken batch scripters out there.
Using "tokens=*" to strip off leading spaces this batch uses a counter to create a (pseudo) array secret[]
:: Q:\Test\2018\12\04\SO_53614102.cmd
#Echo off
set Cnt=0
FOR /F "tokens=*" %%A IN (
'findstr /R /C:"secret.\"" secret_file.txt'
) DO (
set /a Cnt+=1
call Set Secret[%%Cnt%%]=%%A
)
Set Secret[
Sample output:
> SO_53614102.cmd
Secret[1]=A very intersting line = "secret1";
Secret[2]=A very intersting line = "secret2";
Secret[3]=A very intersting line = "secret3";
As variables in a (code block) are expanded at parse time,
delayed expansion is requiered (here through a call and doubled %%)

Batch file how to delete end text in a variable string in a for loop

I am writing some batch code to simplify a process I have of downloading some files, renaming them, and then copying them to replace the old ones. I'm running into an issue where I have a FOR loop read in a list of files from a directory, then try to modify the filenames.
The filenames all have FLY in the name, and I want to remove all text after FLY. I can't use tokens because the filenames are inconsistent in length, have multiple spaces, and wouldn't have a set number of tokens. I can't use substring because there is not a set number of characters after FLY.
I've tried using the examples at SS64 and also read numerous threads on here but nothing really seems to match my situation.
Here's the code snippet, appreciate if someone can tell me where I'm going wrong:
SETLOCAL ENABLEDELAYEDEXPANSION
FOR /F "TOKENS=*" %%A IN ('DIR /B ^"%~DP0VFR^"') DO (
SET FILENAME=%%A
SET REMOVETEXT=!FILENAME:*FLY=!
SET NEWFILENAME=!FILENAME:!REMOVETEXT!=!
ECHO !FILENAME! will be renamed !NEWFILENAME!
)
When I insert echos to see what's going on everything works as expected up until the last SET, where somehow the ending result is !NEWFILENAME! is blank.
Hmm. My results were different from yours.
The " in your dir do not need to be escaped.
The problem with your set statement is that it's interpreted as
SET NEWFILENAME=!FILENAME:! + REMOVETEXT + !=!
and since FILENAME: and = are not existing variables, each will be replaced by nothing yielding "REMOVETEXT", not blank as you claim.
The solution is to use a two-stage evaluation of newname
call SET NEWFILENAME=%%FILENAME:!REMOVETEXT!=%%
which is resolved as
SET NEWFILENAME=%FILENAME:current_value_of_REMOVETEXT=%
in a sub-shell.
After cold brew it occurred to me that I might be going about this all wrong and making it more complicated than it needs to be... I decided to try directly renaming the files with wildcards and that actually worked. Didn't even need the FOR loop.
REN "%~DP0VFR\*FLY*" *FLY
No idea why the first (and overly convoluted) solution I tried didn't work, but this does with a lot less code!

Handling of variables in for-loop of a Windows batch file

I've been investigating this a lot with threads on StackOverflow and the like, but although I feel I'm close to the solution, this problem is giving me headaches.
What I'm trying to do: When a specific external hard drive is connected (distinguished via VolumeSerialNumber over WMIC), the drive letter is found out, and mirroring is done via robocopy. The script is executed via double-click. This is what I have so far:
FOR /F "skip=1" %%i in ('wmic logicaldisk where VolumeSerialNumber^="XXXXXXXX" get deviceid 2^>nul') DO (
SET y=%%i
IF [%y%]==[] GOTO hdmissing
SET "backuphd=%%i"
GOTO endfor
)
:endfor
robocopy "C:\Users\Herbert\Documents" "%backuphd%\Backup\Documents" /MIR
ECHO Backup done
ECHO end
:hdmissing
ECHO Couldn't find external drive
:end
PAUSE
This way, the external HD is never detected (%y% is always an empty string). However, if I execute the script twice in the same console session, everything works as expected. But I want it to work at the first execution.
This is what I've tried so far:
Put SET y=dummy at the beginning of the script. The HD is always found, triggering a backup to C: if the HD is not actually connected (apparently SET y=%%i doesn't alter y?)
Change %y% to !y! - The HD is always found, again
Generation 3,576 of the delayed expansion problem, compounded by a contaminated environment.
There's no setlocal apparent, so y remains set in the environment after the first run - hence the 'later run characteristics different from first run' phenomenon.
Within a block statement (a parenthesised series of statements), the entire block is parsed and then executed. Any %var% within the block will be replaced by that variable's value at the time the block is parsed - before the block is executed - the same thing applies to a FOR ... DO (block).
Hence, IF (something) else (somethingelse) will be executed using the values of %variables% at the time the IF is encountered.
Two common ways to overcome this are 1) to use setlocal enabledelayedexpansion and use !var! in place of %var% to access the changed value of var or 2) to call a subroutine to perform further processing using the changed values.
The key in your case appears to be no setlocal enabledelayeexpansion and !y! - because !y! is just that - a literal string !y! unless delayedexpansion is invoked by the setlocal command.
(having said that,
IF [%%i]==[] GOTO hdmissing
would work just as well, as would
SET "y=%%i"
IF not defined y GOTO hdmissing
because if [not] defined var operates on the run-time value of var. "quoting the set arguments" ensures that any stray trailing spaces on the line are not included in the value assigned
)
As Magoo already pointed out, !y! doesn't work, since I forgot to enable Delayed Expansion. However, enabling it requires you to escape certain characters, which seemed quite irritating and tedious to me. It could be possible to just enclose the command in the FOR-loop with double quotes, as done in the two edits of this question, but I found out after solving it differently.
What I did was moving the block
IF [%y%]==[] GOTO hdmissing
SET "backuphd=%%i"
after :endfor. This way, it's outside of the for loop and %y% gets expanded accordingly.
Mind that this solution works only because I need just one item, and because of the programming flow.
Other useful approaches might be found in this question.
However, if you want to do the same thing: easier (though not as robust) solutions without WMIC might include
Putting a file with a specific name on the hard drive and checking for it with IF EXIST before backing up, hoping the user will not delete it, and the HD is connected to the same drive letter.
Putting the batch script on the drive itself and work with relative path names (optional: put a shortcut on the Desktop and hope again the drive is always under the same letter)

Is it possible to create a sub-script?

Something like: (This is an example)
call :sub
echo comes first.
goto end
:sub (
echo This part
)
:end
Maybe? If so, what would be the proper way to format it?
I'm aware that I can just call .bat files, but I'd prefer to keep this whole thing in one program.
I'd like this to be accessible throughout multiple parts of the program, so a regular call wouldn't suffice because I want the program to go back to wher it left off each time this is called.
Very close.
Using call :label you can indeed call the same batch file as a separate process, as if it were a subscript. Using goto :eof you can return control to the main script, which will continue at the place it was.
It should also work when the subscript just ends, so, your code should actually work, except for the parentheses, which are invalid the way you use them. Just remove them and you script should work. It should echo:
This part
comes first.

Variable in variable batch

Alright, I need help here. I have done this before where you have variable1 (let's say it's eat1=apple), variable2 (this is eat2=orange), and variable3 (appaleorange=apple and orange). I need it to do this:
echo Apple:%eat1%
echo Orange:%eat2%
echo Apple & Orange:%eat1%%eat2%
Now, you can see my problem. That above script wouldn't show the word and, only appleorange. That isn't my script and the reason I need this is because I have multiple variables with numbers in them. I have done this before and I forgot how... I know you can do a call and then multiple %'s.
In this case I want fterm variable to be fterm (not sure how to have it in there and not be a variable) and stermnum as a number that will be changed often on other parts of the script.
My code:
set stermnum=1
call set exsternum=%%fterm%%stermnum%%%
echo Selected term:%stermnum% ^(%exsternum%^)
Does anyone know what to do?
Thanks and sorry it was long :P
~Edit:I found it out... If it helps anyone I did:
call set exsternum=%%fterm%stermnum%%
Sorry for posting this even though I figured it out so fast
The OP appended a solution to the question, but it does not relate to the original question scenario, and it still has a bug.
Here is the OP's solution in terms of the original scenario:
set "eat1=apple"
set "eat2=orange"
set "appleorange=apple and orange"
call echo %%%eat1%%eat2%%%
For the actual code, I believe the OP wants to access an array of variables named fterm1, fterm2, fterm3, etc. And the number suffix is in a variable named stermnum.
call set exsternum=%%fterm%stermnum%%%
If fterm is itself a variable containing the base name of the array, then the solution becomes:
call set exsternum=%%%fterm%%stermnum%%%
But CALL is inefficient - Probably not noticeable with a single CALL, but it becomes painfully slow if executed thousands of times in a loop.
There is a much faster solution using delayed expansion. Delayed expansion must be enabled prior to being used.
Original scenario:
setlocal enableDelayedExpansion
set "eat1=apple"
set "eat2=orange"
set "appleorange=apple and orange"
echo !%eat1%%eat2%!
Actual code, interpretation 1:
setlocal enableDelayedExpansion
REM additonal code ...
set exsternum=!fterm%stermnum%!
Actual code, interpretation 2:
setlocal enableDelayedExpansion
REM additonal code ...
set exsternum=!%fterm%%stermnum%!

Resources