Batch file randomly creates undeletable twin of intended file - batch-file

I'm fumbling around in the dark a little, so please bear with me if I've missed something basic.
We needed to implement a new project management process in our department. So I created a batch file to ask for the name of a project, then copy a template file structure and rename various files within them to the name supplied, then open a word document. The initial catch was that this all had to be done on a network drive. Fairly straightforward stuff in theory, however for one of my colleagues it occasionally does something quite irritating. Instead of creating one copy of the file structure, it creates 2. This second one that it creates is unaccessible, and also undeletable. I can't replicate the error on my own laptop, and happens roughly 1 in 3 times. I can't work out why it would create this second file, or why on earth it would be unremovable once created. Can anyone shed any light?
Here's the code of the file I'm using:
#echo off
pushd $networkdrive\folder\Projects
SET initDir=$networkdrive\folder\Projects
SET newDir=
SET /p ver=<"$networkdrive\folder\projectnum.txt"
SET /P newDir=Please enter the title of the new project: %=%
IF DEFINED newDir (
MD "$networkdrive\folder\Projects\%ver%. %newDir%\"
SET initDir="%initDir%\%ver%. %newDir%\"
)
xcopy "$networkdrive\folder\Creative Briefing Template" %initDir% /s/e
ren %initDir%"1. Campaign Initiation"\"Campaign Initiation Document.docx" "%newDir% Initiation Document.docx"
ren %initDir%"2. Campaign Development"\"Project Finances.xlsx" "%newDir% Project Finances.xlsx"
ren %initDir%"2. Campaign Development"\"1. Briefing"\"Creative Briefing Document.docx" "%newDir% Creative Briefing Document.docx"
ren %initDir%"2. Campaign Development"\"1. Briefing"\"Integrated CRM brief.xlsx" "%newDir% CRM Brief.xlsx"
ren %initDIR%"3. Final Assets and Sign Off Document"\"Sign off sheet.docx" "%newDir% Sign off sheet.docx"
SET /a newVer=%ver%+1
echo %newVer% > "$networkdrive\folder\projectnum.txt"
%initDir%"1. Campaign Initiation"\"%newDir% Initiation Document.docx"
popd
cls

Since it's not possible to reproduce your problem outside of your environment, I'd suggest a few changes:
Insert a setlocal command immediately after the #echo on line so that any variable changes within the routine are discarded when the routine ends.
Change your set syntax so that it follows the set "var=value" formula. This will mean that you don't have crazy amounts of " characters to balance - just one before %var% and one at the end of the required string. Personally, I'd also remove the \\ as the last character of the directoryname and insert it as required when you use the variable.
When you set ver (a logical but inadvisable name since ver is an inbuilt command) you should immediately write the new version number out so that if the user is interrupted (phone call, coffee, being "managed") then there's the least possibility that the current number is acquired by another user (before it's updated by the current one). This would probably be best managed by
(for /f %%a in (%filename%) do set whatever=%%a&set /a newnum=%%a+1&call echo %%newnum%%>%filename%)
Finally, consider what will happen if newdir is empty or contains illegal/unexpected characters.

Related

How to delete a .bat file in the same folder as the executed .bat file (without deleting the executed one)?

While making a batch file, I thought of adding a version number to the name so new versions don't get replaced by old. What I want the new version to do is delete all the old ones, so as an example lets take a folder named apps and in it are Test (v1).bat, Test (v2).bat & Test (v3).bat. What I want Test (v3).bat to do before anything else is delete Test (v1).bat & Test (v2).bat, I know what you might be thinking at this point:
Why don't you just add the lines ( del "Test (v1).bat" ) & ( del "Test (v2).bat" )?
That is not what I want, because lets say after a while I make Test (v4).bat and want it to delete all previous versions if they exist. Obviously it wont be very optimized if I will constantly have to add & not forget to add del "Test (v№).bat". If I do something like this del "*.bat" then all files with a .bat extension will be deleted, even the currently executed one, so that doesn't help. Also I want it so if a old version is executed, then it wont delete a newer version (if Test (v2).bat is executed don't delete Test (v3).bat, Test (v4).bat, etc., but delete Test (v1).bat)
It is dependent on having a rigorous naming standard that won't be co-opted by someone (such assumptions are at the heart of many security and prank attacks, so be careful).
This one is written assuming the current batch file already conforms to the naming standard implied by your question. I did not code a safety into this to avoid a suicidal rampage in the event that the file is not compliant; you can do that for yourself. :-)
Also, the goto ENDIT in this case is superfluous but in case you had other code to add, or subroutines, or what have you, this is a safety best-practice I do in all my batch files. You can remove both the goto ENDIT and the :ENDIT labels for a miniscule performance improvement.
Change the echo del to del once you have confirmed this thing isn't going to kill you in unexpected ways.
#echo off
set PUR_CURFNM=
for /F "usebackq tokens=*" %%F in ('%0') do set PUR_CURFNM=%%~F
for %%F in ("%PUR_CURFNM%") do set PUR_FULFNM=%%~dpnxF
for %%F in ("%PUR_CURFNM%") do set PUR_BASBNM=%%~dpnF
set PUR_BASFNM=
for /F "tokens=1 delims=(" %%A in ("%PUR_BASBNM%") do set PUR_BASFNM=%%A
for %%F in ("%PUR_BASFNM%(v*).bat") do if /I "%%F" NEQ "%PUR_FULFNM%" echo del "%%F"
goto ENDIT
:ENDIT
set PUR_FULFNM=
set PUR_CURFNM=
set PUR_BASBNM=
set PUR_BASFNM=
I'm with #Compo, though; this is a kind of odd request. Have fun with it. :-)

Using xcopy or copy for a single file from multiple folders

So in the batch script I'm building I am taking a single file from a folder, copying it over to a destination folder, and renaming it based on the number of times that the script has been looped. Essentially I need to take a file that's named the samething from a bunch of different folders spread across multiple computers at times and copy them into a new folder to work with. I've read up on xcopy and copy as that seemed like the thing to use but I haven't been able to find anything that lets me tell it to only copy over a single named file. I've posted what I have so far for the script below with commented lines for the sections I haven't figured out:
ECHO off
SETLOCAL enabledelayedexpansion
ECHO Note: Your combined permission list cvs can be found in the desktop folder
SET /A #=-1
:start
SET /A #+=1
:again
ECHO Please input the file path to the permissionoutput.txt
SET /p permissionoutputpath=
SET "sourcefolder=%permissionoutputpath%"
SET "destinationfolder=C:\Users\kayla\Desktop\HOLDER-CombinedPermissionsLists"
IF not exist "%sourcefolder%\permissionoutput.txt" Echo file not found&goto again
copy "%sourcefolder%\permissionoutput.txt" "%destinationfolder%\permissionoutput%#%.txt"
ECHO Add another file to combine: y or n?
SET /p addanotherfile=
if %addanotherfile%==y goto :start
UPDATE: Code corrected with answer to be fully functional for use as a reference
SET /A #=-1
:start
SET /A #+=1
:again
ECHO Please input the file path to the permissionoutput.txt
SET /p permissionoutputpath=
SET "sourcefolder=%permissionoutputpath%"
SET "destinationfolder=C:\Users\kayla\Desktop\HOLDER-CombinedPermissionsLists"
IF not exist "%sourcefolder%\permissionoutput.txt" Echo file not found&goto again
copy "%sourcefolder%\permissionoutput.txt" "%destinationfolder%\permissionoutput%#%.txt"
ECHO Add another file to combine: y or n?
SET /p addanotherfile=
if /i "%addanotherfile%"=="y" goto start
# is a legitimate variable-name. It's initialised to -1 then incremented on each loop through :start so the first value it will have when it's used is 0. (If you want to start at 1 just initialise it to 0 instead)
Next - your sets - BUT spaces are significant in a string set command are would be included in the variablename/value assigned if present in the set instruction. "quoting the assignment" ensures any stray trailing spaces on the line are not included in the value assigned.
Well - next, make sure the file exists and if it doesn't, then produce a message and loop back to :again which bypasses the increment of #.
Otherwise, simply copy the file. You're aware of its sourcename, and your destinationname is constructed by including %#% to include the current value of # (all batch variables without exception are strings - the set /a instruction merely converts from string to binary to perform the required calculation, then converts the result back to a string for storage in the environment.)
Finally, interpreting the request to add another file. if /i makes the comparison case-insensitive. Since you have no direct control over the user's response, "quoting each side" ensures the if syntax isn't violated in case the user enters "yup sure 'nuff" or some other unexpected response.
The leading colon is not required in a goto. I prefer to omit it to keep conguity with the call command where no-colon means an external routine will be called and a colon means the routine is in this batch file.

How to extract text and set as variable Batch Code CMD

I would like to extract what users are online based on this text file:
https://minecraft-statistic.net/en/server/167.114.43.185_25565/json/
I will save it as a text file. Inside that there is something:
"players_list":["Raskhol"]["Lukaka"],"map":...etc
I would like to extract all the text between "_list": and ,"map" and set it as a variable. So that when I call the variable %Playerlist%, it would say:
["Raskhol"]["Lukaka"]
similar to #geisterfurz007's answer, this one assumes the you are after the first instance of "players_list": before the first instance of ,"map"
#Echo Off
Set/P var=<some.json
Set var=%var:,"map"=&:%
Set var=%var:*"players_list":=%
Echo=%var%
Timeout -1
Not tested due to beeing on phone.
#echo off
For /f "delims=: tokens=2" %%g in (PathTo\file.txt) do (
Set var=%%g
Goto:next
)
:next
Set var=%var:,map=%
echo %var%
Assumes players are the first to be listed.
Reads the file, takes the part after the first : up to the second one, stores it in var.
Then ,map gets replaces with nothing to result in just the players beeing echoed in the end.
Feel free to ask questions if something is unclear! Might take a while as I am currently mobile though.

How to create a unique temporary file path in command prompt without external tools? [duplicate]

This question already has answers here:
Create unique file name Windows batch
(12 answers)
Closed 7 years ago.
I am trying to create the path to a temporary file to be used in a batch file.
There are the environment variables %TEMP% and %TMP% to get the temporary directory for the current user. But how to build a file name that does surely not yet exist?
Of course I can use the built-in variable %RANDOM% and create something like bat~%RANDOM%.tmp, but this method does not ensure that the file is currently inexistent (or that it will be created coincidentally by another application, before I first create it on disk and write to it) -- although this all is very unlikely.
I know I could just reduce the probability of such collisions by appending also %DATE%/%TIME%, or by just adding multiple %RANDOM% instances, but this is not what I want...
Note: According to this post, there is a method in .NET (Path.GetTempFileName()) which does exactly what I am asking for (besides the wrong programming language obviously).
Try next code snippet:
#echo off
setlocal EnableExtensions
rem get unique file name
:uniqLoop
set "uniqueFileName=%tmp%\bat~%RANDOM%.tmp"
if exist "%uniqueFileName%" goto :uniqLoop
or create procedures
:uniqGet: create a file of a fix filename template (bat~%RANDOM%.tmp in your case);
:uniqGetByMask: create a file of a variable filename template. Note quadrupled percent signs of %random% reference in a procedure call: prefix%%%%random%%%%suffix.ext. Also note advanced usage: CALLing internal commands in call set "_uniqueFileName=%~2" inside the procedure.
The code could be as follows:
#ECHO OFF
SETLOCAL enableextensions
call :uniqGet uniqueFile1 "%temp%"
call :uniqGet uniqueFile2 "%tmp%"
call :uniqGet uniqueFile3 d:\test\afolderpath\withoutspaces
call :uniqGet uniqueFile4 "d:\test\a folder path\with spaces"
call :uniqGetByMask uniqueFile7 d:\test\afolder\withoutspaces\prfx%%%%random%%%%sffx.ext
call :uniqGetByMask uniqueFile8 "d:\test\a folder\with spaces\prfx%%%%random%%%%sffx.ext"
set uniqueFile
pause
goto :continuescript
rem get unique file name procedure
rem usage: call :uniqGetByMask VariableName "folderpath\prefix%%%%random%%%%suffix.ext"
rem parameter #1=variable name where the filename save to
rem parameter #2=folder\file mask
:uniqGetByMask
rem in the next line (optional): create the "%~dp2" folder if does not exist
md "%~dp2" 2>NUL
call set "_uniqueFileName=%~2"
if exist "%_uniqueFileName%" goto :uniqGetByMask
set "%~1=%_uniqueFileName%"
rem want to create an empty file? remove the `#rem` word from next line
#rem type nul > "%_uniqueFileName%"
exit /B
goto :continuescript
#rem get unique file name procedure
#rem usage: call :uniqGet VariableName folder
#rem parameter #1=variable name where the filename save to
#rem parameter #2=folder where the file should be about
:uniqGet
#rem in the next line (optional): create the "%~2" folder if does not exist
md "%~2" 2>NUL
set "_uniqueFileName=%~2\bat~%RANDOM%.tmp"
if exist "%_uniqueFileName%" goto :uniqGet
set "%~1=%_uniqueFileName%"
#rem want to create empty file? remove the `#rem` word from next line
#rem type nul > "%_uniqueFileName%"
exit /B
:continueScript
Output:
==>D:\bat\SO\32107998.bat
uniqueFile1=D:\tempUser\me\bat~21536.tmp
uniqueFile2=D:\tempUser\me\bat~15316.tmp
uniqueFile3=d:\test\afolderpath\withoutspaces\bat~12769.tmp
uniqueFile4=d:\test\a folder path\with spaces\bat~14000.tmp
uniqueFile7=d:\test\afolder\withoutspaces\prfx26641sffx.ext
uniqueFile8=d:\test\a folder\with spaces\prfx30321sffx.ext
Press any key to continue . . .
I suggest you one of two methods. The "technical approach" is to use JScript's FileSystemObject.GetTempName method. JScript is a programming language that comes pre-installed in all Windows versions from XP on, and its use in Batch via a "Batch-JScript" hybrid script is very simple:
#if (#CodeSection == #Batch) #then
#echo off
setlocal
for /F "delims=" %%a in ('CScript //nologo //E:JScript "%~F0"') do set "tempName=%%a"
echo Temp name: "%tempName%"
goto :EOF
#end
// JScript section
var fso = new ActiveXObject("Scripting.FileSystemObject");
WScript.Stdout.WriteLine(fso.GetTempName());
However, the simplest approach is to store a number in a data file and every time that you want a new name, get the number, increment it and store it back in the same file. This will work for "just" 2147483647 times!
rem Get next number
set /P "nextNum=" < NextNumber.txt
set /A nextNum+=1
echo %nextNum% > NextNumber.txt
set "tempName=File%nextNum%.txt"
echo Temp name: %tempName%
Firstly, using a separate folder will significantly reduce the chances of other programs intruding. So lets store the temp file in a private folder that's really long and specific to prevent any name competition.
When generating the name, you could always use %random% and try generating a name which does not exist, however the more times this operation is use, the more ineffective it becomes. If you plan to use this process 10's of thousands of times as the random function is limited to 32000 (approximately) your program will spend forever generating random attempts.
The next best approach is to start a counter at one and increase it till you have an unused name. That way you can guarantee your program will eventually find a name in a reasonable amount of time, however again your files will pile up (which is never a good experience)
What some people do (and I would recommend for your situation) is combine these to processes to effectively cut the fat in selecting a name, while using a reliable method (the best of both worlds):
#echo off
:: Set Temp Folder Path
set "tp=%temp%\Temporary Name Selection Test"
:: File name will be XXXXXX_YY.txt
:::: X's are randomly generated
:::: Y's are the incremented response to existing files
set x=%random%
set y=0
set "filename=Unexpected Error"
:loop
set /a y+=1
set "filename=%tp%\%x%_%y%.txt"
if exist %filename% goto loop
:: At this point, filename is all good
Echo %filename%
pause

Iterating arrays in a batch file

I am writing a batch file (I asked a question on SU) to iterate over terminal servers searching for a specific user. So, I got the basic start of what I'm trying to do.
Enter a user name
Iterate terminal servers
Display servers where user is found (they can be found on multiple servers now and again depending on how the connection is lost)
Display a menu of options
Iterating terminal servers I have:
for /f "tokens=1" %%Q in ('query termserver') do (set __TermServers.%%Q)
Now, I am getting the error...
Environment variable __TermServers.SERVER1 not defined
...for each of the terminal servers. This is really the only thing in my batch file at this point. Any idea on why this error is occurring? Obviously, the variable is not defined, but I understood the SET command to do just that.
I'm also thinking that in order to continue working on the iteration (each terminal server), I will need to do something like:
:Search
for /f "tokens=1" %%Q in ('query termserver') do (call Process)
goto Break
:Process
for /f "tokens=1" %%U in ('query user %%username%% /server:%%Q') do (set __UserConnection = %%C)
goto Search
However, there are 2 things that bug me about this:
Is the %%Q value still alive when calling Process?
When I goto Search, will the for-loop be starting over?
I'm doing this with the tools I have at my disposal, so as much as I'd like to hear about PowerShell and other ways to do this, it would be futile. I have notepad and that's it.
Note: I would continue this line of questions on SuperUser, except that it seems to be getting more into programming specifics.
Ok, those are quite a few questions/issues/etc. in one :-)
And I still don't quite get where exactly you're headed with that script.
First of all, the syntax for the set command is
set <variable name>=<value>
If you do just
set <variable name>
then it will list all environment variables starting with <variable name>. If there are none, then it will output the error message you're seeing.
If you want to define a variable without actually caring about its value, you still need to provide a value. I usually use 1 for such flags, since it's then more an on/off switch than an actual variable holding a value:
set Foo=1
In your case you probably want something else, though. There are no arrays per se in batch files, you can mimic them by creating a number of variables and holding a count somewhere. I've written about that once before (a little outdated by now, but still valid).
In your case you want to iterate over a number of servers and for each server over a number of users. You can do that with a nested loop:
for /f "tokens=1" %%Q in ('query termserver') do (
for /f "tokens=1" %%U in ('query user ... /server:%%Q' do (
...
)
)
As for your two questions there:
No, the loop variable is only valid inside the loop, not when calling a subroutine. You can pass it to the subroutine, however:
for ... in (...) do call Process %%Q
You can then access it with %1 in the subroutine. Honestly, though, in most cases I think the nested loops are easier to read.
Yes.
Another error (one that will bite you): As mentioned before, the set syntax is
set variable=value
Note that there is no space around the = sign. If there is, then you have a space at the end of the variable name or at the start of the value:
> set foo = bar
> echo %foo%
%foo%
> echo %foo %
bar

Resources