So you must be using FOR /F with multiple tokens, like
for /f "tokens=1-16" %%a in (file) do echo %%~dpabc.txt
Or your code could have nested FOR loops. Something like
for %%a in (something) do (
for %%p in (somethingelse) do (
echo %%~dpabc.txt
)
)
Or even something like
for %%a in (something) do call :sub
exit /b
:sub
for %%p in (somethingelse) do echo %%~dpabc.txt
exit /b
All three code examples above will print out the drive and path of %%~dpa
, followed by "bc.txt". As per the documentation, the FOR variables are global, so the DO clause of the subroutine FOR loop has access to both %%a
and %%p
.
Aschipfl does a good job documenting the rules for how modifiers and variable letters are parsed.
Whenever you use a FOR variable before a string literal, you must be extremely careful that the string literal cannot be interpreted as part of the FOR variable expansion. As can be seen with your example, this can be difficult. Make the literal dynamic, and the problem is even worse.
set /p "myFile=Enter a file name: "
for %%a in (something) do (
for %%p in (somethingelse) do (
echo %%~dp%myFile%
)
)
If the user enters "abc.txt" then we are right back where we started. But looking at the code it is not obvious that you have a potential problem.
As Gerhard and Mofi say, you are safe if you use a character that cannot be interpreted as a modifier. But that is not always easy, especially if you are using FOR /F returning multiple tokens.
There are solutions!
1) Stop the FOR variable parsing with !!
and delayed expansion
If you look at the rules for how cmd.exe parses scripts, you will see that FOR variables are expanded in phase 4 before delayed expansion occurs in phase 5. This provides the opportunity to use !!
as a hard stop for the FOR expansion, provided that delayed expansion is enabled.
setlocal enableDelayedExpansion
for %%a in (something) do (
for %%p in (somethingelse) do (
echo %%~dp!!abc.txt
)
)
The %%~dp
is expanded properly in phase 4, and then in phase 5 !!
is expanded to nothing, yielding your desired result of the drive letter followed by "abc.txt".
But this does not solve all situations. It is possible for !
to be used as a FOR variable, but that should be easy to avoid except under extreme situations.
More troubling is the fact that delayed expansion must be enabled. This is not an issue here, but if the FOR variable expands to a string containing !
then that character will be parsed by delayed expansion, and the results will most likely be messed up.
So the !!
delayed expansion hack is safe to use only if you know that your FOR variable value does not contain !
.
2) Use intermediate environment variables
The only simple foolproof method to avoid problems in all situations is to transfer the value of the FOR variable to an intermediate environment variable, and then toggle delayed expansion and work with the entire desired string.
for %%a in (something) do (
for %%p in (somethingelse) do (
set "drive=%%~dp"
setlocal enableDelayedExpansion
echo !drive!abc.txt
endlocal
)
)
3) Use Unicode characters via environment variables
There is a complex bullet proof solution, but it takes a good bit of background information before you can understand how it works.
The cmd.exe command processor represents all strings internally as Unicode, as are environment variables - Any Unicode code point other than 0x00 can be used. This also applies to FOR variable characters. The sequence of FOR variable characters is based on the numeric value of the Unicode code point.
But cmd.exe code, either from a batch script, or else typed into the command prompt, is restricted to characters supported by the active code page. That might seem like a dead end - what good are Unicode characters if you cannot access them with your code?
Well there is a simple, though non-intuitive solution: cmd.exe can work with predefined environment variable values that contain Unicode values outside the active code page!
All FOR variable modifiers are ASCII characters that are within the first 128 Unicode code points. So if you define variables named $1 through $n to contain a contiguous range of Unicode characters starting with say code point 256 (0x100), then you are guaranteed that your FOR variable can never be confused with a modifier.
So if $1 contains code point 0x100, then you would refer to the FOR variable as %%%$1%
. And you can freely use modifiers like `%%~dp%$1%.
This strategy has an added benefit in that it is relatively easy to keep track of FOR variables when parsing a range of tokens with something like "tokens=1-30" because the variable names are inherently sequential. The active code page character sequencing usually does not match the sequence of the Unicode code points, which makes it difficult to access all 30 tokens unless you use the Unicode variable hack.
Now defining the $n variables with Unicode code points is not a trivial development effort. Thankfully it has already been done :-) Below is some code that demonstrates how to define and use the $n variables.
@echo off
setlocal disableDelayedExpansion
call :defineForChars 1
for /f "tokens=1-16" %%%$1% in (file) do echo %%~d%$16%abc.txt
exit /b
:defineForChars Count
::
:: Defines variables to be used as FOR /F tokens, from $1 to $n, where n = Count*256
:: Also defines $max = Count*256.
:: No other variables are defined or tampered with.
::
:: Once defined, the variables are very useful for parsing lines with many tokens, as
:: the values are guaranteed to be contiguous within the FOR /F mapping scheme.
::
:: For example, you can use $1 as a FOR variable by using %%%$1%.
::
:: FOR /F "TOKENS=1-31" %%%$1% IN (....) DO ...
::
:: %%%$1% = token 1, %%%$2% = token 2, ... %%%$31% = token 31
::
:: This routine never uses SETLOCAL, and works regardless whether delayed expansion
:: is enabled or disabled.
::
:: Three temporary files are created and deleted in the %TEMP% folder, and the active
:: code page is temporarily set to 65001, and then restored to the starting value
:: before returning. Once defined, the $n variables can be used with any code page.
::
for /f "tokens=2 delims=:." %%P in ('chcp') do call :DefineForCharsInternal %1
exit /b
:defineForCharsInternal
set /a $max=%1*256
>"%temp%\forVariables.%~1.hex.txt" (
echo FF FE
for %%H in (
"0 1 2 3 4 5 6 7 8 9 A B C D E F"
) do for /l %%N in (1 1 %~1) do for %%A in (%%~H) do for %%B in (%%~H) do (
echo %%A%%B 0%%N 0D 00 0A 00
)
)
>nul certutil.exe -decodehex -f "%temp%\forVariables.%~1.hex.txt" "%temp%\forVariables.%~1.utf-16le.bom.txt"
>nul chcp 65001
>"%temp%\forVariables.%~1.utf8.txt" type "%temp%\forVariables.%~1.utf-16le.bom.txt"
<"%temp%\forVariables.%~1.utf8.txt" (for /l %%N in (1 1 %$max%) do set /p "$%%N=")
for %%. in (dummy) do >nul chcp %%P
del "%temp%\forVariables.%~1.*.txt"
exit /b
The :defineForChars
routine was developed at DosTips as part of a larger group effort to easily access many tokens with a FOR /F statement.
The :defineForChars
routine and variants are introduced in the following posts within that thread: