10

I've added the return value of the last command to PS1 (aka "the prompt") in my .bashrc.

Now I'd like to have it shown only if the value is nonzero.

Android's shell has it:

${| local e=$? (( e )) && REPLY+="$e|" return $e }

Question: how to convert it to bash?

4 Answers4

15
PS1='${?#0}$ '

It uses a special form of parameter expansion, ${?#0}, which means: "Remove the character zero if it is the first character of ${?}, the exit code of the previous command."

You can also change the color of the prompt if the last exit code were not zero:

PS1='\[\e[0;$(($?==0?0:91))m\]$ \[\e[0m\]'

Prompt

That uses a if-else ternary expression $(($?==0?0:91)) that makes the color code 0;91m (red, see color codes) if the last command exits with non-zero, or 0;0m (your default color) otherwise.

Quasímodo
  • 18,865
  • 4
  • 36
  • 73
  • 12
    My favourite thing about bash is how readable and obvious it is... – Adam Barnes Jun 09 '21 at 10:40
  • 1
    Unfortunately this doesn’t seem to work if the PS1 also contains subshell invocations (simplified example: PS1='$(echo -n)\[\e[0;$(($??0:91))m\]\$ \[\e[0m\]') — in this case, the return code is always 0 inside the PS1, even though I can query $? on the command line and it has the correct value. – Konrad Rudolph Jun 09 '21 at 11:12
  • 1
    @KonradRudolph In that case, just save $? in a variable: PS1='_ps1err=$?;...;$((_ps1err?0:91)).... – Kusalananda Jun 09 '21 at 11:45
  • @Kusalananda Yes, that’s what I ended up doing (inside $((…)), what you’ve posted won’t work directly). But then I’m getting the status echoed in my shell prompt. – Konrad Rudolph Jun 09 '21 at 12:07
  • 2
    @AdamBarnes The ugly parts here are all related to (virtual) terminals and how to get them to do other things than to display plain foreground-coloured text on background-coloured background. All programming languages face the same issue – unless they abstract that part away behind some library API like termcap or curses. You can certainly find or write one in Bash if you want to. – David Foerster Jun 09 '21 at 18:39
  • @Kusalananda & others: directly assigning like this doesn’t work. But the assignment can be performed inside an arithmetic expansion. To prevent the result of the expansion to be included inside the PS1, I’m using a trick I’ve learned on here previously: ${_ps_err##[$((_ps_err=$?))0-9]*}. This exploits the fact that […] is a valid pattern in the ## parameter expansion. The expansion will be the value of $? followed by 0-9 — in other words: an arbitrary digit. This is followed by a * pattern that matches all the rest, to ensure that the entire previous value of _ps_err is removed. – Konrad Rudolph Jun 10 '21 at 17:04
8

This is what I use in my .bashrc:

PS1_PROMPT() {
  local e=$?
  (( e )) && printf "$e|"
  return $e
}
PS1='$(PS1_PROMPT)'"$PS1"
ibug@example:~ $ false
1|ibug@example:~ $ ^C
130|ibug@exame:~ $ true
ibug@example:~ $

This has the advantage over JoL's answer that the value of $? is preserved after displaying (via return $e). It's also basically a rework of Android's default /system/etc/mkshrc that does the same job.

Note that $(PS1_PROMPT) is enclosed in single quotes because $PS1 is evaluated every time it's printed, in addition to when set in a variable assignment expression, so single quotes prevents it from being evaluted right now and instead defers that to when printing.

Alternatively, as suggested by ilkkachu, you can make use of Bash PROMPT_COMMAND special variable:

PS1_PROMPT() {
  local e=$?
  PROMPT_ECODE=
  (( e )) && PROMPT_ECODE="$e|"
  return $e
}
PROMPT_COMMAND=PS1_PROMPT
PS1='$PROMPT_ECODE'"$PS1"

This has one advantage that no subshell is spawned every time PS1 is printed.

iBug
  • 3,508
4

The entire Android PS1 is:

${|
    local e=$?
(( e )) && REPLY+="$e|"

return $e

}$HOSTNAME:${PWD:-?} $

I don't recognize ${|, but this bash is equivalent:

PS1='$(e=$?; (( e )) && echo "$e|")$HOSTNAME:${PWD:-?} $ '

local is not needed because $() is a subshell.

JoL
  • 4,735
1

Thank you, everyone, that's what I settled for:

PS1_PROMPT() {
  local e=$?
  #(( e )) && printf "$e|" # BW
  (( e )) && printf "\033[01;31m$e\033[00m|"  # color
  return $e
}
PS1='$(PS1_PROMPT)'"$PS1"

This displays errno, a.k.a. $?, in red if it's different than 0.

Note that the condition found in .bashrc, "$color_prompt" = yes could not be used above, because the function PS1_PROMPT() is not evaluated in .bashrc, but each time in the prompt. I did not want to export $color_prompt systemwide.

  • You don't have to export color_prompt, just keep it around for the duration of the shell. Though you could use it in bashrc to determine which version of the function to define, or just the prompt string. Also note that you'd need to wrap the color setting within \[ and \] for Bash to calculate the prompt width correctly. – ilkkachu Jun 10 '21 at 10:52
  • @ilkachu, this won't work. This is the reason why: PS1='$(PS1_PROMPT)' – Michał Leon Jun 11 '21 at 12:32
  • I'm not sure what you're trying to say doesn't work, but using e.g. (( e )) && [ "$color_prompt" ] && printf "\033[01;31m$e\033[00m|" inside the function works fine as long color_prompt is set in the shell, it doesn't need to be exported. And by "works fine", I mean works to set that string to the prompt. It still gets the prompt length wrong, because the \[ .. \] guards aren't set properly. (which you'll see if you try to enter a command line long enough to wrap) – ilkkachu Jun 11 '21 at 12:55
  • The function PS1_PROMPT is in single quotation marks, meaning the contents of the function is not evaluated during the running of .bashrc, but each time the prompt is displayed. HTH. – Michał Leon Jun 11 '21 at 19:10
  • 1
    yes. You don't need to export it (which would make it end up in the environment of all programs the shell launches), just keep it set for the duration of the shell (in which case it doesn't get passed to any other programs). – ilkkachu Jun 11 '21 at 23:18