如何在bash中获取光标位置?

36
在一个bash脚本中,我想将光标列数存储到一个变量中。使用ANSI转义码{ESC}[6n似乎是唯一的获取方法,例如以下方式:
# 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

很遗憾,这会将字符打印到标准输出,而我想要在静默的情况下完成。此外,这也不太便携...
有没有一种纯Bash的方法来实现这个?
7个回答

44

你必须采用卑鄙手段:

#!/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))

6
@DennisWilliamson 这里提供了一个更好的解决方案:http://unix.stackexchange.com/a/183121/106138 - niieani
1
我正在使用PROMPT_COMMAND中的此命令,以便找到水平光标位置,以便在反向视频中打印,并在上一个命令的输出不以换行符结尾时打印新行。但是,当我将多行命令粘贴到我的shell中时,我认为read或其他所需命令会吞噬文本。有任何已知的解决方案吗? - onlynone
1
如果您没有这样做,需要使用 \[\] 包围非打印字符序列,以便它们的长度不会计入提示符的长度,如此处所述。stty 应该保护 read,但我不确定。或者,如果问题源自 exec 行,则可能需要保存和恢复 stdin。您还可以尝试在本条评论上面链接的技术。 - Dennis Williamson
1
@tooster:是的,在shell启动脚本中不应该有任何输出。EOF几乎从不缺失(或者极其罕见)。你指的是缺少最后一个换行符。这里是我尝试在Bash中提供zsh功能的方法。这里是某人对该技术的变体(附带更多链接)。 - Dennis Williamson
1
@Tooster:在这里我使用的转义序列(或tput)的“回显”,会导致光标位置被输出,这可能是 vscode 不喜欢的输出。顺便说一下,这与 ssh 在启动脚本产生输出时遇到的问题类似。 - Dennis Williamson
显示剩余11条评论

18

你可以使用-s标志让read静默工作:

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

然后CURPOS等于像21;3这样的东西。


2
对我来说,这在交互式 shell 中可以运行,但在脚本中却不行。 - etuardu
谢谢,我不得不在Makefile中填写剩余的一行,最终我使用了以下函数: echo "\033[6n\c"; read -sdR CURPOS; COLS_LEFT="$$(expr $$TERM_COLS - $$(echo $${CURPOS} | cut -d';' -f 2;) + 1)"; - mVChr
1
echo -en "\033[6n"; sleep 1; read -sdR 这种方法是不正确的。在 echo 命令之前,您必须禁用 lflag ECHO。 - youfu

10
如果有其他人也在寻找这个,我在这里发现了另一种解决方案: https://github.com/dylanaraps/pure-bash-bible#get-the-current-cursor-position 以下是一个略微修改过并带有注释的版本。
#!/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

使用 read -s 选项非常棒,可以让转义序列消失。 - yurenchen
@Alissa H — 这很棒,尽管我花了一些时间才弄清楚发生了什么!比使用数组略微简单,您可以使用这个:IFS='[;' read -sd R -p $'\e[6n' _ ROW COLUMN。我不明白 -r 的意义,我的脚本在没有它的情况下完美地运行,所以我省略了它;如果这是一个错误,请告诉我。 - Paddy Landau

5

您可以通过以下方式在bash中获取光标位置:

xdotool getmouselocation

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

您可以通过以下方式轻松在终端上测试它:

实时变体 1:

watch -ptn 0 "xdotool getmouselocation"

实时变种2:

while true; do xdotool getmouselocation; sleep 0.2; clear; done

3

我(两个)同样的版本...

作为一个函数,设置特定的变量,使用ncurses的用户定义命令:

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

比现在:

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

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

注意:我在第 #4 行使用了ncurses命令:tput u7,希望这种方式比使用VT220字符串的printf "\033[6n"命令更加通用...... 不确定:无论如何,这种方式都可以与它们中的任何一种配合使用。

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

VT220兼容的终端下,工作方式完全相同。

更多信息

您可以在此处找到一些文档:

VT220程序员参考手册-第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)

没有起作用。另外,最后一个函数卡在了 tmux-256color 上。 - Artfaith
刚刚在gnome-terminal(256色)下测试了tmux,运行良好!你使用的是哪个终端? - F. Hauri - Give Up GitHub

3

为了便于移植,我尝试制作了一个纯粹的 POSIX 兼容版本,可以在像 dash 这样的 shell 中运行:

#!/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

...但是我似乎找不到一个可行的替代方案来替换bash中的'read -d'。没有sleep,脚本完全错过了返回输出...


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

-11

你需要使用tput命令。它简单、快速,不会在屏幕上输出内容。

#!/bin/bash
col=`tput col`;
line=`tput line`;

17
你使用的是哪个操作系统和版本,才能使这个方法对你有效?在我的系统中,没有名为“col”和“line”的terminfo功能。然而,复数形式的“cols”和“lines”存在,但它们返回的是总列数和行数,而不是当前光标位置。 - Dennis Williamson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接