尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Linux history 命令:.bash_history、去重与多终端同步

Linux history 命令:.bash_history、去重与多终端同步 你大概率遇到过这种场面上周在 Linux 上跑通了一条特别长的命令参数里塞了七八个排除规则、两三个路径映射今天想原样再跑一遍翻笔记、翻聊天记录都找不到最后只能对着报错一条条试参数。history命令就是为这种场景存在的——它把你在当前 shell 里敲过的每一条命令都记下来需要的时候翻出来、改一改、重跑。这不是什么冷门技巧任何一个长期待在终端里的人日常效率有一半靠的就是对命令历史的使用熟练度。但真正把history用明白的人并不多绝大多数人停留在history | grep xxx这一层遇到改完配置不生效多开几个终端记录就乱了清空了重开又回来这类问题就抓瞎。下面我把这套机制从存储位置、控制变量、检索手法、清理审计到常见坑按我自己的理解顺序拆一遍看完你应该能把它变成真正顺手的工具。1. history 记录究竟存在哪里内存缓冲与落盘文件的双轨机制1.1 一份列表存两处这是所有迷惑行为的根源history命令看到的列表和~/.bash_history这个文件里的内容默认不是同一份数据。你在终端里敲命令的当下bash 只是把它塞进内存里的一个列表只有当 shell 正常退出时才会把这个列表写进~/.bash_history。这就解释了两个非常常见的现象一是新开一个终端history里看不到刚才在另一个终端敲的命令因为它还在那个进程的内存里没落盘二是你在~/.bash_history里删掉几行重开终端它又回来了因为退出时内存列表会把文件覆盖一遍。理解了双轨这件事后面的所有操作就都好解释了。history -w是把内存列表整体写到文件history -a是只把本次会话新增的行追加到文件末尾history -r是把文件整体读回内存history -n是只读文件里我还没见过的新行。这四个写回/读取类操作是整套机制里最关键的四个开关比history | grep重要得多。我个人的习惯是在排查历史记录丢失问题时第一步永远是先确认当前 shell 读的是哪个历史文件echo $HISTFILE再确认有没有开histappend这两条查完八成问题就定位了。1.2 HISTSIZE 和 HISTFILESIZE 不是一个东西改错一个就白改这两个变量名字长得像作用完全不同混用是新手最常见的误区之一。变量作用范围默认值多数发行版影响什么HISTSIZE内存500 或 1000当前会话在内存里最多保留多少条HISTFILESIZE磁盘文件500 或 2000~/.bash_history最多保留多少行HISTSIZE管的是这次会话能翻多远HISTFILESIZE管的是文件能存多长。如果你只改HISTFILESIZE20000而没改HISTSIZE那么当前会话该截断还是会截断只是把截断后剩下的那部分写进一个更大的文件而已等于没解决翻不到几天前的命令这个核心诉求。反过来只改HISTSIZE不改HISTFILESIZE文件还是会在退出时被裁剪长远看依然丢记录。想真正留得住两个都要提上去我给服务器上的推荐值是HISTSIZE10000、HISTFILESIZE20000普通开发机再翻一倍也没什么压力纯文本文件而已体积可以忽略。1.3 histappend多终端用户的必开项histappend这个 shell 选项默认是关闭的。关闭状态下shell 退出时是覆盖写——用当前会话的内存列表把整个~/.bash_history换成新的。想象一下你同时开了三个终端A 终端上午跑了一堆排查命令B 终端下午跑了一堆C 终端晚上随手开了关。因为覆盖顺序取决于谁最后退出极有可能出现上午那批命令全没了的情况。这不是 bug是默认行为的设计取舍。开启方式很简单shopt -s histappend开启之后退出时改为追加写多个会话的记录会按退出顺序叠加不再互相覆盖。代价是文件里可能出现重复行、顺序和实际执行时间不一致但配合HISTCONTROLerasedups和后面要讲的时间戳基本能治住。另外很多发行版的/etc/bashrc或/etc/bash.bashrc里其实已经帮你开了所以最好先shopt histappend查一下当前状态别凭印象。2. 让历史记录变得能读时间戳、去重与噪音过滤的三个开关2.1 HISTTIMEFORMAT没有时间戳的历史等于没有历史默认的history输出只有行号和命令没有时间。等你需要回忆我到底是周二还是周三动过那台机器的时候就知道这个缺陷有多致命了。加时间戳只要一行export HISTTIMEFORMAT%F %T %F是2024-03-15这种日期格式%T是14:32:07这种时间格式后面那个空格是分隔符别漏。设置完新执行的命令带上时间输出大致长这样501 2024-03-15 09:12:33 systemctl status nginx 502 2024-03-15 09:13:02 tail -f /var/log/nginx/error.log 503 2024-03-15 09:20:47 vim /etc/nginx/conf.d/api.conf这里有个必须知道的内幕一旦你设置了HISTTIMEFORMATbash 在写入~/.bash_history时会在每条命令前面插入一行#1710468753这样的 epoch 秒数作为时间标记。所以当你cat ~/.bash_history或者grep这个文件时看到的是一堆#数字行夹杂在命令之间。很多人第一次看到会以为是文件损坏了其实那是时间戳元数据。这也带来一个副作用如果你用脚本处理历史文件、或者用某些同步工具搬运历史文件这些时间戳行容易被压掉之后history里就只剩当前会话的时间了。2.2 HISTCONTROL不记录重复也不记录带前缀空格的HISTCONTROL是控制什么该记、什么不该记的核心变量取值可以组合用冒号分隔。常用的几个值如下。取值含义ignorespace以空格开头的命令不记录ignoredups与上一条完全相同的命令不记录只跟紧邻的比ignoreboth上面两个都开erasedups记录新命令前先删掉列表里所有相同的旧命令这里最关键的是要分清ignoredups和erasedups的差别。ignoredups只跟上一条比所以ls、cd、ls这种交替出现依然会记两条ls。erasedups是全局去重只要你敲过ls之前的ls记录会被删掉、只保留最新一条。日常使用我更推荐HISTCONTROLignoreboth:erasedups理由很实在反复敲的ls、cd、git status会在历史里占掉大量位置把它们压掉之后同样大小的HISTSIZE里能留下更多真正有价值的、只敲过一次的长命令。而ignorespace是隐私保护的关键——在命令前多敲一个空格这条命令就不会进历史处理临时带敏感参数的场景时很方便。2.3 HISTIGNORE按命令名精确打击如果某些高频命令你根本不想看到HISTIGNORE比HISTCONTROL更直接。它接受的是一组用冒号分隔的模式支持通配符export HISTIGNOREls:ll:cd:pwd:exit:clear:history:top:df -h:free -h:whoami注意这里匹配的是整条命令不是命令前缀。ls只会匹配单独的lsls -l不在过滤范围内——如果你希望把ls的所有变体都过滤掉得写成ls*但那样太宽了会把lsof之类的也一起干掉不划算。我的实际做法是过滤掉纯粹的噪音命令把带参数、可能有信息量的版本留下来。另外这个变量是精确匹配加通配符所以在配置里写df -h这种带空格的完整命令是有效的这一点很多人不知道。3. 把历史翻出来的四套检索手法3.1 管道加 grep能用但要知道它的边界最广为人知的用法就是history | grep ssh这条命令没有任何问题但它的边界在于history输出的是内存 读进来的文件混合后的列表所以在多终端场景下你搜不到其他终端还没落盘的命令。另外grep只做字符串匹配不看时间顺序、不看你真正想要什么。想要更实用的效果可以加上根号和时间戳一起做二次加工history | grep -i docker\|kubectl | tail -n 20只看最近 20 条匹配项比滚一屏有意义得多。想按时间段找配合HISTTIMEFORMAT输出后用awk筛也行但说实话到了这个需求层级更聪明的做法是养成写工作日志的习惯别在 shell 历史里考古。history本身还支持一个简写参数history 20直接显示最近 20 条比history | tail -20省事这是很多人不知道的隐式用法。3.2 CtrlR 反向搜索真正的高效入口有经验的终端用户翻历史很少用history | grep而是直接按CtrlR。按下之后出现一个提示符输入任意片段shell 会从最近的记录往前找第一个匹配项。这个交互模式有几个几乎没人提的技巧掌握了之后效率是翻倍的再按一次CtrlR继续往前找下一个匹配项反复按就一路向上回溯。找到目标后不要直接回车。按CtrlE或向右方向键会把这条命令放到当前命令行上但先不执行你可以改参数再回车。这个习惯能挡掉好几次搜到危险命令手一抖直接跑了的事故。搜到一半想放弃按CtrlG或Esc退出不会污染当前命令行。如果你想让搜索更精确可以在~/.inputrc里打开前缀模式# ~/.inputrc \C-r: history-search-backward \C-s: history-search-forward改完之后CtrlR的行为会变成只在以当前已输入内容开头的命令里搜。也就是说你在命令行先敲了ssh再按CtrlR它就只翻以ssh开头的历史。这个改动对我来说是明显提升了检索精准度的推荐试试。注意CtrlS默认在多数终端里被流控占用了要先把流控关掉stty -ixon才好用。3.3 历史展开!!、!$这些符号到底怎么用历史展开是 bash 的一套独立语法在命令被执行前由 shell 先做文本替换。这套语法看着像密码用熟了极快写法含义!!上一条命令经典用法sudo !!把上一条命令用管理员权限重跑!n执行历史里第 n 条!-3倒数第 3 条!ssh最近一条以 ssh 开头的命令!?conf?最近一条包含 conf 的命令!$上一条命令的最后一个参数!*上一条命令的所有参数!!:2上一条命令的第 2 个参数!!:2-4上一条命令第 2 到第 4 个参数^old^new^把上一条命令里的 old 替换成 new 后执行!$:h取路径的目录部分去掉文件名!$:t只取文件名部分举个大白话例子。你刚敲了scp app.jar deploy10.0.0.8:/opt/services/api/现在想在同一台机器上开个 ssh 会话直接敲ssh !$:h就能变成ssh deploy10.0.0.8:/opt/services/api不对这里:h是去尾得到的目录是/opt/services/api所以更合适的场景是cd !$:h。这类小技巧的价值在于避免重复敲长路径尤其是文件名里带一串哈希值时。有两件事要注意。第一历史展开发生在执行前所以你想改一下再跑是来不及的得先敲!!:p只打印不执行确认一下。第二可以在需要时临时关掉set H或者干脆永久关掉set o history之类的配置某些自动化脚本环境里这是必要的因为!在字符串里会被意外展开。3.4 fc把整条命令拉进编辑器改fc是history的兄弟命令很多人完全没用过但它在处理长命令时特别舒服fc -l -20 # 列出最近 20 条等价于 history 20 fc -s 503 # 直接重跑历史里的第 503 条 fc 503 # 把第 503 条拉到编辑器里改完保存退出即执行fc用的是$FCEDIT或$EDITOR指定的编辑器。我自己的配置是export FCEDITvim遇到那种参数多到眼睛花的命令直接fc拉进 vim用 vim 的移动和搜索能力改比在命令行里按左右方向键靠谱太多。这一招在我处理超过两百个字符的命令时几乎是标配。4. 清空、删除与审计写回类操作的实际用法4.1 history -d 删单条注意行号会变想删掉历史里的某一条用history -d Nhistory -d 503这里有一个非常容易踩的坑每删一条后面所有条目的行号都会往前挪一位。所以如果你要连续删多条必须从大到小删或者干脆一次按行号段删history -d 503 505 # 新版 bash 支持这种范围写法到底支持不支持取决于 bash 版本稳妥做法是先bash --version看一眼。删完记得history -w同步到文件否则重开终端它又回来了——这一点和前面讲的双轨机制是同一个道理。4.2 清空要分两步先清内存再清文件最常见的错误操作是只敲一个history -c就以为完事了。history -c清的是内存里的列表文件还在。正确的完整清理流程是history -c history -w先把内存清空再把空列表写回文件。执行完之后文件确实空了。但如果当前系统里还有别的 shell 会话没退出它们的内存列表在退出时依然会写回来所以真正需要彻底清干净的场景比如准备交接机器最好确认没有其他会话在跑或者干脆先把HISTFILE指到别处再处理。还有一种更温和的需求这台机器上的操作不想留痕但也不想破坏别人的记录。那就让当前会话干脆不写文件export HISTFILE/dev/null这样本次会话的命令只存在于内存退出即蒸发。适合临时做演示或者处理敏感信息。想再省事一点set o history可以彻底停止记录连内存里都不进。4.3 多用户与 root谁的记录记在哪Linux 的 shell 历史是按用户隔离的每个用户各自的~/.bash_history彼此看不到。这里有两个容易误解的点。第一用sudo执行命令时记录进的是你自己用户的历史文件不是 root 的。原因是执行sudo的那个 shell 属于你命令是你敲的、由你的 shell 记录的。但如果你是sudo -i或者sudo bash进入了 root 的交互式 shell那之后敲的命令就会记到/root/.bash_history。第二做过安全加固的机器上管理员往往会把HISTFILE指到一个带时间戳和用户名的全局目录配合chattr a之类的只追加属性当作审计日志用。比如export HISTFILE/var/log/shellhistory/${USER}_$(date %Y%m%d).log readonly HISTFILE readonly HISTSIZEreadonly是为了防止有人在会话里直接改掉变量、绕过记录。这类配置一般写在/etc/profile.d/下面全局生效。你在别人的机器上操作时如果发现历史怎么改都不生效先想想是不是被readonly锁住了。5. 五类把历史搞丢的真实场景与排查结论5.1 手工编辑了 .bash_history 却不生效这是出现频率最高的一个。表现是你用 vim 打开~/.bash_history删掉了几行、或者加了几行保存退出重开终端一看——要么删掉的行还在要么加的行没出现。原因前面已经说过shell 退出时会用自己的内存列表覆盖文件。正确的顺序是先让当前会话把内存清空或写盘再编辑再读回history -w # 先把内存写进文件确保两边一致 vim ~/.bash_history # 然后编辑 history -r # 把文件读回内存让当前会话也看到改动这是在同一个会话里操作的顺序。如果你能接受重开一个终端更简单的做法是先exit掉所有会话编辑完再开新的。排查这类问题时永远先问自己一句现在内存里这份列表和文件里那份是不是已经同步了5.2 多终端交叉操作导致记录被覆盖表现是明明上午在 A 终端跑了一堆命令晚上在 B 终端history里就是找不到。根因是histappend没开A 退出时写入B 后来退出时用自己的列表整体覆盖。这个问题在长期挂着 tmux 或者 screen 的机器上尤其常见因为会话开了好几天不关内存列表越攒越长一退出就把别人的记录全压平了。修复方案就三步shopt -s histappend打开追加写HISTCONTROLerasedups控制重复再把PROMPT_COMMAND改造成实时同步export PROMPT_COMMANDhistory -a; history -n; ${PROMPT_COMMAND}这行的意思是每次显示提示符前先把本会话新增的命令追加到文件再从文件里读取别的新增行。这样多个终端基本能做到准实时共享历史CtrlR在 B 终端也能搜到 A 终端刚敲的命令。代价是每次回车都会有一次文件 IO日常使用完全感觉不到但如果你的 shell 提示符本来就很重可能会有一点延迟视情况取舍。5.3 history -c 之后重开又回来了表现是清空了重开终端历史又冒出来。三种可能按概率排序一是只清了内存没写盘~/.bash_history原封不动二是还有别的会话没关退出时又写回来了三是清完之后你用history -r或者配置文件在启动时自动读回了历史文件。排查顺序就是这三条一条条排除很机械也很有效。我遇到过最隐蔽的一次是第三种——某台机器的/etc/bashrc里加了一行history -r每次开 shell 都自动把文件读进来所以在会话里清空完全没意义。5.4 时间戳莫名消失表现是之前查历史明明能看到时间过了几天再看全变成没有时间的了。这通常是因为HISTTIMEFORMAT是你后来才加的之前记录的命令本来就没有时间戳或者历史文件被某个工具同步脚本、配置管理工具、容器的卷挂载重写过把#数字那几行时间标记行吃掉了。前者无解历史就是没有后者的话建议把历史文件从自动同步的范围里排除掉这东西跨机器同步意义不大反而容易出错。5.5 CtrlR 搜不到刚敲过的命令表现是刚敲完一条命令、按了回车、立刻CtrlR搜它搜不到。原因有两个一是在你按回车之前它根本还没进历史二是你开着的其实是另一个 shell 进程两个进程的内存列表不共享只有落盘之后才能互相看见。第二个原因就是上面PROMPT_COMMAND那套做实时同步要解决的问题。如果不想动全局配置临时用history -a手动同步一次也能救急。6. 一份可以直接抄的全局配置与常用函数6.1 /etc/profile.d/history.sh 的完整片段放到/etc/profile.d/下面的好处是所有用户、所有交互式登录都能生效且不用改每个人的~/.bashrc。我给服务器写的版本大致是这样# /etc/profile.d/history.sh # 容量 export HISTSIZE10000 export HISTFILESIZE20000 # 时间戳 export HISTTIMEFORMAT%F %T # 去重与忽略空格开头的命令 export HISTCONTROLignoreboth:erasedups # 噪音命令不进历史 export HISTIGNOREls:ll:la:l:cd:pwd:exit:logout:clear:history:top:free -h:df -h:whoami:id # 追加写避免多终端互相覆盖 shopt -s histappend # 多行命令合并成一行保存避免读历史时错位 shopt -s cmdhist关于最后那个cmdhist值得单独说一句。默认情况下 bash 会把你在命令行里输入的多行命令比如一段for循环、一段while压缩成一行保存用分号连接这样读回历史时不会一行被拆成好几行、导致行号错乱。cmdhist默认是开的但如果你发现历史里的多行命令显示成了怪异的换行检查一下这个选项。它的对立选项是lithist会保留原始换行可读性更好但容易打乱历史结构个人不建议。6.2 几个比 alias 更靠谱的 history 函数alias在非交互式 shell 里默认不生效而且处理参数很别扭所以涉及参数的场景我一律用函数# 搜索历史最多显示 30 条忽略大小写 hg() { if [ -z $1 ]; then history 30 else history | grep -i --coloralways $1 | tail -n 30 fi } # 删除指定行号的历史 hd() { history -d $1 echo 已删除第 $1 条 } # 显示最近 N 条带时间的历史 hh() { history ${1:-20} } # 找出一条命令后放到命令行不执行需要 readline 配合 # 实操上用 CtrlR CtrlE 更方便这里只是示意hg这个函数我每天都在用它的价值在于把history | grep xxx | tail这一串压成一个两字符命令。hd的价值在于防止手滑——直接敲history -d 503很容易删错行号而函数里可以加确认逻辑也可以顺便帮你记住删了哪条。6.3 安全边界别让口令留在历史里这条一定要单独强调。mysql -u root -pPassword123、curl -H Authorization: Bearer xxxxx这类命令会原封不动写进~/.bash_history。而~/.bash_history默认权限通常是 600 没错但很多人会把它同步到网盘、提交进 Git 仓库dotfiles仓库尤其容易犯这个错、或者不小心chmod改宽了。这个问题没有出了事再补救的余地。实操上我建议三层防护。第一层养成习惯任何带凭据的命令前面多敲一个空格配合HISTCONTROLignorespace它就不会进历史。第二层如果已经敲进去了立刻history -d删掉当前会话那一条再history -w同步。第三层别把~/.bash_history纳入任何自动同步或者版本控制的范围这是最根本的。另外像mysql、curl这些工具其实都支持从环境变量或者交互式提示读取凭据能用就别写命令行参数。7. 容易被混为一谈的几组概念hash、Git 历史与其他平台的命令记录7.1 hash 表跟 history 完全是两回事搜相关关键词的时候经常看到有人把这两个放一起问区别确实容易混。hash是 shell 为了加速命令查找维护的一张缓存表——你敲ls的时候shell 本来要遍历$PATH里的每个目录去找可执行文件找到之后把命令名 - 绝对路径这个映射记在 hash 表里下次直接用。用hash -l可以看当前缓存了什么hash -r清空缓存。什么时候需要清最常见的就是你刚装了一个新版本的可执行文件、或者换了个软链接指向但 shell 还在用缓存里的老路径这时候hash -r一下就好了。而history记录的是你敲过什么命令。一个是路径缓存一个是操作日志用途、存储位置、生命周期全都不一样。唯一相似的地方是它们都是 shell 内置维护的状态且都能被 shell 选项影响。7.2 编辑器里的 Git History 不是命令历史另一个高频混淆点是编辑器的 Git 相关插件比如查看提交记录的扩展常常被命名为类似 Git History 的名字。它展示的是仓库的 commit 时间线跟 shell 里敲了哪些命令毫无关系。有人搜history 怎么看搜到这类扩展装完之后发现完全不是自己想要的白白浪费时间。判断标准很简单如果它出现在编辑器侧边栏、展示的是一堆 commit 信息和作者那就是 Git 工具如果它跟着终端会话走、记录的是你敲的每一行字那才是 shell 的 history。同理Windows 平台上的命令记录机制又是另一套CMD 里没有持久化的历史文件只能靠环境变量临时记录PowerShell 有自己的Get-History和历史文件。跨平台时这些概念不能直接搬运。7.3 zsh 和 fish 的历史文件长什么样现在很多人的主力 shell 已经不是 bash 了所以有必要说清楚差异但别指望这套配置能直接照搬。zsh 的历史文件是~/.zsh_history默认情况下写入格式带时间戳和耗时长这样: 1710468753:0;git status冒号后面第一个数字是命令执行的时间戳第二个是执行耗时秒。如果你用文本工具处理这个文件得先知道这个格式规律否则解析出来全是乱的。另外 zsh 的历史共享行为由setopt SHARE_HISTORY、INC_APPEND_HISTORY这些选项控制和 bash 的histappend不是一回事配置项名字也不通用。fish 的历史文件在~/.local/share/fish/fish_history格式更像 YAML 片段带when字段记录时间。它默认就做了不少组件化管理每个会话的历史共享做得比 bash 开箱好用代价是文件结构和 bash 完全不兼容。所以如果你在 bash 和 zsh 之间切换配置别直接复制粘贴尤其别把 bash 的HISTTIMEFORMAT写进 zsh 配置里——zsh 不认这个变量写了也不生效还会让你困惑半天为什么时间戳没出来。最后分享一个我自己吃了亏之后才养成的习惯在服务器上排查完问题、准备关终端之前先敲一次history -a把本次会话新增的命令立刻落盘。这样即使后面进程被强杀、或者终端窗口被误关这次的排查过程也能留下来。我就吃过一次亏——排查了四十分钟的故障终端崩了什么都没留下只能凭记忆重新走一遍。从那之后history -a就成了我关终端前的固定动作成本极低收益在关键时刻是能救命的。
返回列表