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.

Police interceptor
Police interceptor Photo by form PxHere, CC0

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.

Tagged pipeline
Tagged pipeline Photo by Michael Coghlan, CC BY-SA

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 to doskey 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.

  1. 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's DelegatingHandler↩︎