What does this bat code do?
for /f %%i in ('dir /b Client\Javascript\*_min.js') do (
set n=%%~ni
set t=!n:~0,-4!
cp Client\Javascript\%%i build\Client\Javascript\!t!.js
)
What does %%~ni,~n:~0,-4!,%%i,!t! mean?
Keep in mind that in batch files, you need to escape percentage signs unless you're referring to arguments given to the batch file. Once you remove those, you get
for /f %i in ('dir /b Client\Javascript\*_min.js') do (
set n=%~ni
set t=!n:~0,-4!
cp Client\Javascript\%i build\Client\Javascript\!t!.js
)
%i is the declaration of a variable used to place the current file for has found. %~ni extracts the filename portion of %i. !n:~0,-4! uses delayed expansion to remove the last four characters from %n% (set in the previous line) !t! is simply delayed expansion of the %t% variable set in the previous line.
Delayed expansion is used because otherwise, the variables will be substituted as soon as the line is encountered, and future iterations will not re-expand the variable.
for /f %%i in ('dir /b Client\Javascript\*_min.js') do (
Iterate over every file in the Client\Javascript folder that match "*_min.js". Thedircommand andfor /f` are totally unneeded here, though and only complicate things, especially when file names contain spaces, commas and the like. A more robust and simpler alternative would be
for %%i in (Client\Javascript\*_min.js) do (
But that's just beside the point. People tend to write unelegant batch files sometimes, ignoring the pitfalls and common errors. That's just one example of that.
set n=%%~ni
Creates a variable n, containing the file name (without any directory information or extension) of the file currently processed. We remember that the for statement iterates over every file it finds. With this line starts what it does with those files.
set t=!n:~0,-4!
Creates a second variable, t, containing everything but the last four characters of the file name. This essentially strips away the "_min"
cp Client\Javascript\%%i build\Client\Javascript\!t!.js
Finally, this copies the original file to the directory build\Client\Javascript with the new name, just constructed. So a file like Client\Javascript\foo_min.js will be copied to Client\Javascript\foo.js. The !t! here is just a delayed-evaluated environment variable. More on that below. Here it should suffice that it just inserts the contents of said variable at that point in the line.
Again, bad practice here that will break in numerous interesting ways:
cp is not a command on Windows so this batch will assume cygwin, GNUWin32 or similar things installed. I tend to avoid having too many unneeded dependencies and stick to what Windows provides; in this case the copy command. Two bytes won't kill anyone here, I think.
No quotes are around either argument. Leads to interesting results when spaces start appearing in the file name. Not good, either.
As for why delayed expansion was used (! instead of % surrounding the variables: The for command consists of everything in the block delimited by parentheses here as well. The entire block is parsed at once and normal variable expansion takes place when a line/command is parsed. That would mean that every variable in the block would be evaluated before the loop even runs, leaving just the following:
for /f %%i in ('dir /b Client\Javascript\*_min.js') do (
set n=%%~ni
set t=
cp Client\Javascript\%%i build\Client\Javascript\.js
)
which is certainly not what you want in this case.
Delayed expansion is always needed when creating and using variables in a loop such as this. A workaround not needing delayed expansion would be to offload the loop interior into a subroutine:
for /f %%i in ('dir /b Client\Javascript\*_min.js') do call :process "%%i"
goto :eof
:process
set n=%~n1
set t=%n:0,-4%
copy "Client\Javascript\%~1" "build\Client\Javascript\%t%.js"
goto :eof
Since the subroutine is not a single "block" (something delimited by parentheses) it will be parsed line by line as usual. Therefore it's safe to use normal expansion instead of delayed expansion here.
A complete help for the FOR command can be found on the Microsoft TechNet site. See here for more information on delayed expansion :
// Pseudo code
for each file named *_min.js in the specified directory
n is set to the file name (*_min)
t is set to the file name, excluding the last 4 characters (*)
the file is copied and renamed t.js to the specified directory
%~ni expands to just the filename part of i.
!n:~0,-4! expands to all but the last four characters of n.
In general, help for at the command prompt will give an overview of the multitude of ways for can expand variables these days.
Related
I want to create a batch file able to apply some processing on each JPG file in a folder hierarchy. The following script file works very well for that case (here I only echo the name of each file, but this should be replaced by some more complex statements in the real application):
:VERSION 1
#echo off
set "basefolder=C:\Base"
for /r %basefolder% %%f in (*.jpg) do echo %%f
Actually, I don't want to explore all the folder hierarchy under %basefolder%, but only a given list of subfolders. This modified script is able to deal with that case :
:VERSION 2
#echo off
set "basefolder=C:\Base"
set "subfolders=A B C"
for %%s in (%subfolders%) do (
pushd %basefolder%\%%~s"
for /r %%f in (*.jpg) do echo %%f
popd
)
Is there a solution to remove the pushd/popd pair of statements, to get something closer to the initial script. I thought that one of the following scripts would do the job:
:VERSION 3
#echo off
set "basefolder=C:\Base"
set "subfolders=A B C"
for %%s in (%subfolders%) do (
for /r %basefolder%\%%~s" %%f in (*.jpg) do echo %%f
)
or, using delayed expansion:
:VERSION 4
#echo off
setlocal enabledelayedexpansion
set "basefolder=C:\Base"
set "subfolders=A B C"
for %%s in (%subfolders%) do (
set "folder=%basefolder%\%%~s"
echo !folder!
for /r !folder! %%f in (*.jpg) do echo %%f
)
but none of them is working. When running the second one, the echo !folder! command in the external loop shows C:\Base\A, C:\Base\B and C:\Base\C as expected, but the inner loop doesn't echo any JPG file, so I guess that the recursive for /r command does not run correctly.
What am I doing wrong ?
Final edit after answers :
Thanks to #aschipfl who provided a link to the answer posted by #jeb on another question, quoted below:
The options of FOR, IF and REM are only parsed up to the special character phase. Or better the commands are detected in the special character phase and a different parser is activated then. Therefore it's neither possible to use delayed expansion nor FOR meta-variables in these options.
In other words, my versions 3 and 4 do not work because when defining the root folder of the FOR /R command, neither the %%~s nor the !folder! are correctly expanded by the expression parser. There is no way to change that, as this is a parser limitation. As I said in a comment below: the root folder option in the FOR /R command is basically only syntactic sugar to avoid the use of pushd/popd before and after the command. As this syntactic sugar is incomplete, we have to stick to the original syntax for some specific use cases, as the one presented here. The alternatives proposed by #Gerhard (using a subroutine CALL) or by #Mofi (parsing the result of a DIR command) are working, but they are neither more readable nor more efficient than the simple pushd/popd version I proposed initially.
My Approach for this would be really straight forward:
#echo off
set "basedir=C:\Base"
set "subfolders="A","B","C""
for %%i in (%subfolders%) do for /R "%basedir%" %%a in ("%%~i\*.jpg") do echo %%~fa
The double quotes inside of the subfolders variable is important here, it will ensure that folder names with whitespace are not seen as separators for the folder names. For instance:
set "subfolders="Folder A","Folder B","Folder C""
Edit
#echo off
set "basedir=C:\Base"
set "subfolders="A","B","C""
for %%i in (%subfolders%) do call :work "%%~i"
goto :eof
:work
for /R "%basedir%\%~1" %%a in (*.jpg) do echo %%~fa
It is in general not advisable to assign the value of a loop variable to an environment variable and next use the environment variable unmodified without or with concatenation with other strings being coded in batch file or defined already above the FOR loop within body of a FOR loop. That causes just problems as it requires the usage of delayed expansion which results in files and folders with one or more ! are not correct processed anymore inside body of the FOR loop caused by double parsing of the command line before execution, or command call is used on some command lines, or a subroutine is used called with call which makes the processing of the batch file much slower.
I recommend to use this batch file for the task:
#echo off
setlocal EnableExtensions DisableDelayedExpansion
set "basefolder=C:\Base"
set "subfolders=A B C "Subfolder D" SubfolderE"
for %%I in (%subfolders%) do for /F "delims=" %%J in ('dir "%basefolder%\%%~I\*.jpg" /A-D /B /S 2^>nul') do echo %%J
endlocal
The inner FOR loop starts for each subfolder defined in subfolders in background one more command process with %ComSpec% /c and the DIR command line appended as additional arguments. So executed is with Windows installed to C:\Windows for example for the first subfolder:
C:\Windows\System32\cmd.exe /c dir "C:\Base\A\*.jpg" /A-D /B /S 2>nul
The command DIR searches
in specified directory C:\Base\A and all it subdirectories because of option /S
for files because of option /A-D (attribute not directory) including those with hidden attribute set
matching the pattern *.jpg in long or short file name
and outputs to handle STDOUT of background command process just the matching file names because of option /B (bare format)
with full path because of option /S.
The error message output by DIR on nothing found matching these criteria is redirecting from handle STDERR to device NUL to suppress it.
Read the Microsoft documentation about Using command redirection operators for an explanation of 2>nul. The redirection operator > must be escaped with caret character ^ on FOR command line to be interpreted as literal character when Windows command interpreter processes this command line before executing command FOR which executes the embedded dir command line with using a separate command process started in background.
The output to handle STDOUT of background command process is captured by FOR respectively the command process which is processing the batch file. FOR processes the captured output line by line after started cmd.exe terminated itself. This is very often very important. The list of files to process is already in memory of command process before processing the first file name. This is not the case on using for /R as this results in accessing file system, getting first file name of a non-hidden file matching the wildcard pattern, run all commands in body of FOR and accessing the file system once again to get next file name. The for /R approach is problematic if the commands in body of FOR change a file to process like deleting, moving, modifying, copying it in same folder, or renaming a found file because of the entries in file system changes while for /R is iterating over these entries. That can easily result in some files are skipped or some files are processed more than once and it could result also an endless running loop, especially on FAT file system like FAT32 or exFAT. It is never good to iterate over a list of files on which the list changes on each iteration.
Command FOR on usage of /F ignores empty lines which do not occur here. A non-empty line is split up into substrings using a normal space and a horizontal tab as string delimiters by default. This line splitting behavior is not wanted here as there could be full qualified file names containing anywhere inside full name one or more spaces. For that reason delims= is used to define an empty list of delimiters which disables the line splitting behavior.
FOR with option /F would also ignore lines on which first substring starts with ; which is the default end of line character. This is no problem here because of command DIR was used with option /S and so each file name is output with full path which makes it impossible that any file name starts with ;. So the default eol=; can be kept.
FOR with option /F assigns by default just first substring to specified loop variable as tokens=1 is the default. This default can be kept here as splitting the lines (full file names) into substrings is disabled already with delims= and so there is always the full file name assigned to the loop variable.
This example uses just echo %%I to output the file names with full path. But it is now safe to replace this single command by a command block which does more with the JPEG files because of the list of JPEG files for each specified subfolder tree in base folder is always already completely in memory of command process processing the batch file.
Is there any way to get for /f loop (or anything else) to read a specific line?
Here is the code I have so far, it reads first word of every line.
#echo off
set file=readtest.txt
for /f "tokens=1 delims= " %%A in (%file%) do (echo %%A)
pause
If someone can point me in the right direction, it'd be much appreciated.
Thanks
Additional Information: I want to make a batch file which will rename a TXT file to a string within that TXT file, located at a specific location. I have figured out how to rename files, all I need to learn to do is to retrieve a string (located at a specific location) with in the file which will go into the name of that TXT file.
Since you haven't fully defined what you mean by "a specific location", I'll make some (reasonable, in my opinion) assumptions, though the method I present is equally valid no matter what your definition turns out to be.
You can get arbitrary lines and arbitrary words on that line by using a line counter variable in conjunction with tokens.
Let's assume your text file name can be found as the second argument on the fourth line of the infile.txt file. You can get that with something like:
#setlocal enableextensions enabledelayedexpansion
#echo off
set /a "line = 0"
for /f "tokens=2 delims= " %%a in (infile.txt) do (
set /a "line = line + 1"
if !line!==4 set thing=%%a
)
endlocal & set thing=%thing%
echo %thing%
This actually uses a few "tricks" which warrant further explanation:
the line counter to ensure you only grab what you want from a specific line, though you could change the test !line!==4 into anything you need such as a line beginning with #, the fifth line containing the string xyzzy and so on.
the use of setlocal/endlocal to effectively give you a scope from which variables cannot leak. This is good programming practice even for a language often not normally associated with such things :-)
the use of endlocal & set to bypass that scope so that thing is the only thing that does actually leak (as it should).
the use of delayed expansion and !..! variables to ensure they're correct within the for loop. Without this, the %..% will always be expand to the value they were set to when the for loop started.
Those last two bullet points are actually related. %..% variables are expanded when the command is read rather than when it is executed.
For a for loop, the command is the entire thing from the for to the final ). That means, if you use %line% within the loop, that will be evaluated before the loop starts running, which will result in it always being 0 (the variable itself may change but the expansion of it has already happened). However, !line! will be evaluated each time it is encountered within the loop so will have the correct value.
Similarly, while endlocal would normally clear out all variables created after the setlocal, the command:
endlocal & set thing=%thing%
is a single command in the context of expansion. The %thing% is expanded before endlocal is run, meaning it effectively becomes:
endlocal & set thing=whatever_thing_was_set_to_before_endlocal
That's why the use of setlocal and endlocal & set is a very useful way to limit variables "escaping" from a scope. And, yes, you can chain multiple & set stanzas to allow more variables to escape the scope.
I have a batch file that take a directory path as a parameter.
In that file folder, there are any number of .ai.pdf or .pdf files that I need to convert to jpg's. The conversion is not my issue (I am using imageMagick) , but lopping off the full extension is.
I need to be able to either take off the full .ai.pdf (7 characters) or .pdf (4 characters) from the file name and replace it with .jpg I cannot use just ~n in the for loop because it will not take off the .ai in an instance with there is an .ai.pdf (results in file name.ai where I need just the file name)
There are quite a few posts on StackOverFlow about this
StackOverFlow Example
but no matter what I attempt to try, I get an error when truncating the appropriate amount of extension off of the file name.
Here is my code. This is the first major batch file I have ever created, so I am open to anything, other than installing more programs to do the work.
The thing that kills me, is I had this working and in the shuffle from one server to another and a week of vacation, the working code got....misplaced.
#echo off
SETLOCAL ENABLEDELAYEDEXPANSION
set dir1=%1
echo recieved !dir1!
for /R %dir1% %%a in (*.pdf) DO (
echo file found !a!
set b=th_%%~nxa
if x%b:ai.pdf=%==x%b% set b=%%~dpa!b:~0,-7!
if not x%b:ai.pdf=%==x%b% set b=%%~dpa!b:~0,-4!
REM convert -density 64 "%%a" +matte -resize 15%% "!b!.jpg"
#echo !b! converted
)
ENDLOCAL
the file tells me that %~dpa!b:~0,-7! is an invalid substitution
Any ideas? Thanks for the help!
A few things first:
!a! and %%a are two different variables.
if x%b:ai.pdf=%==x%b% does not mean what you think it does. That will only be true when %b% does NOT contain .ai.pdf.
Again, if not x%b:ai.pdf=%==x%b% does not mean what you think. This is true when %b% DOES contain .ai.pdf.
There is no need to do any verification and cutting, just search and replace. ( That is what the %y:x=z% notation does, in this example it replaces every x within %y% with a z.) Let search and replace do the verification. It will only replace what matches the search. That will speed up the your batch file.
Lastly, since you are inside a () code block you will need to use the delayed Expansion turned on with your setlocal statement. This is because everything inside a code block is treated as if it were on a single line. That means that if you change a variable inside a code block, you will not be able to see the new value using the % notation. Instead you need to replace the %'s with !'s. For instance...
setlocal enableDelayedExpansion
set x=Hello
(
set x=Goodbye
echo I don't know why you say "!x!", I say "%x%".
)
...will give you the old Beatles lyric...
I don't know why you say "Goodbye", I say "Hello".
Anyway, on to your answer:
#echo off
SETLOCAL ENABLEDELAYEDEXPANSION
set dir1=%1
echo recieved !dir1!
for /R %dir1% %%a in (*.pdf) DO (
:: Adding a colon to the end of the filename allows the extension to be ID'd
: without explicitly looking at it with an IF statement, while simultaneously
:: avoiding similar looking character groupings inside the filename.
set b=th_%%~nxa:
:: No need to check for case, the substitution will do that for you.
set b=!b:.ai.pdf:=.jpg!
set b=!b:.pdf:=.jpg!
REM convert -density 64 "%%a" +matte -resize 15%% "!b!"
echo %%a converted into !b!
)
ENDLOCAL
The drawback is that both the files...
X.ai.pdf
X.pdf
...will be translated into th_X.jpg, creating the possibility of duplicate filenames for two different files.
But that's intrinsic to your concept of treating both types of filenames the same. If you don't have a method for avoiding this sort of duplication it might not be a bad idea to leave the .ai on the file, thereby creating two files: th_X.jpg and th_X.ai.jpg, eliminating the possibility of duplicate filenames.
Hm... would this work for you:
for /R %F in (*.pdf) do #for %G in ("%~nF") do #echo %~nxF ==^> %~nG.jpg
(as executed directly from cmd, if run from batch, replace % with %%).
This has a peculiar effect of changing the case of a file to case of a directory if there exists one with the same name as base name of your file (file.pdf will become FILE.jpg if you happen to have a subdirectory called FILE), but that's it (I think).
This also assumes your base names differ (so no file.ai.pdf and file.pdf in same directory)
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
the code is:
setlocal EnableDelayedExpansion
FOR /f "usebackq tokens=*" %%X in (`dir /a-d /s /b "!search_path!" 2^>^&1`) DO #(
set file_path=%%X
rem do other stuff
)
Delayed expansion is on because the source path might have special characters like backticks percentages exclamation and ^ escape sing. All these characters are allowed in windows paths and I don't know if and where they will be present.
The problem arise what to do with double percent parameter %%X, how to pass it to another variable without expansion. If DE is on the exclamation sings will be treated as variables with and that would result with a range of weird errors. The same thing is if I disable DE - the same situation, but this time with percentages.
Any idea how to make these lines safe for every possible allowed path that can be found in windows system with no matter how weird characters ?
The problem boils to how to safe pass data from double percent for parameter into normal %variable% so the data can safe passed through delayed expansion from that moment.
I would try to adapt FOR /R to your needs, which will solve some of your escape efforts. You can check the format/match of the file listing in your loop vs. in the dir.
FOR /R will traverse your directory tree (which you're doing anyway) and return the files that match the pattern you give.
Quick example to list all files of type TXT in a directory and it's sub-directories goes like this:
UPDATED:
This prints the contents of two files in my directory that have exclamation points in them:
#echo off
for /r %%i in (ex*!*.txt) do (
type %%~i
)
Note the absense of delayed variable expansion. Add'l variable references are found at the bottom of the for /? listing.