When writing scripts in Windows batch files, sometimes the proper execution of the script requires the use of the setlocal command. My main complaint about using setlocal is that I'm often performing complicated for & if statements in which I set variable values in that section of code. These settings are lost when I issue the command endlocal.
So far I've worked around this by echoing the variable value into a scratch file within the setlocal segment and then read the value back into the variable after the endlocal. However, it seems like there should be a more elegant solution to the problem.
The suggested answer provides a handy way to circumvent the issue if only using one or multiple set statements. However, the linked answer does not provide a solution when the setlocal is in place to allow a for loop to properly expand variable names at time of execution rather than parsing. In my situation, I also have if statement logic tree to perform further checking on that information to set many different possible variable values. The linked solution does not provide a solution to this situation.
This code is supposed to check an install package for its version number. It is called from another script that requires that version number to function correctly. We know the package is named using the form [application][version number].msi. The [version number] can be any of the following:
7
7.5
9
9.5
10
10.1
It is possible that more than one install package exists in the designated directory so it's important to look through them all and select the highest version in the directory.
I inherited and expanded the code to correctly process the 10 & 10.1 versions. If there's a better way to do this without the setlocal (e.g. I've thought of rewriting to use a case statement) I'd love to see a better solution.
However, I'm still interested in learning how to pass variables out of the setlocal / endlocal segment.
setlocal enabledelayedexpansion
for /f %%a in ('dir /b program*.MSI') do (
set FileName=%%a
set tst1=!FileName:~4,3!
if "!tst1!" == "msi" (
rem Only true if it has a 1 digit number (e.g. "9")
set MAIN_VERSION=!FileName:~2,1!
) else (
set tst2=!FileName:~5,3!
if "!tst2!" == "msi" (
rem Only true if it has a 2 digit version number (e.g. "10")
set MAIN_VERSION=!FileName:~2,2!
) else (
... lots more code ...
)
)
)
rem Write results out to a file for temporary storage. This particular
rem form of echo is required to ensure there are no trailing spaces, CR,
rem or LF
echo|set /P ="!MAIN_VERSION!" > %USERPROFILE%\UGV.txt
rem End local variables
endlocal
rem Read the correct version out of our temporary storage file
set /p MAIN_VERSION=<%USERPROFILE%\UGV.txt
How do I pass a variable (such as MAIN_VERSION above) out of a Windows batch script setlocal / endlocal code segment without using a scratch file?
To preserve variables over the setlocal/endlocal scope, there exists different solutions.
It depends of the situation which one you should use.
1) Simple
Works only outside of parenthesis blocks with simple content, but can produce problems with special characters like !^".
setlocal
set localVar=something simple
...
(
endlocal
set "out=%localVar%"
)
set out
2) Medium
Works also in blocks and can handle the most characters, but can fail with !^ and linefeed/carriage return
if 1==1 (
setlocal EnableDelayedExpansion
set "localVar=something medium nasty & "^&"
for /F "delims=" %%V in ("!localVar!") DO (
endlocal
set "out=%%V"
)
)
set out
3) Advanced
Works for all contents in any situation
SO: preserving exclamation marks in variable between setlocals batch
Related
I'm trying to figure out how to create a certain number of folders in a Windows batch file. I want to ask the user how many folder they want, and then use that collected number to loop the asking for names of those folders and make them. Here is what I have so far:
pushd C:\Users\%username%\Desktop
set /p FolderLoop="How many folders?: "
for /l %%x in (1, 1, %FolderLoop%) do (
set /p folder="Folder: " %%x
md %folder% %%x
)
The problem I keep having is that I can not make the folders with the proper collected names. The closest I have gotten so far is creating the right amount of folders, but with sequential numeric names (1,2,3, etc.) based om the FolderLoop variable.
You need to read any of the hundreds of responses on SO with regard to delayedexpansion.
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).
Within a block statement (a parenthesised series of statements), REM statements rather than the broken-label remark form (:: comment) should be used because labels terminate blocks, confusing cmd.
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.
Note therefore the use of CALL ECHO %%var%% which displays the changed value of var. CALL ECHO %%errorlevel%% displays, but sadly then RESETS errorlevel.
You need delayed expansion:
setlocal enabledelayedexpansion
pushd C:\Users\%username%\Desktop
set /p FolderLoop="How many folders?: "
for /l %%x in (1, 1, %FolderLoop%) do (
set /p "folder=Folder: "
md "!folder!"
)
(the %%x after set /p does nothing. I removed it. I also removed it from md, you don't need it - except you want the counter be part of the foldername.)
There are a handful of questions on SO that look similar, but I cannot figure out some behaviour and I am looking for help.
Below is a snippet from a batch file I am trying to write which will load in a set of directories and potentially replace letter substitutions with an expanded path, e.g. the properties file might look like:
location1=C:\Test
location2=[m]\Test
Where location1 points to C:\Test and location2 points to C:\Program Files(x86)\MODULE\Test, because [m] is a shorthand to C:\Program Files(x86)\MODULE.
The batch script, to this point, is simply trying to read in the list of file paths and expand/replace the [m].
SET build.dir=%~dp0%
SET progfiles=%PROGRAMFILES(X86)%
IF "%progfiles%"=="" SET progfiles=%ProgramFiles%
SET local.properties=%build.dir%local.properties
SETLOCAL ENABLEDELAYEDEXPANSION
FOR /F "tokens=1* delims==" %%i IN (%local.properties%) DO (
SET local.dir=%%j
SET local.dir=!local.dir:[m]=%progfiles%\MODULE!
echo !local.dir!
)
ENDLOCAL
Running this kicks out an error:
\MODULE was unexpected at this time.
If I replace the FOR with the following instead:
set test="[m]\Proj\Dir"
set test=!test:[m]=%progfiles%\MODULE!
echo %test%
I get the desired C:\Program Files(x86)\MODULE\Proj\Dir printed out...so I'm confused why it works fine outside of the FOR loop.
My understanding about delayed expansion is that it 'expands' at runtime...which you get to happen using !! instead of %% wrapped around the variable. Furthermore, as I'm creating the local.dir variable inside the FOR loop scope, I must use delayed expansion in order to access it with the updated value for the iteration.
I feel like the problem is using %progfiles%, like there's some special syntax I need to use in order to make it work but nothing is adding up for me. When I echo %progfiles%, it prints out as C:\Program Files(x86 -- note the missing trailing ).
Any ideas? Thanks
Tested suggestion:
D:\Projects\Test\Build>test
*** "D:\Projects\Test\Build\local.properties"
*** "","C:\Program Files (x86)"
[m]=C:\Program Files (x86)\MODULE
Adding quotes around the whole expression makes it work -- can't use other characters for some reason (like []) -- and since I want to append to the path later, we can safely remove the quotes afterwards:
SET local.dir="!local.dir:[m]=%progfiles%\MODULE!"
SET local.dir=!local.dir:"=!
Test this to see if you can nut out the issue:
The double quotes are to provide robust handling in a system with long file/path names.
The () are unquoted which are a problem in a batch script, when inside a loop.
#echo off
SET "build.dir=%~dp0%"
SET "progfiles=%PROGRAMFILES(X86)%"
IF "%progfiles%"=="" "SET progfiles=%ProgramFiles%"
SET "local.properties=%build.dir%local.properties"
echo *** "%local.properties%"
SETLOCAL ENABLEDELAYEDEXPANSION
FOR /F "usebackq tokens=1* delims==" %%i IN ("%local.properties%") DO (
SET "local.dir=%%j"
echo *** "!local.dir!","%progfiles%"
SET "local.dir=!local.dir:[m]=%progfiles%\MODULE!"
echo !local.dir!
)
ENDLOCAL
pause
It has to do with the () characters that end up in your progfiles string. If you take them out, the substitution seems to work fine.
My suggestion is to ditch command for this particular purpose and use one of the other standard tools that Windows comes with. While my personal preference would be Powershell (since it's so much more powerful and expressive), you may just need something quick that you can integrate into existing cmd.exe stuff.
In that case, try the following VBScript file, xlat.vbs:
set arg = wscript.arguments
wscript.echo Replace(arg(0),arg(1),arg(2))
Your batch file then becomes something like, noting the inner for /f which captures the output of the VBS script and assigns it to the variable:
#echo off
SET build.dir=%~dp0%
set progfiles=%PROGRAMFILES(X86)%
if "%progfiles%"=="" set progfiles=%ProgramFiles%
set local.properties=%build.dir%local.properties
setlocal enabledelayedexpansion
for /f "tokens=1* delims==" %%i in (%local.properties%) do (
set local.dir=%%j
for /f "delims=" %%x in ('cscript.exe //nologo xlat.vbs "!local.dir!" "[m]" "%progfiles%\MODULE"') do set local.dir=%%x
echo !local.dir!
)
endlocal
Running that, I get the output:
C:\Test
C:\Program Files (x86)\MODULE\Test
which I think is what you were after.
I've been working on creating a batch file which moves files from one dir to another dir and if the filename already exists rename it then move it over.
I'm really new to creating batch files so heres what I have so far
set temp=C:\Users\Daniel\Desktop\a\a1
set dir=C:\Users\Daniel\Desktop\a\
set /a "counter=0"
set "duplicate=-copy^("
set "bracket=^)"
if exist "%temp%" ( ^
for %%i in (%temp%\*) ^
do ^
if exist "%dir%\%%~ni%%~xi" ( call :checkFileName %%~ni %%~xi) ^
ELSE ( move %temp%\%%~ni%%~xi %dir% ) )^
ELSE ( echo doesnt exist)
:checkFileName
echo test
set fileName=%1
set fileExtenstion=%2
set /a "counter+=1
rem Do whatever you want here over the files of this subdir, for example:
if exist %dir%%fileName%%duplicate%%counter%%bracket%%fileExtenstion% ( IF defined %1 (
IF defined %2 (call :checkFileName %1 %2 )) ) ELSE (ren %temp%\%fileName%%fileExtenstion% %fileName%%duplicate%%counter%%bracket%%fileExtenstion% )
timeout 30
goto :eof
:increment
set /a "counter+=1"
goto :eof
I've no idea to increment a var before calling my checkFileName function. I think recursively calling the same function is the right idea but I'm a bit rusty with the commands/syntax as I only started this on friday.
Any advice or pointers would be appreciated. (If you know any useful links/books that are worth a look let me know!)
timeout 600
#ECHO OFF
SETLOCAL
set "tempdir=C:\Users\Daniel\Desktop\a\a1"
set "dir=C:\Users\Daniel\Desktop\a"
set "tempdir=U:\sourcedir\t w o"
set "dir=U:\destdir"
set "duplicate=-copy("
set "bracket=)"
if exist "%tempdir%" (
for %%i in ("%tempdir%\*") do (
if exist "%dir%\%%~nxi" ( call :checkFileName "%%~ni" "%%~xi"
) ELSE (
move "%tempdir%\%%~nxi" "%dir%" >nul
)
)
) ELSE (
echo doesnt EXIST
)
GOTO :eof
:checkFileName
set "fileName=%~1"
set "fileExtenstion=%~2"
set /a counter=0
:nexttry
set /a counter+=1
rem Do whatever you want here over the files of this subdir, for example:
if exist "%dir%\%fileName%%duplicate%%counter%%bracket%%fileExtenstion%" GOTO nexttry
move "%tempdir%\%fileName%%fileExtenstion%" "%dir%\%fileName%%duplicate%%counter%%bracket%%fileExtenstion%" >nul
goto :eof
Here's a revised version. I'll explain the changes I've made:
#echo off turns off command-echoing
setlocal ensures any changes made to the environment are backed-out when the procedure ends.
I've added to extra sets to re-set the directories to suit my system. You'd need to delete these two lines for yours.
temp is a special name which points to a temporary directory. One of quite a few. Best not to use that particular name - replaced with tempdir
set when used for a numeric set doesn't require quotes. In a string-set, the syntax set "var=value" is used to ensure that trailing spaces on the command-line are not included into the value assigned (which can cause chaos - spaces are sort of - invisible.) Note that in a string set, spaces on both sides of the = are significant...
I prefer to assign directorynames into variables without the trailing \. This allows the value to be extended with the least gymnastics. Personal preference - but you used it both ways...
The carets are not required before ( and are only required before ) where the syntax would close an open parenthesis (ie. in a parenthesised statement-sequence as may occur in an if, else or do.) Used arbitrarily, this can lead to stray literal carets in filenames, for instance.
carets at end-of-line is a valid but easily-lost and a little-used technique. The rule for breaking statements over multiple lines is crudely, keep do, if or else on the same physical line as its ( and else on the same physical line as the closing-parenthesis that precedes it. Then no eol-caret is required.
Batch simply charges on through statements. It has no concept of the end of a procedure and needs to be told when the procedure ends. This can be done with a goto :eof statement (which jumps to the physical end-of-file) or an exit /b statement (which returns from a subroutine, optionally setting errorlevel. goto :eof effectively does the smae thing in most circumstances and is way more common.)
%%~nxi means the name-and-extension of the file %%i. Of course, it's quite legal to use %%~ni and its counterpart individually, but it's not necessary. Note however that these parts should be despatched in "quotes" to the subroutine because each part may contain spaces. "quotes"make a spaces-containing-string appear as one string with spaces rather than a series of strings.
>nul redirects the move command's report "1 file(s) moved" to the bit-bucket.
Setting the two variables within checkfilename should be done after removing the quotes applied in the call - that's the purpose of the ~ before the parameter-number.
counter can be set to zero, then incremented.
If the proposed new filename exists, then simply increment the number and try again until you hit a name that doesn't exist. Yes - counter will run out eventually. It tops out at 2**31-1. Might take a while...
Note the use of quotes in the if exist and move. This is to guard against spaces in file/directorynames. The same goes for the for %%i in ("%tempdir%\*") used earlier...you may notice that in my testing, I used (deliberately) a directoryname that contained spaces. As it happens, the filenames I used also had spaces in them.
One last warning - There is no doubt that some odd filenames may choke on these procedures, but they should be few and far between. Filenames containing carets may be a problem, for instance.
Welcome to batch!
Unless this is a learning project, I recommend you study the XCOPY command.
I'm currently working on a mass user creation script through PowerShell and Batch. At the moment the script is 95 lines and is the largest script I've ever written in Batch.
I want the script to be as automated as possible and plan to give it to clients that need help creating a mass number of users. To do this, I have a 21 line settings file and one of the variables that I need is the full domain name (This is needed for dsadd)
The problem with this is that users may have any number of variables for this - anywhere from two in testlabs to four in places like schools. I am so far able to separate the tokens however I need them all stored as variables like %%a, %%b and not to store everything as %%a. The number of tokens will be dynamic so I need some sort of solution to this. Something like this (Yes I know this is not the correct syntax):
if number of tokens=4 (
dsadd "cn=%%a,ou=%%b,dc=%DSuffix1%,dc=%DSuffix2%,dc=%DSuffix3%,dc=%Dsuffix4% )
In that line %%a and %%b are variables in another for loop later in the code that reads from a user list excel file. I would need something like that for anything from two tokens to four tokens. I don't mind if the solution to this is not purely BATCH however that is my preferred option.
Thanks.
EDIT: Here is the for loop I have at the moment, this is nested in another larger for loop that adds the users:
for /f "tokens=1,2,3,4 delims=." %%a in (Settings.ini) do (
set L=22
if !L!=22 set DSuffix1=%%a&& set DSuffix2=%%b&& set DSuffix3=%%c&& set DSuffix4=%%d
)
The settings.ini file contains various settings such as the Exchange server and path to the user's home directory. The line I need to interpret is line 22 which looks like this:
DOMAINNAME.SUFFIX(.SUFFIX.SUFFIX.SUFFIX)
A real life example would be:
testlab.local
or
testlab.ghamilton.local
For a testlab the setting should only be the domainname and suffix although for others such as schools or institutions the number of domain suffixes can go up to four. I want to interpret this.
EDIT: Managed to indent code correctly, sorry.
If I understand you right, you have a string such as domain.foo.bar.baz and you want to change that to dc=domain,dc=foo,dc=bar,dc=baz.
How about this?
set domain=domain.foo.bar.baz
echo dc=%domain:.=,dc=%
That should echo this:
dc=domain,dc=foo,dc=bar,dc=baz
That %domain:.=,dc=% line is expanding the %domain% environment variable, but replacing all . with ,dc=. See http://ss64.com/nt/syntax-replace.html for more details.
To do string replacement in an environment variable, you do need the value to be in an environment variable first. Here's a script that will read line 22 from settings.ini, combined with the above technique:
setlocal enabledelayedexpansion
set L=0
for /f %%a in (settings.ini) do (
set /a L=!L! + 1
if !L! equ 22 (
set domain=%%a
echo dc=!domain:.=,dc=!
)
)
L is being used to count what line we're on. When we reach line 22, we set an environment variable to the value of the entire line (%%a), then do the string substitution as above (but with ! rather than % to take advantage of delayed expansion).
Of course instead of an echo, you would do something like
dsadd "cn=%cn%,ou=%ou%,dc=!domain:.=,dc=!"
Here's my little snippet to sort of demonstrate how to get the separate tokens (the delims are space and tab, but you can change them).
#echo off & setlocal enabledelayedexpansion
for /f "tokens=1,2,3,4" %%a in (test.txt) do (
set token1=%%a
set token2=%%b
set token3=%%c
set token4=%%d
set full=%%a%%b%%c%%d
)
set token
set full
pause>nul
test.txt contains:
first second third
The output was:
token1=first
token2=second
token3=fourth
full=firstsecondthird
So, you could judge how many tokens there is by something like
for /l %%i in (4,-1,1) do if not defined token%%i set amount=%%i
Or something along the same lines.
Hope that helps.
How would I achieve this:
for i in *.e; do mv $i ${i%-b*.e}.e; done
in a Windows batch file? (It renames files containing "-b" to the part before "-b". Note that this is not necessarily the end of the string! e.g. "file-b-4.e" will become "file.e")
If you really want to do this in batch, this should work
#echo off
setlocal disableDelayedExpansion
for %%F in (*.e) do (
set "var=%%~F"
setlocal enableDelayedExpansion
set "var=!var:-b=.e:!"
for /f "eol=: delims=:" %%A in ("!var!") do (
endlocal
echo ren "%%F" "%%A"
)
)
Edit
The comment by panda-34 alluded to the fact that the original posted code failed if the file name begins with -b. The code above was fixed by incorporating the extension into the replacement string. (thanks panda-34 for alerting me to the problem)
panda-34 also provided an alternate solution that uses command injection with search and replace. The injected command is the REM statement.
The panda-34 solution works as long as the file name does not contain & or ^ characters, but fails if it does.
Below is a modified version of the command injection technique that should work with all valid Windows file names. There are 2 critical mods, 1) make sure the special chars in the file name are always quoted, and 2) do not pass the value as a CALL argument, otherwise ^ will be doubled to ^^.
#echo off
setlocal disableDelayedExpansion
for %%i in (*-b*.e) do (
set old="%%~ni"
call :ren_b
)
exit /b
:ren_b
set v=%old:-b=.e"&rem "%
echo ren "%old:~1,-1%.e" %v%
exit /b
Final Edit (I hope):
As baruch indicates in his comment, the solutions above remove starting with the 1st occurance, whereas the original bash command removes starting with the last occurance.
Below is a version that should be an exact equivalent of the original bash command.
#echo off
setlocal disableDelayedExpansion
set "search=-b"
for %%A in (*%search%*.e) do (
set "old=%%A"
setlocal enableDelayedExpansion
set "new=\_!old:%search%=\_!"
for %%B in ("!new!") do (
endlocal
set "new=%%~pB"
setlocal enableDelayedExpansion
set "new=!new:~2,-1!.e"
echo ren "!old!" "!new:\_=%search%!"
endlocal
)
)
Simple, really
for %%i in (*-b*.e) do call :ren_b %%~ni
goto :eof
:ren_b
set v=%*
set v="%v:-b=.e" ^& rem %
ren "%*.e" %v%
Here's a variant to keep the name till the last -b occurence
setlocal enabledelayedexpansion
for %%i in (*-b*.e) do (
set v=%%~ni
set v=!v:-b=\!
for %%j in ("\!v!") do (
set v=%%~pj
set v=!v:~1,-1!
set v=!v:\=-b!
ren "%%i" "!v!.e"
)
)
It will fail for names containing ! and starting with -b.
P.S, Didn't see, dbenham already provided the equivalent solution, probably with more provisions for terminal cases of file names.
Forget it, some convenient things cannot be done in NT scripting. What you are asking here is not possible to my knowledge. And I've written and maintained complex NT scripts bigger than 50 KiB, using all kinds of tricks. The book "Windows NT Shell Scripting" points out many of these, for the same and more see Rob van der Woude's scripting pages.
I reckon you could do part of this, but certainly not in a one-liner due to how variable expansion works in NT scripting. For example you could extract the part of the string that you expect to be -b and check whether it is -b, then extract the other parts and rename from the original name to the one that is comprised of only the extracted parts.
But you'll likely need ten to fifteen lines to achieve that. In that light, consider using a different scripting language for the purpose. Especially if this is a modern Windows version.
I realize this is not the desired answer (i.e. that this is possible and a sample), but cmd.exe is very limited compared to Bash, albeit by far not as limited as some opponents of traditional batch scripting are pointing out.