I have a fancy prompt with colors, and now bash doesn't seem to know how wide my terminal is. Lines wrap around incorrectly.

This is usually caused by incorrect use of \[ and \]

For all the following examples, assume these variables are set:

   1 host_color=$(tput setaf 2)              # green
   2 dir_color=$(tput bold; tput setaf 4)    # bold blue
   3 branch_color=$(tput bold; tput setaf 3) # bold yellow
   4 reset=$(tput sgr0)                      # reset colors

Escape the colors with \[ \]

You must put \[ and \] around any non-printing escape sequences in your prompt. Thus:

   1 PS1='\[$host_color\]\h\[$reset\]:\[$dir_color\]\w\[$reset\]\$ '

The \[ \] tells bash that the bytes within will not increase the size of the prompt. Without the \[ \], bash will think the bytes which constitute the escape sequences for the color codes will actually take up space on the screen, so bash won't be able to know where the cursor actually is.

Functions that output colored text

The \[ and \] are parsed before any of the expansions take place, so the following will not work:

   1 # BROKEN
   2 git_branch() {
   3   local branch
   4   if branch=$(git branch --show-current 2>/dev/null); then
   5     printf ' (\\[%s\\]%s\[%s\])' "$branch_color" "$branch" "$reset"
   6   fi
   7 }
   8 PS1='\[$host_color\]\h\[$reset\]:\[$dir_color\]\w\[$reset\]$(git_branch)\$ '

This will result in the prompt looking like

myhost:~/repo (\[\]main\[\])$ 

There are several ways to work around this problem:

Using ${parameter@P}

Since bash 4.4,  ${parameter@P}  will expand a variable the same way PS1 is expanded:

   1 git_branch() {
   2   local branch prompt_part
   3   if branch=$(git branch --show-current 2>/dev/null); then
   4     prompt_part='(\[$branch_color\]$branch\[$reset\]) '
   5     printf %s "${prompt_part@P}"
   6   fi
   7 }
   8 PS1='\[$host_color\]\h\[$reset\]:\[$dir_color\]\w\[$reset\]$(git_branch)\$ '

This can also be used to provide a colored prompt for the read command when using its readline mode (-e):

   1 highlight=$(tput bold ; tput setaf 6)   # bold cyan
   2 reset=$(tput sgr0)
   3 
   4 prompt='Which direction will you go, \[$highlight\]north\[$reset\] or \[$highlight\]south\[$reset\]? '
   5 read -ep "${prompt@P}" direction

Hardcoding \001 and \002

The \[ and \] actually just gets replaced with byte values 1 and 2, respectively, which are the markers the readline library uses to know which parts should be ignored when calculating the prompt length.

   1 git_branch() {
   2   local branch
   3   if branch=$(git branch --show-current 2>/dev/null); then
   4     printf ' (\001%s\002%s\001%s\002)' "$branch_color" "$branch" "$reset"
   5   fi
   6 }
   7 PS1='\[$host_color\]\h\[$reset\]:\[$dir_color\]\w\[$reset\]$(git_branch)\$ '

This can also be used to provide a colored prompt for the read command when using its readline mode (-e) in bash versions older than 4.4:

   1 highlight=$(tput bold ; tput setaf 6)   # bold cyan
   2 reset=$(tput sgr0)
   3 
   4 printf -v prompt 'Which direction will you go, \001%s\002north\001%s\002 or \001%s\002south\001%s\002? ' \
   5     "$highlight" "$reset" "$highlight" "$reset"
   6 read -ep "$prompt" direction

Using PROMPT_COMMAND

The value of the special PROMPT_COMMAND variable will be evaluated just before the PS1 prompt is displayed. This can be used to assign variables that the prompt then can use conditionally:

   1 PROMPT_COMMAND='git_branch=$(git branch --show-current 2>/dev/null)'
   2 PS1='\[$host_color\]\h\[$reset\]:\[$dir_color\]\w\[$reset\]${git_branch:+ (\[$branch_color\]$git_branch\[$reset\])}\$ '

Checking if your PS1 variable is correctly set

The following code can tell you if PS1 is correctly set:

   1 tput clear; sed $'s/\1[^\2]*\2//g; h; s/./=/g; H; x' <<< "${PS1@P}"

It first clears the terminal, then sed prints the prompt with all the colors removed, along with a bar showing the width.

Example showing a correct prompt

# Correct
PS1='\[$host_color\]\h\[$reset\]:\[$dir_color\]\w\[$reset\]\$'
tput clear; sed $'s/\1[^\2]*\2//g; h; s/./=/g; H; x' <<< "${PS1@P}"

the result will be

myhost:~$            <- uncolored version of the prompt
=========            <- shows the calculated length
myhost:~$            <- the actual prompt

When the first line is an uncolored, but otherwise identical, version of the actual prompt, and the bar lines up exactly with both, you have a correctly set PS1 variable.

Example showing a prompt that is assumed to be longer than it actually is

# Wrong
PS1='$host_color\]\h\[$reset\]:\[$dir_color\]\w\[$reset\]\$'
tput clear; sed $'s/\1[^\2]*\2//g; h; s/./=/g; H; x' <<< "${PS1@P}"

In this example, the leading \[ is missing, so $host_color is not properly enclosed

myhost:~$            <- green instead of uncolored
===============      <- longer than the visible prompt
myhost:~$

where the first line is green instead of uncolored, and the bar is longer than the actual prompt, because the escape sequence for green (ESC, [, 3, 2, and m) gets included in the calculation of the prompt's width.

Example showing a prompt that is assumed to be shorter than it actually is

# Wrong
PS1='\[$host_color\h\]\[$reset\]:\[$dir_color\]\w\[$reset\]\$'
tput clear; sed $'s/\1[^\2]*\2//g; h; s/./=/g; H; x' <<< "${PS1@P}"

In this example, the \h is inside a pair of \[ \]. The result becomes

:~$                  <- uncolored, but lacks the hostname part
===                  <- shorter than the visible prompt
myhost:~$

the first line is uncolored, but missing the hostname part. The prompt is now assumed to be just three columns wide.

Other causes

If you still have problems, e.g. when going through your command history with the Up/Down arrows, make sure you have the checkwinsize option set:

   1 shopt -s checkwinsize

BashFAQ/053 (last edited 2025-09-01 21:17:29 by geirha)