在不使用 TIOCSTI 的情况下替换 Bash 中的 Ctrl-R
摘要
本文详细介绍了一种使用自定义工具替换 Bash Ctrl-R 历史搜索功能的方法,以适配近期 Linux 内核限制 TIOCSTI 使用的安全变更。
<p><a href="https://lobste.rs/s/zocyee/replacing_ctrl_r_bash_without_tiocsti">评论</a></p>
查看缓存全文
缓存时间: 2026/05/11 00:57
# 在 Bash 中不使用 TIOCSTI 替代 Ctrl-R
来源: https://blog.rickardlindberg.me/2025/01/19/replacing-ctrlr-in-bash-without.html
我之前写过 (https://blog.rickardlindberg.me/2017/05/19/evolution-of-recalling.html) 关于我如何使用 [rlselect](https://projects.rickardlindberg.me/rlselect/) 作为 Bash 中 Ctrl+R 的替代方案。它的工作原理是在 Bash 中为 Ctrl+R 创建一个键绑定,调用 `rlselect` 而不是默认的 Bash 交互式历史搜索命令。`rlselect` 看起来像这样:

rlselect 的截图,显示两个条目:hello 和 world,其中 hello 被选中。
如果按下 Tab,当前选中的内容会插入到提示符处。如果按下 Enter,当前选中的内容会被执行。这与默认的 Ctrl+R 行为相同。
这种机制在最近的 Linux 内核版本中停止工作了。我想出了如何解决它的方法,并在这篇博客文章中解释如何实现。
## 旧机制
当从 Ctrl+R 调用 `rlselect` 时,它会带有 `--tab` 和 `--action` 标志被调用。第一个标志允许使用 Tab 键选择一行,第二个标志使 `rlselect` 在选择之前打印出第一行上采取的操作。这是一个示例,当“hello”被选中并按下 Enter 时:
```bash
$ (echo hello; echo world) | rlselect --tab --action
enter
hello
```
这是一个示例,当“world”被选中并按下 Tab 时:
```bash
$ (echo hello; echo world) | rlselect --tab --action
tab
world
```
这是一个示例,当按下 Ctrl+G 时:
```bash
$ (echo hello; echo world) | rlselect --tab --action
ctrl-g
```
Ctrl+G 表示取消,因此第二行没有打印出任何选择。
为了将这些输出传递到提示符,使用了 TIOCSTI。它模拟在终端中输入字符。Ctrl+R 调用的完整脚本如下所示:
```bash
set -e
result=$(tac ~/.bash_history | rlselect --tab --action -- "$@")
python - "$result" << EOF
import fcntl
import sys
import termios
action, selection = sys.argv[1].split("\n", 1)
if action != "tab":
selection += "\n"
for ch in selection:
fcntl.ioctl(sys.stdout.fileno(), termios.TIOCSTI, ch)
EOF
```
最后一部分使用 TIOCSTI 来模拟按下选择项的键。除非按下 Tab,否则它会在选择项后面添加一个换行符以模拟按下 Enter。
Bash 配置如下所示:
```bash
if [[ $- =~ .*i.* ]]; then
bind '"\C-r": "\C-a rlselect-history \C-j"'
fi
```
它是如何工作的:
- Ctrl+R 被绑定到一系列按键操作。
- 首先模拟 Ctrl+A,这会将光标移到行的开头。
- 然后输入 `rlselect-history`。
- 然后模拟 Ctrl+J,这意味着接受当前行。或者执行它。
前一步中输入的初始空格确保 `rlselect-history` 命令不会出现在历史中。将光标移动到行的开头确保在提示符处输入的任何内容都作为参数传递给 `rlselect-history`。(此配置还使文本 `rlselect-history ...` 出现在终端中。新机制消除了这一点。)
这种机制在最近的 Linux 内核版本中停止工作,因为 TIOCSTI 不能像这样使用。显然,TIOCSTI 存在安全问题,现在只有 root 用户可以使用它。
## 新机制
我想出的新的 Bash 配置用于 Ctrl+R 的行为如下所示:
```bash
rlselect-history() {
local action
local selection
{
read action
read selection
} < <(tac ~/.bash_history | rlselect --tab --action -- "${READLINE_LINE}")
if [ "$action" = "tab" ]; then
READLINE_LINE="${selection}"
READLINE_POINT=${#READLINE_LINE}
bind '"\C-x2":' # 将 Ctrl+x+2 绑定为无操作
elif [ "$action" = "enter" ]; then
READLINE_LINE="${selection}"
READLINE_POINT=${#READLINE_LINE}
bind '"\C-x2": accept-line' # 将 Ctrl+x+2 绑定为接受行
else
bind '"\C-x2":' # 将 Ctrl+x+2 绑定为无操作
fi
}
if [[ $- =~ .*i.* ]]; then
# 将历史命令绑定到 Ctrl+x+1 后跟 Ctrl+x+2:
bind '"\C-r": "\C-x1\C-x2"'
# 将 Ctrl+x+1 绑定为执行 rlselect-history,它做两件事:
# 1. 设置 READLINE_*
# 2. 将 Ctrl+x+2 绑定为接受行或无操作。
bind -x '"\C-x1": rlselect-history'
fi
```
让我们分解一下。
- Ctrl+R 被绑定到一系列按键操作。
- 首先模拟 Ctrl+X+1。
- 然后模拟 Ctrl+X+2。
- Ctrl+X+1 被绑定为执行命令 `rlselect-history`。`-x` 绑定确保可以设置变量 `READLINE_*`。来自 `man bash` 关于 `set -x` 的内容:
> 每当输入 keyseq 时,都会执行 shell-command。当执行 shell-command 时,shell 将 READLINE_LINE 变量设置为 readline 行缓冲区的内容,以及 READLINE_POINT 和 READLINE_MARK 变量 [...] 如果执行的命令更改了 READLINE_LINE、READLINE_POINT 或 READLINE_MARK 中的任何一个值,这些新值将反映在编辑状态中。
- `rlselect-history` 被定义为 Bash 函数,允许它重新配置 Ctrl+X+2 的键绑定。根据当前选择是否应该执行,它将 Ctrl+X+2 绑定为 `accept-line` 或无操作。
因此,新机制依赖于使用两个额外的键绑定:Ctrl+X+1 和 Ctrl+X+2。我选择它们是因为我通常不使用它们。但它们可以是任何两个键绑定。
对我而言,找到这个解决方案的关键在于理解 Bash 键绑定。[这个答案](https://stackoverflow.com/a/13978131) 在 StackOverflow 上写道:
> 使用 `bind`,你可以将键绑定为执行以下三种操作之一,但不能组合使用:
>
> - 执行 readline 命令:`bind '"key": command'`
> - 执行一系列按键操作:`bind '"key": "keystrokes"'`
> - 执行 shell 命令:`bind -x '"key": shell-command'`
这让我明白你不能在 `rlselect-history` 中调用 `accept-line`,因为它是在 `bind -x` 的上下文中执行的,而 readline 命令只能在 `bind '"key": command'` 的上下文中执行。
## 资源
以下是一些关于 TIOCSTI 问题的资源,对我有帮助:
- hstr (https://github.com/dvorka/hstr)(最初启发我编写 `rlselect` 的程序)遇到了类似的问题 (https://github.com/dvorka/hstr/issues/478),并且我在其中找到了解决我的问题的线索。
- fzf-plugins (https://github.com/4z3/fzf-plugins) 仓库和这个讨论 (https://github.com/junegunn/fzf/pull/1492) 为 fzf (https://github.com/junegunn/fzf) 提供了类似的解决方案。
- 文章《Readline and Fuzzy Finder》(https://countdigi.github.io/post/2021/08/readline-and-fuzzy-finder/) 帮助我理解了如何在 Bash 中使用 `READLINE_*`。
相似文章
通过SSH实时查看CI日志
本文介绍了一项新功能,用户可以通过终端用户界面通过SSH实时查看CI日志,无需安装,并能正确处理ANSI代码。
CtrlOps
CtrlOps 是一款由人工智能驱动的工具,用于部署、调试和管理 Linux 服务器。
@hasantoxr:发现这个后,我要卸载 Cursor 和其他所有 AI 编程工具。它叫 DeepSeek TUI,一个完整的编程代理…
DeepSeek TUI 是一个新的开源 CLI 编程代理,用 Rust 构建,利用 DeepSeek API 在终端本地运行。它提供文件编辑、Shell 命令执行和多种控制模式,无需浏览器或 IDE 插件。
stinkpot:基于SQLite的Shell历史记录
Stinkpot是一个轻量级的、基于SQLite的bash Shell历史搜索工具,类似atuin但功能较少,约400行Go代码。
Strace-ui、Bonsai_term 与 TUI 复兴
Jane Street 的工程师介绍了 strace-ui,这是一个用于 strace 的交互式终端 UI,通过过滤、PID 追踪和手册页集成简化了系统调用调试,并强调了由其 Bonsai 框架推动的 TUI 复兴。