How do I remove trailing \ from %~p in a For command? - cmd

I have a Windows batch file that uses the following command
for /r %%i in (dir) do #echo %server%%%~pi>>%filename%
where %server% is network location of the folder (other users use different drive mappings) and %filename% is the text file I am storing the results in (done this way for debugging purposes).
The folders getting listed in the file end up with a trailing backslash, that seems to be causing some issues when the file is read. How can I get rid of the trailing backslashes, without manually editing the created file?

A simpler solution is to append a dot and use a 2nd FOR loop to get the normalized path, name, and extension:
for /r %%i in (dir) do for %%F in ("%%~pi.") do echo %server%%%~pnxF>>%filename%
One big advantage is this does not require delayed expansion, so you don't have to worry about paths with ! getting corrupted when the FOR variable is expanded.

You can assign the directory name (with the trailing backslash) to a normal variable, and then echo it with substring variable expansion of 0,-1 to exclude the final character, all in a command list in the for-loop body:
for /r %%i in (dir) do #(set d=!server!%%~pi& echo !d:~0,-1!) >>!filename!
Here's the help on substring variable expansion from help set:
May also specify substrings for an expansion.
%PATH:~10,5%
would expand the PATH environment variable, and then use only the 5
characters that begin at the 11th (offset 10) character of the expanded
result. If the length is not specified, then it defaults to the
remainder of the variable value. If either number (offset or length) is
negative, then the number used is the length of the environment variable
value added to the offset or length specified.
%PATH:~-10%
would extract the last 10 characters of the PATH variable.
%PATH:~0,-2%
would extract all but the last 2 characters of the PATH variable.
Note that for this to work, you must enable enabledelayedexpansion and use the ! delimiter for the variable in the for-loop body, to ensure that it expands during loop processing, rather than during parsing of the loop. I also changed %filename% to !filename!, because it's good practice to always enable and use this feature, even when it's not essential.

Related

Batch for-loop paths with quotes are strings instead of file-sets

I'm having trouble getting a FOR loop to read a quoted path as a file-set. I have a script similar to this, designed to process every line of the files I drag and drop onto it:
#ECHO OFF
FOR %%F IN (%*) DO (
ECHO %%F
FOR /F "DELIMS=" %%A IN (%%F) DO (
ECHO %%A
)
)
PAUSE
The first ECHO shows the file path. If it contains a space, there will be double-quotes around the path. The second ECHO shows each line in the file, unless the file path had a space. Then instead of lines, it's just the path again, but without quotes.
This makes sense to me after reading Windows' FOR documentation, which shows if quotes are given, it's read as a string, not a file.
FOR /F ["options"] %variable IN (file-set) DO command [command-parameters]
FOR /F ["options"] %variable IN ("string") DO command [command-parameters]
But some paths have spaces and presumably need quotes. How can I force FOR to interpret those as files?
I tried passing %%~f to the second FOR loop to always strip quotes, but then FOR complained it could not find the file, because the path was cut off at the first space. That's when I added the DELIMS option, to stop reading spaces as delimiters, but there was no change.
Read the same documentation and look at the usebackq options.
usebackq - specifies that the new semantics are in force,
where a back quoted string is executed as a
command and a single quoted string is a
literal string command and allows the use of
double quotes to quote file names in
file-set.
A batch file can be called with multiple arguments which can be enclosed in " as required on an argument string like a file name contains a space or one of these characters &()[]{}^=;!'+,`~ (or literally to interpret <>| like in a password string), but can be also passed to the batch file without surrounding double quotes. That must be taken into account on processing the batch file arguments like file names by explicitly removing surrounding " from each argument string and reference the resulting file name string with always enclosing it in " to have finally every file name enclosed in double quotes.
#ECHO OFF
SETLOCAL EnableExtensions DisableDelayedExpansion
FOR %%I IN (%*) DO (
ECHO "%%~I"
FOR /F usebackq^ delims^=^ eol^= %%J IN ("%%~I") DO ECHO(%%J
)
ENDLOCAL
PAUSE
The first two command lines define completely the required execution environment which is:
command echo mode turned off to prevent output of each command before execution,
command extensions enabled as required for this batch file for %* and FOR /F,
delayed variable expansion disabled to process correct all file names even those with an exclamation mark and all lines in the files even those with an exclamation mark.
The outer FOR loop assigns one argument string after the other to the loop variable I exactly as passed to the batch file on starting it which means without or with surrounding double quotes.
The first ECHO outputs the argument string being hopefully the file name of a text file always enclosed in double quotes independent on file name passed to the batch file without or with surrounding double quotes.
The inner FOR loop with option /F assigns each non-empty line to the loop variable J and runs one more ECHO to just output the non-empty line.
FOR /F interprets by default a string in double quotes as string to process. The usage of the option usebackq changes this behavior. A string in double quotes is now interpret as file name of a text file of which lines should be processed one after the other. The file name passed as argument string to the batch file is always enclosed in " because of using "%%~I" which references the argument string assigned to loop variable I with removal of surrounding " and explicitly enclose the resulting file name string in " by the code in the batch file.
FOR /F always ignores empty lines which means lines not containing any character other than the line termination characters carriage return and line-feed. A text file is also processed correct on lines are terminated just with a line-feed (UNIX format). A carriage return is only ignored by FOR on being in byte stream of the file before the line-feed. Otherwise the carriage return is not interpreted as line termination and becomes therefore part of the line to process further.
FOR /F splits up by default a line into substrings (tokens) using normal space and horizontal tab character as string delimiters whereby leading spaces/tabs are removed from each line. Then is checked if the first character of first substring starts with a semicolon being the default end of line character in which case the line is also ignored for further processing independent on option tokens= if that option is also used which is not the case here. Otherwise without usage of option tokens= only the first space/tab delimited string is assigned to the specified loop variable for further processing by the command(s) in the body of the FOR loop.
delims= turns off the line splitting behavior by the definition of an empty list of delimiters. eol= defines no character as end of line character. Both together results in processing all lines in file even blank lines containing only spaces/tabs with the exception of empty lines.
The only possible syntax to define the FOR /F options delims= and eol= together with no delimiter and no end of line character is the used syntax with not enclosing the three options in " as usual as the usage of "usebackq delims= eol=" would result in " being interpreted as end of line character. A normal space, an equal sign, a comma, a semicolon and an OEM encoded no-break space found by cmd.exe on a command line not within an argument string enclosed in " is interpreted as argument string separator. But usebackq delims= eol= should be interpreted by cmd.exe as one argument string to pass to its internal command FOR. The solution is to escape the two spaces and the two equal signs with ^ to get them interpreted as literal characters and not as argument string separators by cmd.exe.
The command ECHO with just spaces/tabs appended would output the current status of command echo mode instead of the spaces/tabs. ECHO(%%J is used instead of ECHO %%J to prevent an output of command echo mode status if the line assigned to loop variable J consists of only spaces/tabs. The opening round bracket is interpreted as argument string separator in this case between the command ECHO and the string to output which is the non-empty line read from the file.
To understand the commands used and how they work, open a command prompt window, execute there the following commands, and read the displayed help pages for each command, entirely and carefully.
echo /?
endlocal /?
for /?
pause /?
setlocal /?
See also:
DosTips forum topic ECHO. FAILS to give text or blank line - Instead use ECHO/ for a full explanation for the usage of ECHO( in this special use case.
Issue 7: Usage of letters ADFNPSTXZadfnpstxz as loop variable in this answer for an explanation why the letters F and A are not used in the code above as loop variables although both could be used here too.

Is there any way to remove specific characters from a filename in Windows, using a batch file?

I would like to rename every file in a directory that contains '[' or ']' characters by removing any instance of those two characters from those filenames.
I have yet to determine any way to read filenames character-by-character with a batch file.
Thanks!
You don't need to read the filename character by character. In batch you can simply replace all occurrences of a substring subs with another substring repl in the value of a variable %var% with %var:subs=repl%. As removing is the same as replacing with an empty string (in batch at least), %var:h=% will remove all hs from the value of the var variable. So all you need is to store the filename in a variable and you can have the new filename without [ and/or ].
To iterate over all files in a directory you'll just need a FOR loop.
So you can iterate over all files in the current directory and rename the ones with [ or ] in their name (or both) with the following script (see edit for version which also handles names with exclamation marks):
#echo off
Setlocal EnableDelayedExpansion
FOR %%G IN (*) DO (
set "filename=%%~G"
set "filename=!filename:[=!"
set "filename=!filename:]=!"
REM Rename only if there is a difference between new filename and old filename in %%G
IF NOT "!filename!" == "%%~G" ren "%%~G" "!filename!"
)
EndLocal
exit /b 0
Because the filename variable is set inside the FOR block and we want to read it inside the same block, we need delayed expansion.
EDIT: As #dbenham said in comment, delayed expansion can cause problems when you have filenames containing exclamation marks ! (%%~G will then contain a ! which normally announces a variable being expanded with delayed expansion). To solve that problem, #dbenham proposed a nice solution: enable delayed expansion only when we're about to use it i.e. inside the FOR block, just before using the filename variable. The ! won't poisin the value of a variable expanded with delayed expansion so !filename! can be used without a ! being misinterpreted in its value. This also means we'll have to move EndLocal to after we're done using delayed expansion in the FOR block (i.e. at the end of the block) and avoid the use of %%~G in the ren command. The latter can be done by using a variable to hold the original name (also before enabling delayed expansion) and use delayed expansion to retrieve the original name. That variable can then actually be used to construct the new filename.
#echo off
FOR %%G IN (*) DO (
REM Set filename without delayed expansion, no misinterpreted '!' possible
set "old_filename=%%~G"
REM before using old_filename, enable delayed expansion
SetLocal EnableDelayedExpansion
set "new_filename=!old_filename:[=!"
set "new_filename=!new_filename:]=!"
REM Rename only if there is a difference between new filename and old filename in %%G
ren "!old_filename!" "!new_filename!"
REM No more delayed expansion needed in block now
EndLocal
)
exit /b 0
Also, #KlitosKyriacou pointed out the IF was actually unnecessary: renaming a file with its original filename causes no harm. I've left it out in my 2nd version (the edit-version).
If you are able to use PowerShell and have one folder where all files are located you can run a command like
Dir | Rename-Item -NewName { $_.Name -replace "[","" }
and
Dir | Rename-Item -NewName { $_.Name -replace "]","" }
to get rid of the characters.
You could use JREN.BAT - a regular expression renaming command line utility. JREN.BAT is pure script (hybrid batch/JScript) that runs natively on any Windows machine from XP onward - no 3rd party exe required.
Using alternation, which requires escape of [
jren "\[|]" ""
or a character set, with escaped ]
jren "[[\]]" ""
or a character set, with ] as first member of set
jren "[][]" ""
Any of the above syntaxes can easily be extended to strip additional characters.
You must use call jren if you put the command within a batch script.
Regardless of what technique you use to solve this, the operation may fail if the renamed value conflicts with an already existing file name.

Batch File: Prevent literal interpretation in a for /f loop

Currently I have a loop that runs through a list of items and copies them to a directory (archive). However, one of the items in the list (which has a global variable in the path-name) is being interpreted 'literally' (as text instead of code).
I know usually you can just escape the line via (^^) to have it interpreted as code instead of text, but evidently I'm doing something wrong here, because it's not working...
The item in the my.list (with the escape in it) that is having issues is:
location\foo^^%date:~0,3%*.zip
The code I'm using is...
for /f "delims=" %%a in (my.list) do (
echo "%%a"
)
Echo's
"location\foo^^%date:~0,3%*.zip"
Instead of
location\fooMON*.zip
Any help would be greatly appreciated.
You are confused as to when you need to escape a character.
Some characters have special meaning ("code" as you describe it). Often times you can escape the character such that it is interpretted as a literal (text) instead of "code".
The most frequent method to escape a character within Windows CMD.EXE is to prefix it with a single ^ character. Sometimes a string is parsed twice, which can require an escape sequence of ^^^, (or perhaps ^^ when dealing with ! when delayed expansion is enabled). More rounds of parsing require ever more ^ characters. It can quickly become confusing, and requires practice to get the hang of it.
But your situation is completely different - It cannot be solved by escaping. You have "code" within your FOR variable, and you want it to be interpreted as such. But instead, it is being interpreted as text. In order to understand why, you must understand the order in which various stages of batch parsing occur. You could refer to How does the Windows Command Interpreter (CMD.EXE) parse scripts?, but it is pretty advanced stuff that takes time to digest.
Here is a crude synopsis showing when various types of expansion occur. (Note - these step numbers do not match up exactly with the phase numbers of the linked answer)
1) Parameter expansion - %1
2) Normal variable expansion - %var%
3) FOR variable expansion - %%A
4) Delayed variable expansion - !var!
5) CALL expansion - repeat steps 1) and 2) if CALL involved
You want your %date:~0,3% string to undergo normal (percent) expansion. Your FOR loop reads the line of text verbatim, without any expansion. The first time the parser sees your "code" is at step 3) when the %%a FOR variable is expanded. You can see that this is already too late to get
%date:~0,3% to expand the way you want.
You have two choices to solve your problem. But beware - each of these solutions potentially add new issues that may need to be solved.
I am assuming the ^^ is your naive attempt to force expansion of the embedded "code". The ^^ should be removed from your list file.
Option 1: Add an extra round of normal expansion by using CALL
for /f "delims=" %%a in (my.list) do call echo "%%a"
But now you have a potential problem that you might have a % literal in your list that you do not want to be expanded. Percents within batch scripts cannot be escaped with ^. Instead you escape a percent by doubling it as %%. So if you have percent literals in your list, they must be doubled.
Note that the original code that was posted with the question was significantly more complicated. It included an IF statement that referenced %%a. You cannot CALL an IF or FOR command. The solution is to CALL a subroutine, passing the value, and include the complex logic in the subroutine.
for /f "delims=" %%a in (my.list) do call :processValue "%%a" >>Logs\xfer.log
exit /b
:processValue
echo Attempting to archive %1...
if exist "c:\%~1" (
echo f | xcopy "c:\%%a" "c:\Lucas\archive\%~1" /E /C /H /R /Y
if %errorlevel%==0 (
echo ...%1 added to archive for transfer
echo.
) else (
echo ERROR: %1 not added to archive
echo.
)
) else (
echo ERROR: %1 Not found on client computer
echo.
)
Option 2: Use delayed expansion
Enable delayed expansion, and change your list to use !date:~0,3! instead of %date:~0,3%. Delayed expansion occurs after FOR variable expansion, so it will be expanded properly.
setlocal enableDelayedExpansion
for /f "delims=" %%a in (my.list) do echo "%%a"
But now you have a potential problem that you might have a ! literal in your list that you do not want to be expanded. You can preserve ! literals by escaping them as ^!.

Batch File to read 'N' characters from a text file

I have searched this across the net and found many codes for retrieving the entire line from a text or replacing the text with another but not for what i was looking for.
Using the For loop with the tokens would return on the set (word) separated with spaces.
I want to pull only a few characters from the line.
Eg: 12345qwerty67890
If this on in a text file i want to pull only '12345' and assign it to a variable.
Any help is greatly appreciated.
set HELP SET and try the following to get you started
#echo off
setlocal enabledelayedexpansion
for /f "tokens=*" %%a in (sample.txt) do (
set line=%%a
set chars=!line:~0,5!
echo !chars! --- !line!
)
At the command prompt, do help set. There you will find more information about the set command than you would ever want to know. The part you are interested in says:
May also specify substrings for an expansion.
%PATH:~10,5%
would expand the PATH environment variable, and then use only the 5
characters that begin at the 11th (offset 10) character of the expanded
result. If the length is not specified, then it defaults to the
remainder of the variable value. If either number (offset or length) is
negative, then the number used is the length of the environment variable
value added to the offset or length specified.
Of course, in order to use that kind of stuff with the for loop variable, you need to first get acquainted with the very peculiar way in which variables are expanded in Windows NT batch files. Let me know if you have problems with that, and I can add more information.

What does this batch file code do?

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.

Resources