我写了一个精美的自定义 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 帖子以获得更好的解释):
- 将转义序列打印
^[[6n
到终端,这要求终端报告光标位置。这是read
由 (ab)using-p prompt
标志打印的(参见help read
)。 - 终端通过
^[[y;xR
在终端中“键入”来报告光标位置,其中:
^[[:Esc 字符^[
后跟文字[
y:从顶部开始的行号(可以大于 9)
x:从上到下的列号左,从 1 开始(可以 >9)
R:文字字符R
read
然后命令通过从 stdin 读取来解析该值并将其分配给pos
数组:
pos[0]:^[
(我们不关心这个)
pos2: y
位置的值2: x 的值
可能的解决方案
主要是防止该_fetch_cursor_position
函数读取 tty/stdin 中已输入的数据。由于这可能是不可能的,也许有一种方法可以保存标准输入的当前值,读取光标位置,然后恢复标准输入值。我对 bash 进行了一些研究,coproc
因为它看起来很有前途;但我不确定那会如何运作。
带有 bash 重定向的东西
我有一种强烈的感觉,解决方案在于使用 bash 的 I/O 重定向做一些“高级”的事情。但我实际上只使用过>/dev/null
、&>/dev/null
、2>&1
和|&
文件/子 shell 重定向,例如< <(echo abc)
和>myfile.txt
,从来没有直接使用额外的 FD。
我脑子里的流程是这样的:
- 将 fd 备份
0
到新的文件描述符(stdout/1
也是吗?) - 打开新的 stdin/stout 到终端(
/dev/tty
/$(tty)
?),以便它们在完全相同的终端上有空的(当然,以上所有操作都无需移动光标) - 向终端询问位置并解析响应
- 恢复原来的
输出位置到不同的FD
让终端以某种方式将坐标响应写入0
不同的 FD,而不是写入 stdin/,并read
从那里读取。
无法访问已存在的标准输入数据的子 shell。
当我使用稍微编辑过的 subshell 扩展版本时_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阶段重定向printf
和read
与 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 1
in 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+ V,Esc
- 在Emacs,输入Ctrl+ Q,。Esc