When I try to redirect multiple files into multiple streams, like in the following example, everything works as expected:
3< stream3.txt 4< stream4.txt
Looks like a bug with the way the streams are being restored after the command has finished. Consider the following batch file:
0< stream0.txt 3< stream3.txt findstr .
findstr .
<CON pause
The first findstr
will output the content of stream0.txt
as expected.
The second findstr
will unexpectedly output the content of stream3.txt
, indicating that stream 0 has unexpectedly become redirected.
When the batch file finishes, the command shell running it will exit too, because it sees an end of file on what is now the standard input stream.
Retrying one of my experiments in the light of MC ND's answer, I can now reproduce the same issue with a stream other than stream 3. Consider test7.cmd
:
0< stream0.txt 4< stream3.txt findstr .
findstr .
:done
<CON pause
Run as cmd /c test7
or as cmd /c test7 3< other.txt
it does not exhibit the unexpected behaviour, but run as cmd /c "test7 3< other.txt"
it does. (That was a facepalm moment; I could have discovered this yesterday if I'd thought about it more carefully. Obviously the initial redirection has to be in the context of same command shell that is running the batch script.)
So the cause isn't "redirecting stream 3 as well as stream 0" but "redirecting the first free stream as well as stream 0". :-)
note: This is a simplification of what happens inside cmd
when the
redirected command is executed.
Let's start with the indicated command
0< file1 3< file2 echo/
The command is parsed and a representation of the needed redirections is created in memory, some kind of table/list that will hold the information about the redirection: which handle is redirected, the old saved handle, to where the handle should point when redirected, ...
Redirection requests
-------------------------------
redirect saved redirectTo
+--------+--------+------------
R1 | 0 file1
|
R2 | 3 file2
At this point (after parsing the command) no stream has been changed.
There is also a system table that handles where each of the file descriptors (in our case, the cmd
streams) really points.
File descriptors
------------------
points to
+-----------------
0 | stdin
1 | stdout
2 | stderr
3 |
4 |
note This is not exactly true, the underlying structure is a little more complex, but that way it is easier to see how it works
When the command is going to be executed, the internal SetRedir
function is called. It iterates over the previous redirection request table saving existing handles and creating required new ones. The initial state is
Redirection requests File descriptors
------------------------------- ------------------
redirect saved redirectTo points to
+--------+--------+------------ +-----------------
R1 | 0 file1 0 | stdin
| 1 | stdout
R2 | 3 file2 2 | stderr
3 |
4 |
First element from redirection request table (R1) is retrieved, the requests to redirect stream 0 to file1. It is necessary to save the current handle to later be able to restore it. For this operation the _dup() function is used. It will create an alias for the passed file descriptor (stream 0 in our code), using the smallest available file descriptor (stream 3 in previous table). After save operation and old handle close the situation is
R1[saved] = _dup( R1[redirect] );
_close( R1[redirect] );
Redirection requests File descriptors
------------------------------- ------------------
redirect saved redirectTo points to
+--------+--------+------------ +-----------------
R1 | 0 3 file1 0 | ---\
| 1 | stdout |
R2 | 3 file2 2 | stderr |
3 | stdin <<--/
4 |
Once saved, the redirection is completed by opening the requested file and associating open file handle in the file descriptors table. In this case the _dup2() function handles the operation
_dup2( CreateFile( R1[redirectTo] ), R1[redirect] );
Redirection requests File descriptors
------------------------------- ------------------
redirect saved redirectTo points to
+--------+--------+------------ +-----------------
R1 | 0 3 file1 0 | file1 <<---
| 1 | stdout
R2 | 3 file2 2 | stderr
3 | stdin
4 |
The first redirection has been done. It is time to do the same operation with
the second one. First, save the old handle using the _dup()
function. This will associate the requested file descriptor (3) with the lowest available descriptor (4)
R2[saved] = _dup( R2[redirect] );
_close( R2[redirect] );
Redirection requests File descriptors
------------------------------- ------------------
redirect saved redirectTo points to
+--------+--------+------------ +-----------------
R1 | 0 3 file1 0 | file1
| 1 | stdout
R2 | 3 4 file2 2 | stderr
3 | ---\
4 | stdin <<--/
The redirection is completed by opening the input file and associating it with the file descriptor
_dup2( CreateFile( R2[redirectTo] ), R2[redirect] );
Redirection requests File descriptors
------------------------------- ------------------
redirect saved redirectTo points to
+--------+--------+------------ +-----------------
R1 | 0 3 file1 0 | file1
| 1 | stdout
R2 | 3 4 file2 2 | stderr
3 | file2 <<---
4 | stdin
The redirection has been completed and the command is executed with the stream 0 redirected to file1
and the stream 3 redirected to file2
.
Once done, it's time to revert the process. ResetRedir()
function handles the operation. It uses again the _dup2()
function to transfer the saved handle to the original file descriptor. Here the problem arises as the saved descriptor was changed
_dup2( R1[saved], R1[redirect] );
R1[saved] = null;
Redirection requests File descriptors
------------------------------- ------------------
redirect saved redirectTo points to
+--------+--------+------------ +-----------------
R1 | 0 file1 0 | file2 <<--\
| 1 | stdout |
R2 | 3 4 file2 2 | stderr |
3 | ---/
4 | stdin
Now, the same operation is done with the second redirection
_dup2( R2[saved], R2[redirect] );
R2[saved] = null;
Redirection requests File descriptors
------------------------------- ------------------
redirect saved redirectTo points to
+--------+--------+------------ +-----------------
R1 | 0 file1 0 | file2
| 1 | stdout
R2 | 3 file2 2 | stderr
3 | stdin <<--\
4 | ---/
Once the redirection has been removed the &0
handle points to file2
and the stdin
stream is stored in &3
. This can be tested as
@echo off
setlocal enableextensions disabledelayedexpansion
>file1 echo This is file 1
>file2 echo This is file 2
echo Test 1 - trying to read from stdin after redirection
cmd /v /c"( 0< file1 3< file2 echo - test1 ) & set /p .=prompt & echo !.!"
echo(
echo(
echo Test 2 - trying to read from stream 3 after redirection
cmd /v /c"( 0< file1 3< file2 echo - test 2 ) & <&3 set /p .=prompt & echo !.!"
That will generate
W:\>testRedirection.cmd
Test 1 - trying to read from stdin after redirection
- test1
prompt This is file 2
Test 2 - trying to read from stream 3 after redirection
- test 2
prompt This is typed text
This is typed text
W:\>
It can be seen that in the first test, the set /p
has read from file2
, and in the second test, trying to read from &3
the stdin
stream can be reached.