获取 tty 中的光标位置而不从 stdin 读取(需要重定向帮助)

获取 tty 中的光标位置而不从 stdin 读取(需要重定向帮助)

我写了一个精美的自定义 bash 提示符,而且效果非常好;我只是在尝试运行由换行符分隔的多个命令时遇到问题。 (对于长度我深表歉意,但我希望我的问题是清楚的)

TL;DR:从 with 读取光标位置stdin会抛出那里的任何数据。数据不能丢弃,请指教。

预期行为

当我运行一个长时间运行的命令,该命令不从标准输入读取(例如sleep 5)并键入下一个命令+输入(例如ls -lah<enter>)时,一旦完成,bash就会开始从标准输入读取,读取命令+输入并开始运行它(ls -lah在这种情况下)。我将称之为“急切的情况”,而不是在打字之前等待下一个提示出现的“耐心的情况”。 (以下示例)

默认 bash 提示符示例

PS1使用类似的默认 bash 提示符(又名) PS1='[\u@\h \W]\$ ',它可以按预期工作。

默认患者情况:

# 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 ~]$ 

默认急切情况:

# 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 ~]$ 

自定义提示错误

定制患者情况

患者情况良好。这里没有问题。

自定义急切情况

问题出在precmd()函数的启动处。 (我在用着这个 bash-preexec 钩子

在那里,我调用了一个函数,该函数向终端询问当前光标坐标,以便知道是否应该打印额外的换行符,以防上次运行命令的输出未以 1 结尾。我从另一个 SO 帖子中得到了这个功能:https://stackoverflow.com/a/52944692

如果我禁用此功能,则不会出现该问题。

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"

        # ...
}

就我而言,它的定义如下:

_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]}"
}

这个函数的工作方式是这样的(请参阅 SO 帖子以获得更好的解释):

  1. 将转义序列打印^[[6n到终端,这要求终端报告光标位置。这是read由 (ab)using-p prompt标志打印的(参见help read)。
  2. 终端通过^[[y;xR在终端中“键入”来报告光标位置,其中:
    ^[[:Esc 字符^[后跟文字[ y:从顶部开始的行号(可以大于 9)
    x:从上到下的列号左,从 1 开始(可以 >9)
    R:文字字符R
  3. read然后命令通过从 stdin 读取来解析该值并将其分配给pos数组:
    pos[0]: ^[(我们不关心这个)
    pos2: y
    位置的值2: x 的值

可能的解决方案

主要是防止该_fetch_cursor_position函数读取 tty/stdin 中已输入的数据。由于这可能是不可能的,也许有一种方法可以保存标准输入的当前值,读取光标位置,然后恢复标准输入值。我对 bash 进行了一些研究,coproc因为它看起来很有前途;但我不确定那会如何运作。

带有 bash 重定向的东西

我有一种强烈的感觉,解决方案在于使用 bash 的 I/O 重定向做一些“高级”的事情。但我实际上只使用过>/dev/null&>/dev/null2>&1|&文件/子 shell 重定向,例如< <(echo abc)>myfile.txt,从来没有直接使用额外的 FD。

我脑子里的流程是这样的:

  1. 将 fd 备份0到新的文件描述符(stdout/1也是吗?)
  2. 打开新的 stdin/stout 到终端(/dev/tty/ $(tty)?),以便它们在完全相同的终端上有空的(当然,以上所有操作都无需移动光标)
  3. 向终端询问位置并解析响应
  4. 恢复原来的

输出位置到不同的FD

让终端以某种方式将坐标响应写入0不同的 FD,而不是写入 stdin/,并read从那里读取。

无法访问已存在的标准输入数据的子 shell。

当我使用稍微编辑过的 subshel​​l 扩展版本时_fetch_cursor_position,这也不起作用,因为它继承了 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"

        # ...
}

如果可以解决这个继承问题,也许这将是一个更简单和/或更优雅的解决方案。

什么不起作用

1:read没有任何重定向

光标位置已正确保存,但急切键入的命令消失了。

IFS='[;' read -r -s -p $'\e[6n' -d 'R' __garbage __cursor_col __cursor_row

2:read开启两个重定向/dev/tty

如果我正确理解 bash,stdin/stdout(和 stderr)默认连接到 tty,所以这不应该与 1 不同,但事实并非如此。

IFS='[;' read -r -s -p $'\e[6n' -d 'R' __garbage __cursor_col __cursor_row </dev/tty >/dev/tty

man 1 bash

交互式 shell 是指不使用非选项参数(除非指定了 -s)和 -c 选项启动的 shell,其标准输入和错误均连接到终端(由 isatty(3) 确定),或使用 -i 选项启动的 shell。如果 bash 是交互式的,则设置 PS1 并包含 $-,从而允许 shell 脚本或启动文件测试此状态。

3:2阶段重定向printfread

与 1 和 2 相同的结果

printf $'\e[6n' >/dev/tty
IFS='[;' read -r -s -d 'R' __garbage __cursor_col __cursor_row </dev/tty

4:使用值$(tty)代替/dev/tty

再说一遍,没有区别

local tty="$(tty)"
printf $'\e[6n' >"${tty}"
IFS='[;' read -r -s -d 'R' __garbage __cursor_col __cursor_row <"${tty}"

什么有效

0:简单的解决方案

不要做所有这些愚蠢的努力,只是总是打印一个额外的换行符。

从完美主义者的角度来看,我真的不想这样做。

1:使用tmux

参见卡米尔·马乔罗夫斯基的回答

我总是使用 tmux,所以这对我来说是一个很好的实用解决方案,只在必要时打印额外的换行符。

如果有不需要 tmux 的 bash-native/terminal-native 解决方案,我很想听听。

答案1

这就是问题:

终端通过“键入”来报告光标位置...

对于从终端读取的任何程序来说,您键入的内容和终端“键入”的内容没有区别。

使用为此类事情提供单独通道的终端。我用tmux其他原因)。在tmux以下作品中:

_fetch_cursor_position() {
  local pos
  pos="$(tmux display-message -p -F '#{cursor_x} #{cursor_y}')"
  _cursor_row="${pos#* }"
  _cursor_col="${pos% *}"
  ((_cursor_row++))
  ((_cursor_col++))
}

重点是这个函数不从标准输入读取根本不

tmux计算从 开始的行/列0。我的代码中的这些++只是为了使您获得的数字与当前代码的其余部分兼容(显然期望从 开始计数1)。如果我从头开始编写,我不会使用++;我会相应地编写测试(例如-gt 0而不是-gt 1in precmd())。

答案2

为了简单起见,我从代码中剥离了数组,并添加了光标定位以供参考。

_fetch_cursor_position() {
  echo -en "\e[25;10H"
  read -s -t 1 -d ' ' -p $'\e[6n ' >&2
  if [[ "$REPLY" ]]; then
    result=$(echo $REPLY | sed 's/^[/\\e/g')
    cat <<< "Cursor position in the terminal:
        $result"
  fi
}
_fetch_cursor_position

输出是

Cursor position in the terminal:  
\e[25;10R  

echo它与定位光标的语句相关。

请注意,命令^[sed是一个硬(实际)Esc 字符;它不是软 Esc(即,只是文本表示)。要输入实际的 Esc,

  • /维姆,输入Ctrl+ VEsc
  • Emacs,输入Ctrl+ Q,。Esc

相关内容