Intercepting commands in Windows Command Processor
Using the interceptor pattern to intercept the invocation of any Windows command
Published in Articles on by Michiel van Oosterhout ~ 11 min read
Last month we designed and implemented a simple extension system for Windows Command Processor in a 4-part series of articles (part 1, part 2, part 3, and part 4). The extension system provides two extension mechanisms: exporting functions and defining macros. Macros can be used to intercept a built-in command or command-line program. But only one macro with a particular name can be recorded, so if two extension define a macro with the same name, then one will be recorded, and one will be ignored. So let's find another way for multiple extensions to intercept the same built-in command or command-line program.

The interceptor pattern
The interceptor pattern is a software design pattern that uses interceptors (also called filters) to preprocess a request and/or postprocess its response. A typical example for those familiar with back-end web development are HTTP request filters 1. These can for example implement authentication and authorization based on a request's URL, or content negotiation based on a request's Accept
header. Sometimes these features are referred to as cross-cutting concerns. Such concerns can be handled transparently by applying the corresponding filters to a request handler.
Multiple filters can be applied to a single request handler, thus forming a pipeline that a request and its response travel through. Typically the application framework gives filters access to a context. This context exposes the request, the response, and a delegate called next
.
The next
delegate is either the next filter or, if the current filter is the last one in the pipeline, it is the request handler itself. The filter may choose to execute next
and return the response it returns, or not, thereby short-circuiting the request. For example, an authentication filter that intercepts an anonymous HTTP request will not execute next
, and instead return a response that challenges the client to provide a credential.
There are two approaches to apply this pattern to Windows Command Processor.
Hiding built-in command-line programs
To intercept a built-in command-line program such as %SystemRoot%\system32\ipconfig.exe
, we can create a filter named ipconfig.cmd
, and prepend the path of its parent directory to the Path
environment variable. The file extensions listed in the PATHEXT
environment variable are used to resolve command lines that execute a command-line program without specifying its file extension, and includes .cmd
. So ipconfig
will resolve to ipconfig.cmd
instead of ipconfig.exe
, effectively hiding it. The hidden command-line program can still be executed by specifying its extension explicitly, e.g. ipconfig.exe
.
Each extension's directory must be added to the Path
environment for this to work, and built-in commands (e.g. dir
) can not be intercepted this way.
When a filter is executing, it must determine next
using the %SystemRoot%\system32\where.exe
command-line program, but this is left as an exercise to the reader, as we will use a different approach.
Recording a macro that matches a built-in command or command-line program
A macro can be recorded with a name that matches a built-in command or command-line program. We've explored this option in part-3, where extensions can provide a macro file, and our extension system simply calls doskey /macrofile=extension\macros.txt
.
Rather than simply recording the macros that each extension defines, our extension system can record a macro on behalf of one or more extensions. Our extension system will build the macro text for a macro as it discovers filters for that macro, and then record it, such that the macro executes the first filter and passes the list of the remaining filters to it via %1
. Let's use an example to illustrate.
Example
Extension A, B, and C all want to filter the built-in dir
command. The filters need to be combined into a pipeline, starting with extension A's filter, followed by extension B and C's filters, and ending with the dir
command itself.

Our extension system creates this pipeline by recording a macro named dir
with a text that executes extension A's filter, and passes the rest of the pipeline as a parameter to the first filter, followed by the rest of the parameters from the command line ($*
):
doskey dir="A" """"B""";"""C""";dir" $*
This means that when you enter the command line dir /B
it will resolve to this command line (notice that two sets of embedded quotes have been removed by doskey
):
"A" """B"";""C"";dir" /B
So extension A's filter will be executed with a specially crafted first parameter. Notice that this first parameter is a quoted, semicolon-separated list of doubly-quoted paths to filters B and C followed by the unquoted name of the macro:
""A""
""B""
dir
The first filter should then extract next
from %1
, and execute it via call
, passing the first parameter (from which the first entry has been removed) and any remaining parameters. When done correctly (the code for this is given below), each filter will be executed in turn, and the last filter will simply execute dir /B
.
The code below is the minimal implementation of a filter that executes next
, does not change the parameters, and does not alter the output.
@echo off
setlocal enabledelayedexpansion
for /f "delims=; tokens=1,*" %%a in ("%~1") do (
if "%%~b"=="" (set next=%%~a) else (set next=call %%~a "%%b")
)
shift
:loop
set next=%next% %1 && shift && if not "%~1"=="" goto loop
for /f "tokens=*" %%l in ('%next%') do (
echo %%l
)
endlocal
A description of this code follows in the next 3 sections.
Extracting next
for /f "delims=; tokens=1,*" %%a in ("%~1") do (
if "%%~b"=="" (set next=%%~a) else (set next=call %%~a "%%b")
)
The for
command takes the first parameter (%1
), removes its surrounding double quotes, if any, (%~1
), and processes it as a string ("%~1"
), using the for
command's file parsing mode (/f
). This mode is configured using the parsing keywords delims
and tokens
. The string is split on semicolons (delims=;
) into 2 tokens: a token for part of the string up to the first delimiter, and a token for the part of the string after the first delimiter (tokens=1,*
). The first token will be %%a
and by the rules of the for
command the second token will be %%b
.
Given the macro recording above the string parsed by the for
command and the resulting tokens in the do
block in the first filter are:
%~1
:""B"";""C"";dir
%%a
:""B""
%%b
:""C"";dir
And when next
is executed correctly, the values in the second filter will be:
%~1
:""C"";dir
%%a
:""C""
%%b
:dir
And finally, the values in the third filter will be:
%~1
:dir
%%a
:dir
%%b
: empty
This illustrates how the original macro was recorded such that a filter can extract next
from %1
, and execute next
with a first parameter that allows the next filter to do the same.
The for
command's do
block checks if %%b
is empty. If so it sets next
to %%~a
which will cause the original command to be executed as is. Otherwise it sets next
to call %%~a "%%b"
which sets up the next filter to be able to do the same kind of processing.
Again given the macro recording above, next
in each filter is extracted from %1
as:
- A:
call "B" """C"";dir"
- B:
call "C" "dir"
- C:
dir
Processing parameters
shift
:loop
set next=%next% %1 && shift && if not "%~1"=="" goto loop
Because the original command line may have contained parameters (e.g. dir /B
), each filter should process the list of parameters. A filter may use this phase to add, remove, or change parameters, but the minimal implementation presented here just passes the parameters to next
without modification.
The first shift
command shifts all parameters to the left, so that %1
refers to the second parameter. The code after the :loop
label appends %1
to next
, shifts all parameters to the left again, and repeats the process as long as there are parameters.
Again given the macro recording above, after this loop next
in each filter is:
- A:
call "B" """C"";dir" /B
- B:
call "C" "dir" /B
- C:
dir /B
So as we can see, the last filter is left to execute the original command as is.
Processing output
for /f "tokens=*" %%l in ('%next%') do (
echo %%l
)
The last for
command illustrates how a filter could process the output from next
. It executes next
and processes the output ('%next%'
), using the for
command's file parsing mode (/f
). This mode is configured using the parsing keyword tokens
. Each line in the output is not split, resulting in a single token (tokens=*
) which is %%l
and its value is equal to the complete line. This for
command's do
block simply writes the line to stdout
(echo %%l
).
Commando: a Windows Command Line extension system, revisited
Now that we have an additional extension mechanism, we need to replace the existing Commando script, created in part 4, with a revised one.
The Windows PowerShell script below creates or replaces the Commando script, the script that initializes our extension system for Windows Command Processor.
# Declare the Commando script contents
$script = @"
@echo off
if defined CMD (goto :EOF) else (set CMD=%~dp0)
setlocal enabledelayedexpansion
for /d %%d in ("%~dp0*") do (
if exist "%%d\macros.txt" doskey /macrofile="%%d\macros.txt"
for %%f in ("%%d\*.cmd") do (
for /f "delims=. tokens=1,2,3" %%a in ("%%~nxf") do (
if %%b==filter if %%c==cmd (
if not defined filters[%%a] (
set filters[%%a]="%%f" "
) else (
set filters[%%a]=!filters[%%a]!"""%%f""";
)
)
if %%b==function if %%c==cmd (
for /f "tokens=1,* delims=." %%x in ("%%f") do (
if not %%~nx==Extend doskey %%~nd::%%~nx="%%~f" $*
)
)
)
)
)
for /f "delims=[]= tokens=1,2,3" %%p in ('set') do (
if %%p==filters (
doskey %%q=%%r%%q" $*
)
)
endlocal
for /d %%d in ("%~dp0*") do (
if exist %%d\Extend.cmd (
call "%%d\Extend.cmd"
)
)
"@
# Ensure the directory exists
$path = "$Env:LOCALAPPDATA\Commando"
$_ = New-Item $path -Force -ItemType Directory
# Create the Commando script
$path = "$path\Commando.cmd"
Set-Content -Path $path -Value $script
As before, this script:
- runs only once per session by checking the
CMD
environment variable; - passes each extension's
macros.txt
file todoskey
for recording; - records a macro for each exported function;
- and runs the extension's installation script (
Extend.cmd
).
But now the script also records a macro for each name for which there is at least one filter. Any of the extensions' files that is named name.filter.cmd
is considered a filter for name
, and its path will be included the text of a macro recorded for name
. For consistency, exported functions should be contained in files whose name ends with .function.cmd
.
Summary
Our extension system for Windows Command Processor now supports the interceptor pattern: extensions can provide a filter for any built-in command or command-line program. Filters can process parameters and output. Every filter must implement the interceptor pattern in code, but the code is relatively simple: a for
command to extract next
, a loop to process parameters, and a for
command to process output. The next article will present some extensions to illustrate how we can use our extension system.
Updates
- Changed the delimiter from tab (
^T
) to semicolon (;
). - Added note about the behavior of changing the current working directory inside a local scope.
- Added a requirement that exported functions should be contained in files whose name ends with
.function.cmd
. - Changed the initialization to call extensions'
Extend.cmd
in global scope.
-
ASP.NET developers may be familiar with OWIN and its concept of middleware, ASP.NET Web API's delegating handlers, ASP.NET Core's middleware, or
HttpClient
'sDelegatingHandler
. ↩︎