带有最后一个退出代码的 Bash 提示符

我一直在尝试定制我的 Bash 提示符,以便它看起来像

[feralin@localhost ~]$ _

还有颜色。我设法获得了常量颜色(每次看到提示时都是相同的颜色) ,但是如果最后一个命令的退出状态为非零,我希望用户名(‘ feralin’)显示为红色,而不是绿色。我想到了:

\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

然而,根据我的观察,当 .bashrc运行时,$(if ...; fi)似乎只被计算了一次,并且结果在运行之后永远被替换了。这使得名称始终是绿色的,即使最后一个退出代码是非零(如 echo $?)。是这样吗?或者只是我的提示出了问题?长话短说 如何让提示符使用最后一个退出代码?

38984 次浏览

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.

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

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
}

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 \$?.

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}]

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.

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

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\]\$ '

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.

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