Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Zsh prompt showing last error code only once

Tags:

zsh

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?

like image 367
Marián Černý Avatar asked Mar 18 '18 11:03

Marián Černý


3 Answers

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
like image 148
Leigh McCulloch Avatar answered Nov 19 '22 05:11

Leigh McCulloch


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
like image 20
sneep Avatar answered Nov 19 '22 04:11

sneep


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

like image 5
Adam Katz Avatar answered Nov 19 '22 06:11

Adam Katz