history 命令详解与参数实测(Ubuntu 22.04)

history 是 bash 内建的历史管理命令。本文在 Ubuntu 22.04(bash 5.1.16)环境逐项实测:无参列出与 history N 截取、-c 清除、-w/-a/-r/-n 与 HISTFILE 的读写时机、-d 删除(含负偏移)、-s 注入与 -p 展开预览;实测 !!、!n、!str、!?str?、!*、!$ 事件指示符与 :t/:h/:r/:e/:s(含 & 与 g)展开修饰符,以及 HISTCONTROL 四种过滤策略、HISTTIMEFORMAT 时间戳、HISTSIZE/HISTFILESIZE 上限、fc 关联命令与退出码行为,并给出敏感命令清理、拼写修正、多终端历史同步等实战场景。

Linux命令Linux系统UbuntuUbuntu 22.04命令行Shell2026/10/16

history 命令详解与参数实测(Ubuntu 22.04)

目标:全面掌握 bash 内建 history 的每一个常用参数——无参列出与 history N 截取、-c 清除、-w/-a/-r/-n 与历史文件的读写时机、-d 删除(含负偏移)、-s 注入与 -p 展开预览,以及 !!/!n/!-n/!str/!?str?/!*/!$ 事件指示符与 :t/:h/:r/:e/:s 展开修饰符、HISTCONTROL/HISTTIMEFORMAT/HISTSIZE 等配套变量、fc 关联命令——全部命令与回显已在 Ubuntu 22.04(GNU bash 5.1.16)实测通过。

一、环境信息

本节记录实测环境与演示数据。history 是 bash 的内建命令(type history 可验证),没有独立的可执行文件,因此版本信息按 bash 记录;它的历史机制由 HISTFILE、HISTSIZE 等一组 shell 变量控制,本篇将逐项实测。

  • 操作系统:Ubuntu 22.04.5 LTS(x86_64)
  • Shell 与 history 版本:GNU bash 5.1.16(1)-release(history 为 bash 内建命令,type history 实测)
  • 演示目录:/root/demo/history/
  • 演示文件:noninteractive.sh(非交互历史演示脚本)、histexpand.sh(非交互历史展开演示脚本)、.exit_hist/.w_hist/.a_hist/.fsize_hist 等各实测环节的历史文件
  • 登录用户:root(HOME=/root,默认历史文件 ~/.bash_history)
  • 实测方式说明:历史行为与"是否交互式会话"强相关,本篇统一采用两种实测载体——非交互脚本(bash 脚本文件)验证脚本场景,bash --norc -i 交互式会话(命令经标准输入喂入)验证交互场景;会话提示符显示为 $ 或 bash-5.1#

二、命令概览

2.1 语法与测试数据

history 的完整语法有三段形态:history [-c] [-d offset] [n](浏览与删除)、history -anrw [filename](与历史文件交互)、history -ps arg [arg...](注入与展开预览)。n 表示只显示最近 n 条;-d offset 删除指定条目,offset 为负数时从末尾倒数。

准备演示目录与两个非交互演示脚本:

mkdir -p /root/demo/history
cat > /root/demo/history/noninteractive.sh <<'EOF'
set -o history
echo ni-one
echo ni-two
history 4
EOF
cat > /root/demo/history/histexpand.sh <<'EOF'
set -o history -o histexpand
echo he-one
!!
echo he-done
EOF

验证目录与脚本就绪:

$ ls -ld /root/demo/history
drwxr-xr-x 2 root root 4096 Oct 15 21:25 /root/demo/history

$ cat /root/demo/history/noninteractive.sh
set -o history
echo ni-one
echo ni-two
history 4

$ cat /root/demo/history/histexpand.sh
set -o history -o histexpand
echo he-one
!!
echo he-done

两个脚本内容与预期一致,后续 3.2 节将直接执行它们观察非交互场景的历史行为。

2.2 help history 全量输出(内建命令帮助)

history 是内建命令,帮助用 help history 查看(而非 man),完整输出如下:

$ help history
history: history [-c] [-d offset] [n] or history -anrw [filename] or history -ps arg [arg...]
    Display or manipulate the history list.
    
    Display the history list with line numbers, prefixing each modified
    entry with a `*'.  An argument of N lists only the last N entries.
    
    Options:
      -c	clear the history list by deleting all of the entries
      -d offset	delete the history entry at position OFFSET. Negative
    		offsets count back from the end of the history list
    
      -a	append history lines from this session to the history file
      -n	read all history lines not already read from the history file
    		and append them to the history list
      -r	read the history file and append the contents to the history
    		list
      -w	write the current history to the history file
    
      -p	perform history expansion on each ARG and display the result
    		without storing it in the history list
      -s	append the ARGs to the history list as a single entry
    
    If FILENAME is given, it is used as the history file.  Otherwise,
    if HISTFILE has a value, that is used, else ~/.bash_history.
    
    If the HISTTIMEFORMAT variable is set and not null, its value is used
    as a format string for strftime(3) to print the time stamp associated
    with each displayed history entry.  No time stamps are printed otherwise.
    
    Exit Status:
    Returns success unless an invalid option is given or an error occurs.

帮助里可直接提炼四个要点:-d 支持负偏移(3.7 实测);FILENAME 直接作为历史文件名,否则依次回退到 HISTFILE 变量、~/.bash_history(3.19 实测 -w 指定文件名);HISTTIMEFORMAT 变量非空时显示 strftime 格式时间戳(3.17 实测);非法选项返回失败(3.20 实测退出码 2)。

2.3 版本确认

history 随 bash 发布,版本与 bash 一致:

$ type history
history is a shell builtin

$ type -a history
history is a shell builtin

$ type fc
fc is a shell builtin

$ echo $BASH_VERSION
5.1.16(1)-release

type -a history 只命中内建一条,说明系统没有同名外部命令;关联命令 fc 同样是内建(3.19 实测)。

三、参数逐项实测

3.1 非交互 shell 的默认历史行为(history 无参)

先明确一个最容易被忽略的前提:非交互 shell(脚本、cron、CI)默认不记录历史。在脚本进程内直接查看:

$ set -o | grep -E 'history|histexpand'
histexpand     	off
history        	off

$ history

$ history | wc -l
0

非交互会话中 history 与 histexpand 两个选项默认都是 off,history 无参输出为空。配套的历史变量此时也全部为空:

$ echo HISTSIZE=$HISTSIZE
HISTSIZE=

$ echo HISTFILESIZE=$HISTFILESIZE
HISTFILESIZE=

$ echo HISTFILE=$HISTFILE
HISTFILE=

$ echo HISTCONTROL=$HISTCONTROL
HISTCONTROL=

$ echo HISTCMD=$HISTCMD
HISTCMD=0

$ echo HISTTIMEFORMAT=$HISTTIMEFORMAT
HISTTIMEFORMAT=

HISTFILE 为空说明非交互会话连"退出写盘"都不会发生;HISTCMD=0 表示当前命令没有历史编号。

💡 说明:交互式 bash 启动时会从 HISTFILE(默认 ~/.bash_history)读入历史、退出时写回;非交互 bash 两者都不做。这就是"脚本里 history 是空的、退出后 .bash_history 也不变"的根因。

3.2 非交互脚本开启 set -o history

在脚本里 set -o history 之后,后续命令开始进入内存历史,history 就能列出它们。执行 2.1 准备的 noninteractive.sh:

$ bash /root/demo/history/noninteractive.sh
ni-one
ni-two
    1  echo ni-one
    2  echo ni-two
    3  history 4

开启 history 后脚本内的三条命令(含 history 4 自己)以 1、2、3 编号入史;set -o history 那一行本身因为执行时选项尚未生效,没有入史。但要注意:脚本退出时这些历史仍然不会写盘(非交互不写 HISTFILE),只存在于脚本进程内存中。

再看 histexpand.sh,验证非交互脚本里事件指示符 !! 是否可用:

$ bash /root/demo/history/histexpand.sh
he-one
echo he-one
he-one
he-done

结果是可用的:!! 被展开为上一条 echo he-one,bash 先回显展开后的行(echo he-one),再执行它输出 he-one。

💡 要点:非交互脚本的 !! 展开有两点与交互不同——需要显式 set -o histexpand(交互会话默认开启),且展开结果会先回显一行。若展开引用不到事件(如历史里没有匹配),非交互脚本会直接报错终止该行。

3.3 交互会话中 history 无参列出与 history N

交互会话是 history 的主战场。新起的交互会话编号从 1 开始:

$ echo demo-one
demo-one
$ echo demo-two
demo-two
$ history 3
    1  echo demo-one
    2  echo demo-two
    3  history 3
$ exit
exit

无参 history 列出全部历史;history 3 只列最近 3 条(含它自己),输出格式为右对齐编号加两个空格再接命令原文。本节会话使用独立 HISTFILE,因此编号从 1 起;正式使用时编号在会话内持续累加,且新会话启动时会把 HISTFILE 已有条目读进来接续编号。

3.4 -w 立即写盘与退出写盘时机

history -w 把当前内存中的全部历史写入 HISTFILE(覆盖文件当时的内容)。先看交互会话的退出写盘时机——正常使用时命令执行完并不落盘,退出时才写:

$ echo will-be-saved-one
will-be-saved-one
$ echo will-be-saved-two
will-be-saved-two
$ exit
exit

会话结束后文件立即出现:

$ ls -l /root/demo/history/.exit_hist
-rw------- 1 root root 51 Oct 15 21:31 /root/demo/history/.exit_hist

$ cat /root/demo/history/.exit_hist
echo will-be-saved-one
echo will-be-saved-two
exit

三条命令(含 exit 自己)在退出瞬间写入,文件权限 600(仅属主可读写,bash 对历史文件的默认权限保护)。

再实测 -w 的"立即"语义——会话中途写盘,之后新增的命令要等退出才补写:

$ echo w-line-one
w-line-one
$ echo w-line-two
w-line-two
$ history -w
$ echo after-w-line
after-w-line
$ exit
exit

查看落盘文件:

$ cat /root/demo/history/.w_hist
echo w-line-one
echo w-line-two
history -w
echo w-line-one
echo w-line-two
history -w
echo after-w-line
exit

文件共 8 行:前 3 行是 history -w 执行瞬间的全量快照(覆盖写);会话退出时 bash 把本会话执行过的条目追加到文件末尾(3 条与快照重复,加上 -w 之后新增的 echo after-w-line 与 exit),形成 3+5=8 行。追加行为可以用对照实验进一步确认:会话存活期间向它的 HISTFILE 外部追加一行,会话退出后该行原样保留,且位于会话条目之前——退出写盘不会抹掉别人的内容。

💡 要点:-w 是"覆盖写当前全量",退出写盘是"追加本会话条目",两者机制不同。会话存活期间想让其他会话读到新命令,用 3.8 的 -a(增量追加)更轻量。

3.5 -c 清除历史

history -c 清空内存中的全部历史条目(只影响内存,不动 HISTFILE 文件):

$ echo pre-clear-line
pre-clear-line
$ history -c
$ history 2
    1  history 2

-c 执行后列表为空,紧接着的 history 2 自身成为第 1 条——不足 N 条时全部显示。实测还能确认 -c 不影响磁盘文件:清除后执行 history -r 仍能把 HISTFILE 内容读回来,见下一节。

3.6 -r 读入历史文件

history -r FILE 把文件内容追加到当前内存历史之后。承接 3.5 的清除动作,把 3.4 落盘的 .w_hist 读回:

$ history -r /root/demo/history/.w_hist
$ history 6
    6  echo w-line-one
    7  echo w-line-two
    8  history -w
    9  echo after-w-line
   10  exit
   11  history 6

.w_hist 的 8 条内容全部追加进列表(编号 2~9 区域),history 6 的窗口恰好落在其中后 5 条加上它自己;编号接续递增而不是从 1 重排,体现"-r 是追加"的语义。

3.7 -d 删除指定条目(含负偏移)

history -d offset 删除指定编号的条目,最典型的用途是抹掉误输入的敏感信息:

$ echo keep-line-one
keep-line-one
$ echo secret-token-line
secret-token-line
$ echo keep-line-two
keep-line-two
$ history -d 2
$ history 3
    2  echo keep-line-two
    3  history -d 2
    4  history 3

-d 2 删除第 2 条后,后面的条目编号立即重排(原第 3 条变成第 2 条)。帮助里说明 offset 支持负数,实测 -d -1:

$ echo neg-one
neg-one
$ echo neg-two
neg-two
$ echo neg-three
neg-three
$ history -d -1
$ history 4
    1  echo neg-one
    2  echo neg-two
    3  echo neg-three
    4  history 4

-1 指向列表最后一条——而 -d 命令行自身在执行前已经入史,所以被删掉的是 -d 命令自己,三条 echo 完好无损。

💡 要点:想在脚本或会话里"删除刚执行的那条",用 history -d $((HISTCMD-1))(HISTCMD 是当前命令的编号,见 3.16);删除后编号会重排,连续删除多条应从大编号往小编号删,或每次删除前重新查看编号。

3.8 -a 追加写盘

history -a 把本会话自上次写盘之后新增的条目追加到 HISTFILE 末尾,是增量写、不会覆盖文件里已有的内容:

$ echo a-line-one
a-line-one
$ history -a
$ echo a-line-two
a-line-two
$ history -a
$ exit
exit

查看文件最终内容:

$ cat /root/demo/history/.a_hist
echo a-line-one
history -a
echo a-line-two
history -a
exit

第一次 -a 追加了此前的新条目,第二次只追加增量,会话退出时再整体写一次。最终文件与内存历史一致。-a 的价值在多终端并发场景:终端 A 随手 -a,终端 B 用 3.9 的 -n 就能读到,互不覆盖。

3.9 -n 读入文件中的新增行(多会话并发)

history -n 从 HISTFILE 读入"尚未读过的行"并追加到内存历史——与 -r 的区别是 -n 有读取偏移量,只补增量。实测模拟两个会话并发:会话启动后休眠期间,外部向它的 HISTFILE 追加一行,随后会话内 -n 拉取:

$ echo 'echo bg-injected' >> /root/demo/history/.n_hist && tail -1 /root/demo/history/.n_hist
echo bg-injected

会话内执行 history -n 后查看:

$ history -n
$ history 3
    4  history -n
    5  echo bg-injected
    6  history 3

echo bg-injected 被读入成为第 5 条——这就是多终端共享历史的协作方式:一边 -a 推、一边 -n 拉。

3.10 -s 注入历史与 -p 展开预览

history -s 把指定字符串作为一条历史记录塞进列表(不执行它):

$ history -s 'echo injected-via-s'
$ history 2
    1  echo injected-via-s
    2  history 2

注入的 echo injected-via-s 占据第 1 条,但从未被执行过。history -p 则对参数做历史展开并显示结果,既不执行也不入史:

$ history -p '!!'
history 2
$ history 3
    1  echo injected-via-s
    2  history 2
    3  history 3

-p '!!' 输出了展开结果 history 2(上一条命令),而历史列表里没有出现 -p 之前的额外条目。

💡 要点:-s 常用于"把没跑过的命令补进历史"(如先 history -s 'kubectl get pods' 给同事留模板);-p 适合预览 !!、!$ 展开结果再决定是否执行,两参数都要把参数用单引号括住,避免当前 shell 抢先展开。

3.11 事件指示符:!! 上一条、!n 第 n 条、!-n 倒数第 n 条

事件指示符(event designator)以 ! 开头,在命令读入时被替换成历史里的命令文本。!! 引用上一条、!n 引用第 n 条、!-n 引用倒数第 n 条:

$ echo one
one
$ echo two
two
$ !!
two
$ !1
one
$ !-1
one
$ history 5
    2  echo two
    3  echo two
    4  echo one
    5  echo one
    6  history 5

三个指示符都正常展开执行:!! 重跑了 echo two,!1 与 !-1 各自命中目标。从 history 5 还能看到一个细节:入史记录的是展开后的命令(echo two、echo one),而不是 !!、!1 原文,因此历史文件始终是"可重放"的。

💡 技术拓展:复用历史命令不止 !! 一条路,常见方案横向对比——

方式形式特点适用场景
事件指示符!!、!$、!str读行时文本展开,可再接修饰符脚本化、快速重跑/改参
fc 修执行fc -s、fc N调编辑器或直接重跑,可做替换批量修正后重执行(3.19)
反向搜索Ctrl-R交互全文检索,找到即编辑执行记不清编号、模糊回忆
快捷键补参Alt+. / Esc+.只粘贴上条末参数,可连按回溯长路径、长文件名复用
history 粘贴history 查看后 !n先看编号再引用偶尔回溯老命令

本文主线采用事件指示符(bash 内建、无需配置、可在命令任意位置展开),fc 作为补充在 3.19 实测。

3.12 事件指示符:!str 前缀匹配 与 !?str? 包含匹配

!str 引用最近一条以 str 开头的命令(匹配从命令行第一个字符开始);!?str? 引用最近一条包含 str 的命令(任意位置):

$ echo prefix-alpha
prefix-alpha
$ echo prefix-beta
prefix-beta
$ !echo
prefix-beta
$ !?alpha?
prefix-alpha
$ history 5
    1  echo prefix-alpha
    2  echo prefix-beta
    3  echo prefix-beta
    4  echo prefix-alpha
    5  history 5

!echo 命中最近一条 echo 开头的命令(第 2 条 prefix-beta);!?alpha? 跳过不含 alpha 的第 3 条,命中第 1 条 prefix-alpha。

⚠️ 注意:!str 匹配的是命令行的开头。若历史里只有 echo prefix-alpha 而没有以 pre 开头的命令,输入 !pre 会得到 bash: !pre: event not found,整行作废不执行——这是历史展开最常见的报错。匹配不到时改用 !?pre?(包含匹配)或先 history 确认。

3.13 事件指示符:!* 全部参数 与 !$ 上条末参

!* 展开为上一条命令的全部参数,!$ 展开为上一条命令的最后一个参数:

$ echo /root/demo/history/f1.txt /root/demo/history/f2.txt
/root/demo/history/f1.txt /root/demo/history/f2.txt
$ echo all-args=!*
all-args=/root/demo/history/f1.txt /root/demo/history/f2.txt
$ echo last-arg=!$
last-arg=/root/demo/history/f2.txt

!* 复用了两个路径参数,!$ 只取了末尾的 f2.txt。

💡 要点:!$ 是日常效率最高的指示符——mkdir -p /a/b/c 之后 cd !$ 直接进目录;交互下等价的快捷键是 Alt+.(可连按继续回溯更早条目的末参数)。!$ 也可以写成 !!:$,!* 等价 !!:*。

3.14 展开修饰符 :t :h :r :e(配合词指示符 :1)

修饰符跟在事件指示符之后,对选中的词做路径加工::t 取文件名(basename)、:h 取目录(dirname)、:r 去扩展名、:e 只留扩展名。先用词指示符 :1 选中上一条命令的第 1 个参数,再套四种修饰符:

$ echo /etc/nginx/nginx.conf
/etc/nginx/nginx.conf
$ echo !!:1:t !!:1:h !!:1:r !!:1:e
nginx.conf /etc/nginx /etc/nginx/nginx .conf

同一个 /etc/nginx/nginx.conf,四个修饰符分别得到文件名、目录、去扩展名、扩展名。再测 :0(第 0 个词,即命令名本身):

$ echo !!:0
echo
$ history 3
    2  echo nginx.conf /etc/nginx /etc/nginx/nginx .conf
    3  echo echo
    4  history 3

!!:0 展开为 echo,于是实际执行了 echo echo。

⚠️ 注意:不带词指示符时(如直接写 !!:h),修饰符作用于整条命令的每个词,echo 这样的命令字也会被处理,结果往往不是想要的。写脚本时建议始终带上词指示符(!!:1:t、!!:$:h),行为才精确可控。

3.15 替换修饰符 :s 与 & 、g 修饰符

:s/old/new/ 把选中范围内的第一处 old 换成 new;加 g 前缀(:gs/old/new/)全局替换;new 里的 & 代表被匹配的原文:

$ echo old-a old-b
old-a old-b
$ echo !!:*:s/old/new/
new-a old-b
$ echo old-a old-b
old-a old-b
$ echo !!:*:gs/old/new/
new-a new-b
$ echo report-v1.pdf
report-v1.pdf
$ echo !!:*:s/v1/&-final/
report-v1-final.pdf

:s 只换了第一处(old-b 不动);:gs 两处全换;&-final 中 & 先展开为匹配到的 v1,得到 v1-final。:* 词指示符把替换范围限定在参数部分。

💡 要点:经典用法是 !!:gs/误写/正确/ 一行修正上条命令的拼写错误再重跑;本节示例都加 echo 前缀以便观察展开结果,实际使用时去掉 echo 就是直接执行替换后的命令。

3.16 HISTCMD 变量:当前命令的编号

HISTCMD 不是命令参数,而是 bash 维护的特殊变量——当前正在执行的命令的历史编号:

$ echo check-histcmd
check-histcmd
$ echo "current HISTCMD=$HISTCMD"
current HISTCMD=2
$ history 2
    2  echo "current HISTCMD=$HISTCMD"
    3  history 2

当前行是第 2 条,HISTCMD 恰为 2。非交互会话里 HISTCMD 为 0(3.1 实测),因为根本没有历史编号体系。

3.17 HISTTIMEFORMAT:带时间戳展示

默认 history 只显示编号与命令。设置 HISTTIMEFORMAT(strftime 格式)后,每条历史前面多出时间戳:

$ HISTTIMEFORMAT='%F %T '
$ echo timed-one
timed-one
$ echo timed-two
timed-two
$ history 3
    2  2026-10-15 21:31:42 echo timed-one
    3  2026-10-15 21:31:42 echo timed-two
    4  2026-10-15 21:31:42 history 3

%F %T 即 年-月-日 时:分:秒。本会话三条命令的时间戳相同,因为非登录交互会话中 bash 给本会话条目统一打上会话时刻。

💡 要点:要让历史文件真正带时间,需在 .bashrc 中设置 HISTTIMEFORMAT='%F %T '——bash 会以时间戳格式写入 HISTFILE(每条记录前加一行 #<epoch秒>)。对旧版本 bash 写入的历史文件,读出的时间戳是"文件写入时刻"而非真实执行时刻,审计用途需从开启该变量那一刻起才算数。

3.18 HISTSIZE 与 HISTFILESIZE:内存与文件的上限

HISTSIZE 限制内存历史的条数,超限后最老的条目被挤出:

$ echo h1
h1
$ echo h2
h2
$ echo h3
h3
$ echo h4
h4
$ history 4
    3  echo h3
    4  echo h4
    5  history 4

HISTSIZE=3 的会话里执行了 4 条 echo,history 4 只能看到 h2 之后的条目,h1 已被挤出内存。HISTFILESIZE 限制历史文件的行数,写盘时超限则截断保留最新部分:

$ cat /root/demo/history/.fsize_hist
echo f3
echo f4
echo f5
echo f6
exit

HISTFILESIZE=5 的会话执行了 f1~f6 共 6 条 echo 加 exit,落盘文件只剩最后 5 行,f1、f2 被截掉。

💡 说明:Ubuntu 交互会话的默认值来自 /etc/bash.bashrc 与 ~/.bashrc(常见 HISTSIZE=1000、HISTFILESIZE=2000);把两个值调大(或 HISTFILESIZE 设为 -1 不限制)可保留更长历史。两值都为 0 时分别表示"不记内存历史/不写历史文件"。

3.19 -w/-r 直接指定文件名与 fc 关联命令

-anrw 四个参数都可以直接接 FILENAME,临时绕开 HISTFILE 变量。实测 -w 指定快照文件:

$ echo named-one
named-one
$ echo named-two
named-two
$ history -w /root/demo/history/.named_snapshot
$ exit
exit
$ cat /root/demo/history/.named_snapshot
echo named-one
echo named-two
history -w /root/demo/history/.named_snapshot

快照文件按指定路径生成,HISTFILE 指向的常规文件不受影响。

fc 是与 history 同源的关联内建命令:fc -l 列历史(默认最近 16 条)、fc -s 重执行:

$ echo replay-me
replay-me
$ echo another-line
another-line
$ fc -l -2
1	 echo replay-me
2	 echo another-line
$ fc -s echo
another-line
$ fc -ln -2
	 echo replay-me
	 echo another-line

fc -l -2 列最近 2 条(编号制表符分隔);fc -s echo 重执行最近一条 echo 开头的命令(输出 another-line);fc -ln -2 加 -n 不显示编号。

💡 要点:fc 的替换重执行语法 fc -s 旧=新 [命令前缀] 与 3.15 的 :s 修饰符能力重叠,但 fc 独立于 histexpand 选项,在不开启 ! 展开的脚本里也能用。

3.20 非法选项与退出码

帮助声明"非法选项返回失败",实测:

$ history --bogus
bash: history: --: invalid option
history: usage: history [-c] [-d offset] [n] or history -anrw [filename] or history -ps arg [arg...]
$ echo rc=$?
rc=2

非法长选项报 invalid option 并打印 usage 一行,退出码 2。正常执行(列出、-c、-w、-d 等)退出码均为 0。

3.21 HISTCONTROL:历史过滤策略(ignorespace/ignoredups/erasedups)

HISTCONTROL 决定"哪些命令不进历史",支持三个值的单独或冒号组合:ignorespace(空格开头的命令不入史)、ignoredups(与上一条相同的命令不重复记录)、erasedups(入史时删除列表中所有旧的同名条目)、ignoreboth(= ignorespace:ignoredups)。

ignorespace——前导空格的命令被执行但被排除在历史外:

$ HISTCONTROL=ignorespace
$ echo normal-visible
normal-visible
$  echo hidden-by-leading-space
hidden-by-leading-space
$ history 3
    1  HISTCONTROL=ignorespace
    2  echo normal-visible
    3  history 3

带一个前导空格的 echo hidden-by-leading-space 正常执行并输出了内容,但 history 3 里没有它。

ignoredups——连续重复只记第一次:

$ HISTCONTROL=ignoredups
$ echo dup-line
dup-line
$ echo dup-line
dup-line
$ echo dup-line
dup-line
$ history 4
    1  HISTCONTROL=ignoredups
    2  echo dup-line
    3  history 4

三条相同的 echo 只在历史里留下一条。

erasedups——新旧同名条目只保留最新(旧条目从原位置删除):

$ HISTCONTROL=erasedups
$ echo erase-target
erase-target
$ echo other-line
other-line
$ echo erase-target
erase-target
$ history 5
    1  HISTCONTROL=erasedups
    2  echo other-line
    3  echo erase-target
    4  history 5

第二次执行 echo erase-target 时,第 2 条位置的旧条目被删除、新条目追加到末尾,编号随之重排。

ignoreboth——空格开头与连续重复同时生效:

$ HISTCONTROL=ignoreboth
$ echo dup-both
dup-both
$ echo dup-both
dup-both
$  echo hidden-dup-both
hidden-dup-both
$ history 4
    1  HISTCONTROL=ignoreboth
    2  echo dup-both
    3  history 4

重复的 dup-both 记一条,空格开头的 hidden 完全缺席。

💡 要点:Ubuntu 的交互会话默认在 ~/.bashrc 中设置 HISTCONTROL=ignoreboth,这就是"空格开头命令失踪"与"重复命令变少"的来源;想完整记录排查过程(包括重复执行)时临时 unset HISTCONTROL 或置空即可。

四、实战示例

4.1 场景一:误把密码敲进命令行,立刻抹掉

历史里留有敏感内容时的两步清理——先删内存条目,再立即写盘覆盖文件:

$ echo connect-with-password-123456
connect-with-password-123456
$ history -d $((HISTCMD-1))
$ history 2
    1  history -d $((HISTCMD-1))
    2  history 2

HISTCMD 是当前行编号,-1 恰好指向刚执行的敏感命令,history 2 里已经看不到它。随后 history -w 立即把干净列表写入 HISTFILE。落盘文件验证:

$ cat /root/demo/history/.sec_hist
history -d $((HISTCMD-1))
history 2
history -w
history -d $((HISTCMD-1))
history 2
history -w
exit

文件里同样没有敏感命令(前 3 行是 -w 的快照,后 4 行是退出时追加的本会话条目,见 3.4 的机制说明)。

4.2 场景二:上条命令拼错一个词,全局替换后重跑

$ echo report-for-ngixn
report-for-ngixn
$ !!:gs/ngixn/nginx/
echo report-for-nginx
report-for-nginx
$ history 3
    1  echo report-for-ngixn
    2  echo report-for-nginx
    3  history 3

!!:gs/ngixn/nginx/ 展开为 echo report-for-nginx 并直接执行(展开行先回显再执行),历史里留下的是修正后的命令,重放历史不会重蹈覆辙。若只想改某一个参数,把 gs 换成 !!:1:s/.../.../ 精确定位。

4.3 场景三:多终端共享历史(增量同步工作流)

默认机制下,会话只在启动时读一次 HISTFILE、退出时追加本会话条目(3.4 实测),两个终端互不感知对方的新命令。推荐在 .bashrc 中配置增量同步:

shopt -s histappend                       # 明确采用追加语义写盘
HISTSIZE=10000
HISTFILESIZE=20000
HISTCONTROL=ignoreboth                    # 空格开头与连续重复都不记
PROMPT_COMMAND='history -a; history -n'   # 每次出提示符前:推送新增并拉取他人条目

实测 shopt -s histappend 与手动 -a 推送:

$ shopt -s histappend
$ shopt histappend
histappend      on
$ echo sync-one
sync-one
$ history -a

落盘文件验证:

$ cat /root/demo/history/.sync_hist
shopt -s histappend
shopt histappend
echo sync-one
history -a
exit

history -a 只追加新条目(3.8 实测),配合另一个会话的 history -n(3.9 实测)即可双向同步;PROMPT_COMMAND 把这一推送-拉取动作自动化到每个提示符周期。

4.4 场景四:带时间戳的命令审计

$ echo audit-check
audit-check
$ history 2
    1  2026-10-15 21:41:37 echo audit-check
    2  2026-10-15 21:41:37 history 2

在 .bashrc 中设置 HISTTIMEFORMAT='%F %T ' 后,每条历史前自动带上"年-月-日 时:分:秒"(3.17 的格式机制),配合 HISTCONTROL=ignoredups 可避免重复命令刷屏。需要更强的审计(记录 TTY、来源 IP)时,history 本身不提供这些信息,要在 PROMPT_COMMAND 中额外落日志。

4.5 场景五:!$ 复用长路径参数

$ tar -czf /root/demo/history/logs-demo.tar.gz /root/demo/history
$ ls -lh !$
total 12K
-rw-r--r-- 1 root root   57 Oct 15 21:41 histexpand.sh
-rw-r--r-- 1 root root 1.6K Oct 15 21:41 logs-demo.tar.gz
-rw-r--r-- 1 root root   49 Oct 15 21:41 noninteractive.sh

!$ 把上条命令的最后一个参数(归档路径)原样复用给 ls -lh,从打包到检查产物,长路径只敲一次。参数不止一个时改用 !*(3.13 实测)或 !!:2(第 2 个参数)。

五、注意事项与常见问题

Q1:脚本里 history 输出是空的,是坏了吗? 不是。非交互 shell 默认 history=off 且不读写 HISTFILE(3.1 实测:history | wc -l 为 0、HISTFILE 为空)。脚本里需要历史时先 set -o history(3.2 实测),但注意脚本退出仍不落盘。

Q2:脚本里 !! 报 command not found 或整行没反应? 两种可能:一是没开 set -o histexpand(非交互默认关闭,3.2 实测显式开启后 !! 可用且会先回显展开结果);二是把 !! 写在了子 shell 复合命令(如 ( ...; !!; ... ))内部——整条复合命令在 histexpand 生效前就被读入,! 不会展开,会被当成名为 !1 的外部命令报 command not found。事件指示符必须写在独立的命令行上。

Q3:!pre 报 event not found? !str 匹配的是命令行开头,历史里没有 pre 开头的命令就匹配不到(3.12 的 ⚠️ 已说明)。换 !?pre? 包含匹配,或改用具体可命中的前缀。另外 !?str? 中的 ? 在展开后若还有其他字符要补全分隔,结尾的 ? 不能省。

Q4:history -d 2 之后编号全变了,怎么连续删多条? -d 删除后列表立即重排(3.7 实测)。连续删除应从最大编号往小删,或者每次删除前先 history | tail -n 确认新编号;脚本场景推荐 history -d $((HISTCMD-1)) 这类相对定位。

Q5:两个终端的历史为什么互不相通、还经常"丢命令"? 实测机制(3.4 区分实验):会话启动时读一次 HISTFILE,退出时只追加本会话条目(不覆盖他人内容)。两个终端各自为政——A 的新命令 B 要等 B 重开或 -n 拉取才能看到;B 退出后文件里是"B 的条目接在老内容后面",A 退出时追加的是 A 的条目,结果同一命令可能重复出现、顺序交错。解决办法见 4.3:shopt -s histappend + -a/-n 增量同步(或 PROMPT_COMMAND 自动化)。

Q6:空格开头的命令没有进历史? 这是 HISTCONTROL=ignorespace(或 ignoreboth)的设计行为——前导空格的命令被刻意排除在历史外(3.21 实测),常用来临时执行不想留痕的命令。觉得"丢了命令"时检查该变量与 ~/.bashrc 的设置。

Q7:history 显示的时间戳全是同一个时间? 本会话条目的时间戳统一是会话启动/执行时刻(3.17 实测);跨会话的真实执行时间依赖历史文件中的 #epoch 记录行,而这只有在一直开启 HISTTIMEFORMAT 的环境下才会逐条写入。老历史文件里没有该记录,读出来都是导入时刻。

Q8:history 3 显示的编号和文件里对不上? 编号只在内存列表中连续;会话启动时把 HISTFILE 条目读入接续编号(3.3 说明),-c 清空后从 1 重排(3.5 实测),-r 追加的内容接在当前编号之后(3.6 实测)。以 history 的当前输出为准,不要凭记忆引用老编号。

六、参考链接