I have written a fancy custom bash prompt, and it works really well; I just have an issue when trying to run multiple commands separated by newlines. (I apologise for the length, but I hope my issue is clear)
TL;DR: getting the cursor position reads from stdin
with throws out any data there. The data should not be thrown away, please advise.
Expected behaviour
When I am running a long running command that doesn't read from stdin (e.g. sleep 5
) and type the next command+enter (eg. ls -lah<enter>
), once it is completed bash starts reading from stdin, reads the command+enter and starts running it (ls -lah
in this case).
I will call this the "eager situation" instead of the "patient situation" of waiting for the next prompt to show up before typing. (examples below)
Default bash prompt example
With the default bash prompt (aka PS1
) of something like PS1='[\u@\h \W]\$ '
it works as expected.
Default patient situation:
# wait for new prompt to show up and type new command
[me@machine ~]$ sleep 5; echo 'sleep done!'<enter>
sleep done!
[me@machine ~]$ ls -A /etc/skel<enter>
.bash_logout .bash_profile .bashrc
[me@machine ~]$
Default eager situation:
# we immediately start typing our next command to be run after we typed the sleep+echo
[me@machine ~]$ sleep 5; echo 'sleep done!'<enter>
ls -A /etc/skel<enter>
sleep done!
# bash now auto-fills the prompt because it reads it from the tty/stdin
# and immediately runs it (because it ends with a newline):
[me@machine ~]$ ls -A /etc/skel
.bash_logout .bash_profile .bashrc
[me@machine ~]$
Custom prompt bug
Custom patient situation
The patient situation works fine. No issues here.
Custom eager situation
The problem lies in the start of the precmd()
function. (I'm using this bash-preexec hook)
There I call a function that asks the terminal for the current cursor coordinates in order to know if I should print an extra newline, in case the output of the last-run command did not end in one. I got that function from another SO post: https://stackoverflow.com/a/52944692
If I disable this function, the issue does not occur.
function precmd() {
# must be 1st
previous_command_exit="${?}"
# saves cursore coordinates to 2 variables: _cursor_col, _cursor_row
# If I comment-out the call to _fetch_cursor_position, I can use the `eager situation` as expected.
_fetch_cursor_position
# add extra newline if command did not end with a newline
[[ "${_cursor_col}" -gt 1 ]] && printf "\n"
# ...
}
In my case it is defined like this:
_fetch_cursor_position() {
local -a pos
IFS='[;' read -p $'\e[6n' -d R -a pos -rs || echo >&2 "failed with error: $? ; ${pos[*]}"
_cursor_row="${pos[1]}"
_cursor_col="${pos[2]}"
}
The way this function works is like this (see the SO post for a better explaination):
- print the escape sequence
^[[6n
to the terminal, this asks the terminal to report the cursor position. This is printed byread
by (ab)using the-p prompt
flag (seehelp read
). - The terminal reports the cursor position by "typing"
^[[y;xR
in the terminal where:
^[[: The Esc character^[
followed by a literal[
y: the row number from the top starting at 1 (can be >9)
x: the column number from the left, starting at 1 (can be >9)
R: a literal characterR
- This value is then parsed by the
read
command by reading from stdin and assigned to thepos
array:
pos[0]:^[
(we don't care about this one)
pos2: value of y
pos2: value of x
Possible solutions
The main thing is preventing the _fetch_cursor_position
function from reading the already-input data that's in the tty/stdin.
Since that's probably not possible, perhaps there is a way to save the current value of stdin, read the cursor position and then restore the stdin value. I've looked a bit into bash's coproc
because that looked promising; but I'm not sure how that would work.
Something with bash redirections
I have a strong feeling that the solution lies in doing something "advanced" with bash's I/O redirections. But I only really ever use >/dev/null
, &>/dev/null
, 2>&1
, |&
and file/subshell redirections like < <(echo abc)
and >myfile.txt
, never with extra FD's directly.
The process in my mind is something like this:
- backup fd
0
to a new file descriptor (stdout/1
too?) - open new stdin/stout to the terminal (
/dev/tty
/$(tty)
?) so that they have empty ones to the exact same terminal (all the above without moving the cursor of course) - ask the terminal for the position and parse the response
- restore the original
Output position to different FD
have the terminal somehow write the coordinate response not to stdin/0
but to a different FD, and have read
read from there.
Subshell that doesn't have access to the stdin data that's already there.
When I use a slightly edited version of _fetch_cursor_position
using subshell expansion, this also doesn't work because it inherits stdin/stout/stderr:
_echo_cursor_position() {
local pos
IFS='[;' read -p $'\e[6n' -d R -a pos -rs || echo >&2 "failed with error: $? ; ${pos[*]}"
_cursor_row="${pos[1]}"
_cursor_col="${pos[2]}"
# this line is added, no other changes apart from the name
printf "${_cursor_row} ${_cursor_col}"
}
function precmd() {
# must be 1st
previous_command_exit="${?}"
#_fetch_cursor_position
pos=( $(_echo_cursor_position) )
_cursor_row="${pos[0]}"
_cursor_col="${pos[1]}"
# add extra newline if command did not end with a newline
[[ "${_cursor_col}" -gt 1 ]] && printf "\n"
# ...
}
Perhaps this would be an easier and/or more elegant solution, if this inheritance issue can be resolved.
What doesn't work
1: read
without any redirections
cursor position is saved correctly, but eagerly typed command is gone.
IFS='[;' read -r -s -p $'\e[6n' -d 'R' __garbage __cursor_col __cursor_row
2: read
with two redirects on /dev/tty
If I understand bash correctly, stdin/stdout(and stderr) are by default connected to the tty so this should not be different from 1, which it isn't.
IFS='[;' read -r -s -p $'\e[6n' -d 'R' __garbage __cursor_col __cursor_row </dev/tty >/dev/tty
from man 1 bash
:
An interactive shell is one started without non-option arguments (unless -s is specified) and without the -c option whose standard input and error are both connected to terminals (as determined by isatty(3)), or one started with the -i option. PS1 is set and $- includes if bash is interactive, allowing a shell script or a startup file to test this state.
3: 2-stage redirect printf
and read
Same result as 1 and 2
printf $'\e[6n' >/dev/tty
IFS='[;' read -r -s -d 'R' __garbage __cursor_col __cursor_row </dev/tty
4: using value of $(tty)
instead of /dev/tty
Again, no differcence
local tty="$(tty)"
printf $'\e[6n' >"${tty}"
IFS='[;' read -r -s -d 'R' __garbage __cursor_col __cursor_row <"${tty}"
What works
0: The easy solution
Don't do all this stupid effort and just always print an extra newline.
I'd really like to not have to do this, from a perfectionist perspective.
1: use tmux
See answer by Kamil Maciorowski
I'm always using tmux, so this is a great pragmatic solution for me that only prints an extra newline when necessary.
If there is a bash-native/terminal-native solution that doesn't require tmux, I'd love to hear it.
/dev/tty
then reading from there for the get-the-cursor-position handling?read [stuff] >/dev/tty </dev/tty
right?read
command inside the function, as well as on the place where I call the function:_fetch_cursor_position </dev/tty >/dev/tty
. (not at the same time) this did not work.printf $'\e[6n' >/dev/tty
and thenread -d R -a pos -rs </dev/tty
(without the-p $'\e[6n'
). This also does not work