12
GREEN="\e[1;32m"
RED="\e[1;31m"
NONE="\e[m"

get_exit_status(){
   es=$?
   if [ $es -eq 0 ]
   then
       echo -e "${GREEN}${es}${NONE}"
   else
       echo -e "${RED}${es}${NONE}"
   fi
}

get_path(){
    #dummy function
    echo "PATH"
}

PROMPT_COMMAND='exitStatus=$(get_exit_status)'

The following gives me the correct exitStatus but colour variables are not expanded:

PS1='${RED}\h $(get_path) ${exitStatus}${NONE} '

However, the one below, gives me the colours but the exit status does not update:

PS1="${RED}\h $(get_path) ${exitStatus}${NONE} "

What is the right way to do this? How can I fix this so that the exitStatus and colours both work?

dogbane
  • 29,677

6 Answers6

8

Gilles identified your main problem, but I wanted to try explaining it differently.

Bash interprets the special prompt escapes only before expanding any variables in the prompt. This means that using \e in a variable that is expanded from the prompt doesn't work, even though it does work directly in PS1.

For example, this works as expected, and gives red text:

PS1='\e[1;31m this is in red '

But this doesn't, it just puts a literal \e in the prompt:

RED='\e[1;31m'
PS1="$RED not in red "

If you want to store the color escapes in variables, you can use ANSI-C quoting ($'...') to put a literal escape character in the variable.

To do this, you can change your definition of GREEN, RED, and NONE, so their value is the actual escape sequence.

GREEN=$'\033[1;32m'
RED=$'\033[1;31m'
NONE=$'\033[m'

If you do that, your first PS1 with the single quotes should work:

PS1='${RED}\h $(get_path) ${exitStatus}${NONE} '

However, then you will have a second problem.

Try running that, then press Up Arrow, then Home, and your cursor will not go back to the start of the line.

To fix that, change PS1 to include \[ and \] around the color escape sequences, e.g.

PS1='\[${RED}\]\h $(get_path) $?\[${NONE}\] '

You can't use get_exit_status properly here, since its output contains both printing (the exit code) and non-printing characters (the color codes), and there's no way to mark it correctly in the prompt. Putting \[...\] would mark it as non-printing in full, which is not correct. You'll have to change the function so that it only prints the proper color-code, and then surround it with \[...\] in the prompt.

Mikel
  • 57,299
  • 15
  • 134
  • 153
  • \[ is \1, and \[ is \2. Those to corresponds to some readline's RL_PROMPT_{START,END}_IGNORE thing which asks it to ignore the bytes when counting the prompt length on screen. See https://lists.gnu.org/archive/html/bug-bash/2015-08/msg00027.html. – Mingye Wang Oct 27 '15 at 18:53
  • @Arthur2e5 Do you mean \] is \2? And do you mean that's why it's needed for ${exitStatus}? My point was that ${exitStatus} does not contain non-printing characters, so bash should be able to correctly determine how many characters it moves the prompt without the \[ and \] in \[${exitStatus}\]. – Mikel Oct 27 '15 at 19:13
  • The problem is it does contain some -- the colors. (ANSI Escapes) – Mingye Wang Oct 27 '15 at 19:28
  • @Arthur2e5 Ew, I totally missed that. :) Why would you put colors... never mind. :) – Mikel Oct 27 '15 at 19:46
  • Well, someone just want to emphasize the codes… Sometimes I do the same: https://github.com/AOSC-Dev/bash-config/issues/8 – Mingye Wang Oct 27 '15 at 19:48
  • Yeah, I was just surprised by mixing of data and presentation, but I get it. :) – Mikel Oct 27 '15 at 20:55
  • 1
    "Bash is effectively calling echo on your PS1, not echo -e" -- well that's wrong or just missing the point. Bash does expand backslash-escapes like \e and \033 (and \[/\], \u, and \h) from the prompt, it just does so before expanding the variables. So PS1='\e[1;31m red' works, red='\e[1;31m'; PS1='$red red' doesn't. – ilkkachu Sep 19 '18 at 13:56
  • @ilkkachu Thanks for the comment and correction. I was about to edit, and noticed you had already done so. Thank you! – Mikel Sep 19 '18 at 14:20
3

When you run PS1='${RED}\h $(get_path) ${exitStatus}${NONE} ', the PS1 variable is set to ${RED}\h $(get_path) ${exitStatus}${NONE}, where only \h is a prompt escape sequence. After the prompt sequences are expanded (yielding ${RED}darkstar $(get_path) ${exitStatus}${NONE}), the shell performs the usual expansions such as variable expansions. You get a displayed prompt that looks like \e[1;31mdarkstar PATH 0\e[m. Nothing along the way expands the \e sequences to actual escape characters.

When you run PS1="${RED}\h $(get_path) ${exitStatus}${NONE} ", the PS1 variable is set to \e[1;31m\h PATH 0\e[m. The variables RED, exitStatus and NONE are expanded at the time of the assignment. Then the prompt contains three prompt escape sequences (\e, \h, and \e again). There are no shell variables to expand at this stage.

In order to see colors, you need the color variables to contain actual escape characters. You can do it this way:

RED=$'\033[1;31m'
NONE=$'\033[m'
PS1='\[${RED}\]\h \w $?\[${NONE}\] '

$'…' expands backslash-octal sequences and some backslash-letter sequences such as \n, but not including \e. I made three other changes to your prompt:

  • Use \[…\] around non-printing sequences such as color-changing commands. Otherwise your display will end up garbled because bash can't figure out the width of the prompt.
  • \w is a built-in escape sequence to print the current directory.
  • You don't need anything complicated to show $? in the prompt if you don't have a PROMPT_COMMAND in the first place.
  • I think the idea was to make the prompt be green on success and red on failure. – mattdm Mar 03 '11 at 01:20
  • Yes, PS1 is wrong, but the advice to use $'...' for RED and GREEN should make it work using dogbane's PS1. – Mikel Mar 03 '11 at 02:05
1

Here is the approach I have gone with, it avoids the use of PROMPT_COMMAND.

# This function is called from a subshell in $PS1,
# to provide a colourised visual indicator of the exit status of the last run command
__COLOURISE_EXIT_STATUS() {
    # uncomment the next line for exit code output after each command, useful for debugging and testing
    #printf -- "\nexit code: $1\n" >&2
    [[ 0 == "$1" || 130 == "$1" ]] && printf -- "$GREEN" || printf -- "$RED"
}

Then my $PS1 is as follows:

PS1='# ${debian_chroot:+($debian_chroot)}'"${GREEN}\u${YELLOW}@${DARK_YELLOW}\h${WHITE}:${LIGHT_BLUE}\w${WHITE}\n"'\[$(__COLOURISE_EXIT_STATUS $?)\]# \$'"\[${WHITE}\] "
Kyle
  • 111
  • 1
    While it doesn't matter in this particular case, as the only value $? can have is an integer, you really should use printf '%b' "$GREEN" instead. Also, avoid using function names prefixed with __ or _ as they are used by bash-completion. – Alexia Luna Jun 12 '14 at 17:18
1

Here you go - This Works For Me (TM) in Ubuntu and other Linuxes (Linuxen?).

The reason for putting the exit code detection in $PS1 is that one host has a read-only $PROMPT_COMMAND set before .bashrc is read.

l0b0
  • 51,350
1

Try:

PS1='`exitStatus=$?;if [ $exitStatus -eq 0 ];then echo "\['${GREEN}'\]";else echo "\['${RED}'\]";fi;echo "\h $(get_path) ${exitStatus}${NONE}"`'
shellholic
  • 6,255
  • 1
    Thanks, this works, but is there any way to accomplish this without having to embed an if-statement within the prompt? – dogbane Mar 02 '11 at 08:34
  • export PS1="$([ $? -gt 0 ] && printf '[\033[31m]' || printf '[\033[32m]'; printf '$PWD [\033[0m]')" – jaygooby Feb 19 '21 at 13:25
0

For PROMPT_COMMAND, it's cleaner to define a function and use that:

prompt_command() {
    # ...
}
PROMPT_COMMAND=prompt_command