How to do MSBuild's GetDirectoryNameOfFileAbove in a batch file? - batch-file

In MSBuild, there's the GetDirectoryNameOfFileAbove function that finds the first ancestor directory that includes certain file.
For those not familiar with that MSBuild function, here's an example.
I have a file:
c:\projects\root.txt
I want to be able to run a script such as:
c:\projects\everything\foo\bar\myscript.cmd
And would like to find c:\projects\, since it's the closest ancestor to c:\projects\everything\foo\bar\ that has that file in it. The script should also be able to detect when the files is not present in any of the ancestors and just get an empty string.
How do I achieve the same in a (Windows) batch script?

#echo off
setlocal EnableDelayedExpansion
set "theFile=%~1"
rem Get the path of this Batch file, i.e. "c:\projects\everything\foo\bar\"
set "myPath=%~DP0"
rem Process it as a series of ancestor directories
rem i.e. "c:\" "c:\projects\" "c:\projects\everything\" etc...
rem and search the file in each ancestor, taking as result the last one
set "result="
set "thisParent="
set "myPath=%myPath:~0,-1%"
for %%a in ("%myPath:\=" "%") do (
set "thisParent=!thisParent!%%~a\"
if exist "!thisParent!%theFile%" set "result=!thisParent!"
)
if defined result (
echo %result%
) else (
echo There is not such file in my ancestors
)
Put the name of the file as parameter of this Batch file. For example, if you name this file GetDirectoryNameOfFileAbove.bat, you may use it this way:
GetDirectoryNameOfFileAbove root.txt

I can name that tune in 2 (long lines). The code is short. The explanation is long.
#rem will search up to 8 levels up from here
#Set Ancestry=.;..;..\..;..\..\..;..\..\..\..;..\..\..\..\..;..\..\..\..\..\..;..\..\..\..\..\..\..;..\..\..\..\..\..\..;..\..\..\..\..\..\..\..;..\..\..\..\..\..\..\..\..;
#REM %~dp$Ancestry:1 -- search paths in Ancestry for arg 1
#REM finds files of any type, not just exe
#REM LocalTop is either the folder where arg 1 (%1) was found or empty
#Set LocalTop=%~dp$Ancestry:1
How it works:
Ancestry is a series of places to look: . then .., then .... and so on. I stopped at 8 levels. Add more, although I have never needed more levels.
The next line uses the magic of cmd. Note this only syntax works for %0..%9 or in a for statement with the for variable.
From FOR /?:
%~$PATH:I - searches the directories listed in the PATH
environment variable and expands %I to the
fully qualified name of the first one found
If the environment variable name is not
defined or the file is not found by the
search, then this modifier expands to the
empty string
What was not obvious to me, is that it is not limited to the environment variable 'PATH'. You may supply any variable, which is what Ancestry is above.

Related

For /r in batch file suddenly adding a period to file path

I was editing some batch files that use doff.exe to set a date variable, then search through the subfolders of a folder named for the date to find a particular file, copy it, rename it, then process it. All of a sudden, without changing the way it was written, the for /r loop is adding a period to the file path. So, after using doff and navigating to the date-named directory this is my code:
for /r %%d in (.) do copy %%d\FILE.TXT Y:\Folder\subfolder\other_folder\
It used to output this:
X:\Stuff\other_stuff\20180314>copy X:\Stuff\Other_stuff\20180314\FILE.TXT Y:\Folder\subfolder\other_folder
The system cannot find the file specified.
until it found the file, then it would copy it. Now, instead, it returns this (note the extra .\ in front of the filename):
X:\Stuff\other_stuff\20180314>copy X:\Stuff\Other_stuff\20180314\.\FILE.TXT Y:\Folder\subfolder\other_folder
The system cannot find the file specified.
And never finds the file, because of the extra .\ in the file path. I haven't changed anything that I know of, we use this code string in tons of places, I feel like I'm taking crazy pills. In case it's relevant, here's the full script to that point:
for /f "tokens=1-3 delims=/ " %%a in ('doff yyyymmdd') do (
set mm2=%%a
set dd2=%%b
set yyyy2=%%c)
X:
cd Stuff
cd Other_stuff
cd %mm2%
for /r %%d in (.) do copy %%d\FILE.TXT Y:\Folder\subfolder\other_folder\
First of all, no command usually changes its behaviour all of a sudden without having changed anything. I can assure you for does not change just like that.
Furthermore, an additional . in a file path does not harm at all, because . just means the current directory. Therefore D:\a\b\c.ext points to the same target as D:\a\.\b\.\c.ext.
(Just for the sake of completeness, there is also .. which points to the parent directory, so it goes one level up, hence D:\a\b\c.ext points to the same item as D:\a\b\d\..\c.ext.)
Anyway, to understand what is happening and where the extra . comes from, you need to know that the for command (without and with the /R option) does not access the file system always but only when needed.
The plain for command accesses the file system only when at least a wildcard like * or ? is used in the set (the set is the part in between the parentheses following the loop variable reference; type for /?). You can easily confirm that with the following code snippet typed into a command prompt window (to use this in a batch file, double the %-signs):
>>> rem /* List all `*.txt` and `*.bin` files in current directory;
>>> rem the example output below lists two files: */
>>> for %I in (*.txt *.bin) do #echo %I
sample.txt
data.bin
>>> rem // Print all items even if there are no such files:
>>> for %I in (just/text no/file) do #echo %I
just/text
no/file
The first for loop returns existing files only, so the file system needs to be accessed. The second loop just returns the given items without accessing the file system. Both items contain a character (/) that is not allowed in file names, so there cannot exist such files.
For the set of for /R (that is again the part in between ()) it is exactly the same. However, to retrieve the recursive directory tree, the file system has to be accessed of course. So check out the following code snippet (given the current directory is C:\Test that contains 3 sub-directories):
>>> rem /* List all `*.txt` and `*.bin` files in current directory tree;
>>> rem the example output below lists some files: */
>>> for /R %I in (*.txt *.bin) do #echo %I
C:\Test\sample.txt
C:\Test\sub1\any.bin
C:\Test\sub1\data.bin
C:\Test\sub2\text.txt
>>> rem // Print all items even if there are no such files:
>>> for /R %I in (just/text no/file) do #echo %I
C:\Test\just/text
C:\Test\no/file
C:\Test\sub1\just/text
C:\Test\sub1\no/file
C:\Test\sub2\just/text
C:\Test\sub2\no/file
C:\Test\sub3\just/text
C:\Test\sub3\no/file
As you can see in the first example the file system is accessed and only matching files are returned. In the second example, the directory tree is retrieved from the file system, but the items in the set are just appended and not checked against the file system.
The . is nothing special to for or for /R, it is just a string without a wildcard, so the files system is not checked. Therefore in our test scenario the output would be:
>>> rem // `.` is the only character in the set:
>>> for /R %I in (.) do #echo %I
C:\Test\.
C:\Test\sub1\.
C:\Test\sub2\.
C:\Test\sub3\.
I hope this explains the behaviour you are facing!
Just for the sake of completeness:
If you want to get rid of the appended . for cosmetic reasons, nest another (standard) for loop and use the ~f modifier:
>>> rem // `.` is the only character in the set:
>>> for /R %I in (.) do #for &J in (%I) do echo %~fI
C:\Test
C:\Test\sub1
C:\Test\sub2
C:\Test\sub3
The trailing period has always displayed when a single period is used for the set when using FOR /R. If you want to get rid of it then do this:
for /r %%G in (.) do copy %%~fG\FILE.TXT Y:\Folder\subfolder\other_folder\

Batch substring for variable in loop [duplicate]

I have a number of files with the same naming scheme. As a sample, four files are called "num_001_001.txt", "num_002_001.txt", "num_002_002.txt", "num_002_003.txt"
The first set of numbers represents which "package" it's from, and the second set of numbers is simply used to distinguish them from one another. So in this example we have one file in package 001, and three files in package 002.
I am writing a windows vista batch command to take all of the files and move them into their own directories, where each directory represents a different package. So I want to move all the files for package 001 into directory "001" and all for 002 into directory "002"
I have successfully written a script that will iterate over all of the txt files and echo them. I have also written a scrip that will move one file into another location, as well as creating the directory if it doesn't exist.
Now I figure that I will need to use substrings, so I used the %var:~start,end% syntax to get them. As a test, I wrote this to verify that I can actually extract the substring and create a directory conditionally
#echo off
set temp=num_001_001.txt
NOT IF exist %temp:~0,7%\
mkdir %temp:~0,7%
And it works. Great.
So then I added the for loop to it.
#echo off
FOR /R %%X IN (*.txt) DO (
set temp=%%~nX
echo directory %temp:~0,7%
)
But this is my output:
directory num_002
directory num_002
directory num_002
directory num_002
What's wrong? Does vista not support re-assigning variables in each iteration?
The four files are in my directory, and one of them should create num_001. I put in different files with 003 004 005 and all of it was the last package's name. I'm guessing something's wrong with how I'm setting things.
I have different workarounds to get the job done but I'm baffled why such a simple concept wouldn't work.
Your problem is that the variable get replaced when the batch processor reads the for command, before it is executed.
Try this:
SET temp=Hello, world!
CALL yourbatchfile.bat
And you'll see Hello printed 5 times.
The solution is delayed expansion; you need to first enable it, and then use !temp! instead of %temp%:
#echo off
SETLOCAL ENABLEDELAYEDEXPANSION
FOR /R %%X IN (*.txt) DO (
set temp=%%~nX
echo directory !temp:~0,7!
)
See here for more details.
Another solution is to move the body of the for loop to a subroutine and call it.
#echo off
FOR /R %%X IN (*.txt) DO call :body %%X
goto :eof
:body
set temp=%~n1
echo directory %temp:~0,7%
goto :eof
Why do this? One reason is that the Windows command processor is greedy about parentheses, and the results may be surprising. I usually run into this when dereferencing variables that contain C:\Program Files (x86).
If the Windows command processor was less greedy, the following code would either print One (1) Two (2) or nothing at all:
#echo off
if "%1" == "yes" (
echo 1 (One)
echo 2 (Two)
)
However, that's not what it does. Either it prints 1 (One 2 (Two), which missing a ), or it prints 2 (Two). The command processor interprets the ) after One as the end of the if statement's body, treats the second echo as if it's outside the if statement, and ignores the final ).
I'm not sure whether this is officially documented, but you can simulate delayed expansion using the call statement:
#echo off
FOR /R %%X IN (*.txt) DO (
set temp=%%~nX
call echo directory %%temp:~0,7%%
)
Doubling the percent signs defers the variable substitution to the second evaluation. However, delayed expansion is much more straightforward.

Renaming multiple files in a for loop with user input Windows batchfile

I want to create a program that can loop through multiple pdf files and have the user rename each file a unique name like so:
234324.pdf to Batch150.pdf
32154687.pdf to AdvancedPayment.pdf
and so on...
Here is my code:
setlocal EnableDelayedExpansion
echo rename pdf files
FOR %%F IN (*.pdf) DO (
set /p x=Enter:
move %%F !x!
)
endlocal
This seems to work for the first file and then when I try to rename the second one it says: The syntax of the command is incorrect..
I have tried using the rename command and haven't had much luck with it.
FOR %%F IN (*.pdf) DO (
SET "x=%%F"
set /p "x=%%F Enter: "
IF /i "!x!.pdf" neq "%%F" ren "%%F" "!x!.pdf"
)
Your code worked fine for me. Since you don't indicate what the "to" and "from" names you used were, we're reduced to guessing.
I developed the above code to perform the rename in the manner you originated. Note that it works as I expect. Using ren the "to" filename must be a filename within the directory where the "from" file resides [ie. it simply renames the file]. If you use "move" then the file can be moved to another directory if you specify that in the "to" name.
The first fix is purely cosmetic. By enclosing the variablename and prompt in the set /p in quotes, you can include a terminal space in the prompt (which I prefer), and including the %%F in the prompt shows you which file is about to be renamed.
The next fix is to quote the arguments to the ren or move. This ensures the syntax remains properly constructed in the case of eithr "to" or "from" name containing a space.
The next is to initialise x with the "from" filename. It's enclosed in quotes so any invisible trailing spaces are not included in the value assigned to x. Note that set /p does not alter the variable if Enter alone is keyed, so setting x ensures that if a file is not to be renamed, all you need do is press Enter
The next is to detect whether the "to" and "from" names are equal. ren will generate an error report if you attempt to rename a file to itself; equally, you can't move a file to itself. Hence, /i=ignore case, and only attempt the operation if the names are different.
Finally, add the .pdf to each usage of !x! in order that you don't need to key it in. Naturally, you could omit this change if you want to alter extensions or you could put .pdf into another variable and use that variable in place of the constant .pdf so that the extension being selected can be easily varied by being changed in one plaace rather than using a mass-edit. You could even use a set /p to assign the extension being processed dynamically at the start of the routine.
Note that if you rename say one.pdf to yellow.pdf then this construct is very likely to propose yellow.pdf for a rename later on. This is because the next-filename logic locates the next filename currently in the directory,then processes the loop, then locates the next filename currently in the directory, and so on.
You would need
For /f "delims=" %%F in ('dir /b/a-d "*.pdf" ') do (
to ensure that each filename is only presented once. This mechanism performs a directory scan of filenames-only in basic form, and stores the list in memory, then processes the list item-by-item and since that list is created and then processed, any subsequent alterations to the directory do not affect the list.

Use Dos commands to copy file and preserve date in file name

I'm having trouble trying to copy and rename a file using only dos commands. I have a file of the format myfile20130218 and want to copy and rename it to some_other_file_20130218.
I know I can use copy source dest but I'm having trouble with how to isolate the date and preserve it. I cannot guarantee that he date will be today's date so that is ruled out, the source file will always be the same name.
I can run either a series of commands or a batch script, but thing that that I am currently having trouble with, is after I find a match that I need to copy, using myfile????????, how can I now get those file names to pull the dates off them?
EDIT: for clarification I will be looking at files in a known directory, as above, I will know the format of the file name, and will only be checking a specific directory for it. The process that checks the directory is a ConnectDirect file watcher, so when a file is found matching myfile20130218 I can fire off some commands, but don't know how to check the directory and get the name of the file present.
Something like this should work:
%oldname:~-8% extracts the last 8 characters from %oldname% which are then appended to the new filename.
Update: If you can identify the file with an external program and then call the batch script with the file name
copyfile.cmd C:\path\to\myfile20130218
you could do something like this:
set oldname=%~nx1
set newname=%~dp1some_other_file_%oldname:~-8%
copy "%~f1" "%newname%"
Update 2: If you know folder and the format you could call the script with the folder
copyfile.cmd C:\folder
and do something like this:
#echo off
setlocal EnableDelayedExpansion
for /f %%f in (
'dir /b "%~f1" ^| findstr /r "myfile[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$"'
) do (
set oldname=%~f1\%%f
set newname=%~f1\my_other_name_!oldname:~-8!
copy "!oldname!" "!newname!"
)
endlocal
Edit: Script breakdown.
setlocal EnableDelayedExpansion enables variable expansion inside loops and conditionals.
for /f %%f in ('...') executes the command between the single quotes and then loops over the output of that command.
dir /b "%~f1" lists the content of the given directory (%~f1 expands to the full path of the first argument passed to the script) in simple mode (no header, no summary).
findstr /r "myfile[0-9]...[0-9]$" filters the input for strings that end with the substring "myfile" followed by 8 digits. The circumflex before the pipe (^|) escapes the pipe, because otherwise it would take precedence over the for command, which would effectively split the for command in half, resulting in an invalid command-line.
set oldname=%~f1\%%f assign the full path to a matching file to the variable oldname.
set newname=%~f1\my_other_name_!oldname:~-8! assign the full path to the new filename ("my_other_name_" followed by the trailing 8 digits from oldname) to the variable newname.
copy "!oldname!" "!newname!" I don't need to explain this, do I?

Bat file find folder of file

So my many attempts to search for a solution have resulted in a million ways to find the folder of the bat file being executed, however what I am looking to do is find the folder for the filename being passed to the bat file.
Example:
C:\Temp\runthis.bat "C:\Blah\Ah Argh\rage.txt"
I want to get a string within that bat file that is simply "C:\Blah\Ah Argh\", alternatively I would also be able to work with getting a string of "rage.txt"
Editing to explain why: Looking to check for the filename within another txt file which is the directory listing of a ftp server to verify that a file successfully uploaded to it. Then if successful I need to move the file to a subfolder of the original folder \uploaded\ but we have many of these folders setup so I can't hard code it.
Thanks
#echo off
The file path is %~dp1
The file name is %~nx1
The parameter modifiers are the same as for FOR variables.
Type 'HELP CALL' from a command prompt for a full list of parameter modifiers.
#echo off
if %1X==X echo Syntax: %0 "path"
rem The for loop doesn't actually loop. You can split strings with it, but in
rem this case we don't. So there is only one iteration in which %%X will
rem contain the full path.
rem Pass it %1, which is the first parameter. Note the quotes, which are
rem required if you don't add quotes around the parameter and optional (but
rem still valid) when you do.
for /F "delims=|" %%X in ("%1") do (
rem FOR LOOP variables can be used with certain modifiers, preceeded by a
rem tilde. In this case I'm using d and p, which stand for drive and path,
rem effectively trimming the file name from the path.
echo %%~dpX
rem The ~n modifier selects the file name only. ~x is for extension
echo %%~nxX
)

Resources