Published in Articles on by Michiel van Oosterhout ~ 8 min read
This will be the last article in a series of articles aiming to reproduce the custom Bash prompt in Windows Command Processor. Previously, we created the Prompt extension to make it easy to create a colorful, segmented prompt mockup. In this article we'll use that to set the actual prompt.
The Windows Command Processor prompt
For Windows Command Processor, the prompt can be changed with the prompt
command. Certain character sequences in the first parameter to prompt
will be replaced, for example $p
with the current drive and directory path, $g
with >
, and $s
with a space.
Unlike Bash, which evaluates the PS1
prompt variable everytime the prompt is rendered, Windows Command Processor requires us to set the prompt explicitly everytime the context changes if we want our prompt to be dynamic (e.g. contain contextual information).
The SetPrompt extension
We will first create a function that can be called to change the prompt if the CMD_PROMPT
environment variable is set. This function can be called by any extension that provides contextual information for the prompt, whenever the relevant context changes.
# Ensure the extension's directory exists
$path = "$Env:LOCALAPPDATA\Commando\SetPrompt"
$_ = New-Item $path -Force -ItemType Directory
# Declare the filter script contents
$script = @"
@echo off
for /f %%p in ('call %CMD%\GetCp\getcp.cmd') do set "codepage=%%p"
chcp 65001 >NUL
if not "%CMD_PROMPT%"=="" (
for /f "tokens=* delims=" %%p in ('call %CMD%\Prompt\prompt.cmd %CMD_PROMPT%') do (
prompt %%p`$s
)
)
chcp %codepage% >NUL
set "codepage="
"@
# Create the Windows Command Processor script
$path = "$path\set_prompt.cmd"
Set-Content -Path $path -Value $script
# Ensure proper line endings
((Get-Content $path) -join "`r`n") + "`r`n" | Set-Content -NoNewline $path
Remember from an earlier article that we need to correctly interpret the bytes in the prompt.cmd
script, so the set_prompt.cmd
script temporarily sets the code page to 65001
before restoring the original code page.
Now we can just call %CMD%\SetPrompt\set_prompt.cmd
and the prompt will be changed, but only if the CMD_PROMPT
environment variable is set (e.g. setx CMD_PROMPT "bW ~/Desktop/test / Y- main"
).

The prompt should be set immediately when Windows Command Processor starts. For this we can use the extension's Extend.cmd
script:
# Ensure the extension's directory exists
$path = "$Env:LOCALAPPDATA\Commando\SetPrompt"
$_ = New-Item $path -Force -ItemType Directory
# Declare the script contents
$script = @"
@echo off
call %CMD%\SetPrompt\set_prompt.cmd
"@
# Create the Windows Command Processor script
$path = "$path\Extend.cmd"
Set-Content -Path $path -Value $script
# Ensure proper line endings
((Get-Content $path) -join "`r`n") + "`r`n" | Set-Content -NoNewline $path
The next set of extensions will use the set_prompt
function to set the prompt whenever some context changes.
Extensions for contextual information
A custom prompt is valuable when it shows contextual information, e.g. information that is updated as you use commands in Windows Command Processor, for example changing the current working directory, or checking out a different Git branch. For this we need some extensions that filter commands like cd
and git
to set some environment variables that we can then use in our custom prompt.
The GitAt extensions
First we'll create a function that can get the current Git branch, tag, or commit ID. The Windows PowerShell script below creates a new extension called GitAt.
# Ensure the directory exists
$path = "$Env:LOCALAPPDATA\Commando\GitAt"
$_ = New-Item $path -Force -ItemType Directory
# Declare the script contents
$script = @'
@echo off
setlocal enabledelayedexpansion
rem Check that current directory is in a Git repository
git rev-parse --is-inside-work-tree >NUL 2>&1
if not errorlevel 1 (
rem Get current branch
for /f %%b in ('git branch --show-current') do set "result=%%b"
if "!result!"=="" (
rem Get first tag instead
for /f %%t in ('git --no-pager tag --points-at HEAD') do (
if "!result!"=="" (
set "result=%%t"
)
)
if not "!result!"=="" (
set "result=[!result!]"
)
)
if "!result!"=="" (
rem Get commit ID instead
for /f %%c in ('git rev-parse --short HEAD') do set "result=%%c"
)
if not "!result!"=="" (
rem Echo the result without a newline
<NUL set /p _=!result!
)
)
endlocal
'@
# Create the Windows Command Processor script
$path = "$path\git_at.cmd"
Set-Content -Path $path -Value $script
# Ensure proper line endings
((Get-Content $path) -join "`r`n") + "`r`n" | Set-Content -NoNewline $path
The extension exports the git_at
function, which checks if the current directory is in a Git repository, and, if so, echo
s the name of the current branch, or the name of the current tag when not on a branch, or the current commit ID. This function can be called like this: call %CMD%\GitAt\git_at.cmd
.
The GitAt.Prompt extension
We can create another extension that will use the git_at
function to set PROMPT_GIT_AT
every time the current working directory is changed, or every time the git
command is executed.
# Ensure the extension's directory exists
$path = "$Env:LOCALAPPDATA\Commando\GitAt.Prompt"
$_ = New-Item $path -Force -ItemType Directory
# Declare the filter script contents
$script = @"
@echo off
setlocal enabledelayedexpansion
for /f "delims=; tokens=1,*" %%a in ("%~1") do (
if "%%~b"=="" (set next=%%~a) else (set next=call %%~a "%%b")
)
set params=0
shift
:loop_params
set next=%next% %1 && shift && if not "%~1"=="" set /a params+=1 && goto loop_params
endlocal && %next%
set "PROMPT_GIT_AT="
for /f %%s in ('call %CMD%\GitAt\git_at.cmd') do set "PROMPT_GIT_AT=%%s"
call %CMD%\SetPrompt\set_prompt.cmd
"@
# Create the filter scripts
@("cd", "chdir", "pushd", "git") | ForEach-Object {
$filePath = "$path\$_.filter.cmd"
Set-Content -Path $filePath -Value $script
# Ensure proper line endings
((Get-Content $filePath) -join "`n") + "`n" | Set-Content -NoNewline $filePath
}
With this extension in place, the PROMPT_GIT_AT
variable is set, and the prompt updated, immediately after the current working directory is changed, or after a git
command. But when Windows Command Processor is started in a directory that is inside a Git repository, then PROMPT_GIT_AT
is not set yet.
To fix this, the extension should also set PROMPT_GIT_AT
as part of its installation.
# Ensure the extension's directory exists
$path = "$Env:LOCALAPPDATA\Commando\GitAt.Prompt"
$_ = New-Item $path -Force -ItemType Directory
# Declare the script contents
$script = @"
@echo off
for /f %%s in ('call %CMD%\GitAt\git_at.cmd') do set "PROMPT_GIT_AT=%%s"
"@
# Create the Windows Command Processor script
$path = "$path\Extend.cmd"
Set-Content -Path $path -Value $script
# Ensure proper line endings
((Get-Content $path) -join "`r`n") + "`r`n" | Set-Content -NoNewline $path
With this extension in place, the PROMPT_GIT_AT
variable is always available for use in a custom prompt, and the prompt is updated immediately afterwards.
The Dir.Prompt extension
Because we also want to display the shortened current working directory in the prompt, we'll create one more extension. This one will set PROMPT_DIR
every time the current working directory is changed, and immediately update the prompt.
# Ensure the directory exists
$path = "$Env:LOCALAPPDATA\Commando\Dir.Prompt"
$_ = New-Item $path -Force -ItemType Directory
# Declare the script contents
$script = @"
@echo off
setlocal enabledelayedexpansion
for /f "delims=; tokens=1,*" %%a in ("%~1") do (
if "%%~b"=="" (set next=%%~a) else (set next=call %%~a "%%b")
)
set params=0
shift
:loop_params
set next=%next% %1 && shift && if not "%~1"=="" set /a params+=1 && goto loop_params
endlocal && %next%
setlocal enabledelayedexpansion
for /f "tokens=*" %%l in ('echo %CD%') do (
set "line=%%l"
)
endlocal && set "PROMPT_DIR=%line:$Env:USERPROFILE=~%"
call %CMD%\SetPrompt\set_prompt.cmd
"@
# Create the filter scripts
@("cd", "chdir", "pushd") | ForEach-Object {
$filePath = "$path\$_.filter.cmd"
Set-Content -Path $filePath -Value $script
# Ensure proper line endings
((Get-Content $filePath) -join "`n") + "`n" | Set-Content -NoNewline $filePath
}
And just as with the GitAt.Prompt extension, the Dir.Prompt extension should also set PROMPT_DIR
as part of its installation.
# Ensure the extension's directory exists
$path = "$Env:LOCALAPPDATA\Commando\Dir.Prompt"
$_ = New-Item $path -Force -ItemType Directory
# Declare the script contents
$script = @"
@echo off
setlocal enabledelayedexpansion
for /f "tokens=*" %%l in ('echo %CD%') do (
set "line=%%l"
set "line=!line:$Env:USERPROFILE=~!"
)
endlocal && set "PROMPT_DIR=%line%"
"@
# Create the Windows Command Processor script
$path = "$path\Extend.cmd"
Set-Content -Path $path -Value $script
# Ensure proper line endings
((Get-Content $path) -join "`r`n") + "`r`n" | Set-Content -NoNewline $path
With this extension in place, the PROMPT_DIR
variable is always available for use in a custom prompt, and the prompt is updated immediately afterwards.
Configuring a dynamic prompt
With everything in place, we can now update CMD_PROMPT
to a dynamic prompt:
setx CMD_PROMPT "bW ^%PROMPT_DIR^% / Y- ^%PROMPT_GIT_AT^%"

Notice that we delay expansion of the PROMPT_
variables by 'escaping' the %
-signs. This way the variables will be expanded when the set_prompt
function is called.
Conclusion
It takes considerably more effort to achieve a dynamic prompt with contextual information in Windows Command Processor. And while it may look pretty on the screenshots, it add noticable latency to the CLI. Finally, due to the myriad ways of changing the context, the prompt may not always contain correct information (for example after mkdir test && cd test
the PROMPT_DIR
variable is not updated). We may revisit the implementation in the future to see if these problems can be fixed.