First I will point out a number of issues that make this problem difficult to solve perfectly. Then I will present the most bullet-proof solution I have been able to come up with.
For this discussion I will use lower case path to represent a single folder path in the file system, and upper case PATH to represent the PATH environment variable.
From a practical standpoint, most people want to know if PATH contains the logical equivalent of a given path, not whether PATH contains an exact string match of a given path. This can be problematic because:
- The trailing \ is optional in a path
- There is one exception: The relative path C: (meaning the current working directory of drive C) is very different than C:\ (meaning the root directory of drive C)
- Some paths have alternate short names
- Windows accepts both / and \ as folder separators within a path.
- Windows treats consecutive folder separators as one logical separator.
- Exceptions: Not only is C:, different than C:\, but C:\ (a valid path), is different than C:\\ (an invalid path).
- Windows trims trailing dots and spaces from file and directory names.
- The current .\ and parent ..\ folder specifiers may appear within a path
- A path can optionally be enclosed within double quotes.
- A path may be fully qualified or relative.
- D: is relative to the current working directory of volume D:
- \myPath is relative to the current working volume (could be C:, D: etc.)
- myPath is relative to the current working volume and directory
Most paths work equally well both with and without the trailing \. The path logically points to the same location either way. The PATH frequently has a mixture of paths both with and without the trailing \. This is probably the most common practical issue when searching a PATH for a match.
Any path that does not meet the old 8.3 standard has an alternate short form that does meet the standard. This is another PATH issue that I have seen with some frequency, particularly in business settings.
This is not seen very often, but a path can be specified using / instead of \ and it will function just fine within PATH (as well as in many other Windows contexts)
C:\FOLDER\\\ and C:\FOLDER\\ are equivalent. This actually helps in many contexts when dealing with a path because a developer can generally append \ to a path without bothering to check if the trailing \ already exists. But this obviously can cause problems if trying to perform an exact string match.
"C:\test. " is equivalent to "C:\test".
Unlikely to be seen in real life, but something like C:\.\parent\child\..\.\child\ is equivalent to C:\parent\child
A path is often enclosed in quotes to protect against special characters like <space> , ; ^ & =. Actually any number of quotes can appear before, within, and/or after the path. They are ignored by Windows except for the purpose of protecting against special characters. The quotes are never required within PATH unless a path contains a ;, but the quotes may be present never-the-less.
A fully qualified path points to exactly one specific location within the file system. A relative path location changes depending on the value of current working volumes and directories. There are three primary flavors of relative paths:
It is perfectly legal to include a relative path within PATH. This is very common in the Unix world because Unix does not search the current directory by default, so a Unix PATH will often contain .\. But Windows does search the current directory by default, so relative paths are rare in a Windows PATH.
So in order to reliably check if PATH already contains a path, we need a way to convert any given path into a canonical (standard) form. The ~s modifier used by FOR variable and argument expansion is a simple method that addresses issues 1 - 6, and partially addresses issue 7. The ~s modifier removes enclosing quotes, but preserves internal quotes. Issue 7 can be fully resolved by explicitly removing quotes from all paths prior to comparison. Note that if a path does not physically exist then the ~s modifier will not append the \ to the path, nor will it convert the path into a valid 8.3 format.
The problem with ~s is it converts relative paths into fully qualified paths. This is problematic for Issue 8 because a relative path should never match a fully qualified path. We can use FINDSTR regular expressions to classify a path as either fully qualified or relative. A normal fully qualified path must start with <letter>:<separator> but not <letter>:<separator><separator>, where <separator> is either \ or /. UNC paths are always fully qualified and must start with \\. When comparing fully qualified paths we use the ~s modifier. When comparing relative paths we use the raw strings. Finally, we never compare a fully qualified path to a relative path. This strategy provides a good practical solution for Issue 8. The only limitation is two logically equivalent relative paths could be treated as not matching, but this is a minor concern because relative paths are rare in a Windows PATH.
There are some additional issues that complicate this problem:
9. Normal expansion is not reliable when dealing with a PATH that contains special characters.
Special characters do not need to be quoted within PATH, but they could be. So a PATH like
C:\THIS & THAT;"C:\& THE OTHER THING" is perfectly valid, but it cannot be expanded safely using simple expansion because both "%PATH%" and %PATH% will fail.
10. The path delimiter is also valid within a path name
A ; is used to delimit paths within PATH, but ; can also be a valid character within a path, in which case the path must be quoted. This causes a parsing issue.
jeb solved both issues 9 and 10 at 'Pretty print' windows %PATH% variable - how to split on ';' in CMD shell
So we can combine the ~s modifier and path classification techniques along with my variation of jeb's PATH parser to get this nearly bullet proof solution for checking if a given path already exists within PATH. The function can be included and called from within a batch file, or it can stand alone and be called as its own inPath.bat batch file. It looks like a lot of code, but over half of it is comments.
Code: Select all
@echo off
:inPath pathVar
::
:: Tests if the path stored within variable pathVar exists within PATH.
::
:: The result is returned as the ERRORLEVEL:
:: 0 if the pathVar path is found in PATH.
:: 1 if the pathVar path is not found in PATH.
:: 2 if pathVar is missing or undefined or if PATH is undefined.
::
:: If the pathVar path is fully qualified, then it is logically compared
:: to each fully qualified path within PATH. The path strings don't have
:: to match exactly, they just need to be logically equivalent.
::
:: If the pathVar path is relative, then it is strictly compared to each
:: relative path within PATH. Case differences and double quotes are
:: ignored, but otherwise the path strings must match exactly.
::
::------------------------------------------------------------------------
::
:: Error checking
if "%~1"=="" exit /b 2
if not defined %~1 exit /b 2
if not defined path exit /b 2
::
:: Prepare to safely parse PATH into individual paths
setlocal DisableDelayedExpansion
set "var=%path:"=""%"
set "var=%var:^=^^%"
set "var=%var:&=^&%"
set "var=%var:|=^|%"
set "var=%var:<=^<%"
set "var=%var:>=^>%"
set "var=%var:;=^;^;%"
set var=%var:""="%
set "var=%var:"=""Q%"
set "var=%var:;;="S"S%"
set "var=%var:^;^;=;%"
set "var=%var:""="%"
setlocal EnableDelayedExpansion
set "var=!var:"Q=!"
set "var=!var:"S"S=";"!"
::
:: Remove quotes from pathVar and abort if it becomes empty
set "new=!%~1:"=!"
if not defined new exit /b 2
::
:: Determine if pathVar is fully qualified
echo("!new!"|findstr /i /r /c:^"^^\"[a-zA-Z]:[\\/][^\\/]" ^
/c:^"^^\"[\\][\\]" >nul ^
&& set "abs=1" || set "abs=0"
::
:: For each path in PATH, check if path is fully qualified and then do
:: proper comparison with pathVar.
:: Exit with ERRORLEVEL 0 if a match is found.
:: Delayed expansion must be disabled when expanding FOR variables
:: just in case the value contains !
for %%A in ("!new!\") do for %%B in ("!var!") do (
if "!!"=="" endlocal
for %%C in ("%%~B\") do (
echo(%%B|findstr /i /r /c:^"^^\"[a-zA-Z]:[\\/][^\\/]" ^
/c:^"^^\"[\\][\\]" >nul ^
&& (if %abs%==1 if /i "%%~sA"=="%%~sC" exit /b 0) ^
|| (if %abs%==0 if /i "%%~A"=="%%~C" exit /b 0)
)
)
:: No match was found so exit with ERRORLEVEL 1
exit /b 1
The function can be used like so (assuming the batch file is named inPath.bat):
Code: Select all
set test=c:\mypath
call inPath test && (echo found) || (echo not found)
continued below...