I am creating a script as a .cmd and have a command that renames a folder from "test" to "test.old" on some occasions there may already be a "test.old" that exists creating a duplicate error so I implemented a very redimentry workaround but am trying to figure out how to loop this command so it adds a variable number if a duplicate exists.
E.g If test1.old it renames to test2.old and if that exists then it renames to test3.old etc..
REN "%userprofile%\AppData\Local\test" "test.old"
if %errorlevel% == 0 goto :TESTNOERRORS if %errorlevel% == 1 goto :TESTDUP
:TESTDUP
REN "%userprofile%\AppData\Local\test" "test1.old"
REN "%userprofile%\AppData\Local\test" "test2.old"
:TESTNOERRORS
What would the most efficient way to do this in a .cmd script?
P.s I am very new to scripting in general as well as programming,
Thank you
You can use it :
REN "%userprofile%\AppData\Local\test" "test.old"
if %errorlevel% == 1 goto :TESTDUP
goto :END
:TESTDUP
REN "%userprofile%\AppData\Local\test" "test%it%.old"
if %errorlevel% == 0 goto :END
set /A it+=1
goto :TESTDUP
:END
If the first REN not work the loop try to rename the folder until it find an available name
Retrying the rename operation based on errorlevel is not a good idea because the rename can also be failed for variety of reasons other than duplicate name conflicts. for example when the source does not exist or when you have not sufficient permissions to rename the object or when the target is in use,...
A more general approach would be something like this
#echo off
setlocal EnableExtensions DisableDelayedExpansion
set "FSObjToRename=PathToFileOrFolder"
set /a "start#=1, maxRetries=10000, end#=start#+maxRetries"
set "q=""
for %%F in ("%FSObjToRename%") do (
set "SpawnCommand=for /L %%# in (%start#%,1,%end#%) do #if not exist %%q%%%FSObjToRename%.%%#.old%%q%% (ren %%q%%%FSObjToRename%%%q%% %%q%%%%~nxF.%%#.old%%q%% & exit)"
REM This is a more readable form of the SpawnCommand:
REM for /L %%# in (%start#%,1,%end#%) do #if not exist "%FSObjToRename%.%%#.old" (ren "%FSObjToRename%" "%%~nxF.%%#.old" & exit)
)
if exist "%FSObjToRename%" cmd /e:on /v:off /d /s /c "%SpawnCommand%"
The above code avoids :Labels and GOTOs completely by executing the rename loop in new instance of CMD.EXE so it can break the loop by exit command but if the string for SpawnCommand looks weird and confusing (mainly because of %%q%% to hide quotation marks from batch file parser) then for small number of retries, the loop can be rewritten like this
if exist "%FSObjToRename%" for %%F in ("%FSObjToRename%") do (
for /L %%# in (%start#%,1,%end#%) do (
if not exist "%FSObjToRename%.%%#.old" (ren "%FSObjToRename%" "%%~nxF.%%#.old" & goto :break)
)
)
:break
Using goto inside of FOR loop will prevent the execution of the loop body but does not break to loop iterator (It will blindly count to %end#%) so there would be a delay between executing goto :break and actually going to :break, by the amount which depends on the number of iterations
What is considered small for number of retries, completely depends on the performance of the computer. For today computers you may not notice a delay even for 100000 iterations. for small enough retries the latter will have slightly better performance than to launching new instance of cmd but the former is more robust in handling arbitrary retries.
Using backward GOTOs (when target Label is above the GOTO command) should be avoided whenever possible specially when the number of iterations have the potential be large and size of the batch script is not small.
Related
I need a oneliner to wait for a file until it is created.
Is there a way to write that as a windows batch command? I cant have GOTO in it because I use pushd.
something like:
IF NOT EXIST \\blablabal\myfile.txt sleep(30)
The solution below is a one-liner that should be executed at the command-prompt, as requested (a Batch file is not a "one-liner"):
cmd /C for /L %i in () do if not exist \\blablabal\myfile.txt (timeout /T 30 ^>NUL) else exit
If you wish, you may insert this line in a Batch file doubling the percent sign.
I'd suppose that you want to avoid goto statement and :label inside a parenthesized code block. Use call as follows:
(
rem some code here
call :waitForFile
rem another code here
)
rem yet another code here
rem next `goto` skips `:waitForFile` subroutine; could be `goto :eof` as well
goto :nextcode
:waitForFile
IF EXIST \\blablabal\myfile.txt goto :eof
TIMEOUT /T 30 >NUL
goto :waitForFile
:nextcode
However, if you need a oneliner to wait for a file until it is created, written as a windows batch script: save next code snippet as waitForFile.bat
#ECHO OFF
SETLOCAL EnableExtensions
:waitForFile
IF EXIST "%~1" ENDLOCAL & goto :eof
TIMEOUT /T 30 >NUL
goto :waitForFile
Use it as follows:
from command line window: waitForFile "\\blablabal\myfile.txt"
from another batch script: call waitForFile "\\blablabal\myfile.txt"
Be sure that waitForFile.bat is present in current directory or somewhere in path environment variable.
cmd /c "#echo off & for /l %z in () do (if EXIST c:\file.ext exit)"
Hammers the cpu though...
A working script that moves various files based on file name runs successfully. After the script is done it will check two directories for any lingering files using IF EXIST *.txt. This works great except I've noticed some files with no extension. These were not an issue before and since that cannot be helped due to processes out of my control, I need to amend my script.
My only idea is the following code. Bear with as there are two conditions:
:check1
PUSHD "\\UNC\path1" &&(
DIR /A-D *.
IF %errorlevel% NEQ 0 GOTO check2
) & POPD
:add1
ECHO Add note to the log file
:check2
PUSHD "\\UNC\path2" &&(
DIR /A-D *.
IF %errorlevel% NEQ 0 GOTO laststep
) & POPD
:add2
ECHO Add note to the log file
:laststep
Some other code before exiting
This should run DIR on the path and if files without extensions exist, it will have %errorlevel% zero and move on to the next check. If there are no files present, it will have %errorlevel% not zero (likely 1) and it will append some text to the log before the next check. Check two will do the same.
This seems to be awfully complicated and I am not able to find a "one-liner" solution that is as easy as IF EXIST. I realize I can use *. but that returns directories as well and may result in an incorrect %errorlevel%.
Updated Code
Where I normally set my variables, I also SET the two paths to run DIR against. This way they can be used more easily elsewhere and I bypass the UNC Path error I normally get - reasons for that are unknown to me. The updated file check, used only for files without an extension, is:
DIR %p1% /b /a-d|FIND /v "." && ECHO Found 1 >> %log%
DIR %p2% /b /a-d|FIND /v "." && ECHO Found 2 >> %log%
FINDSTR /I "Found" %log%
IF %errorlevel% EQU 0 GOTO stillthere
:nofiles
Some code
GOTO domore
:stillthere
Some code
:domore
Other code before exit
Thank you for the responses, I've learned from this.
Is this what you want to find?
dir /b /a-d |find /v "."
#ECHO OFF
SETLOCAL
:check1
PUSHD "u:\path1"
DIR /A-D *. >NUL 2>NUL
IF %errorlevel% EQU 0 ECHO Add note \path1 to the log file
POPD
PUSHD "u:\path2"
DIR /A-D *. >NUL 2>NUL
IF %errorlevel% EQU 0 ECHO Add note \path2 to the log file
POPD
:laststep
:: Some other code before exiting
GOTO :EOF
Your problems include:
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.
and you are potentially jumping out of a PUSHD/POPD bracket which would mean your POPD won't necessarily restore your starting directory.
(Note that I used u:\ rather than a server to suit my system)
You are already using && to verify PUSHD worked. You can do the same with your DIR /-D. I find it makes life easier. Also, you probably want to hide any error message if *. is not found, especially since that is the expected condition. I also hid the display of any files that might be found, but you can certainly get rid of that redirection. You might also want to hide error message if PUSHD fails, but I didn't implement that.
PUSHD "\\UNC\path1" && (
DIR /A-D *. 1>nul 2>nul && ECHO Add note to the log file
POPD
)
PUSHD "\\UNC\path2" && (
DIR /A-D *. 1>nul 2>nul && ECHO Add note to the log file
POPD
)
ECHO Some other code before exiting
So you know , when people write code they do it neat, best look and most understandable. but Is there an optimizer for batch? Could someone make one?
example-
it takes existing variables and replaces them with the shortest possible variants
set whatever=whatever
echo question
set /p answer=:
if %answer%=%whatever% whatever
and turns it into
set a=whatever
echo question
set /p b=:
if %b%=%a% whatever
so it basically shortens the variables , flags (or tags or whatever like :top) and does other things I cannot thing of to basically optimize everything.
There are certain constructs in Batch programs that slow down the execution. The sole construct that have a major impact for this point is assemble a loop via a GOTO instead of any type of FOR command. If a large program with many GOTO's is rewritten with FOR commands, an important time saving is expected. Another aspect that affect this point is the number of commands/lines a program have, that is, a program that get the same result than another one with less lines, will run faster. The way to achieve the same things with less commands is making good use of Batch file capabilities.
For example, this code:
set /A b=a+8
set /A c=b*2
set /A d=c+e
... run slower than this one:
set /A b=a+8, c=b*2, d=c+e
This code:
command-that-return-errorlevel
if %errorlevel% equ 1 goto label-1
if %errorlevel% equ 2 goto label-2
if %errorlevel% equ 3 goto label-3
... run slower than this one:
command-that-return-errorlevel
for %%e in (1 2 3) do if %errorlevel% equ %%e goto label-%%e
... and previous one run slower than this one:
command-that-return-errorlevel
goto label-%errorlevel%
Shorten the variable names have a very little impact in the execution speed.
This way, the best option is to write Batch files using these techniques from the very beginning. There is no easy way to develop a program that read a Batch file and perform the previous changes, that is, replace GOTO's with FOR and "compress" several lines in less ones.
The program below is just for fun!
#echo off
setlocal EnableDelayedExpansion
rem Get a list of current variable names
(for /F "delims==" %%v in ('set') do (
echo %%v
)) > varnames.txt
rem Call the Batch file, so it creates its variables
call "%~1"
rem Create an array with new variable names
set "_numVars="
set "_nextVar="
< varnames.txt (for /F "delims==" %%v in ('set') do (
if not defined _nextVar set /P _nextVar=
if "%%v" neq "!_nextVar!" (
set /A _numVars+=1
set "var[!_numVars!]=%%v"
) else (
set "_nextVar="
)
))
del varnames.txt
rem Rename the variables in a new file
setlocal DisableDelayedExpansion
(for /F "delims=" %%a in ('findstr /N "^" "%~1"') do (
set "line=%%a"
setlocal EnableDelayedExpansion
set "line=!line:*:=!"
if defined line (
for /L %%i in (1,1,%_numVars%) do (
for /F "delims=" %%v in ("!var[%%i]!") do set "line=!line:%%v=v%%i!"
)
)
echo(!line!
endlocal
)) > "%~1.new"
This program get all variables from the Batch file given in the parameter and renames they as "v#" with a sequential number. To use this program you must remove any setlocal command from the Batch file before pass it to this program. Of course, this detail may cause several pitfalls: although the called program have Delayed Expansion already Enabled, there is no way to activate any Disable/Enable delayed expansion that the Batch file may require in order to correctly run. Besides, the program changes all strings in the Batch file that are equal to any variable name, but even if this point could be fixed (and this is a big IF, because it requires to emulate the cmd.exe Batch file parser!) this program would still be useless: as I said in my other answer, renaming the variables in a Batch file have a very little effect on its performance. I just wrote it for fun!
I'm fairly new to batch scripting, and I need to write a pretty simple .bat file that will loop through a directory. I know how to do it pretty easily using the goto command:
#echo off
:BEGIN
::set variable to data in first file
::do operations on file...
IF ::another file exists in the directory
::increment to next file
GOTO BEGIN
ELSE
GOTO END
:END
cls
The problem is that's the only way I can think of to do it. I know goto's are generally very frowned upon to use, so I was wondering if anyone knows another way to do this? Thanks!
Replace the echo.... with your desired command.
From the Command prompt:
for /R %A in (*.*) do echo.%A
In a bat file
for /R %%A in (*.*) do echo.%%A
It can be done just for the current folder too. The %%a metavariable is case sensitive and I choose to use lower case. The script will exit and quit when all files are processed.
#echo off
for %%a in (*.txt) do (
type "%%a"
pause
)
In a DOS Batch File subroutine, how can I turn off echo within the subroutine, but before returning, put it back to what it was before (either on or off)?
For example, if there was a command called echo restore, I would use it like this:
echo on
... do stuff with echoing ...
call :mySub
... continue to do stuff with echoing ...
exit /b
:mySub
#echo off
... do stuff with no echoing ...
echo restore
goto :EOF
My first attempt was an utter failure - thanks jeb for pointing out the errors. For those that are interested, the original answer is available in the edit history.
Aacini has a good solution if you don't mind putting your subroutine in a separate file.
Here is a solution that works without the need of a 2nd batch file. And it actually works this time! :)
(Edit 2 - optimized code as per jeb's suggestion in comment)
:mysub
::Silently get the echo state and turn echo off
#(
setlocal
call :getEchoState echoState
echo off
)
::Do whatever
set return=returnValue
::Restore the echo state, pass the return value across endlocal, and return
(
endlocal
echo %echoState%
set return=%return%
exit /b
)
:getEchoState echoStateVar
#setlocal
#set file=%time%
#set file="%temp%\getEchoState%file::=_%_%random%.tmp"
#(
for %%A in (dummy) do rem
) >%file%
#for %%A in (%file%) do #(
endlocal
if %%~zA equ 0 (set %~1=OFF) else set %~1=ON
del %file%
exit /b
)
If you are willing to put up with the slight risk of two processes simultaneously trying to access the same file, the :getEchoState routine can be simplified without the need of SETLOCAL or a temp variable.
:getEchoState echoStateVar
#(
for %%A in (dummy) do rem
) >"%temp%\getEchoState.tmp"
#for %%A in ("%temp%\getEchoState.tmp") do #(
if %%~zA equ 0 (set %~1=OFF) else set %~1=ON
del "%temp%\getEchoState.tmp"
exit /b
)
The simplest way is to not turn echo off in the first place.
Instead, do what you currently do with the echo off line to the rest of your subroutine - prefix all commands in the subroutine with an # sign. This has the effect of turning off echo for that command, but keeps the echo state for future commands.
If you use commands that execute other commands, like IF or DO, you will also need to prefix the "subcommand" with an # to keep them from being printed when echo is otherwise on.
The easiest way is to extract the subroutine to another .bat file and call it via CMD /C instead of CALL this way:
echo on
... do stuff with echoing ...
cmd /C mySub
... continue to do stuff with echoing ...
exit /b
mySub.bat:
#echo off
... do stuff with no echoing ...
exit /b
This way the echo status will be automatically restored to the value it had when the CMD /C was executed; the only drawback of this method is a slightly slower execution...
Here is a straight forward solution that relies on a single temporary file (using %random% to avoid race conditions). It works and is at least localization resistant, i.e., it works for the two known cases stated by #JoelFan and #jeb.
#set __ME_tempfile=%temp%\%~nx0.echo-state.%random%.%random%.txt
#set __ME_echo=OFF
#echo > "%__ME_tempfile%"
#type "%__ME_tempfile%" | #"%SystemRoot%\System32\findstr" /i /r " [(]*on[)]*\.$" > nul
#if "%ERRORLEVEL%"=="0" (set __ME_echo=ON)
#erase "%__ME_tempfile%" > nul
#::echo __ME_echo=%__ME_echo%
#echo off
...
endlocal & echo %__ME_echo%
#goto :EOF
Add this preliminary code to increase the solution's robustness (although the odd's are high that it's not necessary):
#:: define TEMP path
#if NOT DEFINED temp ( #set "temp=%tmp%" )
#if NOT EXIST "%temp%" ( #set "temp=%tmp%" )
#if NOT EXIST "%temp%" ( #set "temp=%LocalAppData%\Temp" )
#if NOT EXIST "%temp%" ( #exit /b -1 )
:__ME_find_tempfile
#set __ME_tempfile=%temp%\%~nx0.echo-state.%random%.%random%.txt
#if EXIST "%__ME_tempfile%" ( goto :__ME_find_tempfile )
I wasn't really happy with the solution above specially because of the language issue and I found a very simple one just by comparing the result from current echo setting with the result when explicitly set OFF. This is how it works:
:: SaveEchoSetting
:: :::::::::::::::::::::::::::
:: Store current result
#echo> %temp%\SEScur.tmp
:: Store result when explicitly set OFF
#echo off
#echo> %temp%\SESoff.tmp
:: If results do not match, it must have been ON ... else it was already OFF
#for /f "tokens=*" %%r in (%temp%\SEScur.tmp) do (
#find "%%r" %temp%\SESoff.tmp > nul
#if errorlevel 1 (
#echo #echo on > %temp%\SESfix.bat
) else (
#echo #echo off > %temp%\SESfix.bat
)
)
::
:: Other code comes here
:: Do whatever you want with echo setting ...
::
:: Restore echo setting
#call %temp%\SESfix.bat
I was looking for the same solution to the same problem, and after reading your comments I had an idea (which is not the answer to the question, but for my problem is even better).
I wasn't satisfied with the cmd.exe /c mysub.cmd because it makes hard or even impossible to return variables (I didn't check) - (couldn't comment because it's the first time I post here :)
Instead noticed that all we want -in the end- is to suppress stdout:
echo on
rem call "mysub.cmd" >nul
call :mysub >nul
echo %mysub_return_value%
GOTO :eof
:mysub
setlocal
set mysub_return_value="ApplePie"
endlocal & set mysub_return_value=%mysub_return_value%
GOTO :eof
It works fine with labelled subroutines, with subroutines contained in .cmd files, and I suppose it would work fine even with the cmd.exe /c variant (or start).
It also has the plus that we can keep or discard the stderr, replacing >nul with >nul 2>&1
I note that ss64.com scares kids like me stating that with call "Redirection with & | <> also does not work as expected".
This simple test works as expected. He must have been thinking of more complex situations.