
1. 先把 history 的存储模型搞清楚后面的坑一半都出在这Linux history 命令几乎是每个接触命令行的人最早学会的几个命令之一也是被误解最多的一个。绝大多数人停留在敲 history 看一眼前面干了啥这个层面然后就在几个经典问题上卡住明明清空了历史按上箭头怎么还能翻到旧命令在 A 终端敲的命令B 终端为什么看不到时间戳明明开了为什么显示的时间全是刚开机那一刻这些问题不是 bash 的 bug而是因为 history 的数据实际活在两个地方很多人只操作了其中一个。1.1 内存里的历史列表和磁盘上的 .bash_history 是两套数据bash 启动一个交互式 shell 的时候会做一件事把$HISTFILE默认是~/.bash_history里的内容读进内存形成一份历史列表。之后你在终端里敲的每一条命令是先追加到这份内存列表里的跟磁盘文件完全没关系。直到 shell 退出bash 才会把内存列表写回去。这个设计带来两个直接后果。第一会话中途cat ~/.bash_history你看不到刚才敲的命令因为它还在内存里没落盘。第二会话中途手动编辑.bash_history加了几行当前终端的history输出里也不会出现因为内存列表早在启动时就读完了不会自动回头看文件除非你显式执行history -r。理解这条主线之后很多诡异现象就自动解释清楚了。所谓清空历史你到底是清内存还是清文件这是两个动作所谓历史丢了一部分多半是某个终端退出时用自己的内存列表把文件整个覆盖了。1.2 为什么退出终端时历史会被吃掉一段默认情况下 bash 写回文件的方式是整体覆盖不是追加。想象你开着三个终端终端 A 敲了 20 条命令终端 B 敲了 5 条终端 C 敲了 30 条。如果 A 先退出它把启动时的旧历史 自己这 20 条写进文件B 接着退出它内存里根本没有 A 那 20 条于是用启动时的旧历史 自己这 5 条把文件覆盖掉C 最后退出同样覆盖。最后文件里只剩 C 那一路的痕迹A 和 B 的活儿像是没干过。这不是偶然而是默认行为的必然结果。解决办法是打开追加模式shopt -s histappend开了之后bash 退出时是追加而不是覆盖多终端的内容就能拼在一起。想验证是否生效可以在.bashrc里加上shopt -s histappend然后shopt histappend查看状态输出histappend on就对了。但追加模式也有自己的脾气因为每个终端退出时才追加所以三个终端同时开着的时候彼此的 history 依然是互相看不见的直到有人退出。想让它们实时同步得靠PROMPT_COMMAND这个在第 5 节展开说。1.3 顺带把 history 和 hash 这两个内建命令分清楚搜索的时候经常看到有人把history和hash混在一起问这两个都是 bash 的内建命令但干的事完全无关。hash管的是命令路径缓存bash 执行一个不带路径的命令时会先查 PATH 找可执行文件找到之后把命令名 → 绝对路径这个映射缓存起来下次直接命中省去遍历 PATH 的开销。于是就有了那个经典场景你刚编译安装了一个新版本的工具到/usr/local/bin敲命令发现跑的还是老版本因为缓存里记着/usr/bin下的旧路径。这时候的正确操作是hash -r清空整张缓存表或者hash -d 命令名删掉单条。直接敲hash不带参数会打印当前缓存表能看到每个命令被解析到了哪个路径。顺手再区分一个容易被扯进来的东西Git 里的git log、git reflog记录的是仓库操作时间线思路和 shell history 类似都是操作轨迹但它们存放在.git目录里跟~/.bash_history没有任何关系。VS Code 的 Git History 插件读的也是仓库对象不是你的 shell 历史。把这三者分清楚排错时就不会往错误的方向找。2. 查看与检索让几千条历史变成能用的线索默认配置下 Ubuntu、Debian 系的历史长度是 1000 条CentOS 系通常也是 1000 条。对于天天泡在命令行里的人这个量级可能只够覆盖两三天。真要把历史当资料库用光会敲history是不够的得会检索、会看时间、会定位到具体那一条。2.1 基础输出和 history N 的分寸直接敲history输出格式是序号 空格 命令原文序号从 1 开始递增一直到内存列表的长度。条目多了之后最实用的用法是history 30只看最近 30 条。这个数字的选择有个小技巧屏幕一屏大概能显示 40 行左右如果你只是想确认刚刚那条命令怎么写的history 10就够如果要回溯一整个调试过程可以直接history | less用 less 翻页和搜索。列表前面的序号不是摆设它是操作句柄。history -d 1234能精确删掉第 1234 条!1234能重跑它。所以看到一个可疑的序号时先history -d删掉或者记下来比事后翻半天强。有个细节值得注意如果你开了HISTCONTROLerasedups历史里的序号会随着去重动态变化昨天看到的 1234 可能今天变成了 1200所以别把序号写进脚本里。2.2 CtrlR 才是日常主力history | grep 只是补充真的每天用命令行的人靠的是CtrlR反向增量搜索。按下之后开始输入关键字bash 会从最新往最旧匹配显示出第一条命中的命令再按一次CtrlR继续往前找下一条按CtrlG或Esc取消按回车直接执行。这个操作的效率比history | grep高一个数量级因为不需要先看输出再复制粘贴。history | grep的价值场景是批量排查。比如你想知道这台机器上最近有没有人动过某个目录的权限history | grep -i chmod一次看完想知道某个服务重启过几次history | grep systemctl。这种我要看一组命令的全部出现位置的需求grep 比 CtrlR 合适。再补一个常用的组合history | awk {print $2} | sort | uniq -c | sort -rn | head -20能统计出你敲得最频繁的 20 个命令。我拿这个查过自己的习惯结果发现大量时间花在了cd和ls上之后就给它们配了别名并加进HISTIGNORE历史列表立刻清爽了不少。2.3 时间戳的开启方式和那个全是同一秒的坑加时间戳就一行配置export HISTTIMEFORMAT%F %T %F等价于%Y-%m-%d%T等价于%H:%M:%S末尾那个空格是为了和命令原文分开不加的话时间和命令会连在一起。这里有个必踩的坑配置只在生效之后的新命令上起作用对已经在历史里的旧条目bash 没有时间数据可用。这种情况下 bash 会退而求其次用 shell 启动的时间来填充于是你看到 1000 条历史的时间戳一模一样。这不是配置错了是数据本来就不存在。如果你同时执行了history -c清空内存再重新开始记录那从清空那一刻起时间才会是真实的。还有个更隐蔽的问题时间戳写进.bash_history的时候是以#开头的一行 Unix 秒数单独存放的文件长这样#1730000000 ls -la /var/log #1730000042 tail -f /var/log/nginx/access.log所以千万不要手动去编辑.bash_history删那些#行删了之后history显示的时间就会错乱。要清理历史用history系列命令别用文本编辑器硬改。3. 重复执行历史命令写法很多风险也不少事件指示符event designator是 bash 提供的一整套引用历史条目的语法用得好能省大量键盘用不好能直接把生产环境干崩。很多人第一次接触它是在输入sudo !!的时候但!!只是这套语法里最简单的一个。3.1 常用指示符的语义对照先把常用的整理成表方便对着看写法含义!!上一条命令!n第 n 条序号来自 history 输出!-n倒数第 n 条!-2就是上上条!字符串最近一条以该字符串开头的命令!?字符串?最近一条包含该字符串的命令!$上一条命令的最后一个参数!^上一条命令的第一个参数!*上一条命令的所有参数^旧^新把上一条命令中的旧文本替换为新文本后执行!:p只打印展开结果不执行!$和!^是真正提升效率的两个。比如刚敲完mkdir -p /data/project/logs接下来要进去直接cd !$就行不用手打路径。刚敲完vim /etc/nginx/nginx.conf想备份一份cp !$ !$.bak一步到位。^旧^新适合打错字比如systemctl statu nginx敲错了少个 s下一行直接^statu^status就能纠正并执行。3.2 先看后执行:p 和 histverify事件指示符最大的风险是你以为它会展开成 A实际展开成了 B。最保险的习惯是养成先打印的习惯!grep:p这会显示!grep展开后的完整命令确认无误再按上箭头调出来执行。另一个更省事的办法是打开histverifyshopt -s histverify开启之后所有带!的事件指示符展开后都会先把结果放进命令行缓冲区让你过目而不是直接执行。这个开关我强烈建议开代价只是多按一次回车收益是避免几次手一抖。3.3 那些被事件指示符坑过的真实场景第一个坑命令开头的!会被无条件展开。你在交互式 shell 里敲echo Hello!双引号里的感叹号在某些情况下会触发历史展开报出event not found的错误。解决方式是用单引号echo Hello!或者转义\!。第二个坑脚本里!不展开。写 shell 脚本时你写了!!期望它引用上一条实际上什么都不会发生因为非交互式 shell 默认关闭 histexpand。想确认状态用set -o | grep histexpand。第三个坑在sudo后面用!$时参数归属搞混。sudo echo hi /root/test这种写法重定向是由当前 shell 处理的权限不够会失败。正确写法是先切 root 或者用sudo tee。这跟 history 无关但经常和sudo !!一起出现所以顺便提一句。第四个坑也是最要紧的!!会把上一条命令原封不动再跑一遍。如果上一条是rm -rf或者DROP TABLE那后果就是双倍。我的习惯是任何可能造成不可逆影响的命令执行前都把命令原文核对一遍绝不在不确定的情况下按!!。4. 让历史记录按你的规则来HISTCONTROL、HISTIGNORE 和长度控制历史列表里 80% 的内容可能是噪音反复敲的ls、cd、clear、pwd以及调试时敲了一堆中间态命令。与其事后清理不如从一开始就让它们别进来。bash 提供了几个变量来控制这件事。4.1 HISTCONTROL 的四个取值各自的适用场景HISTCONTROL用冒号分隔多个值可取的有取值作用ignorespace以空格开头的命令不记入历史ignoredups连续重复的命令只记第一条ignoreboth上面两个同时生效erasedups新命令如果和历史中已有条目重复删掉所有旧的那条ignorespace特别实用。调试时经常要敲一长串带临时参数的验证命令或者只是想算个东西这些内容进历史纯属污染。习惯性地在命令前加一个空格它就干净地消失了。但要注意这个空格必须是命令的第一个字符前面不能有任何其他东西。ignoredups只处理连续重复。你敲ls然后pwd再敲ls两条ls都会留下。要彻底去重就用erasedups它会让每个命令在历史里只保留最后出现的那一次。代价是历史的时间顺序会被打乱因为旧位置的那条被删了新的排在最后。如果你更看重时间线的完整就别开 erasedups。我的配置是HISTCONTROLignoreboth:erasedups4.2 HISTIGNORE 的写法和你绝对会踩的正则陷阱HISTIGNORE是按模式过滤多个模式用冒号分隔HISTIGNOREls:ll:cd:pwd:exit:clear:history:bg:fg:jobs重点来了它支持的是通配符*和?不是正则表达式。很多人写成HISTIGNOREls.*期望匹配所有 ls 开头的命令结果发现没用因为.在这里就是个普通字符。正确写法是HISTIGNOREls*。ls*这个模式会同时干掉ls、ls -la、ls /tmp所有变体。但也要小心它的副作用如果你有个命令叫lsblk也会被ls*一起滤掉。所以边界要卡准宁可多写几条精确模式。还有一个模式分隔的问题如果你要忽略的命令本身包含冒号需要转义。这种场景极其罕见真遇到了直接写个函数包一层更省事。验证配置是否生效可以敲一条本该被忽略的命令然后history 3看它有没有出现。4.3 HISTSIZE 和 HISTFILESIZE 的区别以及设了却没生效这两个变量是新手最容易搞混的一对HISTSIZE内存中保留的历史条目数决定history命令能看到多少条HISTFILESIZE.bash_history文件的最大行数决定磁盘上留多少典型配置HISTSIZE100000 HISTFILESIZE200000注意HISTFILESIZE和HISTSIZE不是一比一的关系。因为开启时间戳后每个条目在文件里占两行#秒数 命令所以HISTFILESIZE200000实际只能存约 10 万条命令。想让内存和文件容量大致对齐可以把HISTFILESIZE设成HISTSIZE的两倍。设了没生效通常有三个原因。一是改在了错误的文件里.bashrc只对交互式非登录 shell 生效登录 shell 读的是.bash_profile或.profile得确认改对位置。二是改完没 sourcesource ~/.bashrc或者重开终端。三是被后面的配置覆盖了.bashrc里如果还有一处给HISTSIZE赋值以最后执行的为准用echo $HISTSIZE确认实际值。另外调大HISTSIZE之后 CtrlR 会明显变慢因为每次搜索都要扫更大的列表。如果历史超过几万条建议直接用模糊查找工具替代响应速度会好很多。5. 清空、删除与多终端合并history 的写操作全解析到了写操作这一层误解集中爆发。最常见的困惑就是我明明执行了history -c怎么上下箭头还能翻到旧命令。原因很简单history -c只清了内存列表的一部分状态而且它清完之后当前会话从那一刻起记录的新命令依然会写进文件。5.1 history -c 之后为什么还能翻到旧命令先看-c到底做了什么它清空内存中的历史列表让history命令输出为空。但有两件事它没管。第一如果你在清空之前打开过另一个终端那个终端的内存列表还完整保留着它退出时会把自己的列表写回文件你的清空成果就被覆盖了。第二有些终端软件比如某些发行版的终端模拟器会自己缓存一份滚动区的历史上下箭头翻的是终端软件的缓冲区跟 bash 的 history 又不一样这种情况清 bash 历史是没用的。所以要真正把历史清干净顺序是history -c # 清内存列表 history -w # 用空列表覆盖 .bash_historyhistory -w这一步是关键它把当前已经被清空的内存列表写进文件文件才真正变空。或者更直接一点用: ~/.bash_history把文件截断为 0 字节效果一样。如果只是想删掉某几条敏感内容别整个清空用history -d 序号精确删除。删除后记得history -w写回文件否则它还在磁盘上。注意在命令行直接传密码这类信息很容易落进.bash_history比如mysql --passwordxxx。更稳妥的做法是让程序交互式提示输入密码或者把凭据放进权限为 600 的配置文件里。历史记录清理只是补救从源头避免才是正路。5.2 -a、-w、-r、-n 四个写操作的语义差异这四个参数最容易搞混整理成表参数动作影响对象history -a把本次会话新增的条目追加到文件只写文件history -w用整个内存列表覆盖文件只写文件history -r把文件内容读进来追加到内存列表只改内存history -n只读文件中尚未读过的行到内存只改内存记忆方法-a和-w是往文件写区别在追加还是覆盖-r和-n是往内存读区别在全部读还是只读新增。一个实用组合在多终端环境下想查看其他终端刚敲的命令而不退出当前终端执行history -n就会把文件里的新条目拽进来。-w有个隐藏风险如果你的内存列表因为某种原因不完整比如之前执行过history -c-w会把不完整的内容覆盖到文件上直接导致磁盘历史缩水。所以用-w之前先history | wc -l看一眼条数心里有数。5.3 实时同步多终端PROMPT_COMMAND 的用法PROMPT_COMMAND是 bash 每次显示提示符之前都会执行的一段命令正好可以利用它做历史的实时同步shopt -s histappend PROMPT_COMMANDhistory -a; history -n; $PROMPT_COMMAND拆开看这几步history -a把你刚敲完的那条立刻追加到文件history -n把文件里其他终端新加进来的行读进当前内存列表。这一写一读之间所有终端的命令历史就基本对齐了延迟只有一条命令的时间。加上这个之后实践中还有个小副作用因为每条命令执行完都会写一次文件磁盘 IO 会比默认频繁一点。在机械盘的老机器上可能有轻微感知SSD 上完全无感。如果你是那种每秒敲好几条命令的人也可以退一步只保留history -a牺牲一点实时性换取 IO 更少。再配一条shopt -s cmdhist它会把多行输入的命令合并成一条带分号的单行记录。不加这个的话你用 heredoc 或者多行 if 写的内容会被拆成好几条历史看着非常乱。6. 服务器、容器和日常使用中的几条实战经验前面讲的是机制最后这部分是我自己踩过之后固定下来的用法供参考。6.1 共享环境和容器里的历史策略在共享的测试机上histappend加上PROMPT_COMMAND这套配置基本是标配。原因很实际多人共用账号时事后回溯这条命令是谁敲的没有意义因为账号相同但回溯这台机器上被敲过什么非常有价值很多配置漂移问题都是靠翻历史定位的。容器环境是另一个故事。用docker exec进容器敲命令时bash 的 HISTFILE 指向容器内部的 HOME容器一销毁历史就没了。如果你需要留痕得把 HOME 挂载出来或者进容器前先设好HISTFILE环境变量。还有一点很多精简镜像比如基于 alpine 的里/bin/sh是 ash 或 dash不是 bash它们的 history 行为跟 bash 有差异HISTTIMEFORMAT这类变量根本不认。遇到配置不生效先echo $0确认自己面对的是哪个 shell。.bash_history的文件权限值得检查一下chmod 600 ~/.bash_history。默认是 600 的话不用管但如果有脚本改过 umask 把它变成了 644同机器上其他用户就能读到你的命令包括里面可能出现的路径、参数信息。6.2 我现在的 .bashrc 片段和几条验证方法这部分是直接可以抄的# 容量 HISTSIZE100000 HISTFILESIZE200000 # 去重 空格开头不记录 HISTCONTROLignoreboth:erasedups # 常见噪音命令不入库 HISTIGNOREls:ll:cd:pwd:exit:clear:history:bg:fg:jobs:top # 时间戳 HISTTIMEFORMAT%F %T # 追加写入多行命令合并成一条 shopt -s histappend shopt -s cmdhist # 每条命令结束立即同步 PROMPT_COMMANDhistory -a; history -n; $PROMPT_COMMAND改完之后用这几条验证echo $HISTSIZE # 确认容量生效 shopt histappend # 应为 on history 5 # 看时间戳有没有 ls # 再敲一条看它是否被 HISTIGNORE 过滤 history 3 # ls 不该出现在最后关于那个时间戳的坑我再强调一次如果你是刚加上HISTTIMEFORMAT之前的老条目时间显示得不对是正常的让新命令自然积累几天就正常了。急着要干净的时间线就找个不影响工作的时间点清一次空从那一刻重新开始记。最后一个小技巧。历史太长之后找东西原生 CtrlR 一个字符一个字符匹配的效率跟不上装一个模糊查找工具会舒服很多输入几个不连续的字母就能命中目标命令。历史记录这个功能最朴素的价值就是省下重新想命令的时间而把它调教到顺手花的是二十分钟配置省的是之后每一天的重复劳动。