今天早上刷 HN,看到一篇 380 多分的热文,标题就叫 Small Programming Tricks,作者 Will Keleher。核心论点一句话就能说完:日常工程效率的大头,不是什么大架构决策,而是一个个小的知识点------知道某个语言特性存在、知道一条 git 冷命令、知道一个 sed 的写法。 说实话,"小技巧合集"这个题材我平时是直接划走的,它和水文之间只隔一层纸。但这篇有个不一样的地方:给的每个技巧都不需要前置知识,属于"知道就能用",而且作者提了一嘴自己的做法------之前在公司每天往 Slack 发一个技巧,发了一年,反响意外地好。我来了兴致,把终端打开,挑了几个挨个跑。有俩确实好使,还有一处原文的说法有歧义,实测之后才搞清楚坑在哪。 先说最值的。
git log -S,找一段代码的生死记录
原文管这个叫 "git pickaxe"(镐子),命令是 git log -S"字符串"。作用:列出所有让这个字符串在代码库里"出现次数发生变化"的提交。说白了就是找一段代码的生与死------它是什么时候加进来的,又是什么时候被删掉的。
我正好有个现成的案例。去年我们网关里有个限流逻辑,方法叫 checkQuota,后来一次大重构之后整个没了。当时想查它是被哪个提交动掉的,用的是 git blame,但重构把代码挪来挪去,blame 追到的行早就不是原来那行了,查了个寂寞。 这次直接:
lua
git log -S"checkQuota" --oneline
输出大概长这样:
yaml
b7e2d41 perf: 令牌桶替代计数器,网关层限流重写
8a3f19c fix: checkQuota 并发场景下的计数越界
c01d9e2 feat: 网关层接入限流
三个提交,把这段代码的一生讲完了:c01d9e2 把它加进来,8a3f19c 修过一次 bug,b7e2d41 把它删了。注意中间那个 fix 提交-------S 能看到它,是因为那次修复改变了 checkQuota 这个字符串的出现次数(函数体里多了一次调用)。
这和 git blame 的区别值得单独说一句。blame 回答的是"这行代码现在是谁写的",一旦代码被移动或重构,blame 的锚点就断了;-S 回答的是"这个字符串的生与死",它不关心代码在哪一行,只关心出现次数。所以越老的、被反复重构的代码库,-S 越好用。 原文还提了一句 git log -G"正则" 和它类似,但行被移动时也会显示。这个区别我正好构造了个场景试了:把调用 checkQuota 的那几行从方法头部挪到尾部,不增不减。
shell
$ git log -S"checkQuota" --oneline # 无输出:出现次数没变,镐子不看它
$ git log -G"checkQuota" --oneline
e4c1b99 refactor: 调整校验调用位置
结论很清楚:-S 数出现次数,纯移动它不管;-G 看 diff 里有没有匹配正则的增删行,挪动位置也算。想要"生死记录"用 -S,想要"所有碰过它的提交"用 -G。
原文一个有歧义的说法:git checkout -
原文里有句 git checkout -,说它类似 cd -,能"check out your previous HEAD"。我第一反应是这玩意我经常用啊,不就是回到上一个分支吗。但再一琢磨,"previous HEAD"这个说法容易被理解成"回到上一个提交",然后有人在自己分支上敲一下试试------
css
$ git branch
* feat-search
main
$ git checkout -
Switched to branch 'main'
看,在 feat-search 上敲 git checkout -,它把你切回 main 了。"上一个 HEAD"指的是上一个所在的分支位置(等价于 git checkout @{-1}),不是上一个提交。如果你刚从 main 切到 feat-search 想安安静静干活,这一敲直接给你送回 main,工作区的东西还在,但分支换了,接下来的提交就跑到 main 上去了。 它真正好使的场景是来回横跳:改 feature 分支,切 main 看个东西,git checkout - 一步回来,再敲一下又过去,和 cd - 一个手感。至于"回到上一个提交",那是 git reset --hard HEAD~1 的事,别混。原文这句不算错,但确实容易让人想歪。
历史搜索:fzf 好使,atuin 惊艳
Ctrl+R 反向搜索历史,这个估计大多数人都知道。原文推荐的两个进阶,我都装了。 fzf 装好 shell 集成之后,Ctrl+R 变成模糊搜索,敲几个片段就能把那条命令捞出来,体验比原生好一截。这个属于"装了就回不去"系列,没什么好说的。 真正让我意外的是 atuin。它把 shell 历史整个搬进一个 SQLite 数据库:
arduino
curl --proto '=https' --tlsv1.2 -LsSf https://setup.atuin.sh | sh
atuin import auto # 把现有 shell 历史导进去
然后在 .zshrc 里加一行 eval "$(atuin init zsh)",重开终端,Ctrl+R 就变成它的全屏搜索界面了。按目录过滤、按退出码过滤、按时间范围过滤,都能做。命令行里也能直接查:
shell
$ atuin search "docker compose" --limit 3
我库里攒了小半年的历史,import 完两万多条,搜两个月前那条带一长串挂载参数的 docker 命令,两秒的事。以前这种时候我的操作是 history | grep,翻半天。 要说顾虑,就是它默认还想让你注册账号做跨机同步------同步是可选的,不注册纯本地用也没问题,但我能理解有人对"历史命令上云"过敏,我自己是只开本地。
其他几个,顺手验了下
剩下的就不挨个展开了,挑几个说印象:
SELECT 不带 FROM 这条,原文举的例子是 SELECT TRUE <> NULL,让我先猜返回什么。我猜 true,拿 sqlite3 一跑,返回的是空(NULL)------NULL 和任何值比较结果都是 UNKNOWN,SQL 三值逻辑这个坑,光看书真不如自己跑一遍记得牢。
EXPLAIN ANALYZE 是常识级的,PostgreSQL 和 MySQL 8.0.18+ 都支持,真执行一遍查询再给性能数据,比纯 EXPLAIN 靠谱,做慢查询优化的应该都在用,不细说。
globstar 那条我之前真没用过:bash 里 ** 递归匹配默认是关的,shopt -s globstar 打开后 ls **/*.md 能替代大部分 find,zsh 原生就支持。就顺势把 rg 的事也说了------rg 替代 grep 没什么可争的,没换的今天换。 JS 那几条里 Promise.withResolvers() 我之前真不知道,Node 22 起可用:
arduino
const { promise, resolve, reject } = Promise.withResolvers();
以前要自己在外面声明两个变量才能在 Promise 外面控制 resolve,现在一行。写任务队列的时候挺干净。
写在后面
一上午跑下来,我的结论和原文重合度挺高:这堆技巧单拎出来哪个都撑不起一篇文章,但它们是纯赚的------学的时候两分钟,用的时候每次都省一点,攒起来就是复利。
我自己的做法是在笔记里开了一个"终端小技巧"的页面,每次从别人文章或者 HN 评论里捡到一条就记一条。这次实测完又添了三条进去。至于原文作者那种每天往群里发一条的做法,我们组倒是可以试试,就是得做好被同事嫌烦的心理准备。 工具全集齐了,活还是得自己干,这个道理小技巧救不了。