CUA:从终端历史命令到操作习惯分析的轻量工具

📅 发布时间:2026/10/10 11:43:59
CUA:从终端历史命令到操作习惯分析的轻量工具
最近我在整理终端里日常敲过的命令时做了一个内部小工具代号就叫 CUA全称是 Command Usage Analyzer。它做的事情听起来很直白把 bash 历史、zsh 历史、脚本执行记录、甚至构建日志收拢到同一个地方再按时间、目录、命令类型拆开统计最后生成一份能看出“时间都花在哪”的报告。整个过程会涉及一点 bash、一点 Python、一点 cron但核心不是代码量而是分析思路。这个工具最打动我的地方是它把很多原本“凭感觉”的问题变成了可查的数据。比如到底每天在重复构建多少次、哪个目录下命令最多、哪条命令总是占了大量等待时间。单看某一台机器觉得没什么但拉长时间维度之后规律会很明显。适合谁来参考一类是经常在终端里折腾脚本、构建、部署的同学另一类是带小团队、想了解成员操作习惯和自动化覆盖情况的同学。只要你愿意从历史命令里挖信息CUA 这套思路就能直接落地点。1. 为什么要把“cua”理解成“命令使用分析”1.1 一句话说清 CUA 是什么CUA 不是某个现成的开源项目而是一套我们自己约定的命令使用分析方案。采集端负责把终端历史、脚本日志、定时任务输出统一记录成结构化文本分析端则在每天固定时间做清洗、去重、归一化最终输出“命令频率”“高频目录”“耗时估算”“异常行为提示”这几类结果。整个过程不需要装重量级监控软件也不需要所有机器都接入同一个平台只要终端里能追加写文件就能把数据流搭起来。很多人在刚听说这个方案时以为它跟终端回放工具有点像。其实差别很大。终端回放记录的是屏幕内容偏“现场录像”而 CUA 记录的是命令本身、执行目录、时间点、耗时偏“数据统计”。录像适合排查单个事故统计适合发现长期规律。比如你很难从录像里看出“这周 git 操作占比从 30% 涨到了 60%”但在 CUA 聚合结果里这种变化一眼就能定位。1.2 它要解决的真实痛点先说一个最常见的场景。某个小组里开发机、测试机、构建机各有不同的 shell 历史配置默认情况下 bash 历史只能存最近几千条多终端同时开着还会互相覆盖。等你想回溯某次上线操作时历史记录已经丢得七七八八了。CUA 第一件事就是解决这个“记不住”的问题在每次命令执行前把完整记录追加写到独立文件里彻底绕开 shell 历史覆盖机制。第二个痛点是“看得见但没法量化”。大家都会 view 历史但人眼扫几千条命令不现实。CUA 通过规则把命令做归一化比如把 build_linux.sh、build_mac.sh 归为“build 脚本”把所有带随机端口号的 curl 归为“curl 探测”这样聚合出来的结果才有意义。没有归一化时统计会碎成几百个长尾条目完全没法看。第三个痛点是“耗时不透明”。一条构建命令是跑 5 分钟还是 50 秒直接影响工作节奏但日常不会有人专门去计时。CUA 利用命令前后时间戳估算耗时再结合命令类型做分层能识别出构建、单元测试、依赖安装这类重操作的占比。这些数据对个人复盘和团队改进都很有用。1.3 为什么不直接用现成监控平台有人会问市面上的可观测性平台那么多随便接一个 agent 不就能拿到终端数据理论上可以但实际会碰到几个问题。第一终端命令属于高敏感操作记录直接上报到公司级监控平台审批和安全约束很重很多团队根本推不动。第二监控平台的数据模型围绕服务端指标设计对“某个用户在某个目录下敲了什么命令”这种场景覆盖得很别扭还需要额外写转换层。第三平台一旦部署维护成本就上来了dashboard、告警规则、权限模型全都要管理。CUA 这种轻量方案的定位正好相反只在本地落盘由定期任务做聚合结果可以保存在个人目录或共享目录。它不追求实时不追求全量只追求在需要时能还原出操作趋势。对一个小团队来说维护成本基本等于零。对个人开发者来说它就是一套脚本不会绑架你的工作流。下表是我当时对比现成方案和自制方案的几个维度对比维度现成监控平台自制 CUA 方案数据采集需要 agent 部署一行 PROMPT_COMMAND 配置数据隐私默认上报中央端本地文件可控共享数据模型面向服务指标面向终端操作维护成本平台升级、权限管理脚本维护分析灵活度依赖平台能力任意改规则出报告速度配置繁琐每天定时生成 markdown真实选择的时候关键不是“谁更强大”而是“当前阶段要解决什么问题”。如果已经有一整套成熟的可观测性体系接命令记录进去可能更合规如果只是觉得自己的操作习惯需要复盘自制 CUA 的性价比要高得多。1.4 功能边界与预期管理做 CUA 前一定要明确边界否则期望很容易失控。它不能做实时入侵检测不是安全审计工具也没有权限管理能力。它更适合做“个人/小组操作习惯分析”和“高频重复度识别”。如果要做安全审计请去对接真正的审计系统如果要精细记录每条命令的入参那应该在 shell 层做更严格的 hook而不是靠历史日志事后分析。我当时给自己定的功能边界是只采集命令文本、时间戳、目录、退出码这几个字段只做轻量归一化只输出汇总报告不保留全量明细超过 30 天。这样既控制了存储量也避免数据越积越重最后没人清理。这个边界很重要因为一旦开始收集敏感数据后续被各种规范缠住就会寸步难行。2. 核心细节解析从原始命令到可用指标2.1 采集层把丢失的历史记录补回来采集是整个 CUA 的地基。shell 历史默认行为不太稳定bash 在非交互模式只读 HISTFILEzsh 多数时候依赖 INC_APPEND_HISTORY多个终端并发时还会互相覆盖。最稳妥的办法是不依赖 shell 历史文件而是在 PROMPT_COMMAND 里手动追加记录。以 bash 为例可以在 ~/.bashrc 里加这样一段export PROMPT_COMMANDhistory -a; echo [$(date %Y-%m-%dT%H:%M:%S)][$PWD][$?] $(history 1 | sed s/^ *[0-9]* *//) ~/.cua/raw_usage.log这段配置做了三件事先同步内存历史到 HISTFILE再获取当前目录、时间、上一条命令的退出码最后把命令文本追加到独立日志。稍微解释一下退出码字段它不参与日常统计但在定位“哪条命令频繁失败”时非常有用值得提前存下来。zsh 用户也可以找到等价方案在 precmd 钩子里做同样的事precmd() { local cmd$(fc -ln -1) print -r -- [$(date %Y-%m-%dT%H:%M:%S)][$PWD][$?] $cmd ~/.cua/raw_usage.log }需要注意的是采集脚本要写成幂等的。不要用 echo 直接覆盖文件要用追加模式不要加复杂的行数清理逻辑后续由分析脚本统一做轮转避免每次命令都触发额外开销。2.2 清洗层去掉噪音保留模式采集文件里的原始记录很脏。比如同一台机器上有人敲了git commit -m fix: update config有人敲了git commit -m init project如果直接统计命令字符串这两条会变成完全不同的条目无法反映“大家主要在提交代码”这个事实。清洗层就要把参数值替换成占位符保留命令骨架。具体做法是维护一组正则规则。我把规则写在normalize_rules.jsonPython 分析脚本逐条加载匹配到规则就做替换。比如rules [ (rgit commit -m [\\].*?[\\], git commit -m msg), (rgit log --oneline -n \d, git log --oneline -n count), (rcurl .*?--data [\\].*?[\\], curl --data payload), (rcd [~/.\w\-/], cd path), (rpython[\w.]* [\w./_\-]\.py, python script), ]替换顺序很有讲究。带有具体参数的规则要先处理通用规则放到后面。否则git commit -m可能会被git .*这类宽泛规则提前吞掉归一化就失效了。每条规则在生效之前最好取样 200 条历史命令人工核对一遍结果不要一把梭全量替换。2.3 聚合层按时长、频率、目录三维度统计清洗完之后接下来要做聚合。最常见的三个维度是命令名、目录、时间窗口分别回答“我在干什么”“我在哪里干”“什么时候干”。命令频率统计最简单直接按清洗后的命令字符串分组计数。目录统计则是看每个工作目录累计执行了多少条命令、多少相对耗时。时间窗口统计要刻意区分“工作日”和“周末”因为构建机和开发机的执行模式完全不同混在一起会掩盖真实规律。耗时估算是聚合层最容易出错的地方。CUA 里我没有用精确测量而是用相邻两条记录的时间差做近似。这个近似值在交互式终端里偏大因为用户可能盯着屏幕发呆或者思考。更稳的办法是只统计那些明显是“重命令”的操作比如 build、test、deploy再结合命令退出时间来做分层。下面是我常用的聚合字段统计字段来源说明command_skel归一化后的命令用于频率聚合work_dir采集时记录的目录用于目录聚合hour_bucket时间戳的小时字段用于时段分析exit_code命令退出码用于失败率分析duration_sec相邻记录时间差用于耗时近似我当时还做了一个“重命令池”的概念只有匹配池内规则的命令才会被计时的聚合表消费比如 npm install、mvn package、docker build。这样既能减少噪音也让耗时报表更有参考价值。2.4 输出层日报和周报怎么设计最不烦人输出层决定了工具能不能被长期用下去。如果每天打开报告发现大部分内容都没变化很快就会被忽略。我当时把输出拆成了三个级别日报只在有异常时输出摘要周报固定输出整体趋势月度报告走汇总。日报的规则是“没有异常就不单独推送”。异常规则可以自定义比如单条命令失败率达到 20% 以上同一命令一天内执行超过 50 次长时间没有构建命令执行。这些规则很适合用阈值告警不用做复杂模型。周报我建议生成 Markdown 文件放到共享目录包含 Top 命令、Top 目录、失败命令排行、时段活跃度四块。排版以表格为主避免长段落。我曾经在周报里塞了一大段文字总结结果阅读率明显下降改成表格 简短结论之后大家反而愿意看了。输出层还有一个隐藏需求数据要被“遗忘”。CUA 的清洗脚本每周做一次日志轮转超过 30 天的训练数据压缩归档超过 90 天直接删除。这不是为了省存储而是避免数据资产越滚越大给自己惹麻烦。3. 实操记录跑通第一版 CUA 的全过程3.1 环境准备与目录结构CUA 不依赖特定环境只要机器上有 bash 和 Python 3 就行。我当时在一台开发机上先做验证目录结构大概长这样~/.cua/ ├── raw_usage.log # 采集原始日志 ├── archive/ # 周轮转归档 ├── reports/ │ ├── daily.md # 日报 │ └── weekly.md # 周报 ├── normalize_rules.json ├── collect.sh # 采集辅助脚本 ├── analyze.py # 分析主脚本 └── cron.sh # 定时任务入口这个结构有个好处日志、报告、脚本分离避免分析脚本误操作原始数据。建议把 raw_usage.log 权限设为 600毕竟里面包含完整命令历史属于敏感数据。归档目录可以启用简单加密或者只保留当前用户可读。3.2 采集脚本bash 历史自动落盘我在第一版直接用了 PROMPT_COMMAND但后来发现一个问题如果终端开得很多每条命令都触发外部 sed终端响应会有一点点延迟。虽然毫秒级但对终端流畅度敏感的人来说很别扭。于是我把采集逻辑改为先写入内存缓存再定期 flush。简化版本是先继续用 shell 自带功能但把 sed 替换成内置字符串处理。因为 history 输出的行号前会有空格用 bash 参数扩展可以做得更快。下面是我后来用的版本# .cua/collect.sh log_file$HOME/.cua/raw_usage.log ts$(date %Y-%m-%dT%H:%M:%S) line$(history 1) line${line#*[0-9] } line${line#${line%%[![:space:]]*}} printf %s [%s] [%s] %s\n $ts $PWD $? $line $log_file这套方案只依赖 bash 内建能力性能上比调用 sed 好不少。如果你用 zsh可以用fc -ln -1拿命令文本再配合precmd钩子。重点是无论用哪种都不要在命令路径里放空格否则解析字段时很容易错位。3.3 定时任务每天凌晨聚合一次采集是每时每刻都在发生的分析则没必要高频跑。我选择在凌晨 3 点做一次聚合顺便完成日志轮转和异常判定。cron 配置如下0 3 * * * /bin/bash /home/dev/.cua/cron.sh /dev/null 21cron.sh 的主要职责是调用 analyze.py并判断是否生成日报。如果用 crontab 本身管理注意环境变量问题cron 默认 PATH 很短建议在脚本开头显式定义 Python 路径或者直接用绝对路径调用。我踩过好几次脚本在终端正常、在 cron 里却跑不起来的坑后来统一在入口脚本里写了 export PATH。# cron.sh export PATH/usr/bin:/bin:/usr/local/bin cd $HOME/.cua python3 analyze.py --mode daily python3 analyze.py --mode weekly分析脚本需要自带幂等性同一天跑两次不要产生重复报告。我在脚本里用日期做输出文件名同时先检查文件是否已生成如果存在则覆盖不存在则新建。这样手动补跑也不会产生多个碎片文件。3.4 分析脚本Python 处理核心逻辑分析脚本是整个 CUA 里信息密度最高的部分。第一版不需要多复杂只要做到读取、清洗、聚合、输出四件事。我这里摘一段简化的核心逻辑方便你直接参考import json, re, collections, datetime def load_rules(path): with open(path, encodingutf-8) as f: return [tuple(r.items())[0] for r in json.load(f)] def normalize(cmd, rules): for pattern, repl in rules: if re.search(pattern, cmd): cmd re.sub(pattern, repl, cmd) return cmd.strip() def parse_line(line): # 格式: [时间][目录][退出码] 命令 head, cmd line.split(] , 1) head head.lstrip([) ts, work_dir, code head.split(][) return { ts: datetime.datetime.fromisoformat(ts), dir: work_dir, code: int(code), cmd: cmd.strip(), } def aggregate(lines, rules): stats collections.Counter() dirs collections.Counter() for l in lines: rec parse_line(l) skel normalize(rec[cmd], rules) stats[skel] 1 dirs[rec[dir]] 1 return stats, dirs这里隐藏的细节是 parse_line 的字段分割。原始日志中时间和目录都用方括号包裹如果命令本身包含方括号不能用简单 split 直接解决。正确做法是先按]切出头部和命令体再单独解析头部三个字段。别不信真实日志里太多[xxx]风格的命令了不做这一步肯定会出错。3.5 结果抽查先别急着优化先看数据是否可信分析脚本落地之后不要立刻看结论先抽查数据质量。我当时做了一次“对照实验”手动选 100 条历史命令把 CUA 归一化后的结果逐一与真实命令对比同时检查每条记录的目录和时间是否对得上。抽查完发现两个问题一是某些路径带空格导致目录字段被切错二是 cargo 和 npm 这类命令的日志输出包含转义字符会影响正则。校验思路很简单写一个 debug 模式输出“原始行 - 解析字段 - 归一化结果”然后抽样打印到终端。python3 analyze.py --debug --limit 20输出大概长这样raw: [2025-04-01T10:12:33][/home/dev/proj][0] git commit -m fix parsed: ts2025-04-01T10:12:33 dir/home/dev/proj code0 cmdgit commit -m fix normalized: git commit -m msg如果 20 条样本里超过两三条解析异常就要先修解析规则不要强行进入统计阶段。数据质量不合格时任何财报式输出都是自欺欺人。4. 常见问题与排查技巧实录4.1 历史记录缺行或乱序采集中最常见的问题是缺行。缺行通常是多个终端写入同一个 raw_usage.log 文件时发生竞争导致多行内容互相拼接。解决办法是给每行加一个随机或递增的序列号或者把日志按天拆分成独立文件避免多进程并发写同一个文件。乱序问题大多出在 PROMPT_COMMAND 的执行时机。bash 的 PROMPT_COMMAND 在显示提示符之前执行而 history 列表在交互式输入后才会更新如果配置不对可能把上一条命令的内容写到当前时间戳下。我的经验是在交互终端中history -a 必须先执行再取最后一条记录。不要在取记录之后才同步。如果发现某天数据明显变少可以先看 raw_usage.log 大小再对比终端启动数量。只要是“终端没关就不会丢记录”这个判断方向基本不会错。补充一点tmux 或 screen 里跑终端时采集配置要对所有会话生效否则在某个 session 里敲的内容不会进入日志。4.2 别名展开导致统计失真很多人会用别名简化日常命令比如把g log映射成git log --oneline --graph。如果采集层拿到的还是原始输入g log归一化规则根本命中不了但如果采集层拿到了展开后的完整命令统计口径又会和用户实际输入不同。两种方式我都试过最后选择了“原始输入 别名映射表”。做法是把常用别名单独存一份映射文件分析时先做别名展开再做归一化。例如alias gsgit status alias gpgit push alias gppgit pull --rebase映射表里记录gpp对应git pull --rebase。归一化处理时先用简短的别名规则还原成标准命令再走默认的清理规则。这样统计结果既反映真实输入习惯也能归类到同一命令族。如果直接靠 bash 的alias内建命令导出映射结果会混入很多临时环境变量不建议。4.3 目录字段缺失或路径漂移有时候采集日志里目录字段会变成空值。原因往往是用户的 shell 提示符重新计算了 PWD而记录命令的时刻恰好在一个异步任务还没更新路径的间隙。应对方式是不要依赖$PWD改用pwd -P获取物理路径避免符号链接导致的路径漂移。路径漂移在高频切换目录的场景下很明显。比如/home/dev/app和/home/dev/app/.实际是同一个目录但字符串不同统计时不处理会被分成两条。我当时在处理目录时多做了一步先用os.path.realpath()规范化路径再把末尾多余的.去掉。这一步放在 parse_line 阶段完成避免聚合时反复计算。4.4 输出文件过长影响统计raw_usage.log 如果不做轮转半年后可能积累几十万行Python 分析脚本每次加载都会变慢。解决方式是按周归档原始日志并在 cron 脚本里限制分析范围只读取最近 7 天文件。find ~/.cua/archive -name raw_usage_*.log -mtime 90 -delete归档文件命名格式建议带日期raw_usage_2025-W14.log。这样后续如果想做跨周期对比还能按周拼接。分析脚本入口加一个--since参数默认只处理最近 7 天需要回溯时手动扩大范围。90 天前的数据基本失去统计价值删掉即可不要手软。4.5 团队协作时如何避免个人脚本互相覆盖当 CUA 从个人工具变成小组工具时数据共享和脚本冲突会同时出现。各成员机器上都会维护一份自己的 normalize_rules.json如果没有统一版本管理规则很快会出现分支。我的建议是脚本代码放在共享仓库里私有规则保留在各自目录统一个一个规则文件只能作为公共默认值个人可以用局部文件覆盖。共享原始日志时要注意多做一层脱敏。比如把目录中的用户名替换成占位符把git commit -m的 message 部分替换成msg。共享脱敏后的聚合结果而不是原始日志。别人不需要知道你敲过什么只需要知道某个命令族的使用频率。这块边界如果不划清楚团队里很快会有人因为隐私顾虑反对继续推广。最后说一个我在实际使用中很受益的习惯CUA 这类工具真正的价值不在于把报表做得多漂亮而在于它能把“我以为自己每天在干什么”和“实际每天在干什么”之间的差距暴露出来。我会把报警条件调得很保守平时不看日报只看每周统计里不正常波动的部分。某周 build 命令频次突然翻倍我会先去查是不是自动化脚本出了问题某条命令失败率连续抬升我会优先确认依赖源和最近的变更。这个习惯坚持了几个月后我对环境的整体感知比以前靠记忆靠谱得多。