Postexec/predisplay hook for ZSH

zsh

I know zsh has a preexec hook that fires before the command is executed, but is there one that fires after the command has been initiated but before the output is displayed to the screen? I ask because I want to display something based on if there was/will be actual output.

For example:
cd dir does not output anything
ls dir outputs stuff

In that example, I want to display: Output:\n before the ls directory display but not when cd is run since it doesn't have a display. I want it to look like:

~ $: cd dir
~/dir $: ls .
Output:
total 464
drwxr-xr-x+  55 eddie  staff   1.8K May  2 11:07 .
drwxr-xr-x    6 root   admin   204B Apr 22 13:48 ..
~/dir $

I don't just want to wrap cd and ls. Those were examples. I want it to be for every command.

Best Answer

There can't be any predisplay hook as the shell is not involved at all when commands output something to the terminal or any other file.

The hook would have to be in the commands run at the first writing system call made by any process or thread started by the command line to a file descriptor open to the tty device. Or you'd have to redirect the stdout and stderr of the command with some IPC mechanism (would probably have to be a pseudo-tty pair so as to minimise the impact on the command behaviour), and insert the Output: when something is received¹. In any case, that would be quite involved and quite intrusive.

Now, not something I would do, but you could try something like:

preexec() {
  printf '%-*s' $COLUMNS "Output:"
  read -sdR $'pos_before?\e[6n'
}

precmd() {
  read -sdR $'pos_after?\e[6n'
  [[ $pos_after != $pos_before ]] || printf '\r'
}
set +o promptsp

That is, just before running a command output Output: and move the cursor to the right edge of the screen (so the next thing written if any would be at the start of the next line) and record the current cursor position in $pos_before.

And just before the next prompt, query the cursor position again, and if it hasn't moved, move the cursor back to the beginning of the line so the next prompt overrides that Output:. We disable promptsp as it interferes with that.


¹Such a pty-based approach could be implemented with expect for instance:

#! /usr/bin/expect -f

set x 0
set timeout -1
stty raw -echo
log_user 0
spawn -noecho zsh

# tell zsh to send a special sequence before the prompt (precmd)
# and a different one before each command (preexec)
send {precmd() printf "\1\1"; preexec() printf "\2\2";set +o promptsp}
send "\r"

# wait for the second prompt
expect "\1\1"

# set our x flag when the preexec string has been output and reset
# it upon precmd. write "Output:" if some character is received while
# the flag is up.
expect {
  -re "^\2\2" {
    set x 1; exp_continue
  }
  -re "^\1\1" {
    set x 0; exp_continue
  }
  -re "^.\[^\1\2\]*" {
    if {$x} {send_user "Output:\n"; set x 0}
    send_user -- "$expect_out(buffer)"
    exp_continue
  }

  -i $user_spawn_id -re .+ {
   send -- $expect_out(buffer); exp_continue
  }
}