36

In a bash script, I want to get the cursor column in a variable. It looks like using the ANSI escape code {ESC}[6n is the only way to get it, for example the following way:

# Query the cursor position
echo -en '\033[6n'

# Read it to a variable
read -d R CURCOL

# Extract the column from the variable
CURCOL="${CURCOL##*;}"

# We have the column in the variable
echo $CURCOL

Unfortunately, this prints characters to the standard output and I want to do it silently. Besides, this is not very portable...

Is there a pure-bash way to achieve this ?

Thomas Dickey
  • 51,086
  • 7
  • 70
  • 105
nicoulaj
  • 3,463
  • 4
  • 27
  • 32

7 Answers7

44

You have to resort to dirty tricks:

#!/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))
Dennis Williamson
  • 346,391
  • 90
  • 374
  • 439
  • 6
    @DennisWilliamson an even better solution was presented here: http://unix.stackexchange.com/a/183121/106138 – niieani Jan 18 '16 at 23:31
  • 1
    I'm using this command in `PROMPT_COMMAND` in order to find the horizontal cursor position so I can print a `%` in reverse video and then a new line whenever the output of the last command doesn't end with a newline. However I think the `read` or other commands needed are swallowing text when I paste multi-line commands into my shell. Any known solution to this? – onlynone Aug 08 '18 at 19:26
  • 1
    You need to [surround non-printing sequences](https://stackoverflow.com/a/10594949/26428) with `\[` and `\]` so their lengths aren't counted in the length of the prompt as documented [here](https://www.gnu.org/software/bash/manual/html_node/Controlling-the-Prompt.html#Controlling-the-Prompt) if you're not doing that. The `stty` _should_ protect the `read`, but I'm not sure. Or you might need to save and restore stdin if the problem stems from the `exec` line. You might also try the technique linked to in a comment above this one. – Dennis Williamson Aug 08 '18 at 20:42
  • +1: This answer is now linked to [Bash - Clearing the last output correctly](https://stackoverflow.com/a/62040654/1765658) – F. Hauri - Give Up GitHub May 27 '20 at 12:32
  • Is there a solution to this as a PROMPT_COMMAND without swallowing multi-line commands as mentioned by @onlynone above? I tried numerous variations of this from a the other linked options to no avail even with a simple prompt of only $ without any non-printing characters. – proximous Jul 04 '20 at 16:37
  • @proximous: I think this would be a difficult problem to solve. It's a feature of zsh. One option would be to use it as your interactive shell instead of Bash if that capability is important to you. – Dennis Williamson Jun 28 '22 at 18:49
  • I used this for my `.bashrc` and it leads to a problem when using VSCode launch configurations — they do not, in fact, launch :) – Tooster Jul 02 '23 at 17:16
  • @Tooster: Huh? I can't imagine how this would work. This gets the text cursor position in the terminal - not the graphical cursor position on the desktop/screen. And I can't imagine it doing anything useful in `.bashrc`. If somehow you are using this with a text application, how are you passing the coordinates to it? What is it you're trying to accomplish? Trying to anticipate your answer, perhaps you need to return the `exec` redirect to `stdin`. – Dennis Williamson Jul 03 '23 at 02:44
  • @DennisWilliamson It's VSCode's bad imo. The story is a bit long, but basically vscode launch configurations don't like something inside `.bashrc` (maybe `read`?) and they don't follow with running commands. I use it in my custom PS1 prompt to implement printing newline before PS1 if the previous command didn't leave cursor in the first column (same feature that in ZSH prints `%` on inversed background to indicate missing EOF). Long story short - VSCode seems to be assuming a lot and doesn't like custom things in bashrc. I tried with echo iirc and it also didn't run launch configs. – Tooster Jul 03 '23 at 20:20
  • 1
    @tooster: Yeah, there shouldn't be any output in shell startup scripts. EOF is never (or exceedingly rarely) missing. You mean a final newline is missing. [Here](https://serverfault.com/a/97543/1293) is my attempt at providing that zsh feature in Bash. [Here](https://stackoverflow.com/a/20152662/26428) is someone's variation on that technique (with links to more). – Dennis Williamson Jul 04 '23 at 02:11
  • I meant to say that those techniques don't depend on the cursor position technique in my answer here so they may work in your environment (with vscode). – Dennis Williamson Jul 04 '23 at 02:55
  • @DennisWilliamson that's an interesting workaround, thanks for pointing!. I was actually not printing echo (just tested with it), but getting column, comparing if cursor is is 1st column and if so, adjusting the `PROMPT_COMMAND` to include the appropriate ANSI, character and a newline `\n` to be passed to `__git_ps1` or whatever it is that adds git integration. I'm gonna try using your method with padding with `$COLUMNS` spaces, thanks! – Tooster Jul 04 '23 at 20:17
  • 1
    @Tooster: The `echo` of escape sequences (or `tput`) that causes the cursor position to be emitted which is used in my answer here is the one that may be the output that vscode doesn't like. This is similar to the problem with `ssh` when startup scripts produce output, btw. – Dennis Williamson Jul 04 '23 at 22:45
18

You could tell read to work silently with the -s flag:

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

And then CURPOS is equal to something like 21;3.

TrebledJ
  • 8,713
  • 7
  • 26
  • 48
BSK
  • 197
  • 1
  • 2
  • 2
    For me this works in the interactive shell but not inside a script – etuardu Oct 16 '11 at 00:06
  • Thanks for this, I had to fill the rest of the line in a Makefile and ended up with this inside a function: `echo "\033[6n\c"; read -sdR CURPOS; COLS_LEFT="$$(expr $$TERM_COLS - $$(echo $${CURPOS} | cut -d';' -f 2;) + 1)";` – mVChr Mar 12 '14 at 01:05
  • 1
    `echo -en "\033[6n"; sleep 1; read -sdR` This method is incorrect. You have to disable lflag ECHO before the echo command. – youfu Oct 10 '14 at 03:09
10

In case anyone else is looking for this, I came across another solution here: https://github.com/dylanaraps/pure-bash-bible#get-the-current-cursor-position

Below is a slightly modified version with comments.

#!/usr/bin/env bash
#
# curpos -- demonstrate a method for fetching the cursor position in bash
#           modified version of https://github.com/dylanaraps/pure-bash-bible#get-the-current-cursor-position
# 
#========================================================================================
#-  
#-  THE METHOD
#-  
#-  IFS='[;' read -p $'\e[6n' -d R -a pos -rs || echo "failed with error: $? ; ${pos[*]}"
#-  
#-  THE BREAKDOWN
#-  
#-  $'\e[6n'                  # escape code, {ESC}[6n; 
#-  
#-    This is the escape code that queries the cursor postion. see XTerm Control Sequences (1)
#-  
#-    same as:
#-    $ echo -en '\033[6n'
#-    $ 6;1R                  # '^[[6;1R' with nonprintable characters
#-  
#-  read -p $'\e[6n'          # read [-p prompt]
#-  
#-    Passes the escape code via the prompt flag on the read command.
#-  
#-  IFS='[;'                  # characters used as word delimiter by read
#-  
#-    '^[[6;1R' is split into array ( '^[' '6' '1' )
#-    Note: the first element is a nonprintable character
#-  
#-  -d R                      # [-d delim]
#-  
#-    Tell read to stop at the R character instead of the default newline.
#-    See also help read.
#-  
#-  -a pos                    # [-a array]
#-  
#-    Store the results in an array named pos.
#-    Alternately you can specify variable names with positions: <NONPRINTALBE> <ROW> <COL> <NONPRINTALBE> 
#-    Or leave it blank to have all results stored in the string REPLY
#-  
#- -rs                        # raw, silent
#-  
#-    -r raw input, disable backslash escape
#-    -s silent mode
#-  
#- || echo "failed with error: $? ; ${pos[*]}"
#-  
#-     error handling
#-  
#-  ---
#-  (1) XTerm Control Sequences
#-      http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Functions-using-CSI-_-ordered-by-the-final-character_s_
#========================================================================================
#-
#- CAVEATS
#-
#- - if this is run inside of a loop also using read, it may cause trouble. 
#-   to avoid this, use read -u 9 in your while loop. See safe-find.sh (*)
#-
#-
#-  ---
#-  (2) safe-find.sh by l0b0
#-      https://github.com/l0b0/tilde/blob/master/examples/safe-find.sh
#=========================================================================================


#================================================================
# fetch_cursor_position: returns the users cursor position
#                        at the time the function was called
# output "<row>:<col>"
#================================================================
fetch_cursor_position() {
  local pos

  IFS='[;' read -p $'\e[6n' -d R -a pos -rs || echo "failed with error: $? ; ${pos[*]}"
  echo "${pos[1]}:${pos[2]}"
}

#----------------------------------------------------------------------
# print ten lines of random widths then fetch the cursor position
#----------------------------------------------------------------------
# 

MAX=$(( $(tput cols) - 15 ))

for i in {1..10}; do 
  cols=$(( $RANDOM % $MAX ))
  printf "%${cols}s"  | tr " " "="
  echo " $(fetch_cursor_position)"
done
Alissa H
  • 450
  • 4
  • 7
  • the use of `read -s` option is awesome, make escape sequence disappear – yurenchen Nov 05 '18 at 06:45
  • @Alissa H — This is great, though it took me quite some time to figure out what was going on! Slightly simpler than using an array, you can use this: `IFS='[;' read -sd R -p $'\e[6n' _ ROW COLUMN`. I didn't understand the point of `-r`, and my script works perfectly without it, so I've omitted it; please let me know if that's a mistake. – Paddy Landau Aug 12 '21 at 08:52
5

You can get the cursor position in bash by follow:

xdotool getmouselocation

$ x:542 y:321 screen:0 window:12345678

You can test it easy on Terminal by follow also:

Realstime variant 1:

watch -ptn 0 "xdotool getmouselocation"

Realtime variant 2:

while true; do xdotool getmouselocation; sleep 0.2; clear; done
Alfred.37
  • 181
  • 1
  • 12
3

In the interests of portability I've had a go at making a vanilla POSIX-compatible version that will run in shells like dash:

#!/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 $col,$row

...but I can't seem to find a viable alternative for bash's 'read -d'. Without the sleep, the script misses the return output entirely...

mr_jrt
  • 101
  • 1
  • 5
  • 1
    `expr` is slower than the shell's built-in arithmetic and parameter expansion, and not any more portable unless you're trying to target shells that predate the early-1990s POSIX.2 standard. Consider `col=$(( ${col%R} - 1 ))`, and `row=$(( ${row:3} - 1 ))` – Charles Duffy Oct 26 '22 at 20:13
3

My (two) version of same...

As a function, setting specific variable, using ncurses's user defined comands:

getCPos () { 
    local v=() t=$(stty -g)
    stty -echo
    tput u7
    IFS='[;' read -rd R -a v
    stty $t
    CPos=(${v[@]:1})
}

Than now:

getCPos 
echo $CPos
21
echo ${CPos[1]}
1
echo ${CPos[@]}
21 1

declare -p CPos
declare -a CPos=([0]="48" [1]="1")

Nota: I use ncurses command: tput u7 at line #4 in the hope this will stay more portable than using VT220 string by command: printf "\033[6n"... Not sure: anyway this will work with any of them:

getCPos () { 
    local v=() t=$(stty -g)
    stty -echo
    printf "\033[6n"
    IFS='[;' read -ra v -d R
    stty $t
    CPos=(${v[@]:1})
}

will work exactly same, while under VT220 compatible TERM.

More info

You may found some doc there:

VT220 Programmer Reference Manual - Chapter 4

4.17.2 Device Status Report (DSR)

...

Host to VT220 (Req 4 cur pos)  CSI 6 n       "Please report your cursor position using a CPR (not DSR) control sequence."
  
VT220 to host (CPR response)   CSI Pv; Ph R  "My cursor is positioned at _____ (Pv); _____ (Ph)."
                                              Pv =  vertical position (row)
                                              Ph =  horizontal position (column)
Community
  • 1
  • 1
F. Hauri - Give Up GitHub
  • 64,122
  • 17
  • 116
  • 137
-11

The tput commands are what you need to use. simple, fast, no output to the screen.

#!/bin/bash
col=`tput col`;
line=`tput line`;
geohump
  • 3
  • 1
  • 17
    What OS and version are you using that this works for you? On my system, there are no terminfo capabilities called "col" and "line". However, the plurals, "cols" and "lines", exist, but they return the total number of columns and lines rather than the current cursor position. – Dennis Williamson Dec 23 '12 at 12:54