Batch - Inner For-Loop token parameter does not accept delayed Expanded variable within Outer For Loop [duplicate] - windows

This question already has answers here:
Issue with *For loop* and *delayed expansion*
(2 answers)
Closed 4 months ago.
I have numerous mp4 files inside a folder with can take the form below
Dancing_2022-10-30_00-05-32_015_Paris.mp4
Eating_in_the_Restaurant_2022-07-03_04-21-25_497_London.mp4
My goal is to create two folders and move the respective files
I want the last underscore (_) delimited item (in this case Paris and London) extracted to create the first folders and then extract the date part (2022-10-30 and 2022-07-03) and create the subfolder. Then move the files accordingly.
My biggest challenge is to extract the tokens. Usually does allows tokens to be extracted from left to right but this assumes the file names are consistent. However in my case the files names are not consistent. There is some consistency from right to left with regards to the placement of the _ delimited parts I want to extract.
I have a draft batch script but the issue I have is that CMD does not allow delayed expanded variable to be passed to the inner For-Loop within the Outer For loop. Any clues to the solution. Thanks
This is my draft batch script.
#echo off
Setlocal EnableDelayedExpansion
for %%F in ("*.mp4") do (
set file=%%~nF
echo !file!
set files="!file:_=" "!"
echo file=!files!
set cnt=-4
for %%a in (!files!) do set /a cnt+=1
echo !cnt!
for /F "tokens=!cnt!* delims=_" %%a in ("!file!") do set output=%%b
echo Output=%output%
for /F "tokens=1-4 delims=_" %%a in ("!output!") do echo %%d
)
endlocal
pause

#ECHO OFF
SETLOCAL ENABLEDELAYEDEXPANSION
rem The following setting for the directory is a name
rem that I use for testing and deliberately includes spaces to make sure
rem that the process works using such names. These will need to be changed to suit your situation.
SET "sourcedir=u:\your files"
FOR %%e IN ("%sourcedir%\*.mp4") DO (
SET "fname=%%~ne"
SET "fname=!fname:_=,!"
SET "fname=!fname: =#!"
FOR %%y IN (!fname!) DO SET "fdate=!ftime!"&SET "ftime=!fmsec!"&SET "fmsec=!flocation!"&SET "flocation=%%y"
ECHO MD "!flocation:#= !\!fdate!"
ECHO MOVE "%%e" "!flocation:#= !\!fdate!\"
)
GOTO :EOF
Always verify against a test directory before applying to real data.
The required MD commands are merely ECHOed for testing purposes. After you've verified that the commands are correct, change ECHO MD to MD to actually create the directories. Append 2>nul to suppress error messages (eg. when the directory already exists)
The required MOVE commands are merely ECHOed for testing purposes. After you've verified that the commands are correct, change ECHO MOVE to MOVE to actually move the files. Append >nul to suppress report messages (eg. 1 file moved)
Relatively easy to follow. Foe each filename, take the name part. replace each _ with , and each Space with #
The result in fname would thus be a comma-separated list of elements, so process each one, cascading them through the variables.
Finally, replace the # with Space when creating the subdirectory and filename.
Note that the switch between Space and # is intended to take care of places like New York and Los Angeles
== Supplemental == in response to comment:
FOR %%e IN ("%sourcedir%\*.mp4") DO (
SET "fname=%%~ne"
SET "fname=!fname:_=,!"
SET "fname=!fname: =#!"
FOR %%y IN (!fname!) DO SET "fdate=!ftime!"&SET "ftime=!fmsec!"&SET "fmsec=!flocation!"&SET "flocation=%%y"
SET "fname=!fname:,=_!"
CALL SET "fname=%%fname:_!fdate!_!Ftime!_!fmsec!_!flocation!=%%
ECHO MD "!flocation:#= !\!fname!\!fdate!"
ECHO MOVE "%%e" "!flocation:#= !\!fname!\!fdate!\"
ECHO MD "!flocation:#= !\!fname:_= !\!fdate!"
ECHO MOVE "%%e" "!flocation:#= !\!fname:_= !\!fdate!\"
)
After locating the required parts, replace each , with _ in fname, then replace the part from the date onwards with nothing, yielding the activity in fname.
Two sets of reports generated now - one for activity with underscores, the second with spaces.

Related

Batch Create Several Folders Based on Multiple Filenames, and Move Multiple Related Files to The Created Folders

Every week, one of my co-workers has had to go through a folder with hundreds of demuxed video and audio files, rename each one individually for a specific city TV station and then sort them into folders based on the name of the city. I've created a .bat file to rename them all for him, and now I'd like to create a .bat file that creates new directories based on the filenames, and places the corresponding files into the new folders. I copied a few of the files to test with.
So the end result will be a "Houston" folder with all it's corresponding files, a "Compton" folder with it's files, a "Moline" folder, etc, etc... for every city, up to around 200 cities, and we're only getting more.
He's currently searching "Houston", cutting all the files that come up, creating a new folder manually, naming it "Houston" and pasting all the files into his new folder. FOR EVERY CITY. 200 TIMES. And it takes hours.
The files are ALWAYS named with this system: X### Random City, ST
With my little wee programming knowledge, I'm supposing that the script could detect all the characters after the first space, and before the comma, copy those characters (Random City), create a new folder, name it the copied characters (Random City) then move any files containing "Random City" in their filename into the newly created folder. The end result would be as such, just with a lot more folders.
Is there anyone more advanced than me who could explain the best way to to this?
I apologize in advance if I'm in the wrong place or not savvy enough. Cheers!
UPDATE: I messed around, learned about tokens and delimiters, variables etc. Here is what I have which works amazingly, except I'm not sure how to remove the comma at the end of the city name. I'm using space as the delimiter, which makes the text chunks the tokens if I understand correctly, including my comma, using tokens=2. Another problem that arises; Say there's a city with two text chunks (tokens) eg. San Fransisco, Baton Rouge. How could I grab both of them, using the comma as my stopping point? My code is below.
#echo off
setlocal enabledelayedexpansion
for %%A in (*.m2v *.mpa) do (
echo file found %%A
for /f "delims=" %%B in ("%%A") do set fname=%%~nB
for /f "delims=" %%C in ("%%A") do set fextn=%%~xC
for /f "tokens=2* delims= " %%D in ("!fname!") do set folname=%%D
echo folder name !folname!
if not exist "!folname!" (
echo Folder !folname! doesn't exist, creating
md "!folname!"
) else (
echo Folder !folname! exists
)
echo Moving file %%A to folder !folname!
move "%%A" "!folname!"
)
echo Finished
pause
UPDATE 2: I found a meh workaround to get rid of the comma, by adding it as a delimiter, but I'm still trying to wrap my head around the 2 word cities. My Baton Rouge and San Fransisco folders are being named respectively, "Baton" and "San". Here is my code so far, I'll update if I find a better way.
setlocal enabledelayedexpansion
for %%A in (*.m2v *.mpa) do (
echo file found %%A
for /f "delims=" %%B in ("%%A") do set fname=%%~nB
for /f "delims=" %%C in ("%%A") do set fextn=%%~xC
for /f "delims=," %%B in ("%%A") do set fname=%%~nB
for /f "tokens=2* delims= " %%D in ("!fname!") do set folname=%%D
echo folder name !folname!
if not exist "!folname!" (
echo Folder !folname! doesn't exist, creating
md "!folname!"
) else (
echo Folder !folname! exists
)
echo Moving file %%A to folder !folname!
move "%%A" "!folname!"
)
echo Finished
pause
UPDATE 3
Here is my code which worked. However, if the number of characters in your filename prefixes/suffixes changes, it will screw things up and you'll have to edit code.
#ECHO OFF
SETLOCAL enabledelayedexpansion
FOR %%A in (*.m2v *.mpa) do (
ECHO file found %%A
FOR /F "delims=" %%B in ("%%A") do set fname=%%~nB
SET folname=!fname:~5,-4!
ECHO folder name !folname!
if not exist "!folname!" (
ECHO Folder !folname! doesn't exist, creating
MD "!folname!"
) else (
ECHO Folder !folname! exists
)
ECHO Moving file %%A to folder !folname!
MOVE "%%A" "!folname!"
)
ECHO Finished
PAUSE
Using the SET folname=!fname:~5,-4!allows me to trim the M373 prefix, 5 characters in, and the , TX suffix, 4 characters in, removing the comma and salvaging the city name, regardless of how long it is, or how many words it is (eg. West Palm Beach, FL) . Antares mentioned this solution in his answer which worked like a charm.
BUT IT ALSO MADE ME THINK
If the number of characters in the prefix changes, which is likely, I'll either have to edit the batch file every time, or create a specific batch file for each circumstance. Not terrible, but not great either. So I went with Michael Heath's answer which works flawlessly. I'm not smart enough yet to know exactly why, but I'm gonna dissect it and find out. I have a lot of learning to do. Thanks, everyone!
#echo off
setlocal
rem A=Fullpath, B=Name before comma, C=B prefix, D=B without prefix.
for /f "delims=" %%A in ('dir /b *.m2v *.mpa') do (
for /f "delims=," %%B in ("%%~nA") do (
for /f "tokens=1,*" %%C in ("%%~B") do (
if not exist "%%~D\" (
echo Folder "%%~D" doesn't exist, creating
md "%%~D"
)
if exist "%%~D\" (
echo Moving file "%%~A" to folder "%%~D\"
move /y "%%~A" "%%~D\"
) else echo Folder "%%~D\" doesn't exist
)
)
)
echo Finished
pause
3 for loops to get the tokens needed.
1st will get the fullpath.
2nd to get the name before the comma.
3rd to get the name without the prefix.
In the nested for loops check if folder exists, create it if not. Then if folder exists, move file inside.
Here is my code which worked. However, if the number of characters in your filename prefixes/suffixes changes, it will screw things up and you'll have to edit code.
#ECHO OFF
SETLOCAL enabledelayedexpansion
FOR %%A in (*.m2v *.mpa) do (
ECHO file found %%A
FOR /F "delims=" %%B in ("%%A") do set fname=%%~nB
SET folname=!fname:~5,-4!
ECHO folder name !folname!
if not exist "!folname!" (
ECHO Folder !folname! doesn't exist, creating
MD "!folname!"
) else (
ECHO Folder !folname! exists
)
ECHO Moving file %%A to folder !folname!
MOVE "%%A" "!folname!"
)
ECHO Finished
PAUSE
Using the SET folname=!fname:~5,-4!allows me to trim the M373 prefix, 5 characters in, and the , TX suffix, 4 characters in, removing the comma and salvaging the city name, regardless of how long it is, or how many words it is (eg. West Palm Beach, FL) . Antares mentioned this solution in his answer which worked like a charm.
BUT IT ALSO MADE ME THINK
If the number of characters in the prefix changes, which is likely, I'll either have to edit the batch file every time, or create a specific batch file for each circumstance. Not terrible, but not great either. So I went with Michael Heath's answer which works flawlessly. I'm not smart enough yet to know exactly why, but I'm gonna dissect it and find out. I have a lot of learning to do. Thanks, everyone!
Before Image
After Image
I can give you some hints. If you have a more specific problem case, feel free to update your question again.
You can cut the last X characters of a String like this: %variablename:~0,-X%
If you know the variables with the city parts, e.g. %%D and %%E or something, you can concatenate them again like this md "%%D %%E". However, this works just for a fixed number of tokens, like the two here.
You can store this concatenations in an own variable, if you need the result outside of your for-loop. Use set myVariable=%%D %%E for example, and show it with %myVariable% or !myVariable! (when delayed expansion is needed), for example md "%myVariable%".
A nifty workaround: if there are only a small number of "special cities" to take into consideration, then you could just add some rename commands at the end of your script, like rename San "San Francisco", rename Baton "Baton Rouge", etc. Will not work well, if there are more "San" cities (e.g. "San Bernadino"), because this cannot be distinguished anymore. But in this case, the copying into separate folders would already fail as well.
In your script you make a check for an existing folder. I think you can omit that. md or mkdir either create that directory or do nothing if it exists. Well, they do print a message to the console, which can be ignored. If you do not want to see them, redirect the error message stream to nul like this md myFolder 2>nul. This will swallow any error messages, but it is unlikely that you get any other error message than that in your scenario.
You could simplify your approach like this: I reckon your file renaming works well. Your "copy" script could be just a list of commands which are stated explicitly (and also could be edited fairly quickly if new cities are to be considered).
Set the batch file up like this with entries for each city:
#echo off
mkdir "Moline"
copy "*Moline*.*" "Moline"
mkdir "San Francisco"
copy "*San Francisco*.*" "San Francisco"
...
echo done.
Side effect is, that the folders for each city will be created, not just those where files are copied into. May be it suits your needs anyhow.
Also, I would like to give you pointers to sources of help/documentation:
On the command line you can get extensive help by executing the commands used in your batch file and appending /?. For example set /? gives you a lot of useful things you can do with variables/String manipulations.
Try: for /?, if /? (regarding errorlevel for example), maybe call /?, goto /?, and others.
A very good source for all the command line commands is https://www.ss64.com. This site provides extensive help even for PowerShell and Linux Bash and others. Relevant to you in this case would be CMD, direct link: https://ss64.com/nt/
Edit/Update:
Check out symbol replacement on the set command to build something like
if "%myVariable:~0,1%" == "M" (set myvariable=%myvariable:~1%)
The first part "cuts" the first character and checks, if it is an M and if so, it keeps everything except the first character. With this you could make your filenames even out to process them inside your batch file.
You can also "remove" a letter or substring with %myVariable:, TX=% which would replace any ", TX" occurance with "nothing" for example.
Oh, this could also help to remove any spaces in the filename. Then you could extract the "SanFrancisco" without a spaces problem ;) The folder name would be without space though. This could be resolved with further rename commands at the end.
Here's an alternative method, just for the sake of variety:
#Echo Off
SetLocal DisableDelayedExpansion
Set "SourceDir=.\Batch Rename\BACKUP"
If Exist "%SourceDir%\" For /F "EOL=|Delims=" %%G In (
'%__AppDir__%where.exe "%SourceDir%":"*, ??.m??" 2^>NUL'
)Do (Set "FileBaseName=%%~nG"&SetLocal EnableDelayedExpansion
For /F "EOL=|Delims=," %%H In ("!FileBaseName:* =!")Do (EndLocal
%__AppDir__%Robocopy.exe "%%~dpG." "%%~dpG%%H" "%%~nxG" /Mov>NUL))
This method filters your files with the where command. It selects for moving, only file names which end with a comma, followed by a space, followed by two letters, followed by a three letter extension beginning with the character m. It moves the files, automatically creating the destination directories if they do not exist, using the robocopy command. It uses only two for loops, the second of which, isolates the string between the first space and the next comma.
I have made it so that the script can be located anywhere, not necessarily in the directory with the files. This location is set on line 3 of the script, If you wish to modify it, please ensure that your location remains between the = and the closing double-quote, ", and does not end with a trailing back-slash, \. It is currently set to a relative directory, (based upon that visible in your screen-shot), but you could obviously use an absolute path too, e.g. Set "SourceDir=C:\Users\UserName\Videos". If you wish to keep the script in the same directory as the files to be moved, change it to read Set "SourceDir=." and just double-click it to run.
When I had to develop a script for the task at hand I would probably implement a few safety features in order to not move wrong files. Your sample data show pairs of .m2v and .mpa files, but I would likely not consider that as granted. Also would I not rely on a fixed-length prefix. Finally, I would perhaps also account for lit's comment.
So here is my attempt (see all the explanatory rem-remarks in the code):
#echo off
setlocal EnableExtensions DisableDelayedExpansion
rem // Define constants here:
set "_ROOT=%~dp0." & rem // (root directory containing the files to be processed)
set "_MASK=M??? *, ??.m2v" & rem // (mask to find the files to be processed)
set _EXTS=".m2v" ".mpa" & rem /* (list of extensions that must all be present;
rem extensions are not checked if this is empty;
rem `_MASK` should then be changed to end with `.m*`) */
set "_FILT=^M[0-9][0-9][0-9] [^,][^,]*, [A-Z][A-Z]\.[^\.][^\.]*$"
rem // (additional filter to find files; deactivate by `.*`)
set "_SEPS=," & rem /* (defines (a) separator character(s) to derive the
rem sub-directory; leave it blank to use full name) */
rem // Change into the root directory:
pushd "%_ROOT%" && (
rem /* Loop through all files matching the mask as well as the additional filter;
rem if the post-filtering is not needed, remove `^|` and everything behind: */
for /F "delims= eol=|" %%F in ('
dir /B /A:-D-H-S "%_MASK%" ^| findstr /I /R /C:"%_FILT%"
') do (
rem // Get the portion in front of the `,` of the file name:
for /F "delims=%_SEPS% eol=|" %%G in ("%%~nF") do (
rem // Split that portion at the first space to get the city name:
for /F "tokens=1* eol=|" %%H in ("%%G") do (
rem // initialise flag that indicates whether to move the current file:
set "FLAG=#"
rem // Skip the following checks if there are no extensions defined:
if defined _EXTS (
rem // Loop through the extensions in the list:
for %%E in (%_EXTS%) do (
rem /* Reset flag if file with current name and iterated extension
rem cannot be found; this ensures that files with all listed
rem extensions do exist, otherwise no files are moved: */
if not exist "%%~nF%%~E" set "FLAG="
rem /* Reset flag if file with current name and iterated extension
rem is actually a directory (though this is very unlikely): */
rem if exist "%%~nF%%~E\*" set "FLAG="
rem /* Reset flag if file with current name and iterated extension
rem is already located in the target sub-directory: */
if exist "%%I\%%~nF%%~E" set "FLAG="
)
)
rem // Do the following steps only if the flag has not been reset:
if defined FLAG (
rem // Create target sub-directory (suppress potential error message):
2> nul md "%%I"
rem // Check if there are dedicated extensions defined:
if defined _EXTS (
rem // Loop through the extensions in the list again:
for %%E in (%_EXTS%) do (
rem /* Move file with current name and iterated extension;
rem nothing is overwritten due to the preceding checks: */
> nul move "%%~nF%%~E" "%%I\%%~nF%%~E"
)
) else (
rem /* Empty list of extensions, hence just move the current file;
rem if you do want to overwrite, remove the `if exist´ part: */
if not exist "%%I\%%F" > nul move /Y "%%F" "%%I\%%F"
)
)
)
)
)
rem // Return from root directory:
popd
)
endlocal
exit /B
The values in the Define constants here: section at the top of the script are defined to suit your sample data, but they can easily be adapted there to configure the script at one place:
_ROOT: points to the directory where your input files are; %~dp0. points to the parent directory of the script, but you may of course specify any other absolute directory path here;
_MASK: is a file pattern that matches one file per pair (only .m2v files, others are covered by _EXTS); M??? matches the four-character prefix, but you can change it to M?*, for instance, to also match prefixes like M1 or M9999; if you do so, however, also edit _FILT accordingly;
_EXTS: defines a list of extensions that all must be present; that means for a certain base file name (like M372 Houston, TX, there must exist a file per each given extension, hence M372 Houston, TX.m2v and M372 Houston, TX.mpa in our situation, otherwise these files are not going to be moved; if you do not care if such a pair is complete or not, simply state set "_EXTS=" (so clear it) and change the extension of _MASK from .m2v to .m*, so all files with an extension beginning with .m are moved;
_FILT: constitutes an additional filter for file names in order to exclude wrong files; this currently also reflects a four-character prefix, but if this is not always the case, just change M[0-9][0-9][0-9] to M[0-9]*; if you do not want to filter, set this to .*, so it matches everything;
_SEPS: defines the character(s) to split the base file name in order to derive the respective sub-directory, so everything ending before that character and beginning after the first SPACE is the resulting sub-directory name; if you do not define a character here, the whole remaining base file name (so everything after the first SPACE until but not including the (last) .) is taken;

Windows batch file copying/renaming files

I've been working on creating a batch file which moves files from one dir to another dir and if the filename already exists rename it then move it over.
I'm really new to creating batch files so heres what I have so far
set temp=C:\Users\Daniel\Desktop\a\a1
set dir=C:\Users\Daniel\Desktop\a\
set /a "counter=0"
set "duplicate=-copy^("
set "bracket=^)"
if exist "%temp%" ( ^
for %%i in (%temp%\*) ^
do ^
if exist "%dir%\%%~ni%%~xi" ( call :checkFileName %%~ni %%~xi) ^
ELSE ( move %temp%\%%~ni%%~xi %dir% ) )^
ELSE ( echo doesnt exist)
:checkFileName
echo test
set fileName=%1
set fileExtenstion=%2
set /a "counter+=1
rem Do whatever you want here over the files of this subdir, for example:
if exist %dir%%fileName%%duplicate%%counter%%bracket%%fileExtenstion% ( IF defined %1 (
IF defined %2 (call :checkFileName %1 %2 )) ) ELSE (ren %temp%\%fileName%%fileExtenstion% %fileName%%duplicate%%counter%%bracket%%fileExtenstion% )
timeout 30
goto :eof
:increment
set /a "counter+=1"
goto :eof
I've no idea to increment a var before calling my checkFileName function. I think recursively calling the same function is the right idea but I'm a bit rusty with the commands/syntax as I only started this on friday.
Any advice or pointers would be appreciated. (If you know any useful links/books that are worth a look let me know!)
timeout 600
#ECHO OFF
SETLOCAL
set "tempdir=C:\Users\Daniel\Desktop\a\a1"
set "dir=C:\Users\Daniel\Desktop\a"
set "tempdir=U:\sourcedir\t w o"
set "dir=U:\destdir"
set "duplicate=-copy("
set "bracket=)"
if exist "%tempdir%" (
for %%i in ("%tempdir%\*") do (
if exist "%dir%\%%~nxi" ( call :checkFileName "%%~ni" "%%~xi"
) ELSE (
move "%tempdir%\%%~nxi" "%dir%" >nul
)
)
) ELSE (
echo doesnt EXIST
)
GOTO :eof
:checkFileName
set "fileName=%~1"
set "fileExtenstion=%~2"
set /a counter=0
:nexttry
set /a counter+=1
rem Do whatever you want here over the files of this subdir, for example:
if exist "%dir%\%fileName%%duplicate%%counter%%bracket%%fileExtenstion%" GOTO nexttry
move "%tempdir%\%fileName%%fileExtenstion%" "%dir%\%fileName%%duplicate%%counter%%bracket%%fileExtenstion%" >nul
goto :eof
Here's a revised version. I'll explain the changes I've made:
#echo off turns off command-echoing
setlocal ensures any changes made to the environment are backed-out when the procedure ends.
I've added to extra sets to re-set the directories to suit my system. You'd need to delete these two lines for yours.
temp is a special name which points to a temporary directory. One of quite a few. Best not to use that particular name - replaced with tempdir
set when used for a numeric set doesn't require quotes. In a string-set, the syntax set "var=value" is used to ensure that trailing spaces on the command-line are not included into the value assigned (which can cause chaos - spaces are sort of - invisible.) Note that in a string set, spaces on both sides of the = are significant...
I prefer to assign directorynames into variables without the trailing \. This allows the value to be extended with the least gymnastics. Personal preference - but you used it both ways...
The carets are not required before ( and are only required before ) where the syntax would close an open parenthesis (ie. in a parenthesised statement-sequence as may occur in an if, else or do.) Used arbitrarily, this can lead to stray literal carets in filenames, for instance.
carets at end-of-line is a valid but easily-lost and a little-used technique. The rule for breaking statements over multiple lines is crudely, keep do, if or else on the same physical line as its ( and else on the same physical line as the closing-parenthesis that precedes it. Then no eol-caret is required.
Batch simply charges on through statements. It has no concept of the end of a procedure and needs to be told when the procedure ends. This can be done with a goto :eof statement (which jumps to the physical end-of-file) or an exit /b statement (which returns from a subroutine, optionally setting errorlevel. goto :eof effectively does the smae thing in most circumstances and is way more common.)
%%~nxi means the name-and-extension of the file %%i. Of course, it's quite legal to use %%~ni and its counterpart individually, but it's not necessary. Note however that these parts should be despatched in "quotes" to the subroutine because each part may contain spaces. "quotes"make a spaces-containing-string appear as one string with spaces rather than a series of strings.
>nul redirects the move command's report "1 file(s) moved" to the bit-bucket.
Setting the two variables within checkfilename should be done after removing the quotes applied in the call - that's the purpose of the ~ before the parameter-number.
counter can be set to zero, then incremented.
If the proposed new filename exists, then simply increment the number and try again until you hit a name that doesn't exist. Yes - counter will run out eventually. It tops out at 2**31-1. Might take a while...
Note the use of quotes in the if exist and move. This is to guard against spaces in file/directorynames. The same goes for the for %%i in ("%tempdir%\*") used earlier...you may notice that in my testing, I used (deliberately) a directoryname that contained spaces. As it happens, the filenames I used also had spaces in them.
One last warning - There is no doubt that some odd filenames may choke on these procedures, but they should be few and far between. Filenames containing carets may be a problem, for instance.
Welcome to batch!
Unless this is a learning project, I recommend you study the XCOPY command.

using for loop to input multiple data

I am new to programming. Here is my dilemma. I have to replace multiple files in multiple locations across multiple computers.
I have written a bat script where I am defining all the variables and calling a txt file with appropriate information. For example -testing.txt has the values
Apple, Potato,Beef
Apple, Potato,Pork
The logic I am applying is as follows: I am using this txt file for reading and then going to each location to change the file
set Path=%Path%;c:\Tools\UnxUtils\usr\local\wbin
SET SORC=C:\tools\logosource\NEWImages\ApiSite\Content
for /F "usebackq delims=, tokens=1-3" %%a in (C:\tools\xxxx\testing.txt) do (
SET HOSTNAME=%%a
SET CUSTNAME=%%c
SET STYPE=%%b
SET DEST=\\%HOSTNAME%\c$\Documents and Settings\blahblah\My Documents\%CUSTNAME%\%STYPE%\goodman\
echo HOSTNAME is %HOSTNAME%
echo CUSTNAME is %CUSTNAME%
echo STYPE is %STYPE%
echo DEST is %DEST%
echo SORC is %SORC%
)
copy "%DEST%\ApiSite\Content\images\michael.gif" "%DEST%"
copy /b /y "%SORC%\images\george.gif" "%DEST%\ApiSite\Content\images\michael.gif"
goto End
:Error
ECHO Error! You must pass in a servername
goto End
:End
The problem is that my loop is only reading the last line my txt file. ie. it reads "Apple, Potato,Pork" and sets the DEST TO THAT VALUE.
What i really want is to read line 1 (Apple, Potato,Beef) set the DEST using these parameters and change the files, then go back and read the second line (Apple, Potato,Pork) and set the DEST using these parameters and change the files.
Actually your code is reading every line in the file, but your logic is wrong.
Your main problem is you want to do the COPY statements for each line, but you have the COPY statements outside the loop. Of course they will only get executed once, and the values used will be the values that were set by the last line in the file. The solution is to move the COPY statements inside the loop.
Your other problem is you are attempting to set a variable within a parenthesized block, and then access the value using %var% - that cannot work because the expansion occurs when the statement is parsed and the entire block is parsed once before any lines are read. You could solve that problem by using delayed expansion, (type HELP SET from the command line prompt for more information about delayed expansion). But there really isn't any need to save the values in variables. Simply use the FOR variables directly. Because the DEST is used multiple times, I used an additional FOR loop to define a %%d variable that contains the DEST value. The ~ removes the quotes that the FOR loop added.
Also, you are ending the script by using GOTO END and defining an :END label at the end of file. That works, but there is an implicit :EOF label at the end of every script. You can simply use GOTO :EOF without defining a label. The other option is to use EXIT /B
You have an :ERROR routine that is not being called - I presume you have additional code that you are not showing.
set Path=%Path%;c:\Tools\UnxUtils\usr\local\wbin
SET "SORC=C:\tools\logosource\NEWImages\ApiSite\Content"
for /F "usebackq delims=, tokens=1-3" %%a in (C:\tools\xxxx\testing.txt) do (
for %%d in (
"\\%%a\c$\Documents and Settings\blahblah\My Documents\%%b\%%b\goodman\"
) do (
echo HOSTNAME=%%a
echo CUSTNAME=%%c
echo STYPE=%%b
echo DEST=%%~d
echo SORC is %SORC%
copy "%%~d\ApiSite\Content\images\michael.gif" "%%~d"
copy /b /y "%SORC%\images\george.gif" "%%~d\ApiSite\Content\images\michael.gif"
)
)
exit /b
:Error
ECHO Error! You must pass in a servername
exit /b

batch script variable unset in for loop has no effect

Below is my script. I am trying to look into folders one level below and pick out only those folders, hence the ~-9 which extracts the last 9 chars from the path. But the set var= does not unset the variable because the output comes back with the same folder name repeated # times. Also batch doesn't allow me to do this extract trick directly on %%i, hence the need for the local variable.
How do I clear this variable so that it takes the new value in the next iteration?
#echo off
for /d %%i in (%1\*) do (
set var=%%i
echo %var:~-9%
set "var="
)
http://judago.webs.com/variablecatches.htm has an explanation for my problem. The magic lines were setlocal enabledelayedexpansion and calling var as echo !var:~-9!. ! vs % ...wow! cmd still amazes me.
You found the source of your problem, as well as the solution - delayed expansion.
But using FOR while delayed expansion is enabled can cause problems if any of the filenames contain the ! character. The expansion of the for variable %%i will be corrupted if the value contains ! and delayed expansion is enabled. This is not a frequent problem, but it happens.
The solution is to toggle delayed expansion on and off within the loop
#echo off
setlocal disableDelayedExpansion
for /d %%i in (%1\*) do (
set var=%%i
setlocal enableDelayedExpansion
echo !var:~-9!
endlocal
)
I'm also wondering what you mean by "I am trying to look into folders one level below and pick out only those folders, hence the ~-9 which extracts the last 9 chars from the path". I suspect your are trying to get the name of the child folder, without the leading path information. If that is so, then using the substring operation is not a good solution because the length of folder names varies.
There is a very simple method to get the name of the folder without the leading path info:
for /d %%i in (%1\*) do echo %%~nxi

Drag and drop batch file for multiple files?

I wrote a batch file to use PngCrush to optimize a .png image when I drag and drop it onto the batch file.
In the what's next section, I wrote about what I thought would be a good upgrade to the batch file.
My question is: is it possible to create a batch file like I did in the post, but capable of optimizing multiple images at once? Drag and drop multiple .png files on it? (and have the output be something like new.png, new(1).png, new(2).png, etc...
Yes, of course this is possible. When dragging multiple files on a batch file you get the list of dropped files as a space-separated list. You can verify this with the simple following batch:
#echo %*
#pause
Now you have two options:
PngCrush can already handle multiple file names given to it on the command line. In this case all you'd have to do would be to pass %* to PngCrush instead of just %1 (as you probably do now):
#pngcrush %*
%* contains all arguments to the batch file, so this is a convenient way to pass all arguments to another program. Careful with files named like PngCrush options, though. UNIX geeks will know that problem :-)
After reading your post describing your technique, however, this won't work properly as you are writing the compressed file to new.png. A bad idea if you're handling multiple files at once as there can be only one new.png :-). But I just tried out that PngCrush handles multiple files just well, so if you don't mind an in-place update of the files then putting
#pngcrush -reduce -brute %*
into your batch will do the job (following your original article).
PngCrush will not handle multiple files or you want to write each image to a new file after compression. In this case you stick with your "one file at a time" routine but you loop over the input arguments. In this case, it's easiest to just build a little loop and shift the arguments each time you process one:
#echo off
if [%1]==[] goto :eof
:loop
pngcrush -reduce -brute %1 "%~dpn1_new%~x1"
shift
if not [%1]==[] goto loop
What we're doing here is simple: First we skip the entire batch if it is run without arguments, then we define a label to jump to: loop. Inside we simply run PngCrush on the first argument, giving the compressed file a new name. You may want to read up on the path dissection syntax I used here in help call. Basically what I'm doing here is name the file exactly as before; I just stick "_new" to the end of the file name (before the extension). %~dpn1 expands to drive, path and file name (without extension), while %~x1 expands to the extension, including the dot.
ETA: Eep, I just read your desired output with new.png, new(1).png, etc. In this case we don't need any fancy path dissections but we have other problems to care about.
The easiest way would probably be to just start a counter at 0 before we process the first file and increment it each time we process another one:
#echo off
if [%1]==[] goto :eof
set n=0
:loop
if %n%==0 (
pngcrush -reduce -brute %1 new.png
) else (
pngcrush -reduce -brute %1 new^(%n%^).png
)
shift
set /a n+=1
if not [%1]==[] goto loop
%n% is our counter here and we handle the case where n is 0 by writing the result to new.png, instead of new(0).png.
This approach has problems, though. If there are already files named new.png or new(x).png then you will probably clobber them. Not nice. So we have to do something different and check whether we can actually use the file names:
rem check for new.png
if exist new.png (set n=1) else (set n=0 & goto loop)
rem check for numbered new(x).png
:checkloop
if not exist new^(%n%^).png goto loop
set /a n+=1
goto checkloop
The rest of the program stays the same, including the normal loop. But now we start at the first unused file name and avoid overwriting files that are already there.
Feel free to adapt as needed.
To do Drag & Drop in a secure way, isn't so simple with batch.
Dealing with %1, shift or %* could fail, because the explorer is not very smart, while quoting the filenames, only filenames with spaces are quoted.
But files like Cool&stuff.png are not quoted by the explorer so you get a cmdline like
pngCr.bat Cool&stuff.png
So in %1 is only Cool even in %* is only Cool, but after the batch ends, cmd.exe tries to execute a stuff.png (and will fail).
To handle this you could access the parameters with !cmdcmdline! instead of %1 .. %n,
and to bypass a potential error at the end of execution, a simple exit could help.
#echo off
setlocal ENABLEDELAYEDEXPANSION
rem Take the cmd-line, remove all until the first parameter
set "params=!cmdcmdline:~0,-1!"
set "params=!params:*" =!"
set count=0
rem Split the parameters on spaces but respect the quotes
for %%G IN (!params!) do (
set /a count+=1
set "item_!count!=%%~G"
rem echo !count! %%~G
)
rem list the parameters
for /L %%n in (1,1,!count!) DO (
echo %%n #!item_%%n!#
)
pause
REM ** The exit is important, so the cmd.ex doesn't try to execute commands after ampersands
exit
Btw. there is a line limit for drag&drop operations of ~2048 characters, in spite of the "standard" batch line limit of ~8192 characters.
As for each file the complete path is passed, this limit can be reached with few files.
FOR %%A IN (%*) DO (
REM Now your batch file handles %%A instead of %1
REM No need to use SHIFT anymore.
ECHO %%A
)
And to differentiate between dropped files and folders, you can use this:
FOR %%I IN (%*) DO (
ECHO.%%~aI | FIND "d" >NUL
IF ERRORLEVEL 1 (
REM Processing Dropped Files
CALL :_jobF "%%~fI"
) ELSE (
REM Processing Dropped Folders
CALL :_jobD "%%~fI"
)
)
This is a very late answer, Actually I was not aware of this old question and prepared an answer for this similar one where there was a discussion about handling file names with special characters because explorer only quotes file names that contain space(s). Then in the comments on that question I saw a reference to this thread, after that and not to my sureprise I realized that jeb have already covered and explained this matter very well, which is expected of him.
So without any further explanations I will contribute my solution with the main focus to cover more special cases in file names with this ,;!^ characters and also to provide a mechanism to guess if the batch file is directly launched by explorer or not, so the old fashion logic for handling batch file arguments could be used in all cases.
#echo off
setlocal DisableDelayedExpansion
if "%~1" EQU "/DontCheckDrapDrop" (
shift
) else (
call :IsDragDrop && (
call "%~f0" /DontCheckDrapDrop %%#*%%
exit
)
)
:: Process batch file arguments as you normally do
setlocal EnableDelayedExpansion
echo cmdcmdline=!cmdcmdline!
endlocal
echo,
echo %%*=%*
echo,
if defined #* echo #*=%#*%
echo,
echo %%1="%~1"
echo %%2="%~2"
echo %%3="%~3"
echo %%4="%~4"
echo %%5="%~5"
echo %%6="%~6"
echo %%7="%~7"
echo %%8="%~8"
echo %%9="%~9"
pause
exit /b
:: IsDragDrop routine
:: Checks if the batch file is directly lanched through Windows Explorer
:: then Processes batch file arguments which are passed by Drag'n'Drop,
:: rebuilds a safe variant of the arguments list suitable to be passed and processed
:: in a batch script and returns the processed args in the environment variable
:: that is specified by the caller or uses #* as default variable if non is specified.
:: ErrorLevel: 0 - If launched through explorer. 1 - Otherwise (Will not parse arguments)
:IsDragDrop [retVar=#*]
setlocal
set "Esc="
set "ParentDelayIsOff=!"
setlocal DisableDelayedExpansion
if "%~1"=="" (set "ret=#*") else set "ret=%~1"
set "Args="
set "qsub=?"
:: Used for emphasis purposes
set "SPACE= "
setlocal EnableDelayedExpansion
set "cmdline=!cmdcmdline!"
set ^"ExplorerCheck=!cmdline:%SystemRoot%\system32\cmd.exe /c ^""%~f0"=!^"
if "!cmdline!"=="!ExplorerCheck!" (
set ^"ExplorerCheck=!cmdline:"%SystemRoot%\system32\cmd.exe" /c ^""%~f0"=!^"
if "!cmdline!"=="!ExplorerCheck!" exit /b 1
)
set "ExplorerCheck="
set ^"cmdline=!cmdline:*"%~f0"=!^"
set "cmdline=!cmdline:~0,-1!"
if defined cmdline (
if not defined ParentDelayIsOff (
if "!cmdline!" NEQ "!cmdline:*!=!" set "Esc=1"
)
set ^"cmdline=!cmdline:"=%qsub%!"
)
(
endlocal & set "Esc=%Esc%"
for /F "tokens=*" %%A in ("%SPACE% %cmdline%") do (
set "cmdline=%%A"
)
)
if not defined cmdline endlocal & endlocal & set "%ret%=" & exit /b 0
:IsDragDrop.ParseArgs
if "%cmdline:~0,1%"=="%qsub%" (set "dlm=%qsub%") else set "dlm= "
:: Using '%%?' as FOR /F variable to not mess with the file names that contain '%'
for /F "delims=%dlm%" %%? in ("%cmdline%") do (
set ^"Args=%Args% "%%?"^"
setlocal EnableDelayedExpansion
set "cmdline=!cmdline:*%dlm: =%%%?%dlm: =%=!"
)
(
endlocal
for /F "tokens=*" %%A in ("%SPACE% %cmdline%") do (
set "cmdline=%%A"
)
)
if defined cmdline goto :IsDragDrop.ParseArgs
if defined Esc (
set ^"Args=%Args:^=^^%^"
)
if defined Esc (
set ^"Args=%Args:!=^!%^"
)
(
endlocal & endlocal
set ^"%ret%=%Args%^"
exit /b 0
)
OUTPUT with sample files dragged and dropped onto the batch file:
cmdcmdline=C:\Windows\system32\cmd.exe /c ""Q:\DragDrop\DragDrop.cmd" Q:\DragDrop\ab.txt "Q:\DragDrop\c d.txt" Q:\DragDrop\!ab!c.txt "Q:\DragDrop\a b.txt" Q:\DragDrop\a!b.txt Q:\DragDrop\a&b.txt Q:\DragDrop\a(b&^)).txt Q:\DragDrop\a,b;c!d&e^f!!.txt Q:\DragDrop\a;b.txt"
%*=/DontCheckDrapDrop "Q:\DragDrop\ab.txt" "Q:\DragDrop\c d.txt" "Q:\DragDrop\!ab!c.txt" "Q:\DragDrop\a b.txt" "Q:\DragDrop\a!b.txt" "Q:\DragDrop\a&b.txt" "Q:\DragDrop\a(b&^)).txt" "Q:\DragDrop\a,b;c!d&e^f!!.txt" "Q:\DragDrop\a;b.txt"
#*= "Q:\DragDrop\ab.txt" "Q:\DragDrop\c d.txt" "Q:\DragDrop\!ab!c.txt" "Q:\DragDrop\a b.txt" "Q:\DragDrop\a!b.txt" "Q:\DragDrop\a&b.txt" "Q:\DragDrop\a(b&^)).txt" "Q:\DragDrop\a,b;c!d&e^f!!.txt" "Q:\DragDrop\a;b.txt"
%1="Q:\DragDrop\ab.txt"
%2="Q:\DragDrop\c d.txt"
%3="Q:\DragDrop\!ab!c.txt"
%4="Q:\DragDrop\a b.txt"
%5="Q:\DragDrop\a!b.txt"
%6="Q:\DragDrop\a&b.txt"
%7="Q:\DragDrop\a(b&^)).txt"
%8="Q:\DragDrop\a,b;c!d&e^f!!.txt"
%9="Q:\DragDrop\a;b.txt"
In :IsDragDrop routine I specially tried to minimize the assumptions about command line format and spacing between the arguments. The detection (guess) for explorer launch is based on this command line signature %SystemRoot%\system32\cmd.exe /c ""FullPathToBatchFile" Arguments"
So it is very possible to fool the code into thinking it has launched by double click from explorer or by drag'n'drop and that's not an issue and the batch file will function normally.
But with this particular signature it is not possible to intentionally launch batch file this way: %SystemRoot%\system32\cmd.exe /c ""FullPathToBatchFile" Arguments & SomeOtherCommand" and expect that the SomeOtherCommand to be executed, instead it will be merged into the batch file arguments.
You don't need a batch script to optimize multiple PNGs, all you need is the wildcard:
pngcrush -d "crushed" *.png
That will pngcrush all PNGs in the current dir and move them to a sub-dir named "crushed". I would add the -brute flag to likely shave off a few more bytes.
pngcrush -d "crushed" -brute *.png
I'm posting this because it doesn't seem to be well documented or widely known, and because it may be easier for you in the long run than writing and maintaining a drag and drop batch file.

Resources