Zsh prompt showing last error code only once
Asked Answered
P

4

14

I would like my prompt to show a cross (✘) when the previous command fails. I use the following code:

export PROMPT=$'%(?..✘\n)\n› '

This gives me the following output:

› echo Hello
Hello

› asjdfiasdf
zsh: command not found: asjdfiasdf
✘

› 
✘

I would like to modify the prompt so that it does not repeat the cross when the prompt is redrawn after Enter (the third case in the example above).

Is it possible?

Parr answered 18/3, 2018 at 11:49 Comment(6)
Do you use bash or zsh?Carree
Zsh. @TarunLalwani edited the tags and added bash. I don't think it's an appropriate tag here.Rhonarhonchus
@MariánČerný, The reason I added that tag was, this question may have needed expert comment from people who use bash and the solution may work on both bash and zsh. Just to make sure it doesn't get missed by the experts, I did thatSense
@TarunLalwani Thanks. You have got higher reputation so I did trust your decision.Rhonarhonchus
@TarunLalwani – I do not think the bash keyword makes any sense here. There is no solution that would work for both since bash uses $PROMPT_COMMAND while zsh uses preexec() and precmd().Rinderpest
@AdamKatz, agreed and corrected.Sense
J
12

I think I got it. Let me know if you find a bug...

preexec() {
    preexec_called=1
}
precmd() {
    if [ "$?" != 0 ] && [ "$preexec_called" = 1 ]
    then echo ✘; unset preexec_called; fi
}
PROMPT=$'\n› '

Result:

› ofaoisfsaoifoisafas
zsh: command not found: ofaoisfsaoifoisafas
✘  

› 

› echo $? # (not overwritten)
127
Jago answered 25/3, 2018 at 8:56 Comment(3)
Nice. I did not try preexec before. Works like a charm. I have slightly updated your code so there is no ret variable. And added a newline.Rhonarhonchus
If you want the cross to be red color, use: echo "${fg_bold[red]}✘${reset_color}"Rhonarhonchus
Ref 9.3.1 Hook Functions: zsh.sourceforge.net/Doc/Release/Functions.html, List of all such functions: zsh.sourceforge.net/Doc/Release/Functions-Index.htmlChesney
R
5

I do this in my zsh, though with colors rather than unicode characters. It's the same principle.

First, I set up my colors, ensuring that they are only used when they are supported:

case $TERM in

( rxvt* | vt100* | xterm* | linux | dtterm* | screen )
  function PSC() { echo -n "%{\e[${*}m%}"; } # insert color-specifying chars
  ERR="%(0?,`PSC '0;32'`,`PSC '1;31'`)"      # if last cmd!=err, hash=green, else red
  ;;

( * )
  function PSC() { true; }   # no color support? no problem!
  ERR=
  ;;

esac

Next, I set up a magic enter function (thanks to this post about an empty command (ignore the question, see how I adapt it here):

function magic-enter() {    # from https://superuser.com/a/625663
  if [[ -n $BUFFER ]]
    then unset Z_EMPTY_CMD  # Enter was pressed on an empty line
    else Z_EMPTY_CMD=1      # The line was NOT empty when Enter was pressed
  fi
  zle accept-line           # still perform the standard binding for Enter
}
zle -N magic-enter          # define magic-enter as a widget
bindkey "^M" magic-enter    # Backup: use ^J

Now it's time to interpret capture the command and use its return code to set the prompt color:

setopt prompt_subst # allow variable substitution

function preexec() { # just after cmd has been read, right before execution
  Z_LAST_CMD="$1"   # since $_ is unreliable in the prompt
  #Z_LAST_CMD="${1[(wr)^(*=*|sudo|-*)]}"    # avoid sudo prefix & options
  Z_LAST_CMD_START="$(print -Pn '%D{%s.%.}')"
  Z_LAST_CMD_START="${Z_LAST_CMD_START%.}" # zsh <= 5.1.1 makes %. a literal dot
  Z_LAST_CMD_START="${Z_LAST_CMD_START%[%]}" # zsh <= 4.3.11 makes %. literal
}

function precmd() { # just before the prompt is rendered
  local Z_LAST_RETVAL=$?                  # $? only works on the first line here
  Z_PROMPT_EPOCH="$(print -Pn '%D{%s.%.}')"  # nanoseconds, like date +%s.%N
  Z_PROMPT_EPOCH="${Z_PROMPT_EPOCH%.}"    # zsh <= 5.1.1 makes %. a literal dot
  Z_PROMPT_EPOCH="${Z_PROMPT_EPOCH%[%]}"  # zsh <= 4.3.11 makes %. a literal %.
  if [ -n "$Z_LAST_CMD_START" ]; then
    Z_LAST_CMD_ELAPSED="$(( $Z_PROMPT_EPOCH - $Z_LAST_CMD_START ))"
    Z_LAST_CMD_ELAPSED="$(printf %.3f "$Z_LAST_CMD_ELAPSED")s"
  else
    Z_LAST_CMD_ELAPSED="unknown time"
  fi

  # full line for error if we JUST got one (not after hitting <enter>)
  if [ -z "$Z_EMPTY_CMD" ] && [ $Z_LAST_RETVAL != 0 ]; then
    N=$'\n'  # set $N to a literal line break
    LERR="$N$(PSC '1;0')[$(PSC '1;31')%D{%Y/%m/%d %T}$(PSC '1;0')]"
    LERR="$LERR$(PSC '0;0') code $(PSC '1;31')$Z_LAST_RETVAL"
    LERR="$LERR$(PSC '0;0') returned by last command"
    LERR="$LERR (run in \$Z_LAST_CMD_ELAPSED):$N"
    LERR="$LERR$(PSC '1;31')\$Z_LAST_CMD$(PSC '0;0')$N$N"
    print -PR "$LERR"
  fi
}

Finally, set the prompt:

PROMPT="$(PSC '0;33')[$(PSC '0;32')%n@%m$(PSC '0;33') %~$PR]$ERR%#$(PSC '0;0') "

Here's how it looks:

screenshot

 

A more direct answer to the question, adapted from the above:

function magic-enter() {    # from https://superuser.com/a/625663
  if [[ -n $BUFFER ]]
    then unset Z_EMPTY_CMD  # Enter was pressed on an empty line
    else Z_EMPTY_CMD=1      # The line was NOT empty when Enter was pressed
  fi
  zle accept-line           # still perform the standard binding for Enter
}
zle -N magic-enter          # define magic-enter as a widget
bindkey "^M" magic-enter    # Backup: use ^J

function precmd() { # just before the prompt is rendered
  local Z_LAST_RETVAL=$?                  # $? only works on the first line here

  # full line for error if we JUST got one (not after hitting <enter>)
  if [ -z "$Z_EMPTY_CMD" ] && [ $Z_LAST_RETVAL != 0 ]; then
    echo '✘'
  fi
}

PROMPT=$'\n› '

With screen shot:

simpler screenshot

Rinderpest answered 28/3, 2018 at 16:13 Comment(2)
I accidentally included a final line RPROMPT="$(PSC '0;34')%w %*$(PSC '0;0')" in the prompt.zsh source for my first screen shot. I didn't notice it was there when I removed it from what I posted here since it's not relevant.Rinderpest
Good to know there are other options. I have seen some widgets and bindings before. With my current zsh knowledge, I find @sneep's solution easier to understand. BTW, I use red color for my Unicode cross character :-D.Rhonarhonchus
G
1

Use the prexec and precmd hooks:

The preexec hook is called before any command executes. It isn't called when no command is executed. For example, if you press enter at an empty prompt, or a prompt that is only whitespace, it won't be called. A call to this hook signals that a command has been run.

The precmd hook is called before the prompt will be displayed to collect the next command. Before printing the prompt you can print out the exit status. In here we can check if a command was just executed, and if there's a status code we want to display.

This is very similar to the solution suggested by @sneep, which is also a great solution. It's worth using the hooks though so that if you've got anything else registering for these hooks they can do so too.

# print exit code once after last command output
function track-exec-command() {
  zsh_exec_command=1
}
function print-exit-code() {
  local -i code=$?
  (( code == 0 )) && return
  (( zsh_exec_command != 1 )) && return
  unset zsh_exec_command
  print -rC1 -- ''${(%):-"%F{160}✘ exit status $code%f"}''
}
autoload -Uz add-zsh-hook
add-zsh-hook preexec track-exec-command
add-zsh-hook precmd print-exit-code
Gabor answered 7/11, 2021 at 4:33 Comment(0)
A
0

Thanks to everyone for their answers. Four years later, I would like to illustrate a variation on sneep's answer for those looking for the error code and an alert without a symbol. This is a minimalist prompt but when an error occurs it displays the error code and > in red following the top level directory.

preexec() {
    preexec_called=1
}
precmd() {
    if [ "$?" != 0 ] && [ "$preexec_called" = 1 ]; then
        unset preexec_called
        PROMPT='%B%F{blue}%1~%f%b%F{red} $? > %F{black}'
    else
        PROMPT='%B%F{blue}%1~%f%b%F{blue} > %F{black}'
    fi
}
Auld answered 23/5, 2022 at 0:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.