GNU Bash 参考手册(中文版)(五)

7 作业控制

本章讨论什么是作业控制、它如何工作,以及 Bash 如何让你访问其功能。

7.1 作业控制基础

作业控制(job control)指有选择地停止(挂起)进程的执行并在稍后的时间点继续(恢复)其执行的能力。用户通常通过操作系统内核的终端驱动程序与 Bash 联合提供的交互式接口来使用这一功能。

Shell 把「作业(job)」与每个管道(流水线)关联起来。它维护一个当前正在执行的作业表,jobs 命令会显示这个表。每个作业都有一个「作业编号(job number)」,jobs 在括号中显示它。作业编号从 1 开始。当 Bash 异步启动一个作业时,它打印一行类似:

[1] 25647

表示该作业是作业编号 1,与该作业关联的管道中最后一个进程的进程 ID 是 25647。单个管道中的所有进程都是同一作业的成员。Bash 使用「作业」这一抽象作为作业控制的基础。

译者注:一个管道(流水线)中的所有进程同属一个作业,但每个进程仍有自己的进程 ID。[1] 25647 中的 25647 是「该作业的管道中最后一个进程」的 PID(命令末尾加 & 启动后台作业时,这个 PID 也等于 $! 的值,通常就是该作业的进程组 ID)。

为了便于实现作业控制的用户接口,每个进程都有一个「进程组 ID(process group ID)」,操作系统维护当前终端进程组 ID 的概念。这个终端进程组 ID 与「控制终端(controlling terminal)」相关联。

具有相同进程组 ID 的进程被称为属于同一个「进程组」。前台进程组的成员(进程组 ID 等于当前终端进程组 ID 的进程)接收键盘产生的信号,如 SIGINT。前台进程组中的进程被称为前台进程。后台进程是那些进程组 ID 与控制终端不同的进程;这样的进程不受键盘产生的信号的影响。只有前台进程被允许从控制终端读取,或者(如果用户用 stty tostop 指定)向控制终端写入。系统向试图从终端读取(在 tostop 生效时向终端写入)的后台进程发送 SIGTTINSIGTTOU)信号,除非被捕获,否则这会挂起该进程。

译者注:前台/后台进程与终端信号的关系可以示意如下:

控制终端(controlling terminal)
  ├─ 前台进程组(组 ID == 终端进程组 ID)
  │     ← 接收键盘信号(SIGINT / SIGTSTP / SIGQUIT …)
  │     ← 可自由读写终端
  └─ 后台进程组(组 ID ≠ 终端进程组 ID)
        ← 不受键盘信号影响
        ← 读终端 → 收到 SIGTTIN(默认挂起)
        ← 写终端(tostop 生效时)→ 收到 SIGTTOU(默认挂起)

这解释了为什么在后台运行的程序(如 sleep 10 &)按 Ctrl-C 不会被打断——SIGINT 只发送给前台进程组。

译者注SIGTTIN/SIGTTOU 的「TT」指 Teletype(终端),它们是终端与作业控制交互的核心信号。实测(Bash 5.3 交互式会话):后台进程一旦试图从终端读取输入,就会被 SIGTTIN 挂起——把 cat 放到后台运行即可观察到:

# 译者注示例:后台 cat 试图读取终端输入,被 SIGTTIN 挂起(实测)
$ cat &
[1] 51378
$ jobs
[1]+  Stopped                    cat

stty tostop 开启后,后台进程向终端写入也会触发 SIGTTOU 被挂起。可用 kill -l 查看这两个信号:SIGTTIN(编号 21)、SIGTTOU(编号 22)。

如果 Bash 运行所在的操作系统支持作业控制,Bash 就包含使用它的功能。在进程运行时输入「挂起(suspend)」字符(通常是 ^Z,即 Control-Z)会停止该进程并把控制权交还给 Bash。输入「延迟挂起(delayed suspend)」字符(通常是 ^Y,即 Control-Y)会使进程在试图从终端读取输入时停止,并把控制权交还给 Bash。然后用户操纵这个作业的状态:用 bg 命令在后台继续它,用 fg 命令在前台继续它,或用 kill 命令杀死它。挂起字符立即生效,并有一个额外的副作用:丢弃任何待处理的输出和先行输入(typeahead)。如果你想强制后台进程停止,或停止与你的终端会话无关的进程,用 kill 给它发送 SIGSTOP 信号。

译者注:把「挂起 → 查看 → 恢复 → 终止」的完整流程走一遍(Bash 5.3 交互式会话实测;^Z 表示按 Control-Z,进程号随运行而异):

# 译者注示例:Ctrl-Z 挂起前台作业,fg/bg 恢复,kill 终止
$ sleep 10
^Z
[1]+  Stopped                    sleep 10
$ jobs -l
[1]+ 47335 Stopped                    sleep 10
$ bg
[1]+ sleep 10 &
$ kill %1
$ jobs
[1]+  Terminated                 sleep 10

注意 jobs -l 会额外显示进程 ID(这里即进程组 ID 47335);bg 之后作业转到后台继续运行([1]+ sleep 10 &);kill %1 发送 SIGTERM 后,作业状态变为 Terminated 并很快从作业表中移除。

在 Shell 中有多种引用作业的方式。% 字符引入一个「作业规格(job specification,简称 jobspec)」。

作业编号 n 可以被引用为 %n。作业也可以用启动它的名称的前缀引用,或用出现在其命令行中的子串引用。例如,%ce 引用命令名以 ce 开头的作业。另一方面,使用 %?ce 引用命令行中包含字符串 ce 的任何作业。如果前缀或子串匹配多个作业,Bash 报告错误。

符号 %%%+ 引用 Shell 所认为的「当前作业(current job)」。单独的 %(不带伴随的作业规格)也引用当前作业。%- 引用「前一作业(previous job)」。当作业在后台启动、作业在前台停止、或作业在后台恢复时,它就成为当前作业。原来是当前作业的作业变成前一作业。当前作业终止时,前一作业变成当前作业。如果只有一个作业,%+%- 都可以用来引用该作业。在与作业有关的输出中(例如 jobs 命令的输出),当前作业总是用 + 标记,前一作业用 - 标记。

译者注:作业引用形式汇总(JOBSPEC):

%n          作业编号 n          %% 或 %+     当前作业
%ce         命令名以 ce 开头    %-          前一作业
%?ce        命令行含 ce        %           当前作业(同 %+)
%1 &        后台恢复作业 1      %1          前台恢复作业 1(同 fg %1)

例如 %1fg %1 的同义词,%1 & 等价于 bg %1

简单命名一个作业就可以把它带入前台:%1fg %1 的同义词,把作业 1 从后台带入前台。类似地,%1 & 在后台恢复作业 1,等价于 bg %1

每当作业改变状态,Shell 都会立即知道。通常,Bash 等到即将打印提示符时才通知用户作业状态的变化,以免中断任何其他输出,但它会在列表中的前台命令完成后、执行列表中的下一个命令之前通知作业状态的变化。如果启用了 set 内建命令的 -b 选项,Bash 会立即报告状态变化(参见第 4.3.1 节(set 内建命令))。Bash 为每个终止的子进程执行 SIGCHLD 上的任何陷阱。

当作业终止并且 Bash 通知用户时,Bash 从作业表中移除该作业。它不会出现在 jobs 输出中,但只要给它提供与作业关联的进程 ID 作为参数,wait 会报告其退出状态。当表为空时,作业编号从 1 重新开始。

译者注:上文实测会话中可以看到「作业编号从 1 重新开始」:第一个作业被 kill %1 终止并从表中移除后,新启动的 sleep 10 & 打印的又是 [1] ... 而不是 [2] ...。另外,作业被移除后 jobs 不再显示它,但 wait 内建命令仍可凭 PID 查询其退出状态(参见 6.11.2 节第 26 条)。

如果用户在作业被停止时(或者,如果启用了 checkjobs 选项,作业在运行时——参见第 4.3.2 节(shopt 内建命令))试图退出 Bash,Shell 打印一条警告消息,如果启用了 checkjobs 选项,则列出作业及其状态。然后可以用 jobs 命令检查它们的状态。如果用户在没有介入命令的情况下立即再次试图退出,Bash 不再打印警告,并终止任何已停止的作业。

译者注:实测(Bash 5.3 交互式会话):有挂起作业时退出会先收到警告,再次退出才会真正离开并终止被挂起的作业:

# 译者注示例:退出时存在被挂起的作业
$ sleep 10
^Z
[1]+  Stopped                    sleep 10
$ exit
There are stopped jobs.
$ exit

(第二次 exit 后 Shell 终止了该作业并正常退出。)

当 Shell 使用 wait 内建命令等待作业或进程,并且启用了作业控制时,wait 会在作业改变状态时返回。-f 选项使 wait 等到作业或进程终止后才返回。

7.2 作业控制内建命令

bg 内建命令

bg [JOBSPEC ...]

在后台恢复每个挂起的作业 JOBSPEC,就好像它是用 & 启动的一样。如果没有提供 JOBSPEC,Shell 使用它认为的当前作业。bg 返回零,除非它在作业控制未启用时运行,或者(在启用作业控制的情况下运行时)任何 JOBSPEC 未被找到,或指定的作业是在没有作业控制的情况下启动的。

译者注:实测:非交互式 Shell 默认不启用作业控制,此时 bg 报错并返回非零:

# 译者注示例:无作业控制时 bg 的返回状态
$ bash -c 'bg; echo "rc=$?"'
bash: 第 1 行: bg: 无任务控制
rc=1

fg 内建命令

fg [JOBSPEC]

在前台恢复作业 JOBSPEC 并使其成为当前作业。如果没有提供 JOBSPEC,fg 恢复当前作业。返回状态是放入前台的命令的返回状态,或者,如果在作业控制被禁用时运行,或在启用作业控制的情况下运行时 JOBSPEC 未指定有效作业、或 JOBSPEC 指定了一个在没有作业控制的情况下启动的作业,则返回非零。

jobs 内建命令

jobs [-lnprs] [JOBSPEC]
jobs -x COMMAND [ARGUMENTS]

第一种形式列出活动作业。选项的含义如下:

-l

列出进程 ID 以及常规信息。

-n

只显示自用户上次被告知其状态以来状态发生变化的作业的信息。

-p

只列出作业的进程组领导者的进程 ID。

-r

只显示正在运行的作业。

-s

只显示已停止的作业。

如果提供了 JOBSPEC,jobs 把输出限制为关于该作业的信息。如果没有提供 JOBSPEC,jobs 列出所有作业的状态。返回状态为零,除非遇到无效选项或提供了无效的 JOBSPEC。

如果提供了 -x 选项,jobs 用对应的进程组 ID 替换 COMMAND 或 ARGUMENTS 中发现的任何 JOBSPEC,并执行 COMMAND,把 ARGUMENTS 传给它,返回其退出状态。

译者注-x 选项实测(Bash 5.3 交互式会话):jobs -x echo %1%1 替换为该作业的进程组 ID 后执行 echo。注意 JOBSPEC 必须单独作为一个参数——echo PGID=%1 这样的写法不会被替换(实测原样输出 PGID=%1):

# 译者注示例:jobs -x 用进程组 ID 替换作业规格
$ sleep 10 &
[1] 48288
$ jobs -x echo %1
48288
$ jobs -p
48288

进程组 ID 与 jobs -p 列出的进程组领导者 PID 相同,因此 jobs -x 常用于把作业规格转换为可以传给 kill 等命令的进程组 ID。

kill 内建命令

kill [-s SIGSPEC] [-n SIGNUM] [-SIGSPEC] ID [...]
kill -l|-L [EXIT_STATUS]

把 SIGSPEC 或 SIGNUM 指定的信号发送到每个 ID 命名的进程。每个 ID 可以是作业规格 JOBSPEC 或进程 ID PID。SIGSPEC 是一个不区分大小写的信号名称,如 SIGINT(带或不带 SIG 前缀),或一个信号编号;SIGNUM 是一个信号编号。如果 SIGSPEC 和 SIGNUM 都不存在,kill 发送 SIGTERM

-l 选项列出信号名称。如果在提供 -l 时提供了任何参数,kill 列出与这些参数对应的信号的名称,返回状态为零。EXIT_STATUS 是一个数字,指定信号编号或被信号终止的进程的退出状态;如果提供了它,kill 打印导致进程终止的信号的名称。kill 假定进程退出状态大于 128;任何小于该值的都是信号编号。-L 选项等价于 -l

如果至少有一个信号被成功发送,返回状态为零,如果发生错误或遇到无效选项,则返回非零。

译者注kill -l EXIT_STATUS 的用法:进程被信号终止时,其退出状态是 128 加信号编号,所以 kill -l 143 可以反查信号名称:

# 译者注示例:由退出状态反查信号名称
$ kill -l 143
TERM
$ kill -l 2
INT

wait 内建命令

wait [-fn] [-p VARNAME] [ID ...]

等到每个 ID 指定的子进程退出,并返回最后一个 ID 的退出状态。每个 ID 可以是进程 ID PID 或作业规格 JOBSPEC;如果提供了作业规格,wait 等待该作业中的所有进程。

如果没有提供选项或 ID,wait 等待所有正在运行的后台作业和最后执行的进程替换(如果其进程 ID 与 $! 相同),返回状态为零。

如果提供了 -n 选项,wait 等待任何 ID(或者,如果没有提供 ID,任何作业或进程替换)完成并返回其退出状态。如果提供的任何 ID 都不是 Shell 的子进程,或者没有提供参数且 Shell 没有未等待的子进程,退出状态为 127。

如果提供了 -p 选项,wait 把返回其退出状态的作业的进程或作业标识符赋给选项参数命名的变量 VARNAME。该变量不能是只读的,在进行任何赋值之前将被初始取消设置。这只有与 -n 选项一起使用时才有用。

在启用作业控制的情况下提供 -f 选项,强制 wait 等待每个 ID 终止后再返回其状态,而不是在其状态改变时返回。

如果没有任何 ID 指定 Shell 的活动子进程之一,返回状态为 127。如果 wait 被信号中断,任何 VARNAME 都保持未设置,返回状态大于 128,如上文所述(参见第 3.7.6 节(信号))。否则,返回状态是最后一个 ID 的退出状态。

译者注-n-p 组合的实测(Bash 5.3):wait -n 返回最先完成的作业的退出状态;-p 把「返回了状态的作业」的 PID 存入指定变量(PID 随运行而异):

# 译者注示例:wait -n 等待任意一个作业完成
$ bash -c 'sleep 1 & sleep 2 & wait -n; echo "rc=$?"'
rc=0
# 译者注示例:wait -n -p 把完成作业的 PID 存入变量
$ bash -c 'sleep 1 & sleep 2 & wait -n -p v; echo "v=$v rc=$?"'
v=44840 rc=0
# 译者注示例:ID 不是当前 Shell 的子进程时返回 127
$ bash -c 'wait 999999; echo "rc=$?"'
bash: 行 1: wait: pid 999999 不是当前 shell 的子进程
rc=127

disown 内建命令

disown [-ar] [-h] [ID ...]

不带选项时,从活动作业表中移除每个 ID。每个 ID 可以是作业规格 JOBSPEC 或进程 ID PID;如果 ID 是 PID,disown 使用包含该 PID 的作业作为 JOBSPEC。

如果提供了 -h 选项,disown 不从作业表中移除与每个 ID 对应的作业,而是标记它们,使 Shell 在收到 SIGHUP 时不向该作业发送 SIGHUP

如果没有提供 ID,-a 选项意味着移除或标记所有作业;不带 ID 参数的 -r 选项移除或标记正在运行的作业。如果没有提供 ID,且既没有提供 -a 也没有提供 -r 选项,disown 移除或标记当前作业。

返回值为 0,除非 ID 未指定有效作业。

suspend 内建命令

suspend [-f]

挂起此 Shell 的执行,直到它收到 SIGCONT 信号。登录 Shell 或未启用作业控制的 Shell 不能被挂起;-f 选项会覆盖这一点并强制挂起。返回状态为 0,除非 Shell 是登录 Shell、或未启用作业控制且未提供 -f

当作业控制不活动时,killwait 内建命令不接受 JOBSPEC 参数。必须给它们提供进程 ID。

7.3 作业控制变量

auto_resume

此变量控制 Shell 如何与用户和作业控制交互。如果此变量存在,那么仅由单个单词组成、没有重定向的简单命令被视为恢复现有作业的候选。不允许有歧义;如果有多个以该单词开头或包含该单词的作业,则选择最近访问的作业。在此上下文中,已停止作业的名称是用于启动它的命令行,如 jobs 所示。如果此变量设置为值 exact,该单词必须与已停止作业的名称完全匹配;如果设置为 substring,该单词需要与已停止作业名称的子串匹配。substring 值提供的功能类似于 %?string 作业 ID(参见第 7.1 节(作业控制基础))。如果设置为任何其他值(例如 prefix),该单词必须是已停止作业名称的前缀;这提供的功能类似于 %string 作业 ID。

译者注auto_resume 的意思是「输入作业名本身即可恢复它」,无需 fg %n。实测(Bash 5.3 交互式会话):值为 exact 时,单词必须与停止作业的命令行完全一致,输入 sleep 不会匹配「sleep 10」,而是当作普通命令执行(于是报 missing operand);改为 prefix 后,输入 sleep 即恢复该作业:

# 译者注示例:auto_resume=exact 时,sleep 与作业名 sleep 10 不完全匹配
$ auto_resume=exact
$ sleep 10
^Z
[1]+  Stopped                    sleep 10
$ sleep
sleep: missing operand
Try 'sleep --help' for more information.
# 译者注示例:auto_resume=prefix 时,输入 sleep 直接恢复该作业(前台继续运行)
$ auto_resume=prefix
$ sleep 10
^Z
[1]+  Stopped                    sleep 10
$ sleep
sleep 10

第二个示例中 sleep 之后输出的 sleep 10 是 Bash 恢复该作业时打印的作业名称;该作业随后在前台继续运行(再按 Ctrl-Z 可再次挂起它,或按 Ctrl-C 终止它)。值为 substring 时则等价于 %?字符串 的匹配方式。

8 命令行编辑

本章描述 GNU 命令行编辑接口的基本功能。命令行编辑由 Readline 库提供,该库被包括 Bash 在内的多个不同程序使用。除非在调用 Shell 时提供了 --noediting 选项,否则在使用交互式 Shell 时命令行编辑默认处于启用状态。在使用 read 内建命令的 -e 选项时也会用到行编辑(参见第 4.2 节(Bash 内建命令))。默认情况下,行编辑命令与 Emacs 的编辑命令相似;也提供了 vi 风格的行编辑接口。可以随时使用 set 内建命令的 -o emacs-o vi 选项启用行编辑(参见第 4.3.1 节(set 内建命令)),或使用 set+o emacs+o vi 选项将其禁用。

译者注--noediting 是调用 Bash 时的启动选项;如果已经进入交互式 Shell,也可以在运行时用 set -o emacsset -o vi 在两种编辑模式之间切换(vi 模式下的按键详见第 8.5 节(Readline 的 vi 模式))。另外,想在使用 read 内建命令读取输入时也获得行编辑能力,需要给它加上 -e 选项,例如 read -e -p "输入路径: " dir

8.1 行编辑简介

以下段落采用 Emacs 风格来描述表示按键所用的记法。

文本 C-k 读作 "Control-K",表示在按住 Control 键的同时按下 <k> 键所产生的字符。

文本 M-k 读作 "Meta-K",表示按下 Meta 键(如果你的键盘上有该键)并按下 <k> 键(即一个 "meta 字符"),然后松开这两个键所产生的字符。许多键盘上将 Meta 键标记为 <ALT><Option>。在带有两个 <ALT> 键的键盘上(通常位于空格键两侧),左侧的 <ALT> 键一般被设置为充当 Meta 键。其中一个 <ALT> 键也可能被配置为其他修饰键,例如用于输入带重音字符的 Compose 键。

译者注:本章及后续章节出现的按键记法统一遵循 Emacs 风格,请务必先理解以下几点:

  • C- 表示按住 Ctrl(Control)键的同时按下后面的键,如 C-k 即 Ctrl+k;
  • M- 表示按住 Meta 键(多数键盘上是 Alt 或 Option 键)的同时按下后面的键,如 M-f 即 Alt+f;
  • 如果键盘上没有 Meta/Alt 键,或终端不把 Alt 当作 Meta 键使用,可以改用"先按 Esc,再按后面的键"的方式:例如先按 Esc 再按 f 就相当于 M-f。注意这是先后按下,不能像组合键那样同时按住;
  • 行编辑有两种模式:默认的 Emacs 模式(set -o emacs)和 vi 模式(set -o vi)。本章 8.1–8.4 节描述的按键是 Emacs 模式下的默认绑定;vi 模式使用另一套按键(如 k/j 上下移动、i 进入插入模式等),详见第 8.5 节(Readline 的 vi 模式)。

在某些键盘上,Meta 键的修饰会产生第八位(八进制 0200)被置位的字符。如果键盘允许,你可以使用 enable-meta-key 变量控制是否如此。在许多其他键盘上,终端或终端模拟器会把这种 "metafied"(meta 化)的按键转换为以 <ESC> 开头的按键序列,如下一段所述。

译者注:这里的"第八位"指一个字节的最高位。ASCII 字符只占 7 位(取值 0–127),把第八位(二进制 10000000,即八进制 0200、十进制 128)置位后,该字符就成为一个 "meta 字符"。某些老式终端用这种方式发送 Meta 组合键;现代终端模拟器一般改用 <ESC> 前缀序列(如方向键发送的 ESC [ D)或直接发送 Unicode。8.3.1 节中的 enable-meta-keyconvert-metainput-metaoutput-meta 等变量都与八位字符的处理有关。

如果你的键盘没有 Meta 或 <ALT> 键,也没有其他可充当 Meta 键的键,你通常可以_先_按 <ESC>、再按 <k> 来实现同样的效果。<ESC> 字符被称为 "meta 前缀"。

以上两种处理过程都称为对 <k> 键进行 "metafying"(meta 化)。

如果你的 Meta 键产生的是带 <ESC> meta 前缀的按键序列,你可以通过设置 force-meta-prefix 变量,让你指定的 M- 键绑定(参见第 8.3.1 节(Readline 初始化文件语法)中的「键绑定」)也做同样的事。

文本 M-C-k 读作 "Meta-Control-k",表示对 C-k 进行 meta 化后产生的字符。

译者注M-C-k 表示"对 C-k 进行 meta 化",即按住 Meta/Alt 键的同时再按 Ctrl+k;没有 Meta 键时可先按 Esc、再按 C-k(此时 Esc 与 Ctrl 是先后按下的)。8.3.3 节示例文件中的 "\M-\C-v" 就是同一记法在初始化文件中的另一种写法(\M-\C- 前缀表示 Meta+Control 组合)。

此外,几个按键有自己固定的名称。具体来说,<DEL><ESC><LFD><SPC><RET><TAB> 在本文本或初始化文件中出现时均代表它们自身(参见第 8.3 节(Readline 初始化文件))。如果你的键盘没有 <LFD> 键,键入 C-j 会输出相应的字符。在某些键盘上,<RET> 键可能被标记为 <Return><Enter>

译者注:这些键名(<DEL><ESC><LFD><SPC><RET><TAB>)在初始化文件中可以直接用作键名,例如 TAB: completeRET: accept-line。其中 <LFD> 是换行(line feed)字符,键盘上通常对应 Ctrl+j;<RET> 就是回车键(Return/Enter)。C-jC-m 在终端驱动层面分别是换行符与回车符。

8.2 与 Readline 交互

通常在交互式会话中,你输入了一长行文本后才发现行首的单词拼错了。Readline 库为你提供了一组在输入过程中编辑文本的命令,使你只需改正拼写错误,而不必重新输入该行的大部分内容。使用这些编辑命令,你可以把光标移动到需要更正的位置,然后删除或插入更正所需的文本。之后,当你对整行内容满意时,只需按 <RET>。你不需要位于行尾才能按 <RET>;无论光标位于行中什么位置,整行都会被接受。

译者注:Readline 的编辑以"行"为单位:无论光标停在哪,按回车都会把整行交给 Shell 执行,而不是只提交光标之前的部分。另外注意,Readline 编辑的是命令行文本,尚未执行;行编辑期间 Shell 不会执行任何命令(除非你按了回车或调用了 accept-line)。

8.2.1 Readline 基本要点

要向行中输入字符,直接键入即可。键入的字符出现在光标所在位置,然后光标向右移动一格。如果你输错了一个字符,可以使用你的擦除字符(erase character)回退并删除输错的字符。

有时候你可能输错了一个字符,却直到又键入几个字符之后才注意到错误。这种情况下,你可以键入 C-b 把光标向左移动,然后改正错误。之后,你可以用 C-f 把光标向右移动。

当你在行的中间插入文本时,你会注意到光标右边的字符被 "push over"(推挤过去),为插入的文本腾出空间。同样地,当你删除光标之前的文本时,光标右边的字符会被 "pull back"(拉回),以填补删除文本后留下的空白。以下是编辑输入行文本的基本要点:

按键 功能
C-b 向后移动一个字符。
C-f 向前移动一个字符。
<DEL><Backspace> 删除光标左边的字符。
C-d 删除光标所在位置的字符。
可打印字符 在光标处把字符插入行中。
C-_C-x C-u 撤销上一个编辑命令。你可以一路撤销回空行。

取决于你的配置,<Backspace> 键可能被设置为删除光标左边的字符,而 <DEL> 键则像 C-d 一样被设置为删除光标所在位置的字符,而不是删除光标左边的字符。

译者注:"擦除字符"(erase character)是终端驱动的一个设置,通常就是 Backspace(^H)或 Delete(^?),可以用 stty erase 查看和修改。Readline 默认会读取终端的擦除字符,并把 Backspace/DEL 键绑定到相应的删除命令上(参见 8.3.1 节 bind-tty-special-chars 的说明),所以在不同终端上 Backspace 与 Delete 的表现可能互换。

译者注C-_ 是 Emacs 风格的"撤销"键(即按住 Ctrl 同时按 Shift 和 - 得到的字符);C-x C-u 是它的另一种写法。Readline 的撤销以"编辑操作"为单位:每按一次撤销一步,例如一次 C-k 删掉的一整段会作为一步被撤销回来,而不是逐字符撤销。可以一路撤销回空行。

8.2.2 Readline 移动命令

上面的表格描述了编辑输入行所需的最基本按键。为了方便起见,除了 C-bC-fC-d<DEL> 之外,还有许多其他命令可用。下面是一些在行内更快地移动的命令。

按键 功能
C-a 移动到行首。
C-e 移动到行尾。
M-f 向前移动一个单词,其中单词由字母和数字组成。
M-b 向后移动一个单词。
C-l 清屏,并把当前行重新显示在屏幕顶部。

注意 C-f 是向前移动一个字符,而 M-f 是向前移动一个单词。有一个约定俗成的惯例:控制(Control)按键对字符进行操作,而 Meta 按键对单词进行操作。

译者注:这是 Readline(以及 Emacs)的一个重要惯例:控制键操作字符,Meta 键操作单词。例如 C-b/C-f 逐字符移动,M-b/M-f 逐单词移动;C-d 删一个字符,M-d 删一个单词。记住这个规律,就能举一反三。另外,C-l 清屏后会把当前行重新显示在屏幕顶部,方便查看长命令的全貌。

8.2.3 Readline 删除(killing)命令

"删除(killing)"文本意味着把文本从行中删除,但把它保存起来以备后用,通常通过 "粘贴(yanking)"(重新插入)把它拉回行中。('cut'(剪切)和 'paste'(粘贴)是 'kill' 和 'yank' 较新的说法。)

如果一个命令的描述中说它会 'kill' 文本,那么你可以确信以后能在另一个(或同一个)位置把文本取回来。

当你使用 kill 命令时,文本被保存在 "删除环"(kill-ring)中。任意多次连续的 kill 会把所有被删除的文本保存在一起,这样当你把它 yank 回来时,能全部取回。删除环不针对特定行;你在之前输入的行中删除的文本,可以在之后输入另一行时被 yank 回来。

下面是用于删除文本的命令列表。

按键 功能
C-k 删除从当前光标位置到行尾的文本。
M-d 删除从光标到当前单词末尾的文本;如果光标位于单词之间,则删除到下一个单词的末尾。单词边界与 M-f 使用的相同。
M-<DEL> 删除从光标到当前单词开头的文本;如果光标位于单词之间,则删除到前一个单词的开头。单词边界与 M-b 使用的相同。
C-w 删除从光标到前一个空白处的文本。这与 M-<DEL> 不同,因为两者的单词边界不同。

下面介绍如何把文本 "yank"(粘贴)回行中。粘贴意味着把最近一次删除的文本从删除缓冲区(kill buffer)复制到行中当前光标位置。

按键 功能
C-y 把最近删除的文本拉回光标处的编辑缓冲区中。
M-y 轮转删除环,并粘贴新的顶部条目。只有当前一个命令是 C-yM-y 时才能这样做。

译者注:kill 与 yank 就是命令行世界早期的"剪切/粘贴":kill 把文本从命令行删掉并存入"删除环"(可以理解为一个剪贴板),yank 把删除环中最新的内容插入到光标处。举例:

# 译者注示例:当前行是 echo hello world,光标停在行尾
# 按 C-w(删除到前一个空白):行变为 echo hello,world 进入删除环
# 再按 C-y(粘贴):world 被插回光标处,行恢复为 echo hello world

与普通编辑器的剪切/粘贴不同,删除环可以保存多段文本,并且跨命令行有效:你上一行删除的内容,这一行仍能粘贴出来。

译者注:连续的多次 kill 会合并成一段保存,一次 yank 就能全部取回。举例:

# 译者注示例:当前行是 echo a b c,光标停在第一个 a 之前
# 连续按两次 M-d:第一次删 a,第二次删 b(中间没有其他操作)
# 两次删除合并为一段,按 C-y 会一次性粘贴回 ab

注意:只有"连续"的删除才会合并。如果在两次删除之间移动了光标或做了其他操作,它们就是删除环中的两段独立内容,可用 M-y 在它们之间切换(见下文)。

译者注:三个"删词"命令的边界不同,需要区分:

  • M-d:删到当前单词末尾。光标在单词中间时删后半截;在单词之间时删整个下一个单词。单词由字母和数字组成,标点与空白都是边界;
  • M-<DEL>:删到当前单词开头,边界与 M-b 一致;
  • C-w:删到前一个空白,即它把标点也一起删掉。例如在 echo hello, world 中把光标停在 , 之后,M-<DEL> 只删 hello,而 C-w 会把 hello, 一起删掉。

译者注M-y 只能在刚执行过 C-yM-y 之后使用(Emacs 的传统行为)。它把删除环的"栈顶"轮转到下一段,实现在最近删除的几段文本之间切换粘贴内容。例如先删了 foo,随后移动光标再删了 bar(两次删除不连续),按 C-y 粘贴的是 bar,再按 M-y 会换成粘贴 foo

8.2.4 Readline 参数

你可以向 Readline 命令传递数值参数。有时参数用作重复次数,有时起作用的只是参数的正负号。如果你向一个通常向前作用的命令传递负参数,该命令将向后作用。例如,要删除文本直到行首,你可以键入 M-- C-k

向命令传递数值参数的一般方法是:在命令之前键入带 Meta 修饰的数字。如果键入的第一个 "数字" 是负号(-),则参数的正负号为负。一旦你键入了第一个 meta 数字使参数开始输入,就可以键入其余的数字,然后键入命令。例如,要给 C-d 命令一个 10 的参数,你可以键入 M-1 0 C-d,这将删除输入行上接下来的十个字符。

译者注M-- C-kM-- 表示 Meta+-(按住 Meta/Alt 同时按减号键),即给参数一个负号。参数为负时方向反转,原本"删除到行尾"的 C-k 就变成"删除到行首"。这条"负号反转方向"的规则对移动类命令同样有效,例如 M-- M-f 等价于 M-b(向前移动一个单词)。

译者注:数字参数最常见的用途是"重复次数"。注意 M-1 0 C-d 的输入方式:按 Meta+1 开始输入参数,接着不带 Meta 按 0(继续追加数字),最后按 C-d——而不是按 Meta+10。数字参数在输入期间不会插入到行中,只是作为紧接其后的命令的修饰信息。同理,M-5 C-b 把光标向左移动 5 个字符,M-5 M-f 向前移动 5 个单词。

8.2.5 在历史中搜索命令

Readline 提供了在命令历史(参见第 9.1 节(Bash 的历史记录设施))中搜索包含指定字符串的行的命令。有两种搜索模式:"增量式"(incremental)和 "非增量式"(non-incremental)。

增量式搜索在用户输完搜索字符串之前就开始。随着搜索字符串的每个字符被键入,Readline 会显示历史中与到目前为止已输入的字符串匹配的下一条目。增量式搜索只需要找到所需历史条目所需的那么多字符。在使用 emacs 编辑模式时,键入 C-r 可在历史中向后搜索特定字符串。键入 C-s 则向前搜索历史。isearch-terminators 变量的值中出现的字符用于终止增量式搜索。如果该变量尚未被赋值,则 <ESC>C-j 字符会终止增量式搜索。C-g 中止增量式搜索并恢复原始行。当搜索终止时,包含搜索字符串的历史条目成为当前行。

译者注:增量式搜索(Emacs 中称 Isearch)是最常用的历史检索方式。按 C-r 后,提示符区域出现 (reverse-i-search) 提示,此后每敲一个字符都会立即显示历史中最新一条匹配的命令;再按一次 C-r 跳到更早的匹配,C-s 向前(更新)跳。找到后按回车直接执行该命令;按方向键等编辑键则退出搜索、开始编辑找到的那一行;按 C-g 放弃搜索并恢复原行。例如:

# 译者注示例:按 C-r 后输入 gcc,提示符区域显示形如
# (reverse-i-search)`gcc': gcc -o hello hello.c
# 再按一次 C-r 显示更早一条含 gcc 的命令

在 vi 模式下,对应的搜索键不同:/ 向前搜索历史、? 向后搜索历史(均为非增量式)。另外提醒:某些终端把 C-s 用作输出流控制(XOFF),会"吞掉"该按键导致屏幕看似卡住,可用 stty -ixon 关闭这一设置后再使用 C-s 搜索。

要查找历史列表中的其他匹配条目,相应地键入 C-rC-s。这会在历史中向后或向前搜索与到目前为止已键入的搜索字符串匹配的下一条目。任何其他绑定到 Readline 命令的按键序列都会终止搜索并执行该命令。例如,<RET> 会终止搜索并接受该行,从而执行历史列表中的命令。移动命令会终止搜索,使最后找到的行成为当前行,并开始编辑。

Readline 会记住最后一条增量式搜索字符串。如果连续键入两个 C-r,而中间没有输入任何定义新搜索字符串的字符,Readline 会使用记住的搜索字符串。

译者注:"记住搜索串"的行为意味着:按 C-rC-r(第二次 C-r 前不输入任何字符)就能在不重新输入搜索串的情况下,逐条回看上一次搜索的匹配结果。isearch-terminators 变量(见 8.3.1 节)可用来自定义终止搜索的字符;默认情况下 <ESC>C-j 终止搜索并停留在找到的行上,C-g 则中止搜索、恢复原行。

非增量式搜索在开始搜索匹配的历史条目之前会读入整个搜索字符串。搜索字符串可以由用户键入,也可以是当前行内容的一部分。

8.3 Readline 初始化文件

尽管 Readline 库自带一套默认安装的类 Emacs 键绑定,但也可以使用另一套键绑定。任何用户都可以通过在一个 "inputrc" 文件(通常位于其主目录中)中放入命令来定制使用 Readline 的程序。该文件的名称取自 Shell 变量 INPUTRC 的值。如果该变量未设置,默认值为 ~/.inputrc。如果该文件不存在或无法读取,Readline 会查找 /etc/inputrcbind 内建命令也可以用来设置 Readline 的键绑定和变量。参见第 4.2 节(Bash 内建命令)。

当使用 Readline 库的程序启动时,Readline 会读取初始化文件,并设置其中包含的所有变量和键绑定。

此外,C-x C-r 命令会重新读取这个初始化文件,从而把你可能对它所做的任何修改纳入其中。

译者注:初始化文件的查找顺序是:$INPUTRC 指定的文件 →(未设置时)~/.inputrc →(前两者都不存在或不可读时)/etc/inputrc。除了编辑文件,还可以在 Bash 里直接用 bind -f 文件名 从指定文件加载键绑定与变量设置,或先用 bind -p(键绑定)与 bind -V(变量)查看当前生效的内容。改完 ~/.inputrc 后按 C-x C-r 即可立即生效,无需重启 Shell。

8.3.1 Readline 初始化文件语法

Readline 初始化文件中只允许少数几个基本结构。空行会被忽略。以 # 开头的行是注释。以 $ 开头的行表示条件结构(参见第 8.3.2 节(条件初始化结构))。其他行表示变量设置和键绑定。

变量设置(Variable Settings)

你可以通过修改 Readline 变量的值来改变 Readline 的运行时行为,方法是在初始化文件中使用 set 命令。语法很简单:

set 变量名 值

例如,下面演示了如何从默认的类 Emacs 键绑定切换为使用 vi 行编辑命令:

set editing-mode vi

变量名和值(在适当的情况下)的识别不区分大小写。无法识别的变量名会被忽略。

布尔变量(即可以设置为 on 或 off 的变量)在值为 null 或空、ON(不区分大小写)或 1 时被设置为 on。任何其他值都会使变量被设置为 off。

bind -V 命令列出当前 Readline 变量的名称和值。参见第 4.2 节(Bash 内建命令)。

大量的运行时行为可以通过下列变量来改变。

变量名 说明
active-region-start-color 字符串变量,控制显示活动区域(active region)中的文本时所用的文字颜色和背景色(参见下文 enable-active-region 的描述)。该字符串不能占用显示上的任何物理字符位置,因此它应该只包含终端转义序列。它在显示活动区域中的文本之前输出到终端。每当终端类型改变时,该变量会重置为默认值。默认值是使终端进入 standout(高亮)模式的字符串,取自终端的 terminfo 描述。示例值可以是 \e[01;33m
active-region-end-color 字符串变量,用于 "撤销" active-region-start-color 的效果,并在显示活动区域中的文本之后恢复终端的 "正常" 显示外观。该字符串不能占用显示上的任何物理字符位置,因此它应该只包含终端转义序列。它在显示活动区域中的文本之后输出到终端。每当终端类型改变时,该变量会重置为默认值。默认值是使终端退出 standout 模式的字符串,取自终端的 terminfo 描述。示例值可以是 \e[0m
bell-style 控制 Readline 想响终端铃时会发生什么。若设为 none,Readline 从不响铃。若设为 visible,Readline 在可用时使用可视铃。若设为 audible(默认值),Readline 尝试响终端的铃。
bind-tty-special-chars 若设为 on(默认值),Readline 尝试把内核终端驱动程序特殊处理的控制字符绑定到对应的 Readline 命令上。这些绑定会覆盖这里描述的默认 Readline 绑定。在 Bash 提示符下键入 stty -a 可查看当前终端设置,包括特殊控制字符(通常是 cchars)。
blink-matching-paren 若设为 on,当插入右括号时,Readline 尝试短暂地把光标移到与之匹配的左括号上。默认值为 off
colored-completion-prefix 若设为 on,列出补全项时,Readline 用不同的颜色显示一组可能补全项的共同前缀。颜色定义取自 LS_COLORS 环境变量的值。如果 LS_COLORS 中有针对自定义后缀 readline-colored-completion-prefix 的颜色定义,Readline 就用该颜色而不是默认颜色显示共同前缀。默认值为 off
colored-stats 若设为 on,Readline 用不同的颜色显示可能的补全项,以指示它们的文件类型。颜色定义取自 LS_COLORS 环境变量的值。默认值为 off
comment-begin insert-comment 命令在行首插入的字符串。默认值为 "#"
completion-display-width 执行补全时用于显示可能匹配项的屏幕列数。如果该值小于 0 或大于终端屏幕宽度则被忽略。值为 0 时,匹配项每行显示一个。默认值为 -1。
completion-ignore-case 若设为 on,Readline 以不区分大小写的方式执行文件名匹配和补全。默认值为 off
completion-map-case 若设为 on,且启用了 COMPLETION-IGNORE-CASE,Readline 在执行不区分大小写的文件名匹配和补全时,把连字符(-)与下划线(_)视为等价。默认值为 off
completion-prefix-display-length 显示一组可能补全项的共同前缀时不加修改的最大字符数。当设为大于零的值时,Readline 在显示可能补全项时,把长度超过该值的共同前缀替换为省略号。如果某个补全项以句点开头,且 Readline 正在补全文件名,它用三个下划线代替省略号。
completion-query-items 决定何时询问用户是否应显示可能补全项列表的可能补全项数量。如果可能补全项的数量大于或等于该值,Readline 询问用户是否希望查看它们;否则,Readline 直接列出补全项。该变量必须设置为大于或等于零的整数值。值为零意味着 Readline 从不询问;负值按零处理。默认限制为 100
convert-meta 若设为 on,Readline 把它读取到的第八位被置位的字符转换为 ASCII 按键序列:清除第八位并在前面加上一个 <ESC> 字符,把它转换为带 meta 前缀的按键序列。默认值为 on,但如果区域设置(locale)中包含编码可能带第八位字节的字符,Readline 会把它设为 off。该变量依赖于 LC_CTYPE locale 类别,如果 locale 改变,它的值也可能改变。该变量也影响键绑定;参见下文 force-meta-prefix 的描述。
disable-completion 若设为 On,Readline 抑制单词补全。补全字符被插入到行中,就像它们被映射到了 self-insert 一样。默认值为 off
echo-control-characters 设为 on 时,在表明支持此功能的操作系统上,Readline 会回显与从键盘产生的信号相对应的字符(例如按 C-c 时回显 ^C)。默认值为 on
editing-mode editing-mode 变量控制默认的键绑定集。默认情况下,Readline 以 emacs 编辑模式启动,此时的按键与 Emacs 最为相似。该变量可以设为 emacsvi
emacs-mode-string 如果启用了 SHOW-MODE-IN-PROMPT 变量,当 emacs 编辑模式处于活动状态时,该字符串显示在主提示符最后一行之前。该值的展开方式与键绑定(宏)类似,因此可以使用标准的 meta- 和 control- 前缀以及反斜杠转义序列。\1\2 转义用于开始和结束非打印字符序列,可以用来把终端控制序列嵌入模式字符串中。默认值为 @
enable-active-region "point"(插入点)是当前光标位置,"mark"(标记)指保存的光标位置(参见第 8.4.1 节(移动命令))。point 与 mark 之间的文本称为 "region"(区域)。当该变量设为 On 时,Readline 允许某些命令把区域指定为 "活动" 状态。当区域活动时,Readline 使用 active-region-start-color 的值(默认为启用终端 standout 模式的字符串)高亮区域中的文本。活动区域会显示括号粘贴(bracketed paste)插入的文本,以及增量式和非增量式历史搜索找到的任何匹配文本。默认值为 On
enable-bracketed-paste 设为 On 时,Readline 配置终端把每次粘贴的内容作为单个字符串插入编辑缓冲区,而不是把每个字符当作从键盘读取的字符处理。这称为让终端进入 "括号粘贴模式"(bracketed paste mode);它可以防止 Readline 执行任何绑定到粘贴文本中出现的按键序列上的编辑命令。默认值为 On
enable-keypad 设为 on 时,Readline 在被调用时尝试启用应用程序键盘(application keypad)。某些系统需要这样才能启用方向键。默认值为 off
enable-meta-key 设为 on 时,Readline 在被调用时尝试启用终端声称支持的任何 meta 修饰键。在许多终端上,Meta 键用于发送八位字符;该变量检查终端是否具有这样的能力:在键入字符时按住 Meta 键(产生 meta 字符)的情况下,可以启用和禁用设置字符第八位(0200)的模式。默认值为 on
expand-tilde 若设为 on,Readline 在尝试单词补全时进行波浪号展开。默认值为 off
force-meta-prefix 若设为 on,Readline 在绑定包含 \M-Meta- 的按键序列(参见第 8.3.1 节(Readline 初始化文件语法)中的「键绑定」)时修改其行为:把 \M-CMeta-C 形式的按键序列(C 代表任一字符)转换为两字符序列 ESC C(加上 meta 前缀)。如果 force-meta-prefix 设为 off(默认值),Readline 使用 convert-meta 变量的值来决定是否执行此转换:如果 convert-metaon,Readline 执行上述转换;如果它为 off,Readline 通过设置第八位(0200)把 C 转换为 meta 字符。默认值为 off
history-preserve-point 若设为 on,历史记录代码尝试在每次用 previous-historynext-history 取回的历史行上,把 point(当前光标位置)放在相同的位置。默认值为 off
history-size 设置历史列表中保存的历史条目最大数量。若设为零,删除所有现有历史条目并且不保存新条目。若设为小于零的值,历史条目数量不受限制。默认情况下,Bash 把历史条目最大数量设置为 HISTSIZE Shell 变量的值。如果你试图把 HISTORY-SIZE 设置为非数值,历史条目最大数量将被设置为 500。
horizontal-scroll-mode 把该变量设为 on 意味着:被编辑行的文本在行长于屏幕宽度时,将在单行屏幕上水平滚动,而不是换行到新的屏幕行。对于高度为 1 的终端,该变量会自动设为 on。默认情况下,该变量设为 off
input-meta 若设为 on,无论终端声称支持什么,Readline 都启用八位输入(也就是说,它不清除所读取字符的第八位)。默认值为 off,但如果区域设置中包含编码可能带第八位字节的字符,Readline 会把它设为 on。该变量依赖于 LC_CTYPE locale 类别,如果 locale 改变,它的值也可能改变。名称 meta-flaginput-meta 的同义词。
isearch-terminators 用于终止增量式搜索、且随后不把该字符作为命令执行的字符串(参见第 8.4.2 节(操作历史记录的命令)中的「搜索」)。如果该变量没有被赋值,<ESC>C-j 字符终止增量式搜索。
keymap 设置 Readline 对当前键映射(keymap)的认定,供键绑定命令使用。内建的 keymap 名称有 emacsemacs-standardemacs-metaemacs-ctlxvivi-movevi-commandvi-insertvi 等同于 vi-commandvi-move 也是同义词);emacs 等同于 emacs-standard。应用程序可以添加其他名称。默认值为 emacsediting-mode 变量的值也影响默认键映射。
keyseq-timeout 指定 Readline 在读取歧义按键序列(即利用到目前为止读取的输入可以构成一个完整按键序列、也可以再接收额外输入构成更长按键序列的那种)时等待一个字符的时长。如果 Readline 在超时时间内没有收到任何输入,它使用较短的完整按键序列。Readline 用该值判断当前输入源(默认是 rl_instream)上是否有输入可用。该值以毫秒为单位,因此值 1000 意味着 Readline 将等待一秒以获取额外输入。如果该变量设为小于或等于零的值,或设为非数值,Readline 会一直等到按下另一个键才决定完成哪个按键序列。默认值为 500
mark-directories 若设为 on,补全的目录名会附加一个斜杠。默认值为 on
mark-modified-lines 当该变量设为 on 时,Readline 在被修改过的历史行开头显示一个星号(*)。该变量默认值为 off
mark-symlinked-directories 若设为 on,补全的、作为目录符号链接的名称会附加一个斜杠,具体受 mark-directories 的值约束。默认值为 off
match-hidden-files 该变量设为 on 时,强制 Readline 在执行文件名补全时匹配名称以 . 开头的文件(隐藏文件)。若设为 off,用户必须把前导 . 包含在要补全的文件名中。该变量默认值为 on
menu-complete-display-prefix 若设为 on,菜单补全在列表中循环之前,先显示可能补全项列表的共同前缀(可能为空)。默认值为 off
output-meta 若设为 on,Readline 直接显示第八位被置位的字符,而不是显示为带 meta 前缀的转义序列。默认值为 off,但如果区域设置中包含编码可能带第八位字节的字符,Readline 会把它设为 on。该变量依赖于 LC_CTYPE locale 类别,如果 locale 改变,它的值也可能改变。
page-completions 若设为 on,Readline 使用一个类似 more(1) 的内部分页程序,一次显示一屏可能的补全项。该变量默认值为 on
prefer-visible-bell 参见 bell-style
print-completions-horizontally 若设为 on,Readline 显示补全项时把匹配项按字母顺序水平排列,而不是在屏幕上垂直排列。默认值为 off
revert-all-at-newline 若设为 on,Readline 在执行 accept-line 返回之前,撤销对历史行的所有更改。默认情况下,历史行可能被修改,并在多次调用 readline() 之间保留各自的撤销列表。默认值为 off
search-ignore-case 若设为 on,Readline 以不区分大小写的方式执行增量式和非增量式历史列表搜索。默认值为 off
show-all-if-ambiguous 这改变了补全函数的默认行为。若设为 on,有多个可能补全项的单词会立即使匹配项被列出,而不是响铃。默认值为 off
show-all-if-unmodified 这以与 SHOW-ALL-IF-AMBIGUOUS 类似的方式改变补全函数的默认行为。若设为 on,有多个可能补全项、且不存在任何可能的局部补全(各可能补全项没有共同前缀)的单词会立即使匹配项被列出,而不是响铃。默认值为 off
show-mode-in-prompt 若设为 on,在提示符开头添加一个指示编辑模式的字符串:emacs、vi 命令(vi command)或 vi 插入(vi insertion)。模式字符串可由用户设置(例如 EMACS-MODE-STRING)。默认值为 off
skip-completed-text 若设为 on,该变量在把单个匹配项插入行中时改变默认的补全行为。它只在单词中间执行补全时生效。如果启用,Readline 不会插入补全结果中与正在补全的单词中 point 之后的字符相匹配的字符,因此光标之后的部分单词不会被重复。例如,如果启用了该变量,光标位于 Makefile 中第一个 e 之后时尝试补全将得到 Makefile 而不是 Makefilefile,前提是只有一个可能的补全项。默认值为 off
vi-cmd-mode-string 如果启用了 SHOW-MODE-IN-PROMPT 变量,当 vi 编辑模式处于活动状态且处于命令模式时,该字符串显示在主提示符最后一行之前。该值的展开方式与键绑定(宏)类似,因此可以使用标准的 meta- 和 control- 前缀以及反斜杠转义序列。\1\2 转义用于开始和结束非打印字符序列,可以用来把终端控制序列嵌入模式字符串中。默认值为 (cmd)
vi-ins-mode-string 如果启用了 SHOW-MODE-IN-PROMPT 变量,当 vi 编辑模式处于活动状态且处于插入模式时,该字符串显示在主提示符最后一行之前。该值的展开方式与键绑定(宏)类似,因此可以使用标准的 meta- 和 control- 前缀以及反斜杠转义序列。\1\2 转义用于开始和结束非打印字符序列,可以用来把终端控制序列嵌入模式字符串中。默认值为 (ins)
visible-stats 若设为 on,列出可能的补全项时,会在文件名后附加一个表示文件类型的字符。默认值为 off

译者注:布尔变量的取值规则(Bash 5.3 起)比较严格:值只有为空、on(不区分大小写)或 1 时才等于 on,其余任何值(包括 2yestrue 等)都视为 off。这与许多脚本中"非零即真"的习惯不同,例如 set completion-ignore-case yes 并不会生效,应写 on1。变量名与值都不区分大小写,Set Editing-Mode Viset editing-mode vi 等价。

译者注set editing-mode vi 是初始化文件里的写法,作用与在 Bash 命令行执行 set -o vi(或 set +o vi 恢复 Emacs 模式)相同;区别在于前者对所有使用 Readline 的程序和所有会话生效,后者只影响当前 Bash 会话。注意初始化文件中的 set 是 Readline 自己的指令,不是 Bash 的 set 内建命令。

译者注:表中几个变量值得特别说明:

  • bell-style 的三种取值对应三种响铃方式:audible 发声(终端蜂鸣),visible 用屏幕闪烁代替声音(若终端支持),none 完全静音。prefer-visible-bell 是它的历史别名,设为 on 的效果与把 bell-style 设为 visible 类似;
  • history-size 与 Shell 变量 HISTSIZE 的关系:Bash 默认把 HISTSIZE 的值作为 history-size 的初始值;在初始化文件里 set history-size 2000 可覆盖它,设为 0 则清空历史且不再记录,负值表示不限制条数;
  • keymap 所指的"键映射"可以理解为"当前处于哪张按键方案表":Emacs 模式实际包含三张——emacs-standard(普通按键)、emacs-meta(Meta 前缀后的按键)、emacs-ctlxC-x 前缀后的按键);vi 模式有两张——vi-command(命令模式)与 vi-insert(插入模式)。因此 set keymap emacs-ctlx 之后再写的键绑定只对 C-x 前缀下的按键生效;
  • keyseq-timeout(默认 500 毫秒)解决的是"歧义按键序列"问题,最典型的是 <ESC> 键:单独按 <ESC> 是 meta 前缀,但它也可能是一串 ESC [ ... 序列(如方向键)的开头。Readline 收到 <ESC> 后会等待最多 500 毫秒:期间收到后续字符就按长序列处理,否则把 <ESC> 当作单独按键。把该值调小(如 100)可加快 <ESC> 的响应,但可能影响方向键等序列的识别。

键绑定(Key Bindings)

在初始化文件中控制键绑定的语法很简单。首先,你需要找到想要修改的命令名称。后面的各节包含命令名、默认键绑定(如果有)以及命令功能的简短描述的表格(参见第 8.4 节(可绑定的 Readline 命令))。

一旦知道了命令的名称,只需在初始化文件的一行中依次放置:你想绑定该命令的键的名称、一个冒号,然后是命令的名称。键名与冒号之间不能有空格——空格会被解释为键名的一部分。键名可以用不同的方式表达,取决于你觉得哪种方式最方便。

除了命令名之外,Readline 还允许把键绑定到一个按下该键时插入的字符串(即一个宏(MACRO))。宏与命令的区别在于,宏用单引号或双引号括起来。

bind -p 命令以可以直接放入初始化文件的格式显示 Readline 函数名和绑定。参见第 4.2 节(Bash 内建命令)。

KEYNAME: FUNCTION-NAME or MACRO(键名:函数名或宏)

KEYNAME 是用英文拼写出来的键的名称。例如:

Control-u: universal-argument
Meta-Rubout: backward-kill-word
Control-o: "> output"

在上面的示例中,C-u 绑定到函数 universal-argumentM-DEL 绑定到函数 backward-kill-wordC-o 绑定为执行右端的宏(也就是说,把文本 > output 插入到行中)。

这种键绑定语法能识别若干符号字符名:DELESCESCAPELFDNEWLINERETRETURNRUBOUT(一种破坏性的退格,即会真正删除字符)、SPACESPCTAB

译者注:键绑定行由三部分组成:键名、冒号、函数名或宏。两点提醒:

  • 键名与冒号之间不能有空格,否则空格会被当作键名的一部分(Control-u : ... 绑定的是名为 Control-u 的键,永远不会被按到);
  • 绑定目标若是函数名则不加引号;若用单引号或双引号括起来,就成了"宏"——按下该键时插入引号内的文本。示例中的 Control-o: "> output" 的作用是:按 C-o 时把 > output 这几个字符输入到命令行中。键名与函数名都不区分大小写,control-uControl-u 写法均可。

"KEYSEQ": FUNCTION-NAME or MACRO("键序列":函数名或宏)

KEYSEQ 与上面的 KEYNAME 不同之处在于:可以把表示整个按键序列的字符串放在双引号中指定。可以使用一些 GNU Emacs 风格的按键转义(如下例所示),但上面列出的那些特殊字符名在这里都不被识别。

"\C-u": universal-argument
"\C-x\C-r": re-read-init-file
"\e[11~": "Function Key 1"

在上面的示例中,C-u 再次绑定到函数 universal-argument(与第一个示例完全相同),C-x C-r 绑定到函数 re-read-init-file<ESC> <[> <1> <1> <~> 绑定为插入文本 Function Key 1

译者注"\e[11~" 是许多终端(包括 xterm)上 F1 功能键发送的按键序列(ESC[11~ 共五个字符),"\e[12~" 对应 F2,依此类推。终端功能键(方向键、F1–F12、Insert/Delete 等)发送的都是一串以 ESC 开头的字符,因此在初始化文件里把命令绑定到这些序列,就能让功能键按你的意愿工作——这也是 8.3.3 节示例中方向键绑定的原理。

指定按键序列时可以使用以下 GNU Emacs 风格的转义序列:

转义序列 含义
\C- 控制(control)前缀。
\M- 加上 meta 前缀,或把后面的字符转换为 meta 字符,如上文 force-meta-prefix 下所述(参见第 8.3.1 节(Readline 初始化文件语法)中的「变量设置」)。
\e 一个转义字符。
\\ 反斜杠。
\" ",一个双引号。
\' ',一个单引号或撇号。

除了 GNU Emacs 风格的转义序列之外,还有第二组反斜杠转义可用:

转义序列 含义
\a 响铃(bell)
\b 退格
\d 删除
\f 换页
\n 换行
\r 回车
\t 水平制表符
\v 垂直制表符
\NNN 值为八进制数 NNN(一到三位数字)的八位字符。
\xHH 值为十六进制数 HH(一位或两位十六进制数字)的八位字符。

输入宏的文本时,必须使用单引号或双引号来表示宏定义。未加引号的文本被假定为函数名。上述反斜杠转义会在宏体中展开。反斜杠会引用宏文本中的任何其他字符,包括 "'。例如,下面的绑定将使 C-x \ 在行中插入一个 \

"\C-x\\": "\\"

译者注:这条绑定里出现了两层 \\,含义不同:左端的 \C-x\\ 表示按键序列"C-x 加反斜杠"(\\ 转义为一个字面反斜杠);右端的 "\\" 是宏体,展开后也是一个字面反斜杠。因此效果是:按 C-x \ 时在命令行插入一个 \。在初始化文件中写反斜杠的规则与 C 字符串类似,需要转义。

8.3.2 条件初始化结构

Readline 实现了一种与 C 预处理器的条件编译功能精神相似的功能:允许把键绑定和变量设置作为测试的结果来执行。共有四个解析器指令可用。

$if

$if 结构允许根据编辑模式、所使用的终端或使用 Readline 的应用程序来建立绑定。测试文本(任何比较运算符之后的部分)延伸到行尾;除非另有说明,不需要任何字符来隔离它。

mode

$if 指令的 mode= 形式用于测试 Readline 处于 emacs 模式还是 vi 模式。例如,它可以与 set keymap 命令结合使用:只在 Readline 以 emacs 模式启动时,在 emacs-standardemacs-ctlx 键映射中设置绑定。

term

term= 形式可用于包含终端特定的键绑定,也许用于绑定终端功能键输出的按键序列。= 右侧的单词会与终端的完整名称以及终端名称中第一个 - 之前的部分进行比较。例如,这允许 xterm 同时匹配 xtermxterm-256color

译者注term= 的匹配规则意味着:$if term=xtermTERM=xterm-256color 同样成立,$if term=screen 也能匹配 screen-256color 等常见值。这一形式最常见的用途是按终端类型分组绑定功能键序列(8.3.3 节示例中的方向键绑定即按终端模式分组)。注意 mode=term= 的测试值一直延伸到行尾,所以不要在 $if 行末尾追加注释,否则注释会成为测试值的一部分。

version

version 测试可用于与特定 Readline 版本进行比较。version 展开为当前的 Readline 版本。可用的比较运算符包括 =(以及 ==)、!=<=>=<>。运算符右侧提供的版本号由主版本号、可选的小数点和可选的次版本号组成(例如 7.1)。如果省略次版本号,它默认为 0。运算符可以用空白与字符串 version 和版本号参数分隔。下面的示例在所使用的 Readline 版本为 7.0 或更新时设置一个变量:

$if version >= 7.0
set show-mode-in-prompt on
$endif

application

APPLICATION 结构用于包含特定于应用程序的设置。每个使用 Readline 库的程序都会设置应用程序名称(APPLICATION NAME),你可以测试某个特定值。这可以用来把按键序列绑定到对特定程序有用的函数上。例如,下面的命令添加了一个在 Bash 中为当前或前一个单词加引号的按键序列:

$if Bash
# Quote the current or previous word
"\C-xq": "\eb\"\ef\""
$endif

译者注$if Bash 测试的是使用 Readline 的应用程序名:Bash 设置的名字是 Bash,FTP 客户端设置的是 Ftp(8.3.3 节示例末尾的 $if Ftp 就是)。上面示例中的宏 "\C-xq" 展开后依次为 M-b"M-f"——即跳到前一个单词、插入双引号、跳到单词之后、再插入双引号,效果是把光标所在(或前一个)单词用双引号括起来。例如在 echo hello 的光标位于行尾时按 C-x q,得到 echo "hello"

variable

VARIABLE 结构为 Readline 变量和值提供简单的相等性测试。允许的比较运算符是 ===!=。变量名必须与比较运算符用空白分隔;运算符可以与右侧的值用空白分隔。可以测试字符串变量和布尔变量。布尔变量必须与值 ONOFF 进行比较。下面的示例等价于上面描述的 mode=emacs 测试:

$if editing-mode == emacs
set show-mode-in-prompt on
$endif

译者注variable 测试与 mode= 测试的区别:variable 可以测试任何 Readline 变量(如 editing-modebell-stylehistory-size),而 mode= 只能测试 emacs/vi 两种编辑模式。注意 variable 要求变量名与运算符之间必须有空白(否则 editing-mode==emacs 会被当作一个变量名),运算符与右侧的值之间可以有空白也可以没有;布尔变量只能用 ONOFF 与之比较。

$else

如果测试失败,则执行 $if 指令的这一分支中的命令。

$endif

如前面的示例所示,该命令终止一个 $if 命令。

$include

该指令接受单个文件名作为参数,并从该文件中读取命令和键绑定。例如,下面的指令从 /etc/inputrc 中读取:

$include /etc/inputrc

译者注$if/$else/$endif 三个指令构成完整的条件块,作用与 C 预处理器中的 #if/#else/#endif 类似;$include 相当于 #include。条件块可以嵌套。一个完整的例子:

# 译者注示例:Emacs 模式与 vi 模式使用不同的键绑定
$if mode=emacs
"\C-xr": re-read-init-file
$else
"\C-xr": redraw-current-line
$endif

8.3.3 初始化文件示例

下面是一个 INPUTRC 文件的示例。它演示了键绑定、变量赋值和条件语法。

# This file controls the behavior of line input editing for
# programs that use the GNU Readline library.  Existing
# programs include FTP, Bash, and GDB.
#
# You can re-read the inputrc file with C-x C-r.
# Lines beginning with '#' are comments.
#
# First, include any system-wide bindings and variable
# assignments from /etc/Inputrc
$include /etc/Inputrc

#
# Set various bindings for emacs mode.

set editing-mode emacs

$if mode=emacs

Meta-Control-h:	backward-kill-word	Text after the function name is ignored

#
# Arrow keys in keypad mode
#
#"\M-OD":        backward-char
#"\M-OC":        forward-char
#"\M-OA":        previous-history
#"\M-OB":        next-history
#
# Arrow keys in ANSI mode
#
"\M-[D":        backward-char
"\M-[C":        forward-char
"\M-[A":        previous-history
"\M-[B":        next-history
#
# Arrow keys in 8 bit keypad mode
#
#"\M-\C-OD":       backward-char
#"\M-\C-OC":       forward-char
#"\M-\C-OA":       previous-history
#"\M-\C-OB":       next-history
#
# Arrow keys in 8 bit ANSI mode
#
#"\M-\C-[D":       backward-char
#"\M-\C-[C":       forward-char
#"\M-\C-[A":       previous-history
#"\M-\C-[B":       next-history

C-q: quoted-insert

$endif

# An old-style binding.  This happens to be the default.
TAB: complete

# Macros that are convenient for shell interaction
$if Bash
# edit the path
"\C-xp": "PATH=${PATH}\e\C-e\C-a\ef\C-f"
# prepare to type a quoted word --
# insert open and close double quotes
# and move to just after the open quote
"\C-x\"": "\"\"\C-b"
# insert a backslash (testing backslash escapes
# in sequences and macros)
"\C-x\\": "\\"
# Quote the current or previous word
"\C-xq": "\eb\"\ef\""
# Add a binding to refresh the line, which is unbound
"\C-xr": redraw-current-line
# Edit variable on current line.
"\M-\C-v": "\C-a\C-k$\C-y\M-\C-e\C-a\C-y="
$endif

# use a visible bell if one is available
set bell-style visible

# don't strip characters to 7 bits when reading
set input-meta on

# allow iso-latin1 characters to be inserted rather
# than converted to prefix-meta sequences
set convert-meta off

# display characters with the eighth bit set directly
# rather than as meta-prefixed characters
set output-meta on

# if there are 150 or more possible completions for a word,
# ask whether or not the user wants to see all of them
set completion-query-items 150

# For FTP
$if Ftp
"\C-xg": "get \M-?"
"\C-xt": "put \M-?"
"\M-.": yank-last-arg
$endif

(示例中的英文注释与代码保持原样;下面用译者注逐段解释。)

译者注:示例第一行用 $include /etc/Inputrc 引入系统级配置。多数 Linux 发行版提供的系统文件是小写/etc/inputrc——如果你的文件系统区分大小写,这里的 Inputrc 会加载失败(Readline 会静默忽略读取失败)。另外注意 Meta-Control-h 这一行:函数名之后、行尾之前的文字都会被忽略,所以 backward-kill-word 后面的说明文字只是给人看的注释,可以放心保留。

译者注:方向键的绑定依赖终端发送的转义序列。示例中 "\M-[D" 表示 ESC[D 三个字符(\M-[\e[,最后的 D 是左方向键;C 右、A 上、B 下)——这是 ANSI 模式下方向键发送的序列。被注释掉的 "\M-OD" 等是终端处于"键盘应用程序模式"(keypad mode)时发送的序列(xterm 等终端会发送 ESC O D 等),需要时可取消注释试用。不同终端发送的序列可能不同,可以用 cat -v 查看,或在 Bash 里按 C-v 后再按方向键,直接看到它产生的字符。

译者注:几个 Bash 专用宏逐键拆解如下:

  • "\C-xp"(编辑 PATH):先插入字面文本 PATH=${PATH},随后 \e\C-e 构成 M-C-e(绑定到 shell-expand-line,把行中的 ${PATH} 展开为当前 PATH 的实际值),然后 C-a 回行首、M-f 前移一个单词、C-f 再前移一个字符,把光标停在 PATH= 之后便于继续修改;
  • "\C-x\""(即 C-x "):宏体是 ""C-b——先插入一对双引号,再把光标退回一格停在两个引号之间,方便直接输入一个带引号的单词;
  • "\C-x\\":插入一个字面反斜杠(转义规则见 8.3.1 节);
  • "\C-xr":把 C-x r 绑定到 redraw-current-line(重画当前行),当终端输出把命令行弄乱时很有用。

译者注"\M-\C-v" 是"编辑当前行的变量"的宏,效果很好演示:假设你在命令行只键入了 PATH 并停在行尾,按 M-C-v 后,Readline 依次执行 C-a(行首)、C-k(剪切到行尾,PATH 进入删除环)、插入 $C-y(粘贴出 $PATH)、M-C-e(shell-expand-line,把 $PATH 展开为当前值)、C-a(行首)、C-y(再次粘贴 PATH)、插入 =,最终得到形如 PATH=/usr/local/bin:... 的赋值行,可直接在此基础上修改。输入任意变量名后按 M-C-v 即可快速"取回"它的当前值。

译者注:文件末尾是给 FTP 程序用的绑定:"\C-xg" 的宏体会输入 get 并触发 M-?(绑定到 possible-completions,列出补全候选),"\C-xt" 同理用于 put 命令;"\M-." 绑定到 yank-last-arg——把上一条命令的最后一个参数粘贴到当前命令行(这是 Emacs 模式下的默认绑定,Bash 中按 M-. 即可直接使用,例如上一条命令是 make install,按 M-. 会输入 install)。

8.4 可绑定的 Readline 命令

本节介绍可以绑定到按键序列的 Readline 命令。你可以执行 bind -P 列出当前的按键绑定;若要更简洁、适合直接放入 INPUTRC 文件的格式,可以执行 bind -p(参见第 4.2 节(Bash 内建命令))。未附带按键序列的命令名在默认情况下未绑定。

译者注:本节采用 Emacs 风格的按键记法:C-x 表示 Ctrl 键加 x,M-x 表示 Meta 键加 x(通常是 Alt 键;没有 Meta 键的终端上可以用 <ESC> x 代替),M-C-x 表示 Meta、Ctrl 与 x 的组合。形如 C-x C-r 的记法表示「先按 C-x,再按 C-r」的组合按键序列。<TAB><ESC><DEL>、Rubout(即退格键)等直接指具体按键。括号内写「未绑定」的命令默认没有对应按键,需要自己在 INPUTRC 文件或 bind 命令中绑定。

译者注bind -P 的输出每行一个命令:已绑定的显示其按键序列,未绑定的显示「is not bound to any keys」。例如:

# 译者注示例:查看某个命令是否已绑定
$ bind -P | grep menu-complete
menu-complete is not bound to any keys

bind -p 的输出则采用可以直接写进 INPUTRC 文件的格式(每条一行、带引号)。两者配合使用,可以快速查出当前哪些命令尚未绑定。

在下面的描述中,「point(光标点)」指当前光标位置,「mark(标记)」指由 set-mark 命令保存的光标位置。point 与 mark 之间的文本称为「region(区域)」。Readline 有「活动区域(active region)」的概念:当区域处于活动状态时,Readline 在重绘显示时用 active-region-start-color 变量的值高亮该区域。enable-active-region 变量控制这一特性的开关。有若干命令会把区域设为活动状态;这些命令在下面都有注明。

译者注:标记(mark)与光标点(point)之间的文本称为区域(region)。典型用法是:按 C-@set-mark)记下当前位置,移动光标后再按 C-x C-xexchange-point-and-mark)在两个位置之间往返。区域处于活动状态时,Readline 会用 active-region-start-color 变量的值高亮显示区域内的文本,该变量的默认值是 \e[7m(反色显示);不想要这个高亮,可以在 INPUTRC 中写入 set enable-active-region off 关闭。

8.4.1 移动命令

beginning-of-line(C-a)

移动到当前行的行首。在某些键盘上,此命令也可能绑定到 Home 键。

end-of-line(C-e)

移动到当前行的行尾。在某些键盘上,此命令也可能绑定到 End 键。

forward-char(C-f)

向前移动一个字符。在某些键盘上,此命令也可能绑定到右方向键。

backward-char(C-b)

向后移动一个字符。在某些键盘上,此命令也可能绑定到左方向键。

forward-word(M-f)

向前移动到下一个单词的末尾。单词由字母和数字组成。

backward-word(M-b)

向后移动到当前或前一个单词的开头。单词由字母和数字组成。

shell-forward-word(M-C-f)

向前移动到下一个单词的末尾。单词以未加引号的 Shell 元字符为分界。

shell-backward-word(M-C-b)

向后移动到当前或前一个单词的开头。单词以未加引号的 Shell 元字符为分界。

译者注forward-word 类命令把「单词」定义为连续的字母和数字,遇到标点或空白即停止;而 shell-forward-word 类命令按 Shell 语法切分单词——以未加引号的元字符(如 ;|&、空格)为界,并跳过引号内的一切字符。例如在 echo 'a b' 这一行上,从行首连按两次 M-f,光标会停在引号内部的 a 之后;而连按两次 M-C-f 则把 'a b' 整体视为一个单词,光标停在结尾引号之后。

previous-screen-line(未绑定)

尝试把光标点移动到上一物理屏幕行的同一物理屏幕列。如果当前 Readline 行没有占据超过一行的物理屏幕,或者光标点位置没有超过「提示符长度加屏幕宽度」,此命令不会产生预期效果。

next-screen-line(未绑定)

尝试把光标点移动到下一物理屏幕行的同一物理屏幕列。如果当前 Readline 行没有占据超过一行的物理屏幕,或者当前 Readline 行的长度没有超过「提示符长度加屏幕宽度」,此命令不会产生预期效果。

clear-display(M-C-l)

清除屏幕,并在可能的情况下清除终端的回滚缓冲区,然后重绘当前行,使当前行位于屏幕顶部。

clear-screen(C-l)

清除屏幕,然后重绘当前行,使当前行位于屏幕顶部。如果给出数值参数,则只刷新当前行而不清除屏幕。

redraw-current-line(未绑定)

刷新当前行。默认情况下未绑定。

8.4.2 操作历史记录的命令

accept-line(换行或回车)

无论光标在何处,都接受当前行。如果该行非空,则按照 HISTCONTROLHISTIGNORE 变量的设置把它加入历史列表。如果该行是经过修改的历史行,则把该历史行恢复为原始状态。

译者注:被接受的命令是否真正进入历史列表,由 HISTCONTROLHISTIGNORE 两个变量决定:前者用关键字控制(例如 ignoreboth 表示既忽略空白开头的行、也忽略与上一条重复的命令),后者用模式忽略匹配的命令行。两者的细节参见第 5.2 节(Bash 变量)。

previous-history(C-p)

在历史列表中「向后」移动,取回上一条命令。在某些键盘上,此命令也可能绑定到上方向键。

next-history(C-n)

在历史列表中「向前」移动,取回下一条命令。在某些键盘上,此命令也可能绑定到下方向键。

beginning-of-history(M-<)

移动到历史中的第一行。

end-of-history(M->)

移动到输入历史的末尾,即当前正在输入的那一行。

reverse-search-history(C-r)

从当前行开始向后搜索,必要时在历史中「向上」移动。这是增量式搜索。此命令会把区域设置为匹配到的文本,并使区域处于活动状态。

forward-search-history(C-s)

从当前行开始向前搜索,必要时在历史中「向下」移动。这是增量式搜索。此命令会把区域设置为匹配到的文本,并使区域处于活动状态。

non-incremental-reverse-search-history(M-p)

使用非增量式搜索,从当前行开始向后搜索,必要时在历史中「向上」移动,搜索用户提供的字符串。搜索字符串可以匹配历史行中的任意位置。

non-incremental-forward-search-history(M-n)

使用非增量式搜索,从当前行开始向前搜索,必要时在历史中「向下」移动,搜索用户提供的字符串。搜索字符串可以匹配历史行中的任意位置。

history-search-backward(未绑定)

在历史中向后搜索「当前行开头到光标点之间」的字符串。搜索字符串必须与历史行的开头匹配。这是非增量式搜索。默认情况下此命令未绑定,但在某些键盘上可能绑定到 Page Down 键。

history-search-forward(未绑定)

在历史中向前搜索「当前行开头到光标点之间」的字符串。搜索字符串必须与历史行的开头匹配。这是非增量式搜索。默认情况下此命令未绑定,但在某些键盘上可能绑定到 Page Up 键。

译者注history-search-backwardhistory-search-forward 实现的是「按已输入的前缀过滤历史」的翻页:先输入几个字符(如 git),再按绑定键,只会翻到以 git 开头的历史命令——这与 C-p/C-n 的整条历史翻页不同。想要获得 zsh 用户习惯的体验,可在 INPUTRC 中把方向键绑上去:

# 译者注示例:INPUTRC 中的绑定("\e[A" 是上方向键,"\e[B" 是下方向键)
"\e[A": history-search-backward
"\e[B": history-search-forward

history-substring-search-backward(未绑定)

在历史中向后搜索「当前行开头到光标点之间」的字符串。搜索字符串可以匹配历史行中的任意位置。这是非增量式搜索。默认情况下此命令未绑定。

history-substring-search-forward(未绑定)

在历史中向前搜索「当前行开头到光标点之间」的字符串。搜索字符串可以匹配历史行中的任意位置。这是非增量式搜索。默认情况下此命令未绑定。

yank-nth-arg(M-C-y)

在光标点处插入上一条命令的第一个参数(通常是上一行的第二个单词)。带参数 N 时,插入上一条命令的第 N 个单词(上一条命令的单词从第 0 个开始编号)。负数参数则从上一条命令的末尾倒数第 N 个单词。计算出参数 N 之后,此命令借助历史展开机制提取第 N 个单词,如同指定了 !N 历史展开一样。

yank-last-arg(M-. 或 M-_)

插入上一条命令的最后一个参数(上一条历史记录的最后一个单词)。带数值参数时,行为与 yank-nth-arg 完全相同。连续调用 yank-last-arg 会在历史列表中逐步向前移动,依次插入每一行的最后一个单词(或第一次调用时由参数指定的那个单词)。连续调用时给出的任何数值参数决定在历史中的移动方向;负数参数则切换方向(向前或向后)。此命令借助历史展开机制提取最后一个单词,如同指定了 !$ 历史展开一样。

译者注M-. 是「取上一条命令最后一个参数」的快捷键。例如上一条命令是 echo foo bar 时,输入 echo 后按 M-.,行尾立即出现 bar,变成 echo bar。连按 M-. 会依次取更早命令的最后一个参数;M-_ 是它的另一个绑定键,习惯 Emacs 用法的用户常用这个。

operate-and-get-next(C-o)

接受当前行并把该行返回给调用程序,就像键入了换行符一样,然后从历史中取出相对当前行的下一条记录供编辑。如果提供了数值参数,则使用该参数指定的历史记录,而不是当前行。

fetch-history(未绑定)

带数值参数时,从历史列表中取出该条记录并使其成为当前行。不带参数时,移动到历史列表中的第一条记录。

8.4.3 修改文本的命令

end-of-file(通常为 C-d)

表示文件结束的字符,例如由 stty 设置的 EOF 字符。如果在该行没有任何字符、且光标点位于行首时读到这个字符,Readline 会把它解释为输入结束并返回 EOF。

delete-char(C-d)

删除光标点处的字符。如果此函数与 tty 的 EOF 字符绑定在同一个字符上(C-d 通常就是这样),其效果参见上文。在某些键盘上,此命令也可能绑定到 Delete 键。

backward-delete-char(Rubout)

删除光标前的字符。给出数值参数时,这些字符是被「kill(删除)」而不是被普通删除:被删除的文本会保存在删除环(kill ring)中。

forward-backward-delete-char(未绑定)

删除光标下的字符;但如果光标在行尾,则删除光标前的字符。默认情况下此命令未绑定到任何键。

quoted-insert(C-q 或 C-v)

把接下来键入的字符原样添加到行中。例如要插入 C-q 之类的按键序列,就靠这个命令。

译者注C-q/C-v 之后键入的字符按字面插入,常用于输入控制字符:按 C-v 再按 会插入一个真正的制表符(而不是触发补全),按 C-v 再按 C-m 会插入回车符(屏幕上显示为 ^M)。这在构造包含控制字符的文本(例如 printf 的参数字符串)时很有用。

self-insert(a、b、A、1、!……)

插入所键入的字符。

bracketed-paste-begin(默认已绑定)

此函数用于绑定到某些终端发出的「括号粘贴(bracketed paste)」转义序列,并且默认就分配了这样的绑定。它让 Readline 把粘贴进来的文本作为一个整体插入,而不是把每个字符当作从键盘读入的按键来处理。这些字符按 self-insert 的方式插入,不会执行任何编辑命令。

括号粘贴会把区域(光标点与标记之间的字符)设置为被插入的文本,并把区域设为活动状态。

译者注:所谓括号粘贴,是指支持该特性的终端在粘贴文本时,把内容包裹在 ESC [ 200 ~ESC [ 201 ~ 之间发送给应用程序(Readline 默认把 "\e[200~" 绑定到 bracketed-paste-begin)。这样 Readline 就能把整段粘贴内容当作普通字符一次插入;若终端不支持括号粘贴,粘贴文本中的换行会被逐个当作回车处理,多行粘贴的内容就会逐行立即执行——这正是往交互式 Shell 里粘贴多行命令时的经典陷阱。

transpose-chars(C-t)

把光标前的字符向前拖过光标处的字符,光标也随之向前移动。如果插入点在行尾,则交换该行最后两个字符。负数参数不起作用。

transpose-words(M-t)

把光标点前的单词拖过光标点后的单词,光标点也随之越过那个单词。如果插入点在行尾,则交换该行最后两个单词。

shell-transpose-words(M-C-t)

把光标点前的单词拖过光标点后的单词,光标点也随之越过那个单词。如果插入点在行尾,则交换该行最后两个单词。单词边界与 shell-forward-wordshell-backward-word 相同。

upcase-word(M-u)

把当前(或后面的)单词改为大写。带负数参数时,把前一个单词改为大写,但不移动光标。

downcase-word(M-l)

把当前(或后面的)单词改为小写。带负数参数时,把前一个单词改为小写,但不移动光标。

capitalize-word(M-c)

把当前(或后面的)单词的首字母改为大写。带负数参数时,把前一个单词的首字母改为大写,但不移动光标。

overwrite-mode(未绑定)

切换覆盖模式。给出明确的正数参数时切换到覆盖模式;给出明确的非正数参数时切换到插入模式。此命令只影响 emacs 模式;vi 模式的覆盖行为不同。每次调用 readline() 都从插入模式开始。

在覆盖模式下,绑定到 self-insert 的字符会替换光标点处的文本,而不是把文本向右推。绑定到 backward-delete-char 的字符则用空格替换光标点前的字符。

默认情况下此命令未绑定,但在某些键盘上可能绑定到 Insert 键。

8.4.4 删除(Killing)与粘贴(Yanking)

kill-line(C-k)

删除从光标点到当前行行尾的文本。带负数参数时,从光标处向后删除到行首。

backward-kill-line(C-x Rubout)

从光标处向后删除到当前行行首的文本。带负数参数时,从光标处向前删除到行尾。

unix-line-discard(C-u)

从光标处向后删除到当前行行首的文本。

kill-whole-line(未绑定)

无论光标点在何处,删除当前行的所有字符。默认情况下未绑定。

kill-word(M-d)

删除从光标点到当前单词末尾的文本;如果光标点位于两个单词之间,则删除到下一个单词的末尾。单词边界与 forward-word 相同。

backward-kill-word(M-

删除光标点后面的单词。单词边界与 backward-word 相同。

shell-kill-word(M-C-d)

删除从光标点到当前单词末尾的文本;如果光标点位于两个单词之间,则删除到下一个单词的末尾。单词边界与 shell-forward-word 相同。

shell-backward-kill-word(未绑定)

删除光标点后面的单词。单词边界与 shell-backward-word 相同。

unix-word-rubout(C-w)

删除光标点后面的单词,以空白字符作为单词边界,并把删除的文本保存在删除环上。

unix-filename-rubout(未绑定)

删除光标点后面的单词,以空白字符和斜杠字符作为单词边界,并把删除的文本保存在删除环上。

delete-horizontal-space(未绑定)

删除光标点周围所有的空格和制表符。默认情况下未绑定。

kill-region(未绑定)

删除当前区域中的文本。默认情况下此命令未绑定。

copy-region-as-kill(未绑定)

把区域中的文本复制到删除缓冲区,以便随后立即粘贴。默认情况下此命令未绑定。

copy-backward-word(未绑定)

把光标点前的单词复制到删除缓冲区。单词边界与 backward-word 相同。默认情况下此命令未绑定。

copy-forward-word(未绑定)

把光标点后的单词复制到删除缓冲区。单词边界与 forward-word 相同。默认情况下此命令未绑定。

yank(C-y)

把删除环顶端的文本粘贴到光标点处的缓冲区中。

yank-pop(M-y)

旋转删除环,并粘贴新的顶端条目。只有当上一条命令是 yankyank-pop 时才能这样做。

译者注:删除环(kill ring)是一个环形缓冲区,保存最近删除的文本片段。C-y 粘贴顶端的内容;紧随其后按 M-y 会旋转删除环,换贴更早删除的内容。例如在一行上输入 foo bar,按 C-w 删除 bar,再输入 xyz 并按 C-w 删除 xyz,此时按 C-y 得到 xyz(环顶),接着按 M-y 得到 bar——即两次删除的内容可以循环取回。注意 M-y 只有在刚执行过 yankyank-pop 时才有效。

8.4.5 指定数值参数

digit-argument(M-0、M-1……M--)

把这个数字添加到正在累积的参数中,或开始一个新的参数。M-- 开始一个负数参数。

universal-argument(未绑定)

这是指定参数的另一种方式。如果此命令后面跟着一个或多个数字(可选地以减号开头),这些数字就定义了参数。如果命令后面跟的是数字,那么再次执行 universal-argument 会结束数值参数,除此之外会被忽略。作为一种特殊情况,如果此命令后面紧跟一个既不是数字也不是减号的字符,则下一条命令的参数计数乘以四。参数计数初始为 1,因此第一次执行此函数使参数计数变为 4,第二次变为 16,依此类推。默认情况下此命令未绑定到任何键。

译者注:数值参数就是给命令的「重复次数」或「距离」。例如 M-3 C-f 让光标前进 3 个字符,M-2 M-d 删除后面 2 个单词,而 M-- 之后接命令则产生负数参数(如 M-- C-k 从光标处向前删除到行首,效果与 C-u 相同)。在 vi 模式下,先输入数字再按移动键也是同一套机制。

8.4.6 让 Readline 替你输入

complete

尝试对光标点之前的文本进行补全。实际执行的补全方式取决于应用程序。Bash 的补全过程是:首先检查命令单词是否有任何可编程补全(参见第 8.6 节(可编程补全));否则依次把文本当作变量(文本以 $ 开头时)、用户名(文本以 ~ 开头时)、主机名(文本以 @ 开头时)或命令(包括别名、函数和内建命令)来处理。如果这些都不能产生匹配,就回退到文件名补全。

译者注:Tab 补全的完整流程大致如下:按一次 调用 complete,Readline 尽量把光标前的文本补全为唯一的公共前缀;若仍有多个候选,再按一次 相当于执行 possible-completionsM-?),把全部候选列在下面。Bash 判断补全类型的顺序是:先看命令单词有没有可编程补全(compspec);没有的话依次尝试变量(以 $ 开头,如 $HO<TAB> 补全为 $HOME)、用户名(以 ~ 开头,如 ~ro<TAB> 补全为 ~root)、主机名(以 @ 开头)、命令名(别名、函数、内建命令),最后回退到文件名补全。如果某个命令行位置已经绑定了 compspec,则整条默认顺序不再生效,见第 8.6 节(可编程补全)。

possible-completions(M-?)

列出光标点之前文本的所有可能补全。显示补全时,Readline 依次使用 completion-display-width 变量的值、环境变量 COLUMNS 的值或屏幕宽度作为显示列数。

译者注:显示候选列表时,Readline 优先使用 INPUTRC 中 completion-display-width 的设置(set completion-display-width 200),其次取 COLUMNS 环境变量的值,最后才用终端实际宽度——这个优先级对在窄终端里做补全的脚本很有用。

insert-completions(M-*)

插入 possible-completions 本会生成的所有补全,彼此用空格分隔。

menu-complete(未绑定)

complete 类似,但把要补全的单词替换为补全列表中的单个匹配项。重复执行 menu-complete 会在补全列表中循环移动,依次插入每个匹配项。到达补全列表末尾时,menu-complete 会响铃(取决于 bell-style 的设置)并恢复原始文本。参数 N 表示在匹配列表中向前移动 N 个位置;负数参数则在列表中向后移动。此命令本来打算绑定到 键,但默认情况下未绑定。

译者注:把 绑定到 menu-complete 会得到「循环翻页」式的补全体验:按一次 插入第一个候选,再按换成下一个,循环回到原文本。在 INPUTRC 中加一行 TAB: menu-complete 即可;与之配套,常把 menu-complete-backward 绑定到 Shift-Tab(多数终端发送 "\e[Z"),实现反向翻页。

menu-complete-backward(未绑定)

menu-complete 完全相同,但在补全列表中向后移动,就像给 menu-complete 传了负数参数一样。此命令默认未绑定。

export-completions(未绑定)

对光标点之前的单词执行上文所述的补全,并把补全列表按下面的格式写入 Readline 的输出流,各项信息各占一行:

  • 匹配数 N;
  • 正在补全的单词;
  • S:E,其中 S 和 E 分别是该单词在 Readline 行缓冲区中的起始与结束偏移量;
  • 随后是每个匹配项,每行一个。

如果没有匹配项,第一行将是 0,并且此命令在 S:E 之后不输出任何内容。如果只有一个匹配项,则打印包含它的单独一行。如果匹配项多于一个,则在 S:E 之后的第一个行打印这些匹配项的公共前缀(可能为空),然后在后续行打印各个匹配项。此时 N 会把包含公共前缀的那一行也计算在内。

用户或应用程序应当能接受出现空行的可能。其设计意图是:用户或应用程序在含有 S:E 的那一行之后读取 N 行,即可得到匹配列表。此命令默认未绑定。

delete-char-or-list(未绑定)

如果光标不在行首或行尾,则删除光标下的字符(如同 delete-char)。在行尾时,其行为与 possible-completions 完全一致。此命令默认未绑定。

complete-filename(M-/)

对光标点之前的文本尝试文件名补全。

possible-filename-completions(C-x /)

把光标点之前的文本当作文件名,列出其所有可能的补全。

complete-username(M-~)

把光标点之前的文本当作用户名,尝试补全。

possible-username-completions(C-x ~)

把光标点之前的文本当作用户名,列出其所有可能的补全。

complete-variable(M-$)

把光标点之前的文本当作 Shell 变量,尝试补全。

possible-variable-completions(C-x $)

把光标点之前的文本当作 Shell 变量,列出其所有可能的补全。

complete-hostname(M-@)

把光标点之前的文本当作主机名,尝试补全。

possible-hostname-completions(C-x @)

把光标点之前的文本当作主机名,列出其所有可能的补全。

complete-command(M-!)

把光标点之前的文本当作命令名,尝试补全。命令补全会依次尝试把文本与别名、保留字、Shell 函数、Shell 内建命令以及可执行文件名进行匹配。

possible-command-completions(C-x !)

把光标点之前的文本当作命令名,列出其所有可能的补全。

dynamic-complete-history(M-

对光标点之前的文本尝试补全,把文本与历史列表中的条目进行比较以寻找可能的匹配。

dabbrev-expand(未绑定)

对光标点之前的文本尝试菜单式补全,把文本与历史列表中的各行进行比较以寻找可能的匹配。

译者注:这两条命令都从历史里「借」候选:M-<TAB>(即 dynamic-complete-history)直接列出历史中匹配的单词,dabbrev-expand 则逐个循环替换——其行为类似 Emacs 编辑器的 dabbrev-expand(M-/)。对于重复出现的长参数(路径、文件名等),从历史补全往往比翻页更快。

complete-into-braces(M-{)

执行文件名补全,并把所有可能的补全用花括号括起来插入,这样该列表就可以交给 Shell 处理(参见第 3.5.1 节(花括号展开))。

译者注:在目录里有 a1.txta2.txtb.txt 三个文件时,输入 a 后按 M-{,行上会出现 a{1.txt,2.txt}——这是一个合法的花括号展开表达式,可以继续拼接到其他命令里(如 rm a{1.txt,2.txt})。在目录条目很多、只想生成「一批带通配的路径」时很实用。

8.4.7 键盘宏

start-kbd-macro(C-x ()

开始把键入的字符保存到当前的键盘宏中。

end-kbd-macro(C-x ))

停止把键入的字符保存到当前的键盘宏中,并保存该宏的定义。

call-last-kbd-macro(C-x e)

重新执行最后定义的键盘宏,让宏中的字符如同在键盘上键入的一样出现。

print-last-kbd-macro(未绑定)

以适合 INPUTRC 文件的格式打印最后定义的键盘宏。

译者注:键盘宏最常见的用途是录制重复输入的固定前缀。以把 ls -l 录制成一个可随时调用的「按键」为例,两种方式都可行:

# 译者注示例:交互式录制宏
# 按 C-x ( 开始录制,输入 ls -l ,按 C-x ) 结束录制,
# 之后随时按 C-x e 即可"键入"整段 ls -l 

或者在 INPUTRC 里直接写宏绑定(双引号内的内容就是宏的文本):

# 译者注示例:INPUTRC 中的宏绑定
"\C-xl": "ls -l "

注意宏的内容是按「键入」处理而不是立即执行的:C-x e 之后还需要按回车,命令才会真正运行。print-last-kbd-macro(默认未绑定)可以把刚录制的宏以 INPUTRC 可用的格式打印出来,方便固化到配置文件。

8.4.8 其他命令

re-read-init-file(C-x C-r)

读入 INPUTRC 文件的内容,并把其中找到的任何绑定或变量赋值合并进来。

译者注:修改 INPUTRC 文件后不必重启 Shell,按 C-x C-r 即可生效;这也等价于执行 bind -f ~/.inputrc

abort(C-g)

中止当前的编辑命令并让终端响铃(取决于 bell-style 的设置)。

do-lowercase-version(M-A、M-B、M-X……)

如果带 Meta 前缀的字符 X 是大写字母,则执行绑定到相应小写字符(同样带 Meta 前缀)的命令。如果 X 本身就是小写,行为未定义。

prefix-meta

把接下来键入的字符加上 Meta 前缀。键入 <ESC> f 等效于键入 M-f

undo(C-_ 或 C-x C-u)

增量式撤销,对每一行分别记忆。

revert-line(M-r)

撤销对本行所做的全部修改。这就像多次执行 undo 命令直到回到初始状态。

tilde-expand(M-&)

对当前单词执行波浪号展开。

set-mark(C-@)

把标记设置到光标点处。如果提供数值参数,则把标记设置到该位置。

exchange-point-and-mark(C-x C-x)

交换光标点与标记。把当前光标位置设为保存的位置,然后把标记设为原来的光标位置。

character-search(C-])

读取一个字符,并把光标点移动到该字符下一次出现的位置。负数参数则搜索上一次出现的位置。

character-search-backward(M-C-])

读取一个字符,并把光标点移动到该字符上一次出现的位置。负数参数则搜索后续出现的位置。

skip-csi-sequence(未绑定)

读取足够多的字符以吞掉一个多键序列,例如为 Home、End 之类的键定义的序列。CSI 序列以控制序列引导符(Control Sequence Indicator,CSI)开头,通常是 ESC [。如果把这个序列绑定到 "\e[",那么产生 CSI 序列的键除非被显式绑定到某个 Readline 命令,否则将不起作用,也就不会向编辑缓冲区中插入杂散字符。此命令默认未绑定,但通常绑定到 ESC [

insert-comment(M-#)

不带数值参数时,在当前行的开头插入 comment-begin 变量的值。如果给出数值参数,此命令起到切换(toggle)作用:如果行首的字符与 comment-begin 的值不匹配,就插入该值;否则从行首删除 comment-begin 中的字符。无论哪种情况,该行都会被接受,如同键入了换行符一样。comment-begin 的默认值使得此命令把当前行变成一条 Shell 注释。如果数值参数导致注释符被删除,则该行会被 Shell 执行。

译者注M-#(Alt 加井号)是「把当前命令变成注释并送入历史」的快捷键:输入一条暂时不想执行的命令后按 M-#,该行被加上 # 前缀并直接接受——命令不会执行,但会留在历史里,下次翻出来删掉 # 就能再跑。若带数值参数(如 M-1 M-#)则会反其道而行:去掉行首的注释符并执行该行。comment-begin 的默认值是 #,可在 INPUTRC 中修改。

dump-functions(未绑定)

把所有函数及其按键绑定打印到 Readline 的输出流。如果提供数值参数,输出格式将适合作为 INPUTRC 文件的一部分。此命令默认未绑定。

dump-variables(未绑定)

把所有可设置的变量及其值打印到 Readline 的输出流。如果提供数值参数,输出格式将适合作为 INPUTRC 文件的一部分。此命令默认未绑定。

dump-macros(未绑定)

把所有绑定到宏的 Readline 按键序列及其输出的字符串打印到 Readline 的输出流。如果提供数值参数,输出格式将适合作为 INPUTRC 文件的一部分。此命令默认未绑定。

execute-named-command(M-x)

从输入中读取一个可绑定的 Readline 命令名,并执行绑定到它的那个函数,就像它所绑定的按键序列出现在输入中一样。如果此函数带有数值参数,它会把该参数传递给要执行的函数。

spell-correct-word(C-x s)

对当前单词进行拼写纠正,把它当作目录名或文件名来处理,方式与 cdspell Shell 选项相同。单词边界与 shell-forward-word 使用的相同。

glob-complete-word(M-g)

把光标点之前的单词当作路径名展开的模式,并隐式地在后面追加一个星号,然后用该模式生成匹配的文件名列表作为可能的补全。

glob-expand-word(C-x *)

把光标点之前的单词当作路径名展开的模式,并插入匹配的文件名列表以替换该单词。如果提供数值参数,则在路径名展开之前追加一个 *

glob-list-expansions(C-x g)

显示 glob-expand-word 本会生成的展开列表,并重新显示该行。如果提供数值参数,则在路径名展开之前追加一个 *

译者注M-gC-x * 的区别在于是否落地:M-gglob-complete-word)只把匹配结果当作补全候选(可继续按 选择),C-x *glob-expand-word)则直接把通配符展开成具体的文件名列表替换到命令行上。例如输入 ls *.t 后按 C-x *,会得到 ls a1.txt a2.txt b.txt(若目录里恰好只有这三个匹配文件)。

shell-expand-line(M-C-e)

对整个行执行 Shell 单词展开:包括别名展开与历史展开、$'STRING'$"STRING" 引号处理、波浪号展开、参数与变量展开、算术展开、命令替换与进程替换、单词拆分以及引号去除。给出明确参数时,命令替换与进程替换被抑制。

history-expand-line(M-^)

对当前行执行历史展开。

magic-space(未绑定)

对当前行执行历史展开并插入一个空格(参见第 9 章(交互式使用历史记录))。

alias-expand-line(未绑定)

对当前行执行别名展开(参见第 6.6 节(别名))。

history-and-alias-expand-line(未绑定)

对当前行执行历史展开和别名展开。

insert-last-argument(M-. 或 M-_)

yank-last-arg 的同义词。

edit-and-execute-command(C-x C-e)

对当前命令行调用编辑器,并把编辑结果作为 Shell 命令执行。Bash 依次尝试用 $VISUAL$EDITORemacs 作为编辑器。

译者注C-x C-e 是「把当前命令行丢进编辑器」的快捷键:先写好长命令的草稿(例如多行管道),按 C-x C-e 用编辑器打开,保存退出后整段内容作为 Shell 命令执行——与 fc 内建命令的默认行为类似。想换编辑器可在环境里设 export EDITOR=vim

display-shell-version(C-x C-v)

显示当前 Bash 实例的版本信息。

8.5 Readline 的 vi 模式

虽然 Readline 库没有一整套 vi 编辑函数,但它包含的功能足以进行简单的行编辑。Readline 的 vi 模式的行为与 POSIX 标准中对 sh 的描述一致。

你可以用 set -o emacsset -o vi 命令(参见第 4.3.1 节(set 内建命令))在 emacsvi 两种编辑模式之间交互切换。Readline 默认是 emacs 模式。

译者注:在提示符下执行 set -o vi 立即进入 vi 编辑模式,set -o emacs 切回 emacs 模式(也可在 INPUTRC 中写 set editing-mode vi 永久切换)。vi 模式命令模式下常用的按键:h/l 左右移动一个字符,w/b 按单词前后跳,0/$ 跳到行首/行尾,k/j 取上一条/下一条历史命令,/? 搜索历史(n/N 切换下一个/上一个匹配),x 删除光标处字符,X 删除光标前字符,D 删除到行尾,dw 删除一个单词,u 撤销,i/a/I/A 进入插入模式(分别对应插入、追加、行首插入、行尾追加)。按 回到命令模式。

# 译者注示例:当前 Shell 切换编辑模式
$ set -o vi     # 进入 vi 编辑模式(之后按 ESC 进入命令模式)
$ set -o emacs  # 回到 emacs 编辑模式

vi 模式下输入一行时,你已处于「插入(insertion)」模式,就像键入了 i 一样。按 切换到「命令(command)」模式,此时可以用标准的 vi 移动键编辑行文本,用 k 移动到上一条历史记录、用 j 移动到下一条,等等。

8.6 可编程补全

当用户尝试对某个命令或其参数进行单词补全,而该命令已通过 complete 内建命令定义了补全规范(completion specification,简称「compspec」)时(参见第 8.7 节(可编程补全内建命令)),Readline 会调用可编程补全机制。

首先,Bash 识别命令名。如果该命令定义了 compspec,就使用该 compspec 生成单词的可能补全列表。如果命令单词是空字符串(即在一行空行的开头尝试补全),Bash 使用任何以 complete-E 选项定义的 compspec。complete-I 选项表示命令单词是行中第一个非赋值单词,或者是在 ;| 之类的命令分隔符之后的单词。这通常表示命令名补全。

如果命令单词是完整路径名,Bash 先为该完整路径名查找 compspec。如果没有,则尝试为最后一个斜杠之后的部分查找 compspec。如果这些查找都没有找到 compspec,或者命令单词没有对应的 compspec,Bash 使用任何以 complete-D 选项定义的 compspec 作为默认。如果没有默认 compspec,Bash 最后会对命令单词执行别名展开,并尝试为展开后得到的命令单词查找 compspec。

译者注:compspec 的查找顺序可以总结为:① 完整路径名 → ② 去掉最后一个斜杠后的命令名 → ③ -D 选项定义的默认规范 → ④ 对命令单词做别名展开后再找 → ⑤ 都没有则回退到默认补全。另外,-E 规范只在空行开头补全时生效,-I 规范只在行首第一个非赋值单词(或 ;| 之后的位置)补全时生效。

如果没有找到 compspec,Bash 执行上文所述的默认补全(参见第 8.4.6 节(让 Readline 替你输入)中的补全命令)。否则,一旦找到 compspec,Bash 就使用它来生成匹配单词列表。

首先,Bash 执行 compspec 指定的动作(ACTIONS)。这一步只返回待补全单词的前缀匹配。当使用 -f-d 选项进行文件名或目录名补全时,Bash 使用 Shell 变量 FIGNORE 过滤匹配项。FIGNORE 的描述参见第 5.2 节(Bash 变量)。

接下来,可编程补全生成由 -G 选项参数提供的路径名展开模式所指定的匹配项。模式生成的单词不必与待补全单词匹配。Bash 用 FIGNORE 变量过滤这些匹配项,但不使用 GLOBIGNORE Shell 变量。

接下来,补全考虑 -W 选项参数指定的字符串。该字符串首先按特殊变量 IFS 中的字符作为分隔符进行拆分。拆分时尊重字符串内部的 Shell 引号,这样单词中可以包含 Shell 元字符或 IFS 值中的字符。随后每个单词按上文所述进行花括号展开、波浪号展开、参数与变量展开、命令替换和算术展开(参见第 3.5 节(Shell 展开))。展开结果再按上文所述规则进行拆分(参见第 3.5.7 节(单词拆分))。展开的结果与待补全单词进行前缀匹配,匹配上的单词成为可能的补全。

生成这些匹配项之后,Bash 执行任何由 -F-C 选项指定的 Shell 函数或命令。当该命令或函数被调用时,Bash 会按上文所述给 COMP_LINECOMP_POINTCOMP_KEYCOMP_TYPE 变量赋值(参见第 5.2 节(Bash 变量))。如果被调用的是 Shell 函数,Bash 还会设置 COMP_WORDSCOMP_CWORD 变量。当函数或命令被调用时,第一个参数($1)是要补全其参数的命令名,第二个参数($2)是正在补全的单词,第三个参数($3)是当前命令行中位于待补全单词之前的那个单词。生成的补全不会针对待补全单词做任何过滤;函数或命令在生成匹配项时有完全的自由,匹配项不必是待补全单词的前缀。

译者注:一个最小的 complete -F 示例:定义一个函数,往 COMPREPLY 数组里填候选,再把它挂到命令上。下面两版函数都在真实 Bash 5.3 中验证过:

# 译者注示例:为 mycmd 定义补全函数(最简单的一版)
_myfunc()
{
    COMPREPLY=(alpha beta gamma)
}
complete -F _myfunc mycmd

定义之后,输入 mycmd a 再按两次 ,会列出 alpha beta gamma——注意因为 -F 的产出不做前缀过滤,输入 a 并不会把候选缩成 alpha,这是本节的明文规定。想要按输入过滤,就在函数里用 compgen

# 译者注示例:用 compgen -W 让候选随输入过滤
_myfunc2()
{
    COMPREPLY=($(compgen -W "alpha beta gamma" -- "$2"))
}
complete -F _myfunc2 mycmd2

此时输入 mycmd2 a<TAB> 会直接补全为 alpha,输入 mycmd2 b<TAB> 补全为 beta

译者注:补全函数最常用的信息就是三个位置参数和两个数组变量:$1 是命令名,$2 是正在补全的单词,$3 是它前面的单词;COMP_WORDS 是当前命令行按单词拆分后的数组,COMP_CWORD 是当前单词的下标。例如输入 mycmd --opt foo<TAB> 时(正在补全 foo),函数里 $1=mycmd$2=foo$3=--opt,且 COMP_WORDS=(mycmd --opt foo)COMP_CWORD=2。要取更早的单词就按下标访问 COMP_WORDS

任何用 -F 指定的函数会先被调用。该函数可以使用任何 Shell 设施来生成匹配项,包括下文描述的 compgencompopt 内建命令(参见第 8.7 节(可编程补全内建命令))。它必须把可能的补全放入 COMPREPLY 数组变量中,每个数组元素一个。

接下来,任何用 -C 选项指定的命令会在一个与命令替换等价的环境中调用。它应该向标准输出打印补全列表,每行一个。必要时可用反斜杠转义换行符。这些补全会被加入可能的补全集合。

译者注-C-F 的差别在于:-C 执行的是外部命令而不是 Shell 函数,其标准输出按「每行一个补全」解析(输出同样不做前缀过滤)。一个真实陷阱是:执行该命令时,正在补全的命令名、单词等会被追加为命令的参数(与 -F$1$2 同理),所以直接写 date +%Y-%m-%d 会因为多出的操作数报错——实测会看到 date: extra operand 'dtest'。想忽略追加的参数,可以用不带转换符的 printf(无转换符时 printf 忽略多余操作数):

# 译者注示例:-C 选项使用外部命令的输出作为补全
$ complete -C 'printf 2026-08-06' dtest
$ dtest <TAB>       # 行上插入 2026-08-06

生成所有可能的补全之后,Bash 对列表中的补全应用 -X 选项指定的过滤器。过滤器是用于路径名展开的模式;模式中的 & 会被替换为待补全单词的文本。字面意义的 & 可以用反斜杠转义;在尝试匹配之前反斜杠会被去掉。任何匹配该模式的补全都会从列表中被移除。开头的 ! 对模式取反;在这种情况下,Bash 移除任何不匹配该模式的补全。如果启用了 nocasematch Shell 选项(参见第 4.3.2 节(shopt 内建命令)中对 shopt 的描述),Bash 匹配时忽略字母的大小写。

译者注-X 用于从补全结果中排除某些项。例如 complete -A file -X '*.bak' mycmd 会让 mycmd 的文件补全不包含以 .bak 结尾的文件;-X '!*.sh' 则相反,只保留以 .sh 结尾的项。模式中的 & 代表正在补全的单词文本本身:例如当前单词是 foo 时,-X '*.&' 展开为 *.foo,排除所有以 .foo 结尾的补全项。对应行为可以先用 compgen -W 直接验证:compgen -W 'a.o b.c' -X '*.o' -- '' 只输出 b.c

最后,可编程补全分别把 -P-S 选项指定的前缀和后缀添加到每个补全上,并把结果作为可能的补全列表返回给 Readline。

如果前面应用的动作没有生成任何匹配,且定义 compspec 时给 complete 提供了 -o dirnames 选项,Bash 会尝试目录名补全。

如果定义 compspec 时给 complete 提供了 -o plusdirs 选项,Bash 会尝试目录名补全,并把所有匹配项加入可能的补全集合。

默认情况下,只要找到了 compspec,它生成的内容就会作为完整的可能补全集合返回给补全代码。Bash 的默认补全和 Readline 默认的文件名补全都被禁用。如果定义 compspec 时给 complete 提供了 -o bashdefault 选项,且 compspec 没有生成任何匹配,Bash 会尝试它的默认补全。如果 compspec(以及被尝试过的 Bash 默认补全)没有生成任何匹配,且定义 compspec 时给 complete 提供了 -o default 选项,则可编程补全执行 Readline 的默认补全。

提供给 completecompopt 的选项可以控制 Readline 如何处理补全。例如,-o fullquote 选项告诉 Readline 像对待文件名一样为匹配项加引号。详见 complete 的描述(参见第 8.7 节(可编程补全内建命令))。

当 compspec 表示需要目录名补全时,可编程补全函数会强制 Readline 为指向目录的符号链接补全名追加斜杠——这取决于 MARK-DIRECTORIES Readline 变量的值,与 MARK-SYMLINKED-DIRECTORIES Readline 变量的设置无关。

译者注MARK-DIRECTORIES(INPUTRC 中的 set mark-directories on/off)控制补全目录名时是否自动追加斜杠,默认开启。这里的意思是:当 compspec 要求目录名补全时,指向目录的符号链接也统一按 mark-directories 处理——即使 mark-symlinked-directories(默认关闭)不允许给符号链接加斜杠,此时也会加。

可编程补全还支持动态修改补全。这在与 -D 指定的默认补全配合时最为有用。作为补全函数执行的 Shell 函数可以通过返回退出状态 124 来表示应当重试补全。如果 Shell 函数返回 124,并且改变了与正在尝试补全的命令相关联的 compspec(该命令名在函数执行时作为第一个参数传入),可编程补全会从头重新开始,尝试为该命令寻找新的 compspec。这可以用来在补全进行时动态构建一组补全,而不是一次性全部加载。

译者注:退出状态 124 是「请重试补全」的约定信号:补全函数返回 124 后,Bash 会从头再来一遍查找 compspec 的流程。bash_completion 项目正是用这个机制按需加载每个命令的补全定义(见下文示例),而不是在启动时全部读入——这样补全库再多也不会拖慢 Shell 启动。

例如,假设存在一个 compspec 库,每个 compspec 存放在与命令名对应的文件中,下面的默认补全函数会动态加载补全:

_completion_loader()
{
    . "/etc/bash_completion.d/$1.sh" >/dev/null 2>&1 && return 124
}
complete -D -F _completion_loader -o bashdefault -o default

8.7 可编程补全内建命令

有三个内建命令可用于操作可编程补全机制:一个用于指定特定命令的参数如何补全,另外两个用于在补全过程中修改补全行为。

compgen

compgen [-V VARNAME] [OPTION] [WORD]

按照 OPTIONs 为 WORD 生成可能的补全匹配,并把匹配写入标准输出。OPTION 可以是 complete 内建命令接受的任何选项,但 -p-r-D-E-I 除外。

如果提供了 -V 选项,compgen 把生成的补全存入索引数组变量 VARNAME,而不是写入标准输出。

使用 -F-C 选项时,可编程补全机制设置的各个 Shell 变量虽然可用,但不会有有用的值。

匹配的生成方式与可编程补全代码直接用带相同标志的补全规范生成时完全相同。如果指定了 WORD,只显示或存储匹配 WORD 的补全。

除非提供了无效选项,或没有生成任何匹配,否则返回值为真。

译者注compgen 是补全函数的「瑞士军刀」——它把 complete 的各类生成动作单独拿出来,便于在 -F 函数里组合使用。几个常用的例子(均在 Bash 5.3 中验证):

# 译者注示例:compgen 的常用调用
$ compgen -W 'alpha beta gamma' -- a
alpha
$ compgen -A builtin b
bg
bind
break
builtin
$ compgen -W 'a.o b.c' -X '*.o' -- ''    # -X 过滤同样适用于 compgen
b.c

需要把结果存进数组而不是打印时,用 -V VARNAME(本手册对应的 Bash 5.3 支持该选项)。

complete

complete [-abcdefgjksuv] [-o COMP-OPTION] [-DEI] [-A ACTION]
        [-G GLOBPAT] [-W WORDLIST] [-F FUNCTION] [-C COMMAND]
        [-X FILTERPAT] [-P PREFIX] [-S SUFFIX] NAME [NAME ...]
complete -pr [-DEI] [NAME ...]

指定如何补全每个 NAME 的参数。

如果提供了 -p 选项,或者没有提供任何选项或 NAME,则打印现有的补全规范,打印格式可直接复用为输入。-r 选项删除每个 NAME 的补全规范;如果没有提供 NAME,则删除所有补全规范。

译者注complete -pcomplete -r 分别用于「查看」和「清除」补全规范。例如安装了第 8.8 节的示例之后:

# 译者注示例:查看与删除补全规范
$ complete -p cd
complete -o bashdefault -o filenames -o nospace -F _comp_cd cd
$ complete -r cd        # 删除 cd 的补全规范
$ complete -p cd        # 之后会报错:cd 没有补全规范
bash: complete: cd: no completion specification

-D 选项表示其他提供的选项和动作应用于「默认」命令补全,即对之前没有定义过补全的命令尝试补全的情况。-E 选项表示其他提供的选项和动作应用于「空」命令补全,即在空行上尝试补全的情况。-I 选项表示其他提供的选项和动作应用于行中第一个非赋值单词,或 ;| 之类的命令分隔符之后的单词的补全,这通常是命令名补全。如果提供了多个选项,-D 优先于 -E,两者又都优先于 -I。如果提供了 -D-E-I 中的任何一个,其他 NAME 参数都会被忽略;这些补全只应用于该选项指定的情况。

尝试单词补全时应用这些补全规范的过程见上文(参见第 8.6 节(可编程补全))。

其他选项(如果指定)的含义如下。-G-W-X 选项的参数(必要时还有 -P-S 选项的参数)应当加引号,以免在调用 complete 内建命令之前就被展开。

-o COMP-OPTION:COMP-OPTION 控制 compspec 行为中超出简单生成补全之外的多个方面。COMP-OPTION 可以是下列之一:

  • bashdefault:如果 compspec 没有生成任何匹配,则执行其余的 Bash 默认补全。
  • default:如果 compspec 没有生成任何匹配,则使用 Readline 的默认文件名补全。
  • dirnames:如果 compspec 没有生成任何匹配,则执行目录名补全。
  • filenames:告诉 Readline compspec 生成的是文件名,以便它执行任何针对文件名的处理(例如给目录名添加斜杠、给特殊字符加引号,或抑制末尾空格)。此选项旨在与 -F 指定的 Shell 函数配合使用。
  • fullquote:告诉 Readline 即使补全的单词不是文件名,也要全部加引号。
  • noquote:告诉 Readline 如果补全的单词是文件名,则不要加引号(给文件名加引号是默认行为)。
  • nosort:告诉 Readline 不要按字母顺序对可能的补全列表排序。
  • nospace:告诉 Readline 不要(像默认那样)给在行尾补全的单词追加空格。
  • plusdirs:生成 compspec 定义的所有匹配之后,尝试目录名补全,并把所有匹配添加到其他动作的结果中。

-A ACTION:ACTION 可以是下列之一,用于生成可能的补全列表:

  • alias:别名名称。也可以写为 -a
  • arrayvar:数组变量名。
  • binding:Readline 按键绑定名(参见第 8.4 节(可绑定的 Readline 命令))。
  • builtin:Shell 内建命令名。也可以写为 -b
  • command:命令名。也可以写为 -c
  • directory:目录名。也可以写为 -d
  • disabled:被禁用的 Shell 内建命令名。
  • enabled:已启用的 Shell 内建命令名。
  • export:已导出的 Shell 变量名。也可以写为 -e
  • file:文件名和目录名,类似于 Readline 的文件名补全。也可以写为 -f
  • function:Shell 函数名。
  • group:组名。也可以写为 -g
  • helptopichelp 内建命令接受的主题(参见第 4.2 节(Bash 内建命令))。
  • hostname:主机名,取自 HOSTFILE Shell 变量指定的文件(参见第 5.2 节(Bash 变量))。
  • job:作业名(如果启用了作业控制)。也可以写为 -j
  • keyword:Shell 保留字。也可以写为 -k
  • running:正在运行的作业名(如果启用了作业控制)。
  • service:服务名。也可以写为 -s
  • setoptset 内建命令 -o 选项的有效参数(参见第 4.3.1 节(set 内建命令))。
  • shoptshopt 内建命令接受的 Shell 选项名(参见第 4.2 节(Bash 内建命令))。
  • signal:信号名。
  • stopped:已停止的作业名(如果启用了作业控制)。
  • user:用户名。也可以写为 -u
  • variable:所有 Shell 变量名。也可以写为 -v

译者注-A 的每个取值都有对应的单字母快捷写法(见上文的「也可以写为」)。例如 complete -A command mycmdmycmd 的第一个参数按命令名补全;-a-b-c 等单字母写法在命令行上更简短,但含义相同。想试验某个取值的效果,直接用 compgencompgen -A function 会列出当前所有 Shell 函数名。

-C COMMAND:COMMAND 在子 Shell 环境中执行,其输出用作可能的补全。参数传递方式与 -F 选项相同。

-F FUNCTION:Shell 函数 FUNCTION 在当前 Shell 环境中执行。执行时,第一个参数($1)是要补全其参数的命令名,第二个参数($2)是正在补全的单词,第三个参数($3)是位于待补全单词之前的单词,如第 8.6 节(可编程补全)所述。函数结束后,可编程补全从 COMPREPLY 数组变量的值中取出可能的补全。

-G GLOBPAT:展开文件名展开模式 GLOBPAT 以生成可能的补全。

-P PREFIX:在所有其他选项都已应用之后,把 PREFIX 添加到每个可能的补全的开头。

-S SUFFIX:在所有其他选项都已应用之后,把 SUFFIX 追加到每个可能的补全的末尾。

-W WORDLIST:按特殊变量 IFS 中的字符作为分隔符拆分 WORDLIST,并展开得到的每个单词。WORDLIST 内部尊重 Shell 引号,这样单词中可以包含 Shell 元字符或 IFS 值中的字符。可能的补全就是结果列表中与待补全单词前缀匹配的那些成员。

译者注-W 是最常用的选项之一:直接给出候选单词列表,Bash 按 IFS 拆分(默认是空格、制表符、换行),对每个单词做展开,再按输入做前缀过滤。例如:

# 译者注示例:-W 词表补全
$ complete -W 'start stop restart status' mysvc

之后输入 mysvc st 按两次 会列出 startstop 两个候选。词表也可以是命令替换动态生成的:complete -W "$(grep -E '^[a-z]+' /etc/services | cut -d' ' -f1)" mysvc。词表里若需要包含空格或 IFS 中的字符,可用引号包住相应单词(如 -W "a b 'c d'")。

-X FILTERPAT:FILTERPAT 是用于文件名展开的模式。它被应用于前面选项和参数生成的补全列表,任何匹配 FILTERPAT 的补全都会从列表中被移除。FILTERPAT 开头的 ! 对模式取反;在这种情况下,任何不匹配 FILTERPAT 的补全都会被移除。

除非提供了无效选项、提供了 -p-r-D-E-I 之外的选项却没有 NAME 参数、试图删除某个并不存在规范的 NAME 的补全规范,或添加补全规范时出错,否则返回值为真。

compopt

compopt [-o OPTION] [-DEI] [+o OPTION] [NAME]

根据 OPTIONs 修改每个 NAME 的补全选项;如果没有提供 NAME,则修改当前正在执行的补全的选项。如果没有给出 OPTION,则显示每个 NAME 或当前补全的补全选项。OPTION 的可能取值与上文描述的 complete 内建命令的有效取值相同。

-D 选项表示其他提供的选项应用于「默认」命令补全;-E 选项表示其他提供的选项应用于「空」命令补全;-I 选项表示其他提供的选项应用于行中第一个单词的补全。这些情况与 complete 内建命令的判定方式相同。

如果提供了多个选项,-D 优先于 -E,两者又都优先于 -I

除非提供了无效选项、试图修改某个不存在补全规范的 NAME 的选项,或发生输出错误,否则返回值为真。

译者注compopt 的典型用法是在补全函数内部「现场」调整选项:-o 打开某个选项,+o 关闭某个选项。例如下面的函数让补全不再自动追加空格(与 complete 定义时的 -o nospace 等效,但可在函数运行中动态决定):

# 译者注示例:在补全函数内动态调整选项
_myfunc()
{
    COMPREPLY=($(compgen -W 'alpha beta gamma' -- "$2"))
    compopt -o nospace
}
complete -F _myfunc mycmd

注意:不带 NAME 调用 compopt 只能在补全函数执行期间使用,在提示符下直接运行会报错「compopt: not currently executing completion function」。带 NAME 时则可以在命令行直接修改该命令的 compspec 选项(如 compopt +o nospace mycmd)。

8.8 可编程补全示例

要在 completecompgen 提供的默认动作之外获得额外的补全功能,最常见的方式是编写一个 Shell 函数,并用 complete -F 把它绑定到某个特定命令上。

下面的函数为 cd 内建命令提供补全。对「用于补全的 Shell 函数需要做哪些事」来说,这是一个相当不错的示例。该函数使用作为 $2 传入的单词来确定要补全的目录名。你也可以使用 COMP_WORDS 数组变量;当前单词由 COMP_CWORD 变量索引。

该函数依赖 completecompgen 内建命令完成大部分工作,只额外处理 Bash cd 在基本目录名之外支持的那些特性:波浪号展开(参见第 3.5.2 节(波浪号展开))、在 $CDPATH 中搜索目录(参见第 4.1 节(Bourne Shell 内建命令)),以及对 cdable_vars Shell 选项的基本支持(参见第 4.3.2 节(shopt 内建命令))。_comp_cd 会修改 IFS 的值,使它只包含一个换行符,以容纳含空格和制表符的文件名——因为 compgen 每行打印一个它生成的补全。

可能的补全放入 COMPREPLY 数组变量,每个数组元素一个补全。函数返回时,可编程补全系统从中取出补全。

下面先给出完整脚本(代码与注释保留英文原文):

# A completion function for the cd builtin
# based on the cd completion function from the bash_completion package
_comp_cd()
{
    local IFS=$' \t\n'    # normalize IFS
    local cur _skipdot _cdpath
    local i j k

    # Tilde expansion, which also expands tilde to full pathname
    case "$2" in
    \~*)    eval cur="$2" ;;
    *)      cur=$2 ;;
    esac

    # no cdpath or absolute pathname -- straight directory completion
    if [[ -z "${CDPATH:-}" ]] || [[ "$cur" == @(./*|../*|/*) ]]; then
        # compgen prints paths one per line; could also use while loop
        IFS=$'\n'
        COMPREPLY=( $(compgen -d -- "$cur") )
        IFS=$' \t\n'
    # CDPATH+directories in the current directory if not in CDPATH
    else
        IFS=$'\n'
        _skipdot=false
        # preprocess CDPATH to convert null directory names to .
        _cdpath=${CDPATH/#:/.:}
        _cdpath=${_cdpath//::/:.:}
        _cdpath=${_cdpath/%:/:.}
        for i in ${_cdpath//:/$'\n'}; do
            if [[ $i -ef . ]]; then _skipdot=true; fi
            k="${#COMPREPLY[@]}"
            for j in $( compgen -d -- "$i/$cur" ); do
                COMPREPLY[k++]=${j#$i/}        # cut off directory
            done
        done
        $_skipdot || COMPREPLY+=( $(compgen -d -- "$cur") )
        IFS=$' \t\n'
    fi

    # variable names if appropriate shell option set and no completions
    if shopt -q cdable_vars && [[ ${#COMPREPLY[@]} -eq 0 ]]; then
        COMPREPLY=( $(compgen -v -- "$cur") )
    fi

    return 0
}

这个函数的工作过程可以按步骤拆解如下:

  1. local IFS=$' \t\n' 先把 IFS 显式归一化为「空格、制表符、换行」,避免调用者环境的 IFS 设置影响函数内的命令替换与单词拆分(详见下文译者注);
  2. case "$2" 分支处理波浪号:若 $2~ 开头,用 eval cur="$2" 让 Shell 完成波浪号展开(~ 展开为家目录的完整路径,~user 展开为对应用户的家目录);否则原样使用;
  3. $CDPATH 为空,或 $cur./..// 开头(绝对路径或明确指定当前目录),直接在当前目录下用 compgen -d -- "$cur" 补全目录名;
  4. 否则沿 CDPATH 搜索:先把 CDPATH 预处理(把空字段替换成 .,见下文译者注),再逐个目录执行 compgen -d -- "$i/$cur",并通过 ${j#$i/} 去掉 $i/ 前缀,让补全显示为相对路径;[[ $i -ef . ]]-ef(判断是否同一文件)检测该 CDPATH 目录是否就是当前目录,若是则置 _skipdot=true,最后一行 $_skipdot || COMPREPLY+=( $(compgen -d -- "$cur") ) 保证当前目录中的子目录不会被重复加入;
  5. 兜底逻辑:若启用了 cdable_vars 且仍然没有任何匹配,退化为补全变量名(compgen -v)——这样 cd MYVAR 之类的用法也能得到补全(cdable_vars 开启时,cd 会把非目录参数当作「值为目录的变量名」处理)。

译者注[[ "$cur" == @(./*|../*|/*) ]] 里的 @(...) 是扩展模式匹配(extglob)语法。在 [[ ]] 条件表达式中,== 右侧的模式始终按「已启用 extglob」处理,因此脚本里无需先执行 shopt -s extglob。这一点可以直接验证:bash -c '[[ x = @(x|y) ]] && echo yes' 在 extglob 关闭时也输出 yes

译者注:CDPATH 与 PATH 一样用冒号分隔目录,并允许空字段表示当前目录。脚本的三行预处理把空字段统一替换成 .${CDPATH/#:/.:} 处理开头的空字段(CDPATH 以 : 开头时在开头补 .),${_cdpath//::/:.:} 处理中间的空字段,${_cdpath/%:/:.} 处理结尾的空字段。这样每个候选目录都会被 compgen 实际检查,行为与 cd 命令本身对空字段的解释完全一致。

译者注local IFS=$' \t\n' 是补全函数的惯用开头:显式设定 IFS,避免继承调用环境的怪异 IFS 影响函数内命令替换的结果拆分。随后在需要按「compgen 每行一个输出」处理时,临时把 IFS 改为只含换行符(IFS=$'\n'),处理完立即恢复成 $' \t\n'。若省略这一步,文件名里的空格会被 IFS 当成分隔符,$(compgen -d ...) 会把一个含空格的文件名拆成两个数组元素。

译者注:补全函数也可以在交互式 Shell 里手动调用以验证其行为(函数内 localCOMPREPLY 赋值都会照常执行):

# 译者注示例:手动调用 _comp_cd 观察 COMPREPLY
$ mkdir -p src            # 假设当前目录没有其他以 sr 开头的子目录
$ _comp_cd cd "sr"
$ printf '%s\n' "${COMPREPLY[@]}"
src

我们用 complete-F 选项安装这个补全函数:

# Tell readline to quote appropriate and append slashes to directories;
# use the bash default completion for other arguments
complete -o filenames -o nospace -o bashdefault -F _comp_cd cd

由于我们希望 Bash 和 Readline 帮我们处理其他一些细节,所以用另外几个选项告诉 Bash 和 Readline 该怎么做。-o filenames 选项告诉 Readline 把可能的补全当作文件名处理,并适当地加引号。该选项还会让 Readline 给它能确定为目录的文件名追加斜杠(这正是我们可能想扩展 _comp_cd、让它对通过 CDPATH 找到的目录也追加斜杠的原因:Readline 无法判断那些补全是不是目录)。-o nospace 选项告诉 Readline 不要给目录名追加空格字符,以防我们还想继续输入。-o bashdefault 选项引入其余的「Bash 默认」补全——即 Bash 在默认 Readline 补全集合之上添加的那些补全,包括命令名补全、对以 $${ 开头的单词的变量补全、包含路径名展开模式的补全(参见第 3.5.8 节(文件名展开))等等。

译者注:安装行各选项的配合逻辑是:-F _comp_cd 安装补全函数;-o filenames 让 Readline 按文件名处理结果(目录加斜杠、特殊字符加引号);-o nospace 不在行尾补全后自动加空格,方便继续输入目录名(例如 cd src 补全成 cd src/ 后继续敲子目录名);-o bashdefault 则保证当 _comp_cd 一无所获时,回退到 Bash 默认补全而不是什么都不做。安装后可用 complete -p cd 查看生成的规范。

一旦用 complete 安装好,_comp_cd 就会在我们每次对 cd 命令尝试单词补全时被调用。

还有更多示例——为大多数常见 GNU、Unix 和 Linux 命令提供的、内容丰富的补全集合——作为 bash_completion 项目的一部分提供。许多 GNU/Linux 发行版默认安装了它。该项目最初由 Ian Macdonald 编写,现在托管在 https://github.com/scop/bash-completion/。它还有其他系统的移植版本,如 Solaris 和 Mac OS X。

一个较旧版本的 bash_completion 包随 bash 一起发布,位于 examples/complete 子目录中。

9 交互式使用历史记录

本章从用户的角度描述如何交互式地使用 GNU 历史库。它应当被视为一份用户指南。关于在其他程序中使用 GNU 历史库的信息,参见《GNU Readline 库手册》。

9.1 Bash 的历史记录设施

set 内建命令的 -o history 选项被启用时(参见第 4.3.1 节(set 内建命令)),Shell 便提供对“命令历史”(command history)的访问——即先前输入过的命令列表。HISTSIZE 这个 Shell 变量的值用于决定在历史列表中保存多少条命令:Shell 保存最近 $HISTSIZE 条命令的文本(默认 500 条)。Shell 在参数展开和变量展开之前、但在历史展开完成之后,把每条命令存入历史列表,具体还受 Shell 变量 HISTIGNOREHISTCONTROL 取值的影响。

译者注:存入历史列表的是历史展开之后、参数展开与变量展开之前的文本:!$!! 之类会先展开再入历史(因此重放出来的命令不会再被展开),而 $HOME 之类的变量名会原样保存。HISTSIZEHISTFILESIZE 是两个不同的上限:前者限制当前会话历史列表(内存中)保存的命令条数,后者限制历史文件(磁盘上)的行数,两者默认都是 500;把 HISTFILESIZE 设为空、非数值或负数时,历史文件不会被截断。

当 Shell 启动时,Bash 从 HISTFILE 变量所指名的文件(默认 ~/.bash_history)中读取历史记录来初始化历史列表。该文件称为“历史文件”。必要时,历史文件会被截断,使其包含的历史记录条数不超过 HISTFILESIZE 变量值所指定的数目。如果 HISTFILESIZE 未设置、设为空、设为非数值,或设为小于零的数值,历史文件不会被截断。

读取历史文件时,以历史注释字符开头、且其后紧跟一个数字的行,被解释为下一条历史记录的时间戳。是否显示这些时间戳,取决于 HISTTIMEFORMAT 变量的值(参见第 5.2 节(Bash 变量))。存在时间戳时,历史时间戳作为历史记录之间的分界,从而使多行记录成为可能。

当启用了历史的 Shell 退出时,Bash 把历史列表中最近的 $HISTSIZE 条记录复制到 $HISTFILE 所指名的文件中。如果设置了 histappend Shell 选项(参见第 4.2 节(Bash 内建命令)),Bash 把记录追加到历史文件末尾,否则覆盖历史文件。如果 HISTFILE 未设置或为空,或者历史文件不可写,历史不会被保存。保存历史之后,Bash 按上文所述把历史文件截断为不超过 $HISTFILESIZE 行。

译者注:默认(未设置 histappend)时,每次 Shell 退出都会用本次会话的历史覆盖历史文件,同时打开多个终端会话时后退出者会覆盖先退出者;shopt -s histappend 改为追加模式可缓解这一问题。另外,历史是在退出时才写入文件的:若会话被强制终止(如断电、杀掉进程),尚未写入的部分会丢失。HISTFILE 未设置、为空或文件不可写时,历史不会保存。

如果设置了 HISTTIMEFORMAT 变量,Shell 把与每条历史记录关联的时间戳信息写入历史文件,用历史注释字符标记,以便时间戳跨 Shell 会话保留。读取历史文件时,以历史注释字符开头、且其后紧跟一个数字的行,被解释为下一条历史记录的时间戳。如上所述,使用 HISTTIMEFORMAT 时,时间戳把多行历史记录分隔开。

译者注:历史注释字符是 histchars 变量的第三个字符,默认是 #。设置 HISTTIMEFORMAT 后,历史文件中每条记录前会多一行以 # 开头、紧跟数字时间戳的注释行(如 #1722921600),history 命令也会按该格式显示时间戳。格式与 strftime(3) 相同,例如:

$ HISTTIMEFORMAT='%F %T '; history 3
100  2026-08-06 10:24:11 echo hello
101  2026-08-06 10:24:15 ls -l
102  2026-08-06 10:24:20 cd src

fc 内建命令可以列出历史列表的一部分,或编辑并重新执行它。history 内建命令可以显示或修改历史列表,并操作历史文件。使用命令行编辑时,每种编辑模式都提供可访问历史列表的搜索命令(参见第 8.4.2 节(操作历史记录的命令))。

Shell 允许控制哪些命令被保存到历史列表。HISTCONTROLHISTIGNORE 变量用于只保存输入的命令中的一部分。如果启用了 cmdhist Shell 选项,Shell 会尽量把多行命令的每一行保存在同一条历史记录中,并在必要时添加分号以保持语法正确。lithist Shell 选项修改 cmdhist 的行为:保存命令时保留内嵌的换行符而不是分号。这些选项用 shopt 内建命令设置。shopt 的说明参见第 4.3.2 节(shopt 内建命令)。

译者注:默认情况下,多行命令(如 for 循环)的每一行会作为独立的记录保存,重放时会比较别扭。启用 cmdhist 后整条多行命令合并为一条记录,行间用 ; 连接;再启用 lithist 则保留换行符。例如:

$ shopt -s cmdhist lithist
$ for i in 1 2; do
> echo $i
> done
1
2
$ history 2
    2  for i in 1 2; do
echo $i
done

9.2 Bash 历史记录内建命令

Bash 提供两个操作历史列表和历史文件的内建命令。

fc

fc [-e ENAME] [-lnr] [FIRST] [LAST]
fc -s [PAT=REP] [COMMAND]

第一种形式从历史列表中选出从 FIRST 到 LAST 的命令范围,并显示它们,或编辑并重新执行它们。FIRST 和 LAST 都可以指定为字符串(用于定位以该字符串开头的最近一条命令),也可以指定为数字(历史列表中的索引,负数用作距当前命令编号的偏移量)。

列出时,FIRST 或 LAST 为 0 等价于 −1,而 −0 等价于当前命令(通常就是 fc 命令本身);在其他情况下,0 等价于 −1,−0 无效。

译者注fc 的 FIRST/LAST 可以是命令编号(fc -l 20 30)、负数偏移(fc -l -3 列出最近 3 条)、或字符串(fc -s grep 重新执行最近一条以 grep 开头的命令)。0 等价于 −1(即当前命令的前一条),-0 在列出时等价于当前命令本身。注意 fc -s 重新执行的命令会回显,并被记入历史列表。

如果未指定 LAST,则列出时它被设为当前命令,其他情况下被设为 FIRST。如果未指定 FIRST,则编辑时它被设为上一条命令,列出时被设为 −16。

如果给出 -l 选项,命令被列到标准输出。-n 选项在列出时抑制命令编号。-r 选项反转列出的顺序。

否则,fc 在包含这些命令的文件上调用 ENAME 所指定的编辑器。如果未提供 ENAME,fc 使用下列变量展开的值:${FCEDIT:-${EDITOR:-vi}}。这表示:如果 FCEDIT 变量已设置则使用它的值;否则,如果 EDITOR 变量已设置则使用 EDITOR 的值;若两者都未设置则使用 vi。编辑完成后,fc 读取编辑后的命令文件,回显并执行它们。

第二种形式中,fc 把所选命令中的每一处 PAT 替换为 REP 后,重新执行 COMMAND。COMMAND 的解释方式与上面的 FIRST 相同。

fc 命令配合使用的一个实用别名是 r='fc -s':输入 r cc 会执行以 cc 开头的最后一条命令,输入 r 会重新执行最后一条命令(参见第 6.6 节(别名))。

译者注alias r='fc -s' 是经典 Unix 用法:r 重新执行上一条命令;r cc 重新执行以 cc 开头的最近一条命令;r pat=rep 则先做替换再执行。注意 fc -sfc -s PAT=REP(不带 COMMAND)会回显将被执行的命令,确认无误后再执行。

如果使用第一种形式,返回值为零,除非遇到无效选项,或 FIRST 或 LAST 指定的历史行超出范围。当编辑并重新执行命令文件时,返回值为最后执行的那条命令的返回值;如果临时文件出错,则为失败。如果使用第二种形式,返回状态为重新执行的命令的返回状态;但如果 COMMAND 没有指定有效的历史记录,fc 返回非零状态。

history

history [N]
history -c
history -d OFFSET
history -d START-END
history [-anrw] [FILENAME]
history -ps ARG

不带选项时,显示带编号的历史列表。以 * 为前缀的记录已被修改。参数 N 只列出最后 N 条记录。如果 Shell 变量 HISTTIMEFORMAT 已设置且非空,它被用作 strftime(3) 的格式字符串,用来显示每条显示出来的历史记录所关联的时间戳。如果 history 使用了 HISTTIMEFORMAT,它不会在格式化后的时间戳与历史记录之间打印分隔空格。

选项(如果给出)含义如下:

选项 含义
-c 清空历史列表。可与其他选项组合,以替换历史列表。
-d OFFSET 删除位置 OFFSET 处的历史记录。如果 OFFSET 为正数,应按历史显示时的编号指定。如果 OFFSET 为负数,它被解释为相对于历史最后一个位置加 1 的偏移量,因此负索引从历史末尾往回数,索引 -1 指当前这条 history -d 命令。
-d START-END 删除位置 START 与 END 之间(含两端)范围内的历史记录。START 和 END 的正负值按上述方式解释。
-a 把“新的”历史行追加到历史文件。这些历史行是自当前 Bash 会话开始以来输入、但尚未追加到历史文件的行。
-n 读取历史文件中尚未读过的历史行,并把它们加入当前历史列表。这些行是自当前 Bash 会话开始以来被追加到历史文件的行。
-r 读取历史文件,并把其内容追加到历史列表。
-w 把当前历史列表写入历史文件,覆盖历史文件。
-p 对 ARGs 执行历史替换,并把结果显示到标准输出,不把结果存入历史列表。
-s 把 ARGs 作为单条记录添加到历史列表末尾。添加之前,历史列表中的最后一条命令会被移除。

译者注history -d 的负数偏移从历史末尾往回数:-1 指当前这条 history -d 命令本身(交互式 Shell 中该命令在被执行前已记入历史),因此 history -d -1 的效果是把自己的记录从历史中抹掉;history -d -3 删除倒数第 3 条。范围形式如 history -d 5-8 删除第 5 至第 8 条(含两端)。删除后其余记录的编号保持不变。

译者注-p 适合在真正执行前预览某个历史展开的结果(也常用于脚本中),-s 适合把命令“预存”进历史,随后用 !!!str 或 Readline 的反向搜索(Ctrl-R)调出,二者都不执行命令。例如:

$ echo /usr/bin/foo.sh
/usr/bin/foo.sh
$ history -p '!$:h'      # 只显示展开结果:上一条命令最后一个单词的头部
/usr/bin
$ history -s 'echo hello'      # 把命令存入历史,但不执行
$ !!                           # 现在 !! 展开为 echo hello
echo hello
hello

如果 -w-r-a-n 选项中的任意一个带有 FILENAME 参数,Bash 使用 FILENAME 作为历史文件;否则使用 HISTFILE 变量的值。如果 HISTFILE 未设置或为空,这些选项不起作用。

如果设置了 HISTTIMEFORMAT 变量,history 把每条历史记录关联的时间戳信息写入历史文件,如上所述用历史注释字符标记。读取历史文件时,以历史注释字符开头、且其后紧跟一个数字的行,被解释为下一条历史记录的时间戳。

返回值为 0,除非遇到无效选项、读写历史文件时出错、把无效的 OFFSET 或范围作为 -d 的参数提供,或作为 -p 参数提供的历史展开失败。

9.3 历史展开

Shell 提供一种与 csh 提供的历史展开类似的历史展开功能(适当场合下也称历史替换)。本节描述用于操作历史信息的语法。

历史展开在交互式 Shell 中默认启用,可以用 set 内建命令的 +H 选项关闭(参见第 4.3.1 节(set 内建命令))。非交互式 Shell 默认不进行历史展开,但可以用 set -H 启用。

译者注set +H 等价于 set +o histexpandset -H 等价于 set -o histexpand。关闭后 ! 不再触发展开,可以原样输入。注意 ! 作为保留字(逻辑非,如 if ! grep ...)与历史展开无关,set +H 不影响它。另外实测表明:非交互式 Shell 中仅仅 set -H 并不生效,还必须启用 set -o history(该选项在非交互式 Shell 中默认关闭),历史展开才会起作用。

历史展开把历史列表中的单词引入输入流,从而可以方便地重复命令、把上一条命令的参数插入当前输入行,或快速修正上一条命令中的错误。

历史展开在一整行读入后立即执行,先于 Shell 把该行拆分成单词,并且对每一行分别进行。Bash 会尽量把前一行仍然生效的引号状态告知历史展开函数。

历史展开分两步进行。第一步确定替换时应使用历史列表中的哪一条记录。第二步选择该记录的哪些部分纳入当前行。

从历史中选出的记录称为“事件”(event),被处理的记录部分称为“单词”(word)。有多种“修饰符”(modifier)可用于处理所选单词。记录按 Bash 读取输入时同样的方式拆分为单词,因此由引号括起来的多个单词被视为一个单词。“事件指示符”(event designator)选择事件,可选的“单词指示符”(word designator)从事件中选择单词,还有各种可选的“修饰符”可用于处理所选单词。

历史展开由历史展开字符的出现引入,该字符默认为 !。历史展开可以出现在输入的任何位置,但不能嵌套。

历史展开实现类似 Shell 的引号约定:反斜杠可用来取消下一个字符的特殊处理;单引号括起逐字的字符序列,可用于抑制历史展开;而双引号内的字符可能受历史展开影响——因为反斜杠可以转义历史展开字符,但单引号不能,因为单引号在双引号内不被特殊处理。

使用 Shell 时,只有 \' 可以用来转义历史展开字符;不过,如果历史展开字符在双引号字符串中紧挨着右双引号,它也被视为已加引号。

译者注:历史展开的转义规则与 Shell 引号规则不同,容易混淆,以下行为均已实测:

  • 反斜杠可转义 !echo \!$ 输出字面 !$
  • 单引号内的 ! 不展开:echo '!$' 输出 !$
  • 双引号内的 ! 通常仍会展开:echo "!$" 会取出上一条命令的最后一个单词。双引号内只能用反斜杠转义 !(如 echo "\!$",不展开,但反斜杠本身会保留在输出中);单引号在双引号内没有特殊含义,"!'!" 中的 ! 仍会展开;
  • 例外:! 若紧挨着双引号字符串的右引号,则视为已加引号:echo "hi!" 输出 hi!(否则 hi 之后紧跟 ! 会尝试展开)。
$ echo /usr/bin/foo.sh
/usr/bin/foo.sh
$ echo "!$"          # 双引号内仍展开
echo "/usr/bin/foo.sh"
/usr/bin/foo.sh
$ echo "hi!"         # 紧挨右双引号的 ! 不展开
hi!

有若干字符如果紧跟在历史展开字符之后,即使历史展开字符未被加引号,也会抑制历史展开:空格、制表符、换行、回车、= 以及其余的 Shell 元字符。

替换还有一种特殊的缩写形式:当 QUICK SUBSTITUTION 字符(上文在 histchars 下描述过)位于行首时,该缩写生效。它选择上一条历史列表记录(使用等价于 !! 的事件指示符),并在这条记录中做一次字符串替换。它将在下面描述(参见第 9.3.1 节(事件指示符))。这是唯一不以历史展开字符开头的历史展开。

译者注:快速替换字符是 histchars 变量的第二个字符,默认是 ^,而且必须是行的第一个字符(前面不能有空格)。它总是作用于上一条命令,等价于 !!:s^STRING1^STRING2^。例如:

$ echo /usr/bin/foo.sh
/usr/bin/foo.sh
$ ^foo.sh^foo.c
echo /usr/bin/foo.c
/usr/bin/foo.c

shopt 内建命令(参见第 4.3.2 节(shopt 内建命令))设置的几个 Shell 选项会修改历史展开的行为。如果启用了 histverify Shell 选项,并且正在使用 Readline,历史替换不会立即传给 Shell 解析器,而是把展开后的行重新载入 Readline 编辑缓冲区,供进一步修改。如果正在使用 Readline,并且启用了 histreedit Shell 选项,失败的历史展开会被重新载入 Readline 编辑缓冲区,以便改正。

译者注shopt -s histverify 后,输入 !$ 回车不会立即执行,而是把展开后的完整命令行放回 Readline 编辑缓冲区,检查或修改后再按回车才执行——适合在重放前先确认。shopt -s histreedit 则让展开失败(如 !nosuchcmd)时把原行放回缓冲区供修改,而不是直接报错放弃。这两个选项只在交互式、使用 Readline 时有效。

history 内建命令的 -p 选项在使用某个历史展开之前,先显示它将会做什么。history 内建命令的 -s 选项可用于把命令添加到历史列表末尾而不真正执行它们,以便以后可以重新调出。这与 Readline 配合使用时最为有用。

Shell 允许用 histchars 变量控制历史展开机制使用的各种字符,如上所述(参见第 5.2 节(Bash 变量))。写历史文件时,Shell 用历史注释字符标记历史时间戳。

9.3.1 事件指示符

事件指示符是对历史列表中某条记录的引用。事件指示符由单词中以历史展开字符开头、延伸到单词指示符(如果存在)或单词末尾的部分组成。除非引用是绝对的,事件相对于历史列表中的当前位置。

指示符 含义
! 开始一次历史替换,但当其后紧跟空格、制表符、行尾、= 或上文定义的其余 Shell 元字符时除外(参见第 2 章(定义))。
!N 引用历史列表中的第 N 条记录。
!-N 引用当前命令之前的第 N 条记录。
!! 引用上一条记录。是 !-1 的同义词。
!STRING 引用历史列表中位于当前位置之前、以 STRING 开头的最近一条命令。
!?STRING[?] 引用历史列表中位于当前位置之前、包含 STRING 的最近一条命令。如果 STRING 之后紧跟换行符,末尾的 ? 可以省略。如果省略 STRING,则使用最近一次搜索中的字符串;若没有先前的搜索字符串,这是一个错误。
^STRING1^STRING2^ 快速替换。重放上一条命令,把 STRING1 替换为 STRING2。等价于 !!:s^STRING1^STRING2^
!# 到目前为止输入的全部命令行。

译者注:事件指示符示例。为便于对照,假设历史中只有最初输入的三条命令(可设置 HISTCONTROL=ignorespace 并以空格开头输入展开行,使其不记入历史);交互式 Shell 会先回显展开后的行,以下输出均已实测:

$ echo /usr/bin/foo.sh
/usr/bin/foo.sh
$ echo a b c
a b c
$ echo !$          # 上一条命令的最后一个单词:c
echo c
c
$ echo !^          # 上一条命令的第一个参数:a
echo a
a
$ echo !-2         # 当前命令之前的第 2 条记录:echo /usr/bin/foo.sh
echo /usr/bin/foo.sh
/usr/bin/foo.sh
$ echo !echo       # 以 echo 开头的最近一条命令
echo a b c
a b c
$ echo !?usr       # 包含 usr 的最近一条命令
echo /usr/bin/foo.sh
/usr/bin/foo.sh

译者注!?STRING 的搜索字符串遇到 ? 才结束。当 !?STRING 后面还要跟单词指示符或修饰符时,必须写全结尾的 ?(如 !?one?:%),否则搜索字符串会把后面的内容也吞进去:!?one:% 实际搜索的是 one:%,会报 event not found。此外,省略 STRING 的 !? 会复用最近一次搜索的字符串(失败或从未搜索过则报错)。

译者注!# 展开为到目前为止输入的全部命令行文本,适合在输入中途“自我引用”。例如:

$ echo hello !#
echo hello echo hello
hello echo hello

9.3.2 单词指示符

单词指示符用于从事件中选择所需的单词。它们是可选的:如果不提供单词指示符,历史展开使用整个事件。: 分隔事件说明与单词指示符;如果单词指示符以 ^$*-% 开头,: 可以省略。单词从行首编号,第一个单词记为 0(零)。第一个单词通常是命令单词,参数从第二个单词开始。单词插入当前行时以单个空格分隔。

例如:

  • !!:表示上一条命令。输入它时,上一条命令被整体重复。
  • !!:$:表示上一条命令的最后一个单词,可简写为 !$
  • !fi:2:表示以字母 fi 开头的最近一条命令的第二个参数。

下面是单词指示符:

指示符 含义
0(零) 第 0 个单词。对 Shell 及许多其他应用程序而言,这是命令单词。
N 第 N 个单词。
^ 第一个参数:单词 1。
` 指示符
--- ---
0(零) 第 0 个单词。对 Shell 及许多其他应用程序而言,这是命令单词。
N 第 N 个单词。
^ 第一个参数:单词 1。
最后一个单词。通常是最后一个参数,但如果该行只有一个单词,则展开为第 0 个单词。
% 最近一次 ?STRING? 搜索匹配到的第一个单词,前提是搜索字符串以单词字符开头。默认情况下,搜索从每行末尾开始向行首进行,因此第一个匹配的单词是离行尾最近的单词。
X-Y 单词范围;-Y0-Y 的缩写。
* 除第 0 个单词外的所有单词,是 1-$ 的同义词。如果事件中只有一个单词,使用 * 也不算错误——此时它展开为空字符串。
X* X-$ 的缩写。
X- X* 一样是 X-$ 的缩写,但省略最后一个单词。如果 X 缺省,默认为 0。

如果提供了单词指示符而没有指定事件,则用上一条命令作为事件,等价于 !!

译者注:单词编号从 0 开始。以 cp file1 file2 file3 为例(以下输出均已实测;!2 是绝对编号,指历史列表中的第 2 条记录):

$ touch file1 file2 file3
$ cp file1 file2 file3
$ echo !2:0      # 第 0 个单词:命令名
echo cp
cp
$ echo !2:1-2    # 第 1 到第 2 个单词
echo file1 file2
file1 file2
$ echo !2:^      # 第一个参数(= 单词 1)
echo file1
file1
$ echo !2:$      # 最后一个单词
echo file3
file3
$ echo !2:*      # 除第 0 个外的所有单词(= 1-$)
echo file1 file2 file3
file1 file2 file3
$ echo !2:1-     # 第 1 个到倒数第 2 个(X- 省略最后一个)
echo file1 file2
file1 file2
$ echo !2:-2     # 第 0 到第 2 个(-Y 即 0-Y)
echo cp file1 file2
cp file1 file2

译者注:展开时,事件中原本带引号的单词会连同引号一起插入。例如:

$ echo "hello world"
hello world
$ printf '<%s>\n' !$
printf '<%s>\n' "hello world"
<hello world>

如果引号没有保留,hello world 会被当作两个单词,printf 将输出两行。同理,!$ 插入到新的命令行后,还会经过一次常规的单词拆分。

译者注% 依赖最近一次 !?STRING? 搜索,且搜索从每行末尾向行首进行,因此匹配到的是离行尾最近的、包含搜索串的单词:

$ echo one one2
one one2
$ echo !?one?:%    # 搜索 one;从行尾往回,先匹配到 one2
one2

9.3.3 修饰符

在可选的单词指示符之后,可以添加一个或多个下列修饰符组成的序列,每个修饰符前面都要加 :。这些修饰符修改或编辑从历史事件中选出的单词。

修饰符 含义
h 去掉末尾的文件名成分,只保留头部(head)。
t 去掉所有前导的文件名成分,只保留尾部(tail)。
r 去掉形如 .SUFFIX 的末尾后缀,保留基名(basename)。
e 去掉除末尾后缀以外的所有内容。
p 打印新命令,但不执行它。
q 给替换出的单词加引号,转义进一步的替换。
x q 一样给替换出的单词加引号,但按空格、制表符和换行符把单词拆开。qx 修饰符互斥;展开时使用最后给出的那个。
s/OLD/NEW/ 把事件行中第一个出现的 OLD 替换为 NEW。任何字符都可以代替 / 用作分隔符。分隔符可以在 OLD 和 NEW 中用单个反斜杠引用。如果 NEW 中出现 &,它会被替换为 OLD。单个反斜杠可在 OLD 和 NEW 中转义 &。如果 OLD 为空,则把它设为上一次被替换的 OLD;如果此前没有进行过历史替换,则设为 !?STRING[?] 搜索中的最近一个 STRING。如果 NEW 为空,则删除每个匹配的 OLD。如果末尾的分隔符是输入行的最后一个字符,可以省略。
& 重复上一次替换。
ga 使修改应用于整个事件行。与 s 连用(如 gs/OLD/NEW/),或与 & 连用。
G 把后面的 s& 修饰符对事件中的每个单词各应用一次。

译者注:文件名修饰符示例(h/t/r/e,输出均已实测):

$ echo /usr/bin/foo.sh
/usr/bin/foo.sh
$ echo !1:$:h     # 只保留头部
echo /usr/bin
/usr/bin
$ echo !1:$:t     # 只保留尾部
echo foo.sh
foo.sh
$ echo !1:$:r     # 去掉 .SUFFIX 后缀
echo /usr/bin/foo
/usr/bin/foo
$ echo !1:$:e     # 只保留后缀
echo .sh
.sh

译者注s/OLD/NEW/ 中的 OLD 是字面字符串匹配,不是正则表达式。例如 . 在正则中匹配任意字符,但这里只匹配字面句点:对 echo "main main" 执行 !!:s/./X/ 会报 substitution failed。如果 NEW 中含 &,它代表 OLD 本身(可用 \& 转义);OLD 为空时复用上一次替换中的 OLD(若之前没有替换,则用最近一次 !?STRING 搜索的 STRING);NEW 为空则删除所有匹配的 OLD;/ 分隔符可用其他任意字符代替,如 !!:s|main|begin|

$ echo main
main
$ echo main2
main2
$ echo main main
main main
$ echo !!:s/main/begin   # 末尾分隔符是行尾时可省略
echo begin main
begin main
$ echo !1:s/main/&.c/    # NEW 中的 & 被替换为 OLD(main)
echo main.c
main.c

译者注g 作用于整个事件行(一行中所有出现都被替换),G 对事件中的每个单词各应用一次替换。只有同一单词中出现多次目标串时两者才有差别:echo "main main" 中引号把两个 main 括成一个单词,G 每个单词只替换一次,所以结果仍是 begin maing 则全部替换为 begin begin

$ echo "main main"
main main
$ echo !!:s/main/begin/   # 只替换第一次出现
echo "begin main"
begin main
$ echo !!:&               # 重复上一次替换(上一条命令已是展开后的 echo "begin main")
echo "begin begin"
begin begin
$ echo "main main"
main main
$ echo !!:gs/main/begin/  # g:一行内全部替换
echo "begin begin"
begin begin
$ echo "main main"
main main
$ echo !!:Gs/main/begin/  # G:每个单词各替换一次("main main" 是一个单词)
echo "begin main"
begin main

译者注q 给选出的单词加上引号,防止其被再次拆分或替换;x 则先把单词按空格、制表符、换行拆开,再分别加引号。两者互斥,以最后出现者为准。示例(输出均已实测):

$ echo "hello world"
hello world
$ printf '<%s>\n' !$:q    # 整个单词加引号,作为一个参数
printf '<%s>\n' '"hello world"'
<"hello world">
$ printf '<%s>\n' !$:x    # 按空格拆成两个词,分别加引号
printf '<%s>\n' '"hello' 'world"'
<"hello>
<world">
posted @ 2026-08-07 04:40  立体风  阅读(12)  评论(0)    收藏  举报