16

This might sound pretty weird, but I know how to set the vertical cursor position in Bash like this:

echo -e "\e[12H"

This moves the cursor to the 12th line (starting with 1).

So how do I get the cursor position (line number) using linux bash? It would be helpful if I could simply store this value in a variable so I can calculate with it.

EDIT:

This is the error I get:

$ sh rowcol.sh
-en
    read: 9: Illegal option -d
                              test.sh: 12: Bad substitution
BrainStone
  • 3,654

4 Answers4

31

Using the -p option instead of echo I found solved the hanging problem in a script. Tested with GNU bash, version 3.00.16(1)-release (x86_64-redhat-linux-gnu).

IFS=';' read -sdR -p $'\E[6n' ROW COL;echo "${ROW#*[}"

works interactively or in a script:

#!/usr/bin/env bash
function pos
{
    local CURPOS
    read -sdR -p $'\E[6n' CURPOS
    CURPOS=${CURPOS#*[} # Strip decoration characters <ESC>[
    echo "${CURPOS}"    # Return position in "row;col" format
}
function row
{
    local COL
    local ROW
    IFS=';' read -sdR -p $'\E[6n' ROW COL
    echo "${ROW#*[}"
}
function col
{
    local COL
    local ROW
    IFS=';' read -sdR -p $'\E[6n' ROW COL
    echo "${COL}"
}
tput sc         # Save cursor position
tput cup 5 10   # Move to row 6 col 11
POS1=$(pos)     # Get the cursor position
ROW1=$(row)
COL1=$(col)
tput cup 25 15  # Move to row 25 col 15
POS2=$(pos)     # Get the cursor position
ROW2=$(row)
COL2=$(col)
tput rc # Restore cursor position
echo $BASH_VERSION
echo $POS1 $ROW1 $COL1
echo $POS2 $ROW2 $COL2

Outputs:

$./cursor.sh
3.00.16(1)-release
6;11 6 11
26;16 26 16
  • 1
    This works very well. Sadly however, it doesn't work in background processes, and as the screen scrolls, the line under the saved column changes. – Leon S. Jul 04 '18 at 15:51
  • All-caps variable names are, per POSIX specification, used for names defined by / meaningful to the shell and operating system; applications, like your script, should use lower-case names for variables they define themselves. See https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html – Charles Duffy Aug 24 '22 at 15:07
9

I was able to use some of the examples from the same article on SO, titled: How to get the cursor position in bash?. I'm posting this here just to show that they work and that the contents of solutions is actually on U&L as well.

Bash solutions

From inside a script

#!/bin/bash
# based on a script from http://invisible-island.net/xterm/xterm.faq.html
exec < /dev/tty
oldstty=$(stty -g)
stty raw -echo min 0
# on my system, the following line can be replaced by the line below it
echo -en "\033[6n" > /dev/tty
# tput u7 > /dev/tty    # when TERM=xterm (and relatives)
IFS=';' read -r -d R -a pos
stty $oldstty
# change from one-based to zero based so they work with: tput cup $row $col
row=$((${pos[0]:2} - 1))    # strip off the esc-[
col=$((${pos[1]} - 1))

echo "(row,col): $row,$col"

NOTE: I changed the output slightly!

Example

$ ./rowcol.bash 
(row,col): 43,0
$ clear
$ ./rowcol.bash 
(row,col): 1,0

Interactive shell

This command chain worked for getting the row and column positions of the cursor:

$ echo -en "\E[6n";read -sdR CURPOS; CURPOS=${CURPOS#*[};echo "${CURPOS}"

Example

$ echo -en "\E[6n";read -sdR CURPOS; CURPOS=${CURPOS#*[};echo "${CURPOS}"
13;1
$ clear
$ echo -en "\E[6n";read -sdR CURPOS; CURPOS=${CURPOS#*[};echo "${CURPOS}"
2;1

NOTE: This method doesn't appear to be usable from any type of script. Even simple commands in an interactive terminal didn't work for me. For example:

$ pos=$(echo -en "\E[6n";read -sdR CURPOS; CURPOS=${CURPOS#*[};echo "${CURPOS}")

just hangs indefinitely.

dash/sh solutions

From inside a script

This solution is for Ubuntu/Debian systems that come stock with dash, which is POSIX compliant. Because of this, the read command doesn't support the -d switch among other differences.

To get around this there is this solution which uses a sleep 1 in place of the -d switch. This isn't ideal but offers at least a working solution.

#!/bin/sh

exec < /dev/tty
oldstty=$(stty -g)
stty raw -echo min 0
tput u7 > /dev/tty
sleep 1
IFS=';' read -r row col
stty $oldstty

row=$(expr $(expr substr $row 3 99) - 1)        # Strip leading escape off
col=$(expr ${col%R} - 1)                        # Strip trailing 'R' off

echo "(row,col): $col,$row"

Example

$ ./rowcol.sh 
(row,col): 0,24
$ clear
$ ./rowcol.sh 
(row,col): 0,1

Interactive shell

I couldn't find a workable solution that worked for just sh in an interactive shell.

slm
  • 369,824
  • 1
    @slm, a shell is not POSIX because it does not support read -d. POSIX is a tool to help write scripts that are portable across conformant shells. A shell that supports read -d is still POSIX conformant but a script that uses it would not be as the behavior there is unspecified. An echo that doesn't output -e upon echo -e is not compliant. A POSIX script that uses echo -e is compliant. POSIX guarantees that it will output -e there ;-) (OK, not the best example). – Stéphane Chazelas Aug 27 '13 at 06:31
4

You can get the cursor position via ANSI CSI DSR (Device Status Report): \e[6n. Note it returns it in a format similar to ANSI CSR CUP (Cursor Position) that you mention in your question, however it follows the form \e[n;mR (where n is the row and m the column).

More details of ANSI escape codes on wikipedia.

For getting hold of the value into a variable, this was answered on StackOverflow.

As mentioned in a previous answer/comment (and detailed in the wikipedia article), these codes are not always portable (from terminal to terminal and OS to OS). I still think this is better handled with termcap/curses ;)

Drav Sloan
  • 14,345
  • 4
  • 45
  • 43
  • And how could I store this in a variable? – BrainStone Aug 26 '13 at 23:50
  • I can't get it work. I always get problems with echo -e, echo -en and read .... This only happens when the code is present in the file! I don't really understand this! – BrainStone Aug 27 '13 at 00:54
  • It seems like I broke some setting. echo -e worked before but now it doesn't! What might have caused this and how do I restore it? – BrainStone Aug 27 '13 at 01:11
4

With POSIX sh syntax:

if [ -t 0 ] && [ -t 1 ]; then
  old_settings=$(stty -g) || exit
  trap 'stty "$(old_settings)"' INT TERM QUIT ALRM
  stty -icanon -echo min 0 time 3 || exit
  printf '\033[6n'
  pos=$(dd count=1 2> /dev/null)
  pos=${pos%R*}
  pos=${pos##*\[}
  x=${pos##*;} y=${pos%%;*}
  stty "$old_settings"
  trap - INT TERM QUIT ALRM
fi
  • I'd just add trap stty "$old_settings" EXIT INTjust after saving old_settings so it can restore it in case of a problem. – Léa Gris Jan 31 '22 at 07:16
  • @LéaGris, I've added it, though it feels like unnecessary effort as the window where that could be killed is very small. Suspension (where we'd need to restore settings and apply again upon resume) is still not handled. – Stéphane Chazelas Jan 31 '22 at 08:53
  • Awesome solution. Too bad trap isn't able to handle all possible scenarios though. Better than nothing, I guess. Also, one can always put this inside a subshell ( ... ) in case trap is already being used in the script and you do not want to mess with that – user1593842 Aug 11 '23 at 21:42