Do batch scripts treat variables differently in blocks? - batch-file

I'm running a batch file which updates some variables, notably %PATH%. My environment has a known bug where on of the directories in %PATH% is quoted, i.e.
PATH=c:\windows;...;"c:\program files\foo"\bin;c:\program files\...
I have a script which appends to PATH. When I do it inside an IF block, I get an error, e.g:
IF "1"=="1" (
SET "PATH=%PATH%;c:\foo"
)
Gives the error
\Microsoft was unexpected at this time.
Where \Microsoft is obviously a fragment from one of the directories in %PATH%.
I don't get the error if the SET is not within a conditional block. Why is this?
Edit: It seems that this has more to do with the fact that PATH also contains parenthesis. Here's a better example of the issue:
C:\temp>SET "FOO=C:\Program Files (x86)\;foo"
C:\temp>ECHO %FOO%
C:\Program Files (x86)\;foo
C:\temp>IF "1"=="1" ( ECHO %FOO% )
\ was unexpected at this time.
C:\temp>IF "1"=="1" ECHO %FOO%
C:\Program Files (x86)\;foo
So my question is really, why does it break if it's in the paren-delimited block?

JosefZ properly identified the problem with ) in the path, but his suggested solution of removing all quotes is not safe.
Any path within the PATH variable may include ;, !, &, ) and/or ^, all of which can cause various issues when using normal %PATH% expansion.
There may be some paths with & or ) etc. quoted, as well as some unquoted paths with problem characters, so both %PATH% and "%PATH%" can fail.
The only guaranteed safe way to expand PATH is via delayed expansion, but you want the new value to survive the ENDLOCAL. What to do? . . .
. . . FOR /F and delayed expansion toggling to the rescue :-)
if "1"=="1" (
setlocal enableDelayedExpansion
for /f "eol=: delims=" %%P in ("!path!") do (
endlocal
set "path=%%P;c:\foo"
)
)
Simplistic implementation of PATH extension via code like set path=%path%;c:\foo,
or set "path=%path%;c:\foo" is rampant, but it is not safe. By and large, people do not realize the subtle complexities involved with PATH management.
If you are ever trying to modify the PATH variable in a batch script that can be released into the wild, then you should always use a safe method like the one I have shown above.
The problem becomes even more complex if you want to conditionally append a path to PATH if and only if path is not already there. See How to check if directory exists in %PATH%? for a list of potential issues, as well as a fairly robust solution.

SET "PATH=%PATH:"=%;c:\foo"
should fix the problem, since the quoes are irrelevant
Caution: This may not be true if a path-entry contains ";". In that case, you should rename the directory to something sensible.

You are right that this has more to do with the fact that PATH also contains parentheses. Next example shows the issue unambiguously:
==> ( set "varName=var"value(s) with"space(s) and"right parentheses"" )
with"space(s) and"right was unexpected at this time.
==> ( set varName=var"value(s) with"space(s) and"right parentheses" )
and"right parentheses" was unexpected at this time.
==>
Read answers to How does the Windows Command Interpreter (CMD.EXE) parse scripts? for explanation.
I can see the only solution: correct (adjust) your path variable once for always by removing all " double quotes as follows (check user variable path as well if present). Restart required.

Just remove the outer quotes in your setting of the path variable. They're not required, by any means. Certainly not in this instance, where you have quotes being used internally within those quotes. The quotes should be inside your path variable, not outside the whole thing.
SET PATH=%PATH%;c:\foo
To further how it should be done, here's some additional examples to clarify...
SET PATH=%PATH%;C:\foo
SET PATH=%PATH%;"C:\Some Folder With Spaces In It"
SET PATH=%PATH%;"C:\Program Files\Foobar Inc Software"
But never...
SET "PATH=%PATH%;SomePath"

Related

Batch file how to delete end text in a variable string in a for loop

I am writing some batch code to simplify a process I have of downloading some files, renaming them, and then copying them to replace the old ones. I'm running into an issue where I have a FOR loop read in a list of files from a directory, then try to modify the filenames.
The filenames all have FLY in the name, and I want to remove all text after FLY. I can't use tokens because the filenames are inconsistent in length, have multiple spaces, and wouldn't have a set number of tokens. I can't use substring because there is not a set number of characters after FLY.
I've tried using the examples at SS64 and also read numerous threads on here but nothing really seems to match my situation.
Here's the code snippet, appreciate if someone can tell me where I'm going wrong:
SETLOCAL ENABLEDELAYEDEXPANSION
FOR /F "TOKENS=*" %%A IN ('DIR /B ^"%~DP0VFR^"') DO (
SET FILENAME=%%A
SET REMOVETEXT=!FILENAME:*FLY=!
SET NEWFILENAME=!FILENAME:!REMOVETEXT!=!
ECHO !FILENAME! will be renamed !NEWFILENAME!
)
When I insert echos to see what's going on everything works as expected up until the last SET, where somehow the ending result is !NEWFILENAME! is blank.
Hmm. My results were different from yours.
The " in your dir do not need to be escaped.
The problem with your set statement is that it's interpreted as
SET NEWFILENAME=!FILENAME:! + REMOVETEXT + !=!
and since FILENAME: and = are not existing variables, each will be replaced by nothing yielding "REMOVETEXT", not blank as you claim.
The solution is to use a two-stage evaluation of newname
call SET NEWFILENAME=%%FILENAME:!REMOVETEXT!=%%
which is resolved as
SET NEWFILENAME=%FILENAME:current_value_of_REMOVETEXT=%
in a sub-shell.
After cold brew it occurred to me that I might be going about this all wrong and making it more complicated than it needs to be... I decided to try directly renaming the files with wildcards and that actually worked. Didn't even need the FOR loop.
REN "%~DP0VFR\*FLY*" *FLY
No idea why the first (and overly convoluted) solution I tried didn't work, but this does with a lot less code!

Last index of a character in an input string in a batch file

I have a batch script running in windows. Every time the batch script is invoked an input path is provided as a parameter. I need to extract the folder name from the path.For that i need to get the last index of "/" from the path and then take a substring from that path until the end of the string.
Suppose I have a string /home/home1/home2/home3
The output I require is home3. Is there any way to extract the same.
For plain DOS/Windows batch files, this should work:
set FILE="c:\foo\bar.txt"
for /F %%i in ("%FILE%") do #echo %%~ni
That comes from DOS BAT file equivalent to Unix basename command?
Windows PowerShell and the Unix shell offer other alternatives.
So you have a string representing a folder path, and you want the name of the right most folder within the path. CMD.EXE provides convenient tools to work with file/folder paths, so there is no need to search for the last \.
You don't state where the string resides. Most likely it is either 1) in a batch parameter from a CALL to a batch script or :function, or 2) within an environment variable. I will assume a parameter, and show a succession of solutions, each one better (more robust) than the prior one. At the very end I will show a slight twist to adapt to working with an environment variable.
So if parameter %1 contains /home/home1/home2/home3, then all you need is %~n1.
But folder names can include dots, and the text after the last dot is considered to be an extension. So you need the x modifier as well to include the extension. For example, if %1 contains /home.1/home.2/home.3, then %~nx will yield home.3. It works just as well if there is no extension.
But folder names can include a poison character like &, so you should enclose the string in quotes: "%~nx1". If you are assigning the result to a variable, then you should enclose the entire assignment in quotes: set "folder=%~nx1". The value will not include the quotes, so be sure to add quotes when expanding the variable: "%folder%", or else use delayed expansion !folder!.
But sometimes people include a trailing backslash when passing a folder path, as in \home1\home2\home3\. The trailing backslash positively indicates that the path is to a folder, but %~nx1 will yield nothing. There is a simple trick using a FOR loop and an appended dot to solve this.
for %%F in (%1.) do set "folder=%%~nxF"
Note that FOR variables use the exact same modifiers as parameters. The appended dot elegantly solves the problem in a tricky way. If there is a trailing backslash, then the dot represents the "current" directory from that postion. The expansion modifiers automatically normalize the result into a canonical form, which yields the correct answer. But it also works if there is not a trailing backslash. File and folder names are not allowed to end with a dot or a space, and the expansion normalization will strip the trailing dot.
There are a few other esoteric cases dealing with UNC paths and long paths that are solved by adding the ~f modifier to convert %1 into the full canonical path.
Here is the ultimate solution that should "always" work with batch parameter %1.
for %%F in ("%~f1.") do set "folder=%%~nxF"
I put the word "always" in quotes because it is possible for the code to fail, but it would require the end user doing something stupid like passing "c:\This"^&"that\". In this case, the correct thing to do is simply pass "c:\This&that\".
Note that the passed path could be the root of a volume, like "c:\". In this case the result is an empty string, which is correct - there is no folder name.
How to work with an environment variable instead
Suppose you have variable folderPath that contains the path string. If you know that the value does not contain any quotes, and it is not a UNC or long path, then you can simply use:
for %%F in ("%folderPath%.") do set "folder=%%~nxF"
But that can fail in multiple ways if the value already contains quotes, or if it is a UNC or long path. The solution is to add an extra FOR /F loop coupled with delayed expansion to safely transfer the value into a FOR variable. Then the ~f modifier can be used as before. But paths can include the ! character, and FOR variable expansion will strip any ! if delayed expansion is enabled. So delayed expansion must be strategically toggled on and off.
Here is the ultimate solution for working with a variable
I'm assuming that delayed expansion is currently off.
setlocal enableDelayedExpansion
for /f "eol=: delims=" %%A in ("!folderPath!") do (
endlocal
for %%F in ("%%~fA.") do set "folder=%%~nxF"
)
I'm not aware of any scenarios where this last code can fail.

Handling of variables in for-loop of a Windows batch file

I've been investigating this a lot with threads on StackOverflow and the like, but although I feel I'm close to the solution, this problem is giving me headaches.
What I'm trying to do: When a specific external hard drive is connected (distinguished via VolumeSerialNumber over WMIC), the drive letter is found out, and mirroring is done via robocopy. The script is executed via double-click. This is what I have so far:
FOR /F "skip=1" %%i in ('wmic logicaldisk where VolumeSerialNumber^="XXXXXXXX" get deviceid 2^>nul') DO (
SET y=%%i
IF [%y%]==[] GOTO hdmissing
SET "backuphd=%%i"
GOTO endfor
)
:endfor
robocopy "C:\Users\Herbert\Documents" "%backuphd%\Backup\Documents" /MIR
ECHO Backup done
ECHO end
:hdmissing
ECHO Couldn't find external drive
:end
PAUSE
This way, the external HD is never detected (%y% is always an empty string). However, if I execute the script twice in the same console session, everything works as expected. But I want it to work at the first execution.
This is what I've tried so far:
Put SET y=dummy at the beginning of the script. The HD is always found, triggering a backup to C: if the HD is not actually connected (apparently SET y=%%i doesn't alter y?)
Change %y% to !y! - The HD is always found, again
Generation 3,576 of the delayed expansion problem, compounded by a contaminated environment.
There's no setlocal apparent, so y remains set in the environment after the first run - hence the 'later run characteristics different from first run' phenomenon.
Within a block statement (a parenthesised series of statements), the entire block is parsed and then executed. Any %var% within the block will be replaced by that variable's value at the time the block is parsed - before the block is executed - the same thing applies to a FOR ... DO (block).
Hence, IF (something) else (somethingelse) will be executed using the values of %variables% at the time the IF is encountered.
Two common ways to overcome this are 1) to use setlocal enabledelayedexpansion and use !var! in place of %var% to access the changed value of var or 2) to call a subroutine to perform further processing using the changed values.
The key in your case appears to be no setlocal enabledelayeexpansion and !y! - because !y! is just that - a literal string !y! unless delayedexpansion is invoked by the setlocal command.
(having said that,
IF [%%i]==[] GOTO hdmissing
would work just as well, as would
SET "y=%%i"
IF not defined y GOTO hdmissing
because if [not] defined var operates on the run-time value of var. "quoting the set arguments" ensures that any stray trailing spaces on the line are not included in the value assigned
)
As Magoo already pointed out, !y! doesn't work, since I forgot to enable Delayed Expansion. However, enabling it requires you to escape certain characters, which seemed quite irritating and tedious to me. It could be possible to just enclose the command in the FOR-loop with double quotes, as done in the two edits of this question, but I found out after solving it differently.
What I did was moving the block
IF [%y%]==[] GOTO hdmissing
SET "backuphd=%%i"
after :endfor. This way, it's outside of the for loop and %y% gets expanded accordingly.
Mind that this solution works only because I need just one item, and because of the programming flow.
Other useful approaches might be found in this question.
However, if you want to do the same thing: easier (though not as robust) solutions without WMIC might include
Putting a file with a specific name on the hard drive and checking for it with IF EXIST before backing up, hoping the user will not delete it, and the HD is connected to the same drive letter.
Putting the batch script on the drive itself and work with relative path names (optional: put a shortcut on the Desktop and hope again the drive is always under the same letter)

How to use a directory that contains round brackets in a batch file

I have a batch file in which the 2 first lines read:
set DIRwhereRUN=C:\UNIVERSITY\testSTABLEunstable(WITHrandomBED)
PUSHD %DIRwhereRUN%
but the batch does not work.
If I create a directory named testSTABLEunstable_WITHrandomBED and copy my stuff there everything works smoothly. Is there a way to make it work with the brackets? I don't want to rename for at least 2 reasons.
It's very difficult - and misleading - to isolate two lines as you have. There's nothing wrong with those two lines.
The difficulty that you are having is with "block" statements like
IF ... (something
something
somethinginvolving DIRWHERERUN
)
This is because batch substitutes the value of any %var% with its the-current value before executing the command(s) and hence misinterprets the ) in %Dirwhererun% as the closing-parenthesis of the IF (or ELSE or FOR.)
The way to overcome this is to "escape" %dirwhererun%'s ) (ie. temporarily suspend its special meaning) - this is done with the caret ^ - which itself is a special character (with the special meaning "the following character is just a character, not a special character".)
So - how to do this?
Here's a demonstration:
#ECHO OFF
setlocal
set "DIRwhereRUN=U:\UNIVERSITY\testSTABLEunstable(WITHrandomBED^)"
SET dir
set DIRwhereRUN=U:\THISWILLBEWRONG(WITHrandomBED^)
SET dir
set DIRwhereRUN=U:\UNIVERSITY\testSTABLEunstable(WITHrandomBED^^)
SET dir
set DIRwhereRUN=U:\UNIVERSITY\testSTABLEunstable(WITHrandomBED)
SET dir
MD %DIRwhereRUN%
PUSHD "%DIRwhereRUN%"
DIR
POPD
SET dirwhererun=%dirwhererun:)=^^)%
SET dir
(Note that I use U: as a temporary drive. I'm creating the directory using your original SET deliberately to show that it's not the SET or normal operations that are causing the problem)
Note that where the set uses quotes around the parameters, the value is applied literally. This form is often used to ensure that stray trailing spaces in lines are not included in the value set into the variable.
Note that the ^ seems ineffectual in the next set - because all it is doing is escaping the ) - and ) is NOT a special character in an ordinary SET
With the third version, the caret is included - but only one, because ^ escapes ^ and ) is an ordinary character.
Then we do all of the operations using the ) unadorned. Obviously attempting to re-create a directory is going to cause an error - but it's because the directory already exists, not because there's anything wrong with the command itself.
As demonstrated, the directory will be listed, so the PUSHD works correctly.
Finally, there's a method of setting a variable dynamically - possibly better set into another variable-name. This is useful where the variable may be read from a file or input from a user - that is, not specified literally.
Well - not quite finally. Two further quirks: First, % is not escaped by ^ but by %, and second, ECHO( appears to be the most flexible form of ECHO (where the character immediately following ECHO, normally space, but may be a number of others) - and doesn't participate in the statement-blocking mechanism.
( and ) are special characters to the shell, so you need to escape them. Cmd.exe's escape character is ^. So you can do the following:
set DIRWHERERUN=C:\UNIVERSITY\testSTABLEunstable^(WITHrandomBED^)
Bill
Use double quotes.
set "DIRwhereRUN=C:\UNIVERSITY\testSTABLEunstable(WITHrandomBED)"
PUSHD "%DIRwhereRUN%"

How to get a the directory path from a variable in a cmd batch file

Within my batch file I have a variable that contains a file path:
SET VAR1=C:\Folder1\Folder2\File.txt
I would like to extract on the directory structure and retreive:
C:\Folder1\Folder2\
I have read threads like this where I need to use %~dp0 where 0 I believe is passed as a parameter. I have tried %~dpVAR1 but that doesn't work. How can I get the output I'm looking for, but with a variable containing the file path?
Also, to make matters difficult, I have to perform all of this within an IF condition which means that once the variable is declared, I will need to refer to it with ! instead of % (I have declared setlocal enableextensions enabledelayedexpansion at the beginning of my script to allow for this).
Any help is much appreciated!
Thanks!
Andrew
You are attempting to use parameter expansion syntax on an environment variable - that cannot work. But it is relatively easy to do what you want.
Using a CALL (relatively slow):
(...
call :getPath "!var!" var
...
)
exit /b
:getPath
set "%2=%~dp1"
exit /b
Using FOR, assuming the variable does not contain any wildcards (fast)
(...
for %%F in ("!var!") do set "var=%%~dpF"
...
)
Using FOR, if the variable may contain wildcards (also fast)
(...
for /f "delims=" %%F in ("!var!") do set "var=%%~dpF"
...
)
Note 1: If the variable does not contain the full path, then all the solutions will attempt to resolve the name into an absolute path and will return the full absolute path. For example, if var contains foobar\test.txt, then the solutions will include the full path to the current directory, even if the file is not found. Something like c:\pathToCurrentDirectory\foobar\.
Note 2: All solutions above will remove all quotes from the path.
Note 3: A path could include the ! character, which will cause problems when expanding %~dp1 or %%~dpF because you have delayed expansion enabled. The delayed expansion will corrupt both ^ and ! if the value contains !. There is a solution that involves protecting both ! and ^. Here is a demonstration applied to the last solution above. The protection requires normal expansion, and since you are within a code block, it requires at least one CALL. It could be done without a subroutine, but it is easier with a subroutine. The subroutine assumes the variable is named var.
(...
call :getPath
...
)
exit /b
:getPath
set "var=!var:"=!"
set "var=!var:^=^^^^!"
set "var=%var:!=^^^!%" !
for /f "delims=" %%F in ("!var!") do set "var=%%~dpF" !
exit /b
I do believe (once again) many questions are on the same topic (string constraints, or splitting strings).
Instead of giving you the whole code, I'm going to give you a template and explain why %~dpVAR! didn't work.
Firstly, why %~dpVAR! did't work.
Before I get into modifiers, let's discuss parameters. You may know that batch files can parse parameters to each other. These parameters can be called by using a single percent sign (%) in front of the numbers 0-9. As far as I'm aware (someone might have made a way for more to be parsed), only 9 parameters can be parsed. You may think that is wrong (there's 10 parameters right?). Parameters 1-9 are parsed to the batch file (or function within one), %0 is the file path of the batch file (or function name). If you look, %~dp0 shares some (not really) resemblance to %0. This will be discussed below.
Secondly, the term %~dp0 has modifiers in it. Modifiers are things that modify variables (only in the case of parameters and those declared in for loops, you know the ones with double percent signs like %%i) and parameters. The modifier d expands the parameter to a drive letter only while p expands the parameter to a path only. You may think that these would contradict themselves, but parameters can be combined to create extremely wacky formats.
So, as you can see, you attempt at replacing 0 with your variable name failed because it's not specified for those sort of things.
Now, on to the template.
You can constrain variables (and put them into other variables) like this:
set variable=!variable:~offset,amount!
Don't worry if that seems confusing, I'm about to explain the components.
Firstly, notice that there is no /a switch. This is because this is not a mathematical function (don't really know why I added this). So, before I explain it, here's an example of what it would do to a variable name numbers that has the value of 0123456789.
set numbers=!numbers:~5,1!
By using that line of code, numbers would now equal 5. This is because it is recreating the variable with a smaller version of the original value (gee this is hard to explain). As you can see, there is a 5 where offset was on the template above. This is because it is skipping the first 5 characters and setting the variable as the next amount, or 1 character (I really hope you're getting this).
So basically, it sets a variable as a shorter value of a different (or the same) variable determined by the offset and the amount of characters to contain in it.
I really hope this helps because I probably wouldn't understand a word of this.
Can someone redirect this poor guy to a link explaining this better (I tried, ok!)?
Complete example of extracting paths from variable:
#echo off
set /p Fullpath="Specify full path: "
call :getPath %Fullpath% filename folder
echo %filename%
echo %folder%
pause
exit /b
:getPath
set "%2=%~nx1"
set "%3=%~dp1"
exit /b
Would this work:
SET VAR1=C:\Folder1\Folder2\File.txt
echo %var1%
Where Echo is the name of your exe.
%CD% may work as well: Echo %CD%

Resources