1
13
2024
12

使用 atuin 管理 shell 命令历史

atuin 是最近在群里看到的工具。功能和我自己用 skim 糊的脚本一样,搜索并执行 shell 的命令历史用的。但是,它的数据存储使用的是 SQLite3,并且它是使用 Rust 编程语言编写的。于是事情有了一些好的变化。

首先,因为 atuin 并不像 Web 服务那样,会持续打开并操作数据库,所以 SQLite3 并发容易报错的问题并不需要担心。而 atuin 会记录执行时间、耗时、工作目录和退出码等信息。更多的元信息,能给之后的搜索和分析提供更多帮助。

其次,因为搜索走的数据库查询,因此并不需要像我用 skim 那样,每次把全部历史加载到内存。这样就可以保留更多的历史记录而不用怕越用越慢了。不知道 SQLite3 的搜索功能效率如何,但我几万条记录,已经可以明显感觉到加载耗时的差异了。

最后,它是 Rust 写的——这点很重要,因为这大大地方便了我对它进行修改(而不像某 Zig 写的工具,我翻了半天文档都没改对最后只好放弃了)。

当然,让我没多犹豫就决定尝试 atuin 的最重要的原因是:它独立于 shell 原本的历史记录功能,并不会取而代之。所以它要是不合我意,我只要把它删掉就好了,原本的历史记录丝毫不受影响。

于是我到现在已经用半个月了,结果非常满意。不过也已经对它做了好多修改了。比较重要的如下:

  1. 支持 Shift+Del 键删除记录。有时候会不小心打错命令。这种命令记在历史里时不时被翻出来,不但占用显示空间,还容易不小心选错然后相同的错误又犯一遍,甚至因为没看清命令细节而不小心删掉好多文件(还好我有快照)。atuin 的作者最近也加了删除功能,但是是在另一个界面操作,对于我这种经常在找命令的时候要删除多条命令的用法来说,不光麻烦,而且上下文切换的代价很大,会忘记自己原本是要干什么的。

  2. 精确匹配模式,这是 skim 的叫法。你叫它多子字符串匹配也行。自从 fzf 流行以来,大家都迷上了子序列匹配的所谓「模糊匹配(fuzzy match)」。但是我不喜欢这种匹配方法,会给出太多不相关的结果,加大脑力负担。真正好的模糊匹配是 agrep 那种基于编辑距离的算法,打错点字没关系那种。所以我给 atuin 也加上了精确匹配模式,同时还提升了查询性能呢。

  3. 反转 UI 的 --invert 命令行选项。像我之前使用 skim 那样,当光标位于终端窗口的上半部分时,我希望我搜索时打字的地方在上方;而当光标来到终端窗口的下半部分时,搜索时光标也放下边。这样关注的焦点就不会跳很远,有连贯性,节省认知脑力。atuin 本身有反转 UI 的功能,但是是写在配置文件里的,而我需要视情况决定要不要反转 UI,所以还是得加个命令行选项。

  4. 更改了选中项的颜色。atuin 原本用的是红色,我总觉得是哪里有报错……

还有些不太重要的修改,可以来我的分支查看:https://github.com/lilydjwg/atuin/commits/lilydjwg。注意这个分支我会经常 rebase 的。

另外我修改过的 zsh 插件在 https://github.com/lilydjwg/dotzsh/blob/master/plugins/atuin.zsh

值得一提的是,atuin 还支持同步。同步的数据本身是加密的,但是还是会泄露一堆诸如什么时候在跑命令之类的元数据,所以我自己跑了个服务。服务很好跑,但是同步似乎有些问题。我两个系统,两边都导入了之前的历史记录并同步,但是后同步的那个系统上的历史,很难被同步过去。甚至 atuin 发现本地比远程多,就从最新开始慢慢上传,直到两边一样多;如果远程比本地多,那就把远程的删掉一些(我也不知道它删了哪些,我是看到访问日志里巨量的 DELETE 请求才意识到问题的)。总之经过我不懈地反复运行,最终它只比远程多几条记录了,并且绝大部分历史记录已经两边都有了。我猜它可能没想到我会从不同的系统上同步已有且已分歧的数据吧……

Category: shell | Tags: linux shell Rust zsh atuin
9
16
2018
10

人生苦短,我用 skim

前两天我又看到了基于子序列匹配的字符串过滤工具 fzf 的绚丽效果了。实际上我很早就听说了这个工具,只是懒得动手配置。此次提及,我发现 fzf 已经在官方软件源里了,而我也正好有时间,所以打算试一试。

然后呢,Arch Linux CN 群组里艾穎初提到 skim 这么一个工具。了解了一下,这个就是 Rust 版本的 fzf,并且在 archlinuxcn 源里也有(git 版本,即 skim-git)。这太好了,就是它了!

skim 的操作很简单。文章开头的链接里已经有效果演示了。常用的也就是输入子序列去过滤,然后再输入一个进一步过滤,直到看到想要的。使用 ! 前缀可以反向过滤,^ 匹配开头 $ 匹配结尾。Ctrl-p/n 来上下移动。提示符那里也支持通常的行编辑。

到现在为止,我自行实现了 sk-cd、sk-search-history、sk-vim-mru 三个功能。另外使用了自带的 completion.zsh 文件。由于各种不满意,没有使用自带的 key-bindings.zsh 文件(也就包含 cd 和历史命令搜索功能啦)。

completion.zsh 里目前有两个功能。kill 时通过 ps 补全进程 pid。这个想法很好,以后我可能专门做一个通用的方便 strace 啊 lsof 啊 gdb 啊之类的用。

另一个是遇到两个星号(**)时按 Tab 补全,查找并替换成当前目录下的文件。

我实现的 sk-cd 是从 autojump 取目录列表,然后喂给 skim。于是就成了交互式的 autojump~这是一个我很需要的功能。原来我都是通过 Tab 补全列出可能的项,然后再 Tab 过去选的,有些慢也有些麻烦。

sk-search-history 就是在历史命令里找东西。因为遇到特殊字符时无法正确地加载预览,我并没有开启预览功能。反正找到的命令只会放在命令行上,并不会自动执行的,选错了可以及时取消。

以上两个功能分别绑定到 Alt-s d 和 Alt-s r 上。我使用 Alt-s 作为 skim 快捷键的开头,以便保留 zsh 原本的快捷键,避免冲突,特别是以后可能会有更多功能被加入。我在 Vim 里,也是类似的做法,Alt-q 是 easymotion 的开头快捷键,Alt-d 是 denite 的开头快捷键。

sk-vim-mru 仅仅是个命令了。使用的数据是 mru.vim 的历史记录文件。然后做了两个函数:vim-mru 使用 Vim 编辑文件,vv-mru 使用我自己的 vv 命令在已有的 gVim 里编辑文件。

我做的版本和 skim 自带版本,最大的差别在于,我的版本会尽量使用全部的窗口空间,而 skim 自带的总是会使用 40% 窗口高度。(所以我有个函数用来获取当前光标位置,有需要的可以自己拿去用。)

如果你想用我的配置,可以 wget https://github.com/lilydjwg/dotzsh/raw/master/plugins/sk-tools.zsh 回去,然后 source 一下就好。有需要的话(比如数据来源、键绑定等)可以自行修改。


2018年09月17日更新:我尝试了一下把 sk-search-history 映射到 Ctrl-r 上,然后很快就放弃了。因为 skim 的结果是不可预测的,而默认的 Ctrl-r 的结果是完全可预测的(只要还记得;当然你不能开(那个让我在服务器上误杀过进程的)实时历史共享)。可预测性对提高效率非常关键,因为你不需要中断思维,停下来等结果。

Category: shell | Tags: linux shell zsh Rust
9
28
2017
9

To hup or not to hup

故事起源于同事想在后台跑一个服务:

$ nohup node xxx.js &

一切如愿。

——是吗?

实际情况是,这时退出 bash 是如愿了,但是直接关掉终端窗口的话,那个服务会死掉。

bash 奇怪行为之五

(我好像没有写前四个耶。等有时间了简单写一下吧。)

man bash 然后搜索 SIGHUP,你会发现,其实默认设置,bash 正常退出时,根本不会杀害后台进程。它们会和从脚本里运行时一样欢快地继续跑下去。只有 bash 因为收到 SIGHUP 而退出时,它才会给后台进程发 SIGHUP。

所以,直接 Ctrl-D 或者 exit 退出的话,(处理好了重定向的话,)要不要 nohup 都一样,进程不会死。

zsh 默认退出时会给后台任务发送 SIGHUP(除非你 disown 了)。

但这还是不能解释关窗口的时候,服务为什么会死掉呀?nohup 不是已经忽略掉 SIGHUP 了么?

与众不同的 nodejs

通常情况下,nohup 工作得很好。但是,UNIX 世界里来了位不了解、也不愿意遵循 UNIX 传统惯例的年轻气盛的小伙子。

我还记得 npm 直接往 /usr 下安装东西。

我还记得 npm 把 http_proxy 当 https_proxy 而我的缓存代理不支持 HTTPS,造成无法安装任何东西。

现在,nodejs 将所有信号的处理重置为默认行为,除了它自己想处理的那几个。

「nohup?那是什么鬼?我搞不懂!」nodejs 说,然后它被 SIGHUP 杀死了。

结语

The devil is in the detail!

Category: Linux | Tags: nodejs linux bash shell
11
3
2016
24

诡异多多的 bash

要说哪个 shell 最复杂难学,我肯定回答 zsh。而要说哪个 shell bug 最多,毫无疑问是 bash 了。shellshock 这种大家都知道的我就不说了。bash 有很多很诡异的角落,昨天我亲身碰到一个。

我有一个 Python 程序 A,会使用 subprocess 带 shell=True 跑一行 shell 命令。那条命令会在后台跑另外一个 Python 程序 B。诡异的事情是,当我向 B 的进程发送 SIGINT 时,无法结束它,以及它下边带的一个 tail 进程。一开始我还没注意到 B 的进程本身没有被 SIGINT 杀死,是在无效的情况下被 A 用 SIGKILL 杀死的。我只看到那个 tail 程序还活着。所以我去处理了一下 KeyboardInterrupted 异常,来结束掉那个 tail。

结果很诡异:KeyboardInterrupted 异常并没有发生。通过 strace 观察可以看到,B 进程在读 tail 的输出,然后收到了 SIGINT,然后接着读 tail 的输出……我一开始还以为这个和 PEP 475 相关,以为是 Python 自动重启了被中断的系统调用,所以没来得及处理信号(Python 的信号并不是及时处理的)。然后就去仔细看文档。结果文档告诉我,如果注册了信号处理函数,并且它抛出异常的话,那么被中断的系统调用是不会被重试的。所以这就不对了。

然后我又测试了直接在终端运行 B,而不是通过 A 去运行。本来我开发的时候就是这么测试它的,也没遇到什么怪异的现象。结果确实没有什么怪异的事情发生:即使我使用 kill 命令只给 B 发送 SIGINT 信号,Python 的 KeyboardInterrupted 逻辑会被触发,然后它主动杀掉 tail 进程。(使用 Ctrl-C 的话,B 和 tail 都会收到 SIGINT 信号的。)

疑惑的时候,我又想到了拿 SIGINT 去杀那个不死的 tail 进程,这才发现它也出现奇怪的行为了:正在读 inotify 的文件描述符呢,来了个 SIGINT 信号,然后它接着读 inotify 去了……跟 B 出现的问题一样。我又去查了 tail.c 的源码,也没发现它对 SIGINT 有特殊的处理啊。

难道是继承过来的?man 7 signal 了一下,果然:

During an execve(2), the dispositions of handled signals are reset to the default; the dispositions of ignored signals are left unchanged.

所以 tail 和 B 继承了一个「忽略 SIGINT」的行为。(nohup 就是用的类似的手段啊。)

于是 strace -f 了整个从 A 开始的进程树,最后发现这问题和 Python 并没有什么关系,而是 bash 的错!

A 是用 shell=True 调用的命令,所以它调用了 /bin/sh。系统是 CentOS,所以 /bin/sh 是指向 bash 的。所以这里实际上调用了 bash,而它的处理有问题。

要重现这个 bug 很容易:

bash -c 'sleep 1000 &'

然后这个 sleep 进程就会忽略 SIGINT 和 SIGQUIT 了。我也不明白 bash 这是想要做什么。

之前也遇到过另外几个 bash 的 bug(或者是 feature?)——

  1. 在终端中,在脚本中执行交互式 bash 时,第一个 bash 进程会将自己设为前台进程组,导致后来的进程收到 SIGTTIN 或者 SIGTTOU。很神奇,两行同样的命令,第一条和后边的行为不一致

  2. 在 bash 中,执行不带 shebang 的 shell 脚本时,脚本会在当前 bash 进程内执行,造成 history 命令的行为异常

  3. 这个是听说的。输出失败时,未写入目标的内容仍留在缓冲区内,会在奇怪的地方冒出来

以后还是尽量避开 bash 吧。有 zsh 用 zsh,有 dash 用 dash;它们都没有本文提到的这些问题。

Category: Linux | Tags: shell bash
3
4
2015
7

Awesome 的 GitHub 今日贡献指示器:今天你 push 了吗?

GitHub 用户页有个 calendar,花花绿绿的甚是好看。不过,经常一不小心断掉了几十天的 steak 着实可惜,特别是用了私有仓库之后,自己看,有贡献,可别人看不到那些私有贡献的呀。其实要维持 steak 也不难,一个小小的提交就足够了——只要我知道我今天还没 push 什么公开的东西的时候。

当然啦,写个脚本天天推个无意义的更新挺容易的,但那样就没有乐趣了不是吗?一天快结束的时候发封邮件提示一下自己不错,但可能已经来不及了。而且这种事情还是随意点好,太刻意了就不好玩了,所以不需要那么强的提醒。弄一个简单的指示器在 Awesome 面板上正好。

效果图(指示器在右上角):

GitHub 今日贡献指示器的 Awesome 桌面

如果这天没有贡献(公开的提交或者 issue 等),那么这只 Octocat 就会失去色彩(变成灰度图)。

代码已上传至 myawesomerc 仓库。以下是实现细节:

首先,创建一个显示图片的 widget:

-- {{{ GitHub contribution indicator
github_contributed = awful.util.getdir("config") .. "/image/github_contributed.png"
github_not_contributed = awful.util.getdir("config") .. "/image/github_not_contributed.png"
github_widget = wibox.widget.imagebox()
function update_github(has_contributions)
    if has_contributions then
        github_widget:set_image(github_contributed)
    else
        github_widget:set_image(github_not_contributed)
    end
end
update_github(false)
-- }}}

-- 在 wibox 中添加这个 widget,需要放到正确的地方:
right_layout:add(github_widget)

函数update_github是给外部脚本用的。不可在 Awesome 配置里直接发起 HTTP 请求,会阻塞的!

当然,还要准备前两行代码提到的图片。从这里下载 Octocat 的图片,并做成彩色和灰度小图:

convert -resize 48x48 -background white -alpha remove Octocat.png github_contributed.png
convert -resize 48x48 -background white -alpha remove -colorspace Gray Octocat.png github_not_contributed.png

把图片放到相应的地方。然后写个脚本来更新这个指示器的状态,也就是获取数据之后再通过 awesome-client 调用update_github函数了。

#!/bin/bash -e

github_contributed () {
  count=$(curl -sS "https://github.com/users/$USER/contributions" | grep -oP '(?<=data-count=")\d+' | tail -1)
  [[ $count -gt 0 ]]
}

get_display () {
  if [[ -n "$DISPLAY" ]]; then
    return
  fi

  pid=$(pgrep -U$UID -x awesome)
  if [[ -z "$pid" ]]; then
    echo >&2 "awesome not running?"
    exit 1
  fi

  display=$(tr '\0' '\n' < /proc/"$pid"/environ | grep -oP '(?<=^DISPLAY=).+')
  if [[ -z "$display" ]]; then
    echo >&2 "can't get DISPLAY of awesome (pid $pid)"
    exit 2
  fi

  export DISPLAY=$display
}

get_display

if github_contributed; then
  s='true'
else
  s='false'
fi

echo "update_github($s)" | awesome-client

GitHub calender 目前是个 SVG 图片,位于https://github.com/users/用户名/contributions

awesome-client 需要设置正确的DISPLAY环境变量才可以使用。这里使用pgrep取当前用户的 awesome 进程里的DISPLAY变量值。ps命令不太好用,不能同时指定多个条件。

万事俱备,只需要拿 cron 或者 systemd.timer 定时跑跑这个脚本就可以啦~


2015年3月14日更新:update_github脚本改用 Python 实现了,更好的错误处理(不会因为网络问题而认为今天没有贡献了),也改用当前日期而非 GitHub calender 的最后一个方块。更新后的脚本在 GitHub 上

8
5
2014
9

CoolShell 解密游戏解答

这里有人给出了自己的解法,但是我不喜,所以有了本文。注意,本文中的 shell 代码均为 zsh。如果你在用 Windows,那建议还是不要玩了,那个对付这种事情太难用了。

可惜知道这个题晚了两天,没能进前X名 :-(

0. Fuck your brain

机器上没有 brainfuck 编译器,于是 Google「brainfuck online」,得到这个。贴进去运行即可。

1. Multiply

一个值是 42。另一个要观察数列。直接把数列贴到数列百科全书即可。然后乘起来。

2. Keyboard

这个也很容易,不是把 Dvorak 键盘当成 Qwerty,那就是把 Qwerty 当成 Dvorak 了。对照着 Dvorak 的键位输入下边那串字符串,得到一 C 源码。编译、运行之即可。

3. QR Code

扫码,得到:

[abcdefghijklmnopqrstuvwxyz] <=> [pvwdgazxubqfsnrhocitlkeymj]

是个字符映射关系。Python 有现成的函数来处理这个。也是有正反两种可能,都试试就可以了。

>>> T = str.maketrans('pvwdgazxubqfsnrhocitlkeymj', 'abcdefghijklmnopqrstuvwxyz')
>>> s = 'Wxgcg txgcg ui p ixgff, txgcg ui p epm. I gyhgwt mrl lig txg ixgff wrsspnd tr irfkg txui hcrvfgs, nre, hfgpig tcm liunz txg crt13 ra "ixgff" t
r gntgc ngyt fgkgf.'
>>> s.translate(T)
'Where there is a shell, there is a way. I expect you use the shell command to solve this problem, now, please try using the rot13 of "shell" to enter next level.'

按照提示执行命令:

rot13 <<< shell

Arch 上,rot13 命令位于 bsd-games 包。或者在 Vim 里把光标移动到「shell」单词上按g?aw也能得到结果。

题目开始有趣起来了~

4. cat

这个题目更有趣了。源码里一堆乱七八糟的数据。先把它们弄到一个单独的 Vim 缓冲区,然后找到所有的五字符回文字符串:

%!grep -oP '(.)(.)(.)\2\1'

不是所有回文都被接受。仔细观察示例可以发现,中间一定是个小写字母,左边一定是一字母一数字。但是过滤后还是有太多结果。限制左边的字母为大写字母之后可以得结果。删掉不符合条件的,然后把中间的字符连起来即可。

v/\v^([A-Z][0-9]|[0-9][A-Z])\l/d

5. variables

初看,提示莫名其妙。后来注意到图片链接到了有意思的地方。访问得到另一个数字「32722」。显然是要用这个数字放在 URL 上继续访问了。直接拿 shell 访问:

$ n=1024
$ while true; do n=$(curl -sS http://fun.coolshell.cn/n/$n); echo $n; done

访问上百次之后出现一句话,给出了下一关的地址。

6. tree

这关要求从一棵二叉树的中序和后序遍历中还原其最深的路径。不知道怎么做,直接 Google「reconstruct a binary tree from in-order and post-order」,看来有不少人都在做类似的东西啊。我看的是 LeetCode 上的这篇文章。有代码,但我懒得写程序把树画出来或者是找最深的了。反正这树也不大,懂得了方法,直接在 dia 里手工构建出来了。当然,我只构建了最深的那部分。SVG 导出图片

然后就是拿密码解那个字符串了。要注意的是,不要自己去解 base64,不然 openssl 报错的……

7. N Queens

八皇后问题的变种。我直接使用了 Rosetta Code 上的代码。当然要小改一下,直接输出结果而不是打印出图案:

main = mapM_ print $ queens 9

然后找到符合那个 SHA1 值的解就可以了:

$ ./queens | tr -d ',[]' | while read code; do [[ $(sha1sum <<<zWp8LGn01wxJ7$code | awk '{print $1}') == e48d316ed573d3273931e19f9ac9f9e6039a4242 ]] && echo $code; done

8. Excel Column

26 进制转十进制:

>>> def debase26(x):
...   return sum(26 ** i * (ord(d) - ord('A') + 1) for i, d in enumerate(x[::-1]))
...
>>> debase26('COOLSHELL') // base26('SHELL')
85165

结果得到的页面说要转回 26 进制。好吧:(可惜没能在一行内搞定)

>>> def base64(x):
...   L = []
...   while True:
...     x, d = divmod(x, 26)
...     if d == 0: break
...     L.append(d)
...   return ''.join(chr(x + ord('A') - 1) for x in L[::-1])
...
>>> base64(85165)
'DUYO'

9. Fraternal Organisation

这个我没能解出来 QAQ 这两个图片看起来有些莫名其妙。我没注意到图片的名字和鼠标放上去的小提示。最后是看前边那个链接里的答案才知道原来还有个「猪圈密码」-_-|||

PS: 最近博客访问和评论速度都挺慢的,请见谅。

Category: Linux | Tags: linux python shell fun
11
14
2013
4

zsh 按 shell 参数移动

很早以前,我就想,在命令比较长的时候,M-fM-b按单词移动太慢了,特别是遇到长的 URL 或者文件名的时候。用鼠标吧,选择文本又比较麻烦了。所以很希望按 shell 参数来移动的功能,甚至尝试自己写过,但是因为对 zsh 了解太少,终究移动不正常。

昨天夜读 zsh 手册时才发现,原来,我曾见过这个功能的背影。

文档 26.6.1 节(「User Contributions」->「ZLE Functions」->「Widgets」)第一个,讲的是「bash-style word functions」。之前我也在哪里看到过,但是不知道其实这家伙支持好几种风格。使用以下配置就可以把 ZLE 里原来的「单词」概念变成 shell 解析出来的参数了:

autoload -Uz select-word-style
select-word-style shell

但是,我不想替换掉默认的,而是使用另外的键来这样子移动。研究了下代码,最终弄出来了:

# move by shell word {{{2
zsh-word-movement () {
  # see select-word-style for more
  local -a word_functions
  local f

  word_functions=(backward-kill-word backward-word
    capitalize-word down-case-word
    forward-word kill-word
    transpose-words up-case-word)

  if ! zle -l $word_functions[1]; then
    for f in $word_functions; do
      autoload -Uz $f-match
      zle -N zsh-$f $f-match
    done
  fi
  # set the style to shell
  zstyle ':zle:zsh-*' word-style shell
}
zsh-word-movement
unfunction zsh-word-movement
bindkey "\eB" zsh-backward-word
bindkey "\eF" zsh-forward-word
bindkey "\eW" zsh-backward-kill-word

只绑了M-BM-FM-W这三个含大写字母的组合键。其它-match函数的功能以后用到时再加好了。

Category: shell | Tags: zsh shell
8
29
2013
0

不是所有 PAGER 都叫 less

在 Linux 下,最常见的 pager(翻页器)就是 less 了,所以很多时候,我都忘记了还有$PAGER这个环境变量,直到有一天我写了这么个 shell 函数:

repodo () {
  for f in $(cat ~/workspace/.my-repos); do
    echo "\n>>> $f\n"
    cd ~/workspace/$f && stdoutisatty $@
    cd - > /dev/null
  done | less
}

这个函数对于~/workspace/.my-repos中记录的每一个项目,在对应的目录下执行同一条命令,并使用 less 来查看输出。其中,stdoutisatty 是一个把标准输出伪装成 tty 的脚本,这样一些命令就不会因为实际输出到管道而关掉彩色高亮之类的了。

比如

repodo git st

ststatus的 git 别名。

这一句命令就可以查看所有项目的工作区状态了。

后来,我执行这样一条命令,它就出问题了:

repodo git grep string

因为 stdoutisatty 的缘故,git grep 会自动调用翻页器。于是,出现了两个 less 同时要读终端输入。

首先想到的是 git 的--no-pager参数,但这个很显然对其它命令无效。于是才想起自设置之后一直没再搭理的$PAGER环境变量:

repodo () {
  for f in $(cat ~/workspace/.my-repos); do
    echo "\n>>> $f\n"
    cd ~/workspace/$f && PAGER=cat stdoutisatty $@
    cd - > /dev/null
  done | less
}

PAGER指定为cat直接输出,这样就不会有多个 less 在运行了。

但这样还没有结束,因为我的不少脚本里都是直接调用 less 的,现在得改成这样子了:

command | ${PAGER:-less}

或者在 Python 里:

p = subprocess.Popen([os.environ.get('PAGER', 'less')], stdin=subprocess.PIPE,
                      universal_newlines=True)

附:less 默认是会转义来自输入的彩色转义字符序列的。我使用了-FRXM参数,也是通过环境变量传递的:

export LESS=-FRXM

这四个选项的意义是:

-F
如果一屏能显示下,那么显示完就退出
-R
不要转义 ANSI 彩色转义字符序列
-X
不要发布终端初始化和结束字符串。这样才不会使用终端的备用屏幕,less 的输出才会留在主屏幕上(使用-F选项时必须,不然可能看不到东西)
-M
在 less 提示符(最后一行)显示更多信息(比如文件的百分比位置)
Category: shell | Tags: linux shell 环境变量 less
8
22
2013
6

BSD 版 xargs

BSD 版 xargs 与 GNU 版有一个显著的不同——它支持-J选项。

比如说,你使用 find 命令得到了一个文件列表。你要将它们传递给一个叫concat_files的程序来处理后生成一个指定的新文件,比如:

concat_files file1 file2 file3 output

而且,这个命令不像 cp 或者 mv 那样,有个-t参数来把目标文件放到不定长的文件列表之前。总之呢,你不得不构建一行命令,它的中间部分是你会从管道传过去的文件列表。而 GNU xargs 要么全给你放末尾(默认),要么每项执行一次命令(指定-I时)。而 BSD xargs 则可以用-J选项指定一个占位符,使用这个占位符指明参数插入的位置:

find ... | xargs -J % concat_files % output

BSD xargs 的另一个特有参数是-o,作用你们就自己看文档啦=w=

我想在 Linux 上使用 BSD xargs,怎么办呢?在 AUR 里搜索到了这个,但是已经编译不过去了。安装 bmake 后手动边改边编译,最终终于成功编译了 obase 中的大多数工具。我知道的比较有特色的也就这个 xargs 了,于是单独打了个包 bsdxargs,放在我的 lilydjwg 源 里。

附,obase 的补丁:

diff --git a/Makefile b/Makefile
index 2bb18b4..96acf8a 100644
--- a/Makefile
+++ b/Makefile
@@ -8,6 +8,8 @@ LIBOBASE=${.CURDIR}/libobase/libobase.a
 INCLUDES_libobase=-isystem ${.CURDIR}/libobase/include
 COPTS_libobase=-D_GNU_SOURCE
 DPLIBS+=${LIBOBASE}
+LDADD+= ${LIBOBASE}
+.export LDADD
 .export COPTS DPLIBS HOSTCC HOSTCFLAGS USE_DPADD_MK

 SUBDIR=\
diff --git a/src/bin/ls/Makefile b/src/bin/ls/Makefile
index defd607..6ad4725 100644
--- a/src/bin/ls/Makefile
+++ b/src/bin/ls/Makefile
@@ -3,6 +3,6 @@
 PROG=  ls
 SRCS=  cmp.c ls.c main.c print.c util.c
 DPADD= ${LIBUTIL}
-LDADD= -lutil
+LDADD+= -lutil

 .include <bsd.prog.mk>
diff --git a/src/usr.bin/awk/Makefile b/src/usr.bin/awk/Makefile
index 54857d3..9d2d243 100644
--- a/src/usr.bin/awk/Makefile
+++ b/src/usr.bin/awk/Makefile
@@ -2,7 +2,7 @@

 PROG=  awk
 SRCS=  ytab.c lex.c b.c main.c parse.c proctab.c tran.c lib.c run.c
-LDADD= -lm
+LDADD+=    -lm
 DPADD= ${LIBM}
 CLEANFILES+=proctab.c maketab ytab.c ytab.h stamp_tabs
 CFLAGS+=-I. -I${.CURDIR} -DHAS_ISBLANK -DNDEBUG
diff --git a/src/usr.bin/dc/Makefile b/src/usr.bin/dc/Makefile
index b0a2396..f8ee358 100644
--- a/src/usr.bin/dc/Makefile
+++ b/src/usr.bin/dc/Makefile
@@ -3,7 +3,7 @@
 PROG=  dc
 SRCS=  dc.c bcode.c inout.c mem.c stack.c
 COPTS+= -Wall
-LDADD= -lcrypto
+LDADD+=    -lcrypto
 DPADD= ${LIBCRYPTO}

 .include <bsd.prog.mk>
diff --git a/src/usr.bin/du/Makefile b/src/usr.bin/du/Makefile
index feb644d..9676f37 100644
--- a/src/usr.bin/du/Makefile
+++ b/src/usr.bin/du/Makefile
@@ -2,6 +2,6 @@

 PROG=  du
 DPADD= ${LIBUTIL}
-LDADD= -lutil
+LDADD+= -lutil

 .include <bsd.prog.mk>
diff --git a/src/usr.bin/gzsig/Makefile b/src/usr.bin/gzsig/Makefile
index 0dc7b81..f4f0664 100644
--- a/src/usr.bin/gzsig/Makefile
+++ b/src/usr.bin/gzsig/Makefile
@@ -3,7 +3,7 @@
 PROG   = gzsig
 SRCS   = gzsig.c key.c sign.c ssh.c ssh2.c util.c verify.c x509.c

-LDADD  = -lcrypto -lm
+LDADD  += -lcrypto -lm
 DPADD  = ${LIBCRYPTO} ${LIBM}

 CLEANFILES += TAGS *~
diff --git a/src/usr.bin/lex/Makefile b/src/usr.bin/lex/Makefile
index 080a151..27a783e 100644
--- a/src/usr.bin/lex/Makefile
+++ b/src/usr.bin/lex/Makefile
@@ -17,7 +17,7 @@ SRCS= ccl.c dfa.c ecs.c gen.c main.c misc.c nfa.c parse.c sym.c tblcmp.c \
    yylex.c
 OBJS+= scan.o skel.o
 CLEANFILES+=parse.c parse.h scan.c skel.c y.tab.c y.tab.h
-LDADD= -lfl
+LDADD+=    -lfl
 DPADD= ${LIBL}

 MAN = flex.1
diff --git a/src/usr.bin/m4/Makefile b/src/usr.bin/m4/Makefile
index 7c510f5..16a282c 100644
--- a/src/usr.bin/m4/Makefile
+++ b/src/usr.bin/m4/Makefile
@@ -8,7 +8,7 @@ CFLAGS+=-DEXTENDED -I.
 CDIAGFLAGS=-W -Wall -Wstrict-prototypes -pedantic \
    -Wno-unused -Wno-char-subscripts -Wno-sign-compare

-LDADD= -ly -lfl -lm
+LDADD+= -ly -lfl -lm
 DPADD= ${LIBY} ${LIBL} ${LIBM}

 SRCS=  eval.c expr.c look.c main.c misc.c gnum4.c trace.c tokenizer.l parser.y
diff --git a/src/usr.bin/make/Makefile b/src/usr.bin/make/Makefile
index a63ed94..1d12280 100644
--- a/src/usr.bin/make/Makefile
+++ b/src/usr.bin/make/Makefile
@@ -14,7 +14,7 @@ CDEFS+=-DHAS_EXTENDED_GETCWD

 CFLAGS+=${CDEFS}
 HOSTCFLAGS+=${CDEFS}
-LDADD= -lrt
+LDADD+=    -lrt

 SRCS=  arch.c buf.c cmd_exec.c compat.c cond.c dir.c direxpand.c engine.c \
    error.c for.c init.c job.c lowparse.c main.c make.c memory.c parse.c \
diff --git a/src/usr.bin/mandoc/Makefile b/src/usr.bin/mandoc/Makefile
index cf565fd..6086a81 100644
--- a/src/usr.bin/mandoc/Makefile
+++ b/src/usr.bin/mandoc/Makefile
@@ -9,7 +9,7 @@ CFLAGS+=-W -Wall -Wstrict-prototypes
 CFLAGS+=-Wno-unused-parameter
 .endif

-LDADD= -ldb
+LDADD+= -ldb

 SRCS=  roff.c tbl.c tbl_opts.c tbl_layout.c tbl_data.c eqn.c mandoc.c read.c
 SRCS+= mdoc_macro.c mdoc.c mdoc_hash.c \
diff --git a/src/usr.bin/script/Makefile b/src/usr.bin/script/Makefile
index d7dbf01..8837084 100644
--- a/src/usr.bin/script/Makefile
+++ b/src/usr.bin/script/Makefile
@@ -1,7 +1,7 @@
 #  $OpenBSD: Makefile,v 1.3 1997/09/21 11:50:42 deraadt Exp $

 PROG=  script
-LDADD= -lutil
+LDADD+=    -lutil
 DPADD= ${LIBUTIL}

 .include <bsd.prog.mk>
diff --git a/src/usr.bin/ul/Makefile b/src/usr.bin/ul/Makefile
index bab290c..12295ec 100644
--- a/src/usr.bin/ul/Makefile
+++ b/src/usr.bin/ul/Makefile
@@ -2,6 +2,6 @@

 PROG=  ul
 DPADD= ${LIBCURSES}
-LDADD= -lcurses
+LDADD+=    -lcurses

 .include <bsd.prog.mk>
diff --git a/src/usr.bin/vacation/Makefile b/src/usr.bin/vacation/Makefile
index 6f08990..f9ef0d6 100644
--- a/src/usr.bin/vacation/Makefile
+++ b/src/usr.bin/vacation/Makefile
@@ -1,6 +1,6 @@
 #  $OpenBSD: Makefile,v 1.3 1997/09/21 11:51:42 deraadt Exp $

 PROG=  vacation
-LDADD= -ldb
+LDADD+=    -ldb

 .include <bsd.prog.mk>
diff --git a/src/usr.bin/wc/Makefile b/src/usr.bin/wc/Makefile
index 3f3c619..0f3d1a2 100644
--- a/src/usr.bin/wc/Makefile
+++ b/src/usr.bin/wc/Makefile
@@ -2,6 +2,6 @@

 PROG=  wc
 DPADD= ${LIBUTIL}
-LDADD= -lutil
+LDADD+= -lutil

 .include <bsd.prog.mk>

参见

Category: Linux | Tags: xargs BSD shell
7
26
2013
5

flock——Linux 下的文件锁

当多个进程可能会对同样的数据执行操作时,这些进程需要保证其它进程没有也在操作,以免损坏数据。

通常,这样的进程会使用一个「锁文件」,也就是建立一个文件来告诉别的进程自己在运行,如果检测到那个文件存在则认为有操作同样数据的进程在工作。这样的问题是,进程不小心意外死亡了,没有清理掉那个锁文件,那么只能由用户手动来清理了。像 pacman 或者 apt-get 一些数据库服务经常在意外关闭时留下锁文件需要用户清理。我以前写了个 pidfile,它会将自己的 pid 写到文件里去,所以,如果启动时文件存在,但是对应的进程不存在,那么它也可以知道没有其它进程要访问它要访问的数据(这里只讨论如何避免数据的并发讨论,不考虑进程意外退出时的数据完整性)。但是,Linux 的 pid 是会复用的。而且,检查 pidfile 也有点麻烦不是么?(还有竞态呢)

某天,我发现了 flock 这个系统调用。flock 是对于整个文件的建议性锁。也就是说,如果一个进程在一个文件(inode)上放了锁,那么其它进程是可以知道的。(建议性锁不强求进程遵守。)最棒的一点是,它的第一个参数是文件描述符,在此文件描述符关闭时,锁会自动释放。而当进程终止时,所有的文件描述符均会被关闭。于是,很多时候就不用考虑解锁的事情啦。

flock 有个对应的 shell 命令也叫 flock,很好用的。使用最广泛的 cronie 这个定时任务服务很笨的,不像小巧的 dcron 那样同一任务不会同时跑多个。于是乎,服务器上经常看到一堆未退出的 cron 任务进程。把所有这样的任务包一层 flock 就不会导致 cronie 启动 N 个进程做同一件事啦:

flock -n /tmp/.my.lock -c 'command to run'

即使是 dcron,有时会有两个操作同一数据的任务,也需要使用 flock 来调度。不过这次不用-n参数让文件被锁住时失败退出了。我们要等拥有锁的进程完事再执行。如下,两个任务(有所修改),一个是从远程同步数据到本地的,另一个是备份同步过来的数据的。同时执行的话,就会备份到不完整的数据了。

*/7 *    * * * ID=syncdata       LANG=zh_CN.UTF-8 flock /tmp/.backingup -c my_backup_script
@daily         ID=backupdata     LANG=zh_CN.UTF-8 [ -d ~/data ] && cd ~/data && nice -n19 ionice -c3 flock /tmp/.backingup -c "tar cJf backup_$(date +"%Y%m%d").tar.xz data_dir --exclude='*~'"

flock 命令除了接收文件名参数外,还可以接收文件描述符参数。这种方法在 shell 脚本里特别有用。比如如下代码:

lockit () {
  exec 7<>.lock
  flock -n 7 || {
    echo "Waiting for lock to release..."
    flock 7
  }
}

exec行打开.lock文件为 7 号文件描述符,然后拿 flock 尝试锁它。如果失败了,就输出一条消息,并且等待锁被释放。那个 7 号文件描述符就让它一直开着了,反正脚本执行完毕内核会释放,也不用去调用trap内建命令了。

上边有一点很有意思的是,flock 是一个子进程,但是因为文件描述符在 fork 和 execve 中会共享,而 flock 锁在 fork 和 execve 时也不会改变,所以子进程在那个文件描述符上加锁后,即使它退出了,因为那个文件描述符父进程还有一份,所以不会被关闭,锁也就得以保留。(所以,如果余下的脚本里要是有进程带着那个文件描述符 fork 到后台锁就不会在脚本执行完后自动解除啦……)

PS: 经我测试,其它一些类 Unix 系统上或者没有 flock 这个系统调用,只有 fcntl 那个,或者行为和 Linux 的不一样。

Category: Linux | Tags: linux shell

Mastodon | Theme: Aeros 2.0 by TheBuckmaker.com