dbenham wrote: ↑21 Dec 2018 20:52
I know there are differences, but you got the outcomes incorrect.
Yes you are correct that was my mistake. I know
echo !str! | break works (assuming delayed expansion was enabled before of course). I was intended to use
(echo !str! )| break, and my description was not precise for why it is not working when I said: because it will be wrapped by (cmd /d /s /c...). As
echo !str! | break will wrapped by another cmd too. I was focused more on the difference between
(cmd /c command n>&m ) | break and
cmd /c command n>&m | break but at the same time I was talking about delayed expansion so I doubled my mistake.
I'm aware that you know how delayed expansion or handle redirection works because I just learned them(along with other aspects CMD/Batch like parser, ...) from you and jeb. I brought delayed expansion to your attention as an attempt to pinpoint the difference between those two types of redirection in pipes but I failed.
dbenham wrote: ↑21 Dec 2018 20:52
... I intended to write the following:
Code: Select all
(cmd /v:on /c "<nul set /p "=!str:~0,%1!"" 0>&3 4>&0) | break
But the the above won't work. The parentheses break your technique for some reason.
Now this is what I was intended to talk about. It won't work because be entire left hand command will be wrapped in another layer of cmd
Code: Select all
cmd /d /s /c "cmd /v:on /c "<nul set /p "=!str:~0,%1!"" 0>&3 4>&0"
So the redirection will take place in the child CMD not in the main CMD.
The following illustrates the difference
Code: Select all
MAIN_CMD
| executing: (cmd /v:on /c "<nul set /p "=!str:~0,%1!"" 0>&3 4>&0) | break
|
|--Child1---cmd /d /s /c "cmd /v:on /c "<nul set /p "=!str:~0,%1!"" 0>&3 4>&0"
| | executing: cmd /v:on /c "<nul set /p "=!str:~0,%1!"" 0>&3 4>&0
| | 0>&3 4>&0 -------> The redirection occurs here
| |
| |--Child3---cmd /v:on /c "<nul set /p "=!str:~0,%1!
|
|--Child2---cmd /d /s /c "break"
MAIN_CMD
| executing: cmd /v:on /c "<nul set /p "=!str:~0,%1!"" 0>&3 4>&0 | break
| 0>&3 4>&0 -------> The redirection occurs here
|
|--Child1---cmd /v:on /c "<nul set /p "=expanded""
|
|
|--Child2---cmd /d /s /c "break"
This explains why wrapping the pipe operand in parenthesis or directly using internal commands, breaks the technique.
This technique only works with explicit external processes.
How It works:
The above information is the key to understand how the technique works.
Before that I would like to briefly describe how cmd redirects standard handles of its child processes.
Generally any process can redirect the handles of its child processes in two ways:
1. Implicitly by inheritance, The process redirects its own standard handle(s) then creates the child process. The child process automatically inherits the parent process standard handles(stdin , stdout, stderr). The handles must be flagged as inheritable and the parent process must allow handle inheritance when creating the child process.
2. Explicitly by specifying the standard handle(s) in the
STARTUPINFO structure when calling the
CreateProcess API function.
CMD uses the first method.
So how CMD creates pipes and how it redirects the child processes input/output?
Assuming a typical case without any explicit redirections:
The ordered steps that CMD will perform to create the pipe connection between the two processes are as follows: (The values for internal handles 3-9 are based on the assumption that they are all undefined and clean)
1. It creates an anonymous pipe by calling CreatePipe function. It receives two handles, one handle to the write side of the pipe which I will refer to it as
PipeOut and another handle to the read side of the pipe which I will refer to it as
PipeIn
2. It then saves
PipeIn to handle slot
&3 and
PipeOut to handle slot
&4. At this time the state of the CMD internal handles are as follows:
Code: Select all
&0 : Current stdin
&1 : Current stdout
&2 : Current stderr
&3 : PipeIn
&4 : PipeOut
&5-&9 : UNDEFINED
3. It then redirects its own stdout to the handle which is pointed by
&4. In this process the original value of
&1 will saved to
&5 and the value of
&4 will be cleared. At this time the state of the CMD internal handles are as follows:
Code: Select all
&0 : Unaltered
&1 : PipeOut
&2 : Unaltered
&3 : PipeIn
&4 : UNDEFINED
&5 : Original value of &1
&6-&9 : UNDEFINED
4. It creates the left hand process in suspended state. The left hand process have already inherited the standard handles from CMD. This is the state of the standard handles of the left hand process:
Code: Select all
stdin : CMD's current stdin
stdout : CMD's current stdout (PipeOut)
stderr : CMD's current stderr
5. CMD restores its internal handles to their original state. and updates its own process OS level standard handles accordingly. In this process it closes the handle to
PipeOut by calling CloseHandle API
Code: Select all
&0 : Unaltered
&1 : Restored original value from &5
&2 : Unaltered
&3 : PipeIn
&4-&9 : UNDEFINED
6. This time It redirects its own stdin to the handle which is pointed by
&3. In this process the original value of
&0 will saved to
&4 and the value of
&3 will be cleared. At this time the state of the CMD internal handles are as follows:
Code: Select all
&0 : PipeIn
&1 : Unaltered
&2 : Unaltered
&3 : UNDEFINED
&4 : Original value of &0
&5-&9 : UNDEFINED
7. It then creates the right hand process in suspended state. The right hand process have already inherited the standard handles from CMD. This is the state of the standard handles of the right hand process:
Code: Select all
stdin : CMD's current stdin (PipeIn)
stdout : CMD's current stdout
stderr : CMD's current stderr
8. Then restores its internal handles to their original state. and updates its own process OS level standard handles accordingly. In this process it closes the handle to
PipeOut by calling CloseHandle API
Code: Select all
&0 : Restored original value from &4
&1 : Unaltered
&2 : Unaltered
&3-&9 : UNDEFINED
9. It resumes both processes to start execution
10. It waits for both processes to terminate then continues its own execution flow.
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Now It should be clear how the technique works. But for completeness I will describe the details of the technique used in the first post.
Code: Select all
cmd /d /c "echo test" 0>&3 4>&0 | break
findstr "^"
1. CMD obtains PipeIn and PipeOut and sets its own internal handles as below:
Code: Select all
&0 : Unaltered
&1 : PipeOut
&2 : Unaltered
&3 : PipeIn
&4 : UNDEFINED
&5 : Original value of &1
&6-&9 : UNDEFINED
2. Then comes the redirection:
0>&3 4>&0.
2-1. The first one is
0>&3 It firsts saves the current value of
&0 in the first UNDEFINED slot which is
&4 then sets the value of
&0 to what is pointed by
&3 which is
PipeIn. But one additional important thing will be done: The handle to the
PipeIn will be duplicated by calling
DuplicateHandle API. At this point the state of the internal CMD handles are as follows
Code: Select all
&0 : PipeIn (Duplicated_1)
&1 : PipeOut
&2 : Unaltered
&3 : PipeIn (Original)
&4 : Original value of &0
&5 : Original value of &1
&6-&9 : UNDEFINED
2-2. The next one is
4>&0 It firsts saves the current value of
&4 in the first UNDEFINED slot which is now
&6 then then sets the value of
&4 to what is pointed by
&0 which is
Duplicated PipeIn. The handle to the
Duplicated PipeIn again will be duplicated. At this point the state of the internal CMD handles are as follows
Code: Select all
&0 : PipeIn (Duplicated_1)
&1 : PipeOut
&2 : Unaltered
&3 : PipeIn (Original)
&4 : PipeIn (Duplicate_2)
&5 : Original value of &1
&6 : Original value of &0
&7-&9 : UNDEFINED
3. It creates the left hand process (cmd /d /c "echo test") in suspended state. The state of the left hand child standard handles are:
Code: Select all
&0 : CMD's current stdin (PipeIn_Duplicated_1) | This is actually a self pipe. The child process can potentially write to pipe and read it back
&1 : CMD's current stdeout (PipeOut) | But this will happen in the child process. It can not only do this by pause(eat) or set /p but not with findstr
&2 : CMD's current stderr | It cause a dead lock because findstr will not terminate until it reads EOF and but it is also the writer of pipe.
4. CMD attempts to restores its own handles: Closes PipeOut handle and restores original value of
&1 from
&5 and clears
&5. Closes the handle that is pointed by
&0 which is
PipeIn (Duplicated_1). Copies the value of handle
&4 which is
PipeIn (Duplicated_2) to
&0. Then copies the value of handle
&6 to
&4 and clears the value of handle
&6 because it was UNDEFINED before. The final state of CMD internal handles will be
Code: Select all
&0 : PipeIn (Duplicated_2)
&1 : Restored original value from &5
&2 : Unaltered
&3 : PipeIn (Original)
&4 : Original value of &0
&5-&9 : UNDEFINED
5. This time It redirects its own stdin to the handle which is pointed by
&3. In this process the original value of
&0 will saved to
&5 and the value of
&3 will be cleared. At this time the state of the CMD internal handles are as follows:
Code: Select all
&0 : PipeIn (Original)
&1 : Unaltered
&2 : Unaltered
&3 : UNDEFINED
&4 : Original value of &0
&5 : PipeIn (Duplicated_2)
&6-&9 : UNDEFINED
6. Creates the right hand process (cmd /d /s /c "break") in suspended state.
7. Closes the handle to
PipeIn (Original) and restores value of
&0 from
&5 which is
PipeIn (Duplicated_2) and clears value of
&5
Code: Select all
&0 : Restored from &5 (PipeIn_Duplicated_2)
&1 : Unaltered
&2 : Unaltered
&3 : UNDEFINED
&4 : Original value of &0
&5-&9 : UNDEFINED
So the above will be state of the CMD after pipe processes have finished. It still have access to pipe input
(PipeIn_Duplicated_2) and the original stdin is saved in handle
&4