I have a batch file with following content:
echo %~dp0
CD Arvind
echo %~dp0
Even after changing directory value of %~dp0
is the same.
However, if I run this batch file from CSharp program, the value of %~dp0
changes after CD. It now points to new directory. Following is the code that I use:
Directory.SetCurrentDirectory(//Dir where batch file resides);
ProcessStartInfo ProcessInfo;
Process process = new Process();
ProcessInfo = new ProcessStartInfo("mybatfile.bat");
ProcessInfo.UseShellExecute = false;
ProcessInfo.RedirectStandardOutput = true;
process = Process.Start(ProcessInfo);
process.WaitForExit();
ExitCode = process.ExitCode;
process.Close();
Why is there a difference in output on executing same script by different ways?
Do I miss something here?
Joey's suggestion helped. Just by replacing
with
did the trick.
Each new line in your batch called by your ProcessStart is independently considered as a new cmd command.
For example, if you give it a try like this:
It works.
I'll try to explain why this behaves so oddly. A rather technical and long-winded story, I'll try to keep it condense. Starting point for this problem is:
You'll see that if you omit this statement or assign true that it works as you expected.
Windows provides two basic ways to start programs, ShellExecuteEx() and CreateProcess(). The UseShellExecute property selects between those two. The former is the "smart and friendly" version, it knows a lot about the way the shell works for example. Which is why you can, say, pass the path to an arbitrary file like "foo.doc". It knows how to look up the file association for .doc files and find the .exe file that knows how to open foo.doc.
CreateProcess() is the low-level winapi function, there's very little glue between it and the native kernel function (NtCreateProcess). Note the first two arguments of the function,
lpApplicationName
andlpCommandLine
, you can easily match them to the two ProcessStartInfo properties.What is not so visible that CreateProcess() provides two distinct ways to start a program. The first one is where you leave lpApplicationName set to an empty string and use lpCommandLine to provide the entire command line. That makes CreateProcess friendly, it automatically expands the application name to the full path after it has located the executable. So, for example, "cmd.exe" gets expanded to "c:\windows\system32\cmd.exe". But it does not do this when you use the lpApplicationName argument, it passes the string as-is.
This quirk has an effect on programs that depend on the exact way the command line is specified. Particularly so for C programs, they assume that
argv[0]
contains the path to their executable file. And it has an effect on%~dp0
, it too uses that argument. And flounders in your case since the path it works with is just "mybatfile.bat" instead of, say, "c:\temp\mybatfile.bat". Which makes it return the current directory instead of "c:\temp".So what your are supposed to do, and this is totally under-documented in the .NET Framework documentation, is that it is now up to you to pass the full path name to the file. So the proper code should look like:
And you'll see that
%~dp0
now expands as you expected. It is usingpath
instead of the current directory.It's a problem with the quotes and
%~0
.cmd.exe handles
%~0
in a special way (other than%~1
).It checks if
%0
is a relative filename, then it prepend it with the start directory.If there a file can be found it will use this combination, else it prepends it with the actual directory.
But when the name begins with a quote it seems to fail to remove the quotes, before prepending the directory.
That's the cause why
cmd /c myBatch.bat
works, as thenmyBatch.bat
is called without quotes.You could also start the batch with a full qualified path, then it also works.
Or you save the full path in your batch, before changing the directory.
A small
test.bat
can demonstrate the problems of cmd.exeCall it via (in C:\temp)
The output should be
So,
cmd.exe
was able to findtest.bat
, but only for%~fx0
it will prepend the start directory.In the case of calling it via
It fails with
cmd.exe
isn't able to find the batch file even before the directory was changed, it can't expand the name to the full name ofc:\temp\test.bat
This question started the discussion on this point, and some testing was done to determine why. So, after some debugging inside
cmd.exe
... (this is for a 32 bit Windows XP cmd.exe but as the behaviour is consistent on newer system versions, probably the same or similar code is used)Inside Jeb's answer is stated
and here Jeb is correct.
Inside the current context of the running batch file there is a reference to the current batch file, a "variable" containing the full path and file name of the running batch file.
When a variable is accessed, its value is retrieved from a list of available variables but if the variable requested is
%0
, and some modifier has been requested (~
is used) then the data in the running batch reference "variable" is used.But the usage of
~
has another effect in the variables. If the value is quoted, quotes are removed. And here there is a bug in the code. It is coded something like (here simplified assembler to pseudocode)And yes, this means that when the batch file is quoted, the first part of the
if
is executed and the full reference to the batch file is not used, instead the value retrieved is the string used to reference the batch file when calling it.What happens then? If when the batch file was called the full path was used, then there will be no problem. But if the full path is not used in the call, any element in the path not present in the batch call needs to be retrieved. This retrieval assumes relative paths.
A simple batch file (
test.cmd
)When called using
test
(no extension, no quotes), we obtainc:\somewhere\test.cmd
When called using
"test"
(no extension, quotes), we obtainc:\somewhere\test
In the first case, without quotes, the correct internal value is used. In the second case, as the call is quoted, the string used to call the batch file (
"test"
) is unquoted and used. As we are requesting a full path, it is considered a relative reference to something calledtest
.This is the why. How to solve?
From the C# code
Don't use quotes :
cmd /c batchfile.cmd
If quotes are needed, use the full path in the call to the batch file. That way
%0
contains all the needed information.From the batch file
Batch file can be invoked in any way from any place. The only reliable way to retrieve the information of the current batch file is to use a subroutine. If any modifier (
~
) is used, the%0
will use the internal "variable" to obtain the data.This will echo to console the full path to the current batch file independtly of how you call the file, with or without quotes.
note: Why does it work? Why the
%~f0
reference inside a subroutine return a different value? The data accessed from inside the subroutine is not the same. When thecall
is executed, a new batch file context is created in memory, and the internal "variable" is used to initialize this context.Command line interpreter cmd.exe has a bug in code on getting path of batch file if the batch file was called with double quotes and with a path relative to current working directory.
Create a directory C:\Temp\TestDir. Create inside this directory a file with name PathTest.bat and copy & paste into this batch file the following code:
Next open a command prompt window and set working directory to C:\Temp\TestDir using the command:
Now call Test.bat in following ways:
PathTest
PathTest.bat
.\PathTest
.\PathTest.bat
..\TestDir\PathTest
..\TestDir\PathTest.bat
\Temp\TestDir\PathTest
\Temp\TestDir\PathTest.bat
C:\Temp\TestDir\PathTest
C:\Temp\TestDir\PathTest.bat
Output is four times C:\Temp\TestDir\ as expected for all 10 test cases.
The test cases 7 and 8 start the batch file with a path relative to root directory of current drive.
Now let us look on results on doing the same as before, but with using double quotes around batch file name.
"PathTest"
"PathTest.bat"
".\PathTest"
".\PathTest.bat"
"..\TestDir\PathTest"
"..\TestDir\PathTest.bat"
"\Temp\TestDir\PathTest"
"\Temp\TestDir\PathTest.bat"
"C:\Temp\TestDir\PathTest"
"C:\Temp\TestDir\PathTest.bat"
Output is four times C:\Temp\TestDir\ as expected for the test cases 5 to 10.
But for the test cases 1 to 4 the second output line is just C:\Temp\ instead of C:\Temp\TestDir\.
Now use
cd ..
to change working directory to C:\Temp and run PathTest.bat as follows:"TestDir\PathTest.bat"
".\TestDir\PathTest.bat"
"\Temp\TestDir\PathTest.bat"
"C:\Temp\TestDir\PathTest.bat"
The result for second output for the test cases 1 and 2 is C:\TestDir\ which does not exist at all.
Starting the batch file without the double quotes produces for all 4 test cases the right output.
This makes it very clear that the behavior is caused by a bug.
Whenever a batch file is started with double quotes and with a path relative to current working directory on start,
%~dp0
is not reliable on getting the path of batch file when current working directory is changed during batch execution.This bug is also reported to Microsoft according to Windows shell bug with how %~dp0 is resolved.
It is possible to workaround this bug by assigning the path of the batch file immediately to an environment variable as demonstrated with the code above before changing the working directory.
And then reference the value of this variable wherever path of batch file is needed with using double quotes where required. Something like
%BatchPath%
is always better readable as%~dp0
.Another workaround is starting the batch file always with full path (and with file extension) on using double quotes as class
Process
does.