93

I've been trying to customize my Bash prompt so that it will look like

[feralin@localhost ~]$ _

with colors. I managed to get constant colors (the same colors every time I see the prompt), but I want the username ('feralin') to appear red, instead of green, if the last command had a nonzero exit status. I came up with:

\e[1;33m[$(if [[ $? == 0  ]]; then echo "\e[0;31m"; else echo "\e[0;32m"; fi)\u\e[m@\e[1;34m\h \e[0;35m\W\e[1;33m]$ \e[m

However, from my observations, the $(if ...; fi) seems to be evaluated once, when the .bashrc is run, and the result is substituted forever after. This makes the name always green, even if the last exit code is nonzero (as in, echo $?). Is this what is happening? Or is it simply something else wrong with my prompt? Long question short, how do I get my prompt to use the last exit code?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
feralin
  • 3,268
  • 3
  • 21
  • 37
  • 4
    I've never succeeded at something like that either; what I have done, however, is put ${?#0} into the prompt which prints the numeric exit status if and only if it's non-zero. – Wes Hardaker May 23 '13 at 13:32
  • 3
    It works as is. You just have reversed green and red. – n. m. could be an AI Jul 12 '14 at 18:42
  • 3
    Shameless plug for [prompt.gem](https://bitbucket.org/dimo414/prompt.gem) which provides an extensible prompt that includes both the exit code and duration of the previous command. – dimo414 Aug 15 '16 at 04:42
  • 2
    Re: "evaluated once, when the `.bashrc` is run" -- that means you're putting the wrong kinds of quotes around it when assigning to `PS1`. Needs to be single, not double. If that doesn't help, then you're getting `$?` reset by something else that's running before your prompt is printed; `set -x` will enable tracking such commands down. – Charles Duffy Sep 21 '16 at 01:13
  • Works for me too, except green and red reversed. Are you doing `PS1='...'` with single quotes? Do `echo $PS1` to verify it's being set properly. – wisbucky Mar 10 '17 at 20:37
  • @wisbucky I haven't used this bash prompt in several years, so unfortunately I cannot verify anything about it anymore... – feralin Mar 10 '17 at 20:41
  • has bash5 changed anything? there are so many broken suggestions out there... – dotbit Jul 02 '19 at 17:18

10 Answers10

149

As you are starting to border on a complex PS1, you might consider using PROMPT_COMMAND. With this, you set it to a function, and it will be run after each command to generate the prompt.

You could try the following in your ~/.bashrc file:

PROMPT_COMMAND=__prompt_command    # Function to generate PS1 after CMDs

__prompt_command() {
    local EXIT="$?"                # This needs to be first
    PS1=""

    local RCol='\[\e[0m\]'

    local Red='\[\e[0;31m\]'
    local Gre='\[\e[0;32m\]'
    local BYel='\[\e[1;33m\]'
    local BBlu='\[\e[1;34m\]'
    local Pur='\[\e[0;35m\]'

    if [ $EXIT != 0 ]; then
        PS1+="${Red}\u${RCol}"        # Add red if exit code non 0
    else
        PS1+="${Gre}\u${RCol}"
    fi

    PS1+="${RCol}@${BBlu}\h ${Pur}\W${BYel}$ ${RCol}"
}

This should do what it sounds like you want. Take a look a my bashrc's sub file if you want to see all the things I do with my __prompt_command function.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
demure
  • 7,337
  • 3
  • 22
  • 17
  • Interesting. I didn't know about PROMPT_COMMAND. I'll try it now. – feralin May 23 '13 at 13:49
  • All right, it works! I just changed it a little to include the [...] delimeters before the "$ ". Other than that, it was perfect. Thanks! – feralin May 23 '13 at 14:07
  • Glad it does what you want. You can also trash the color vars if you want, but using them makes it so much more readable. – demure May 23 '13 at 14:23
  • @demure - your `PROMPT_COMMAND` is no longer in your `.bashrc` file. Also, it doesn't appear that `$HOME/.subbash` is in your github repo. I had to go back 3-4 history versions in your .bashrc to see its former contents. – Matt Anderson Jul 17 '15 at 16:55
  • One thing to note is that PROMPT_COMMAND can introduce terminal lag on boxes with significant load. This can cause issues with random space characters being inserted whilst holding down . Because of this, I chose to only include exit status via `PS1` (eg: `${?##0}`) – Blaskovicz Aug 18 '16 at 18:52
  • 3
    I'd suggest using a variable name with at least one lower-case character for `EXIT`, as a forward compatibility practice. See http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap08.html, fourth paragraph -- the namespace of all-caps names is used for variables with meaning to the system or shell; sticking to lowercase names prevents overwriting an environment variable or shell-builtin variable when trying only to assign a shell variable. Granted, with a local declaration that overwrite is temporary, which makes it less of an issue than usual here, but still not ideal. – Charles Duffy Sep 11 '16 at 16:28
  • 7
    On mac you need `PROMPT_COMMAND="__prompt_command; ${PROMPT_COMMAND}"` to enable opening a new tab / window in the current working directory still works. `PROMPT_COMMAND=update_terminal_cwd` by default, which logs the cwd. – Isaac Turner Feb 12 '17 at 22:28
  • 5
    Bash variable `PIPESTATUS` provides more information about the last executed command or pipe. `${PIPESTATUS[@]}` will be a string of zeroes like `0 0 0` in case all commands were executed correctly, otherwise it will have non zeroes like `0 127 1`. One can check for success with `if $(echo ${PIPESTATUS[@]} | grep -qEe '^0( 0)*$'); then echo "good"; else echo "bad"; fi`. – Nik O'Lai Aug 21 '19 at 10:03
  • Is there a way of doing this without breaking updating the PS1 by other programs, e.g, activation of python virtualenv? – A T Dec 20 '20 at 07:54
  • 1
    BTW: I got this working with `if [ ! -z "$VIRTUAL_ENV" ]; then PS1+=" (${VIRTUAL_ENV##*/})" fi` – A T Dec 26 '20 at 06:12
  • I'm curious why this retains the exit code if I just hit RET on an empty prompt. For example, if I hit ^C the prompt turns red and remains that way until I actually run some command with exit code 0. – Jani Feb 04 '21 at 17:38
  • 1
    @Jani, I presume that the reason is that the `$?` variable retains the value of the _last executed command_. Pressing Ctrl+C or Enter does not run any command. – tukusejssirs Feb 23 '21 at 15:56
  • Is it possible to do it on git bash for Windows? I tried updating `C:\Program Files\Git\etc\profile.d\git-prompt.sh`, but nothing worked. – macabeus Feb 26 '21 at 12:02
25

If you don't want to use the prompt command there are two things you need to take into account:

  1. getting the value of $? before anything else. Otherwise it'll be overridden.
  2. escaping all the $'s in the PS1 (so it's not evaluated when you assign it)

Working example using a variable

PS1="\$(VALU="\$?" ; echo \$VALU ; date ; if [ \$VALU == 0 ]; then echo zero; else echo nonzero; fi) "

Working example without a variable

Here the if needs to be the first thing, before any command that would override the $?.

PS1="\$(if [ \$? == 0 ]; then echo zero; else echo nonzero; fi) "

Notice how the \$() is escaped so it's not executed right away, but each time PS1 is used. Also all the uses of \$?.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
helios
  • 13,574
  • 2
  • 45
  • 55
  • 3
    This is so amazingly awesome! I've been an old dirty bashtard since 1995 and it's never occurred to me to use command substitution in my PS1. Thank you. – Bruno Bronosky Aug 30 '17 at 15:00
  • 3
    Though I do prefer simple ternary notation: `export PS1="\$([ \$? == 0 ] && echo ✅ || echo ⚠️ ) \h:\W \u\n\$ "` – Bruno Bronosky Aug 30 '17 at 15:03
  • This is cool and i'm currently editing my .bashrc file to incorporate. Tiny nit: "-eq" instead of "==" (and "=" instead of "=="). – keithpjolley Oct 18 '17 at 17:32
  • 2
    This solution is particularly interesting when using tools like python's `virtualenv` which prefix `PS1` variable with information when activated. Using `PROMPT_COMMAND` would not work! – samb Dec 21 '17 at 16:25
  • 2
    It seems if you use `\${PIPESTATUS[-1]}` you can get the exit status of the last command even if it isn't the first thing in your `PS1`. – David Ongaro Jun 13 '20 at 16:48
  • We can make the whole PS1 variable inside single quote so escaping is not needed. – Seff Jun 26 '21 at 05:05
11

Compact solution:

PS1='... $(code=${?##0};echo ${code:+[error: ${code}]})'

This approach does not require PROMPT_COMMAND (apparently this can be slower sometimes) and prints [error: <code>] if the exit code is non-zero, and nothing if it's zero:

... > false
... [error: 1]> true
... >

Change the [error: ${code}] part depending on your liking, with ${code} being the non-zero code to print.

Note the use of ' to ensure the inline $() shell gets executed when PS1 is evaluated later, not when the shell is started.

As bonus, you can make it colorful in red by adding \e[01;31m in front and \e[00m after to reset:

PS1='... \e[01;31m$(code=${?##0};echo ${code:+[error: ${code}]})\e[00m'

--

How it works:

  • it uses bash parameter substitution
  • first, the ${?##0} will read the exit code $? of the previous command
  • the ## will remove any 0 pattern from the beginning, effectively making a 0 result an empty var (thanks @blaskovicz for the trick)
  • we assign this to a temporary code variable as we need to do another substitution, and they can't be nested
  • the ${code:+REPLACEMENT} will print the REPLACEMENT part only if the variable code is set (non-empty)
  • this way we can add some text and brackets around it, and reference the variable again inline: [error: ${code}]
Alexander Klimetschek
  • 3,585
  • 31
  • 36
  • Thanks - I actually think this is the cleanest solution of all. I always end up selecting this when I want this behavior for a new environment. Personally, the red color and status if non-zero in itself suffices. To prepend space, `""` must be used: `PS1='\e[01;31m$(code=${?##0};echo "${code:+${code} }")\e[00m'"$PS1"` – V.S. Mar 01 '22 at 13:38
8

I wanted to keep default Debian colors, print the exact code, and only print it on failure:

# Show exit status on failure.
PROMPT_COMMAND=__prompt_command

__prompt_command() {
    local curr_exit="$?"

    local BRed='\[\e[0;91m\]'
    local RCol='\[\e[0m\]'

    PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '

    if [ "$curr_exit" != 0 ]; then
        PS1="[${BRed}$curr_exit${RCol}]$PS1"
    fi
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Velkan
  • 7,067
  • 6
  • 43
  • 87
3

The following provides a leading green check mark when the exit code is zero and a red cross in all other cases. The remainder is a standard colorized prompt. The printf statements can be modified to present the two states that were originally requested.

PS1='$(if [ $? -eq 0 ]; then printf "\033[01;32m""\xE2\x9C\x93"; else printf "\033[01;31m""\xE2\x9C\x95"; fi) \[\e[00;32m\]\u@\h\[\e[00;30m\]:\[\e[01;33m\]\w\[\e[01;37m\]\$ '
Community
  • 1
  • 1
1

Why didn't I think about that myself? I found this very interesting and added this feature to my 'info-bar' project. Eyes will turn red if the last command failed.

#!/bin/bash
eyes=(O o ∘ ◦ ⍤ ⍥) en=${#eyes[@]} mouth='_'
face () { # gen random face
    [[ $error -gt 0 ]] && ecolor=$RED || ecolor=$YLW
    if [[ $1 ]]; then printf "${eyes[$[RANDOM%en]]}$mouth${eyes[$[RANDOM%en]]}"
                 else printf "$ecolor${eyes[$[RANDOM%en]]}$YLW$mouth$ecolor${eyes[$[RANDOM%en]]}$DEF"
    fi
}
info () { error=$?
    [[ -d .git ]] && {  # If in git project folder add git status to info bar output
        git_clr=('GIT' $(git -c color.ui=always status -sb)) # Colored output 4 info
        git_tst=('GIT' $(git                    status -sb)) # Simple  output 4 test
    }
    printf -v line "%${COLUMNS}s"                            # Set border length
    date=$(printf "%(%a %d %b %T)T")                         # Date & time 4 test
    test=" O_o $PWD  ${git_tst[*]} $date o_O "               # Test string
    step=$[$COLUMNS-${#test}]; [[ $step -lt 0 ]] && step=0   # Count spaces
    line="$GRN${line// /-}$DEF\n"                            # Create lines
    home="$BLD$BLU$PWD$DEF"                                  # Home dir info
    date="$DIM$date$DEF"                                     # Colored date & time
           #------+-----+-------+--------+-------------+-----+-------+--------+
           # Line | O_o |homedir| Spaces | Git  status | Date|  o_O  |  Line  |
           #------+-----+-------+--------+-------------+-----+-------+--------+
    printf "$line $(face) $home %${step}s ${git_clr[*]} $date $(face) \n$line" # Final info string
}
PS1='${debian_chroot:+($debian_chroot)}\n$(info)\n$ '
case "$TERM" in xterm*|rxvt*)
    PS1="\[\e]0;${debian_chroot:+($debian_chroot)} $(face 1) \w\a\]$PS1";;
esac

Enter image description here

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Ivan
  • 6,188
  • 1
  • 16
  • 23
0

Improved demure answer:

I think this is important because the exit status is not always 0 or 1.

if [ $EXIT != 0 ]; then
    PS1+="${Red}${EXIT}:\u${RCol}"      # Add red if exit code != 0
else
    PS1+="${Gre}${EXIT}:\u${RCol}"      # Also displays exit status
fi
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
  • 2
    Is there a reason you aren't quoting the expansion? If a user with this PROMPT_COMMAND set `IFS=0`, then this would become `if [ != 0 ]` on expansion (with a zero exit code, of course); making it `if [ "$EXIT" != 0 ]` will avoid that. – Charles Duffy Sep 11 '16 at 16:30
0

To preserve the original prompt format (not just colors), you could append following to the end of file ~/.bashrc:

PS1_ORIG=$PS1 # original primary prompt value
PROMPT_COMMAND=__update_prompt # Function to be re-evaluated after each command is executed
__update_prompt() {
    local PREVIOUS_EXIT_CODE="$?"
    if [ $PREVIOUS_EXIT_CODE != 0 ]; then
        local RedCol='\[\e[0;31m\]'
        local ResetCol='\[\e[0m\]'
        local replacement="${RedCol}\u${ResetCol}"
    
        # Replace username color
        PS1=${PS1_ORIG//]\\u/]$replacement}
        ## Alternative: keep same colors, append exit code
        #PS1="$PS1_ORIG[${RedCol}error=$PREVIOUS_EXIT_CODE${ResetCol}]$ "
    else
        PS1=$PS1_ORIG
    fi
}

See also the comment about the alternative approach that preserves username color and just appends an error code in red to the end of the original prompt format.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
atsu85
  • 511
  • 6
  • 7
  • this solution is somewhat based on ideas/solutions of demure and Velkan (but preserves original format and colors) – atsu85 Jun 03 '20 at 12:02
0

You can achieve a similar result to include a colored (non-zero) exit code in a prompt, without using subshells in the prompt nor prompt_command.

You color the exit code portion of the prompt, while having it only appear when non-zero.

prompt lines color variations with zero & non-zero exit codes

Core 2$ section of the prompt: \\[\\033[0;31;4m\\]\${?#0}\\[\\033[0;33m\\]\$ \\[\\033[0m\\]

Key elements:

  • return code, if not 0: \${?#0} (specificly "removes prefix of 0")
  • change color without adding to calculated prompt-width: \\[\\033[0;31m\\]
    • \\[ - begin block
    • \\033 - treat as 0-width, in readline calculations for cmdline editing
    • [0;31;4m - escape code, change color, red fg, underline
    • \\] - end block

Components:

  • \\[\\033[0;31;4m\\] - set color 0;31m fg red, underline
  • \${?#0} - display non-zero status (by removing 0 prefix)
  • \\[\\033[0;33m\\] - set color 0;33m fg yellow
  • \$ - $ or # on EUID
  • \\[\\033[0m\\] - reset color

The full PS1 I use (on one host):

declare -x PS1="\\[\\033[0;35m\\]\\h\\[\\033[1;37m\\] \\[\\033[0;37m\\]\\w \\[\\033[0;33m\\]\\[\\033[0;31;4m\\]\${?#0}\\[\\033[0;33m\\]\$ \\[\\033[0m\\]"

Note: this addresses a natural extension to this question, in a more enduring way then a comment.

mcint
  • 804
  • 8
  • 18
0

Bash

function my_prompt {
    local retval=$?
    local field1='\u@\h'
    local field2='\w'
    local field3='$([ $SHLVL -gt 1 ] && echo \ shlvl:$SHLVL)$([ \j -gt 0 ] && echo \ jobs:\j)'"$([ ${retval} -ne 0 ] && echo \ exit:$retval)"
    local field4='\$'

    PS1=$'\n'"\e[0;35m${field1}\e[m \e[0;34m${field2}\e[m\e[0;31m${field3}\e[m"$'\n'"\[\e[0;36m\]${field4}\[\e[m\] "


}

PROMPT_COMMAND="my_prompt; ${PROMPT_COMMAND}"

Zsh

PROMPT=$'\n''%F{magenta}%n@%m%f %F{blue}%~%f%F{red}%(2L. shlvl:%L.)%(1j. jobs:%j.)%(?.. exit:%?)%f'$'\n''%F{cyan}%(!.#.$)%f '

Images of prompt

image of prompt image of prompt