tac(1)やtail -rの代替品 迷走編

LinuxというかGNU coreutilsにはtac(1)というコマンドがある。デフォルトではtac(1)はファイルを行番号の降順に(最終行から先頭行に向かって)表示する。

$ seq 1 10
1
2
3
4
5
6
7
8
9
10
$ seq 1 10 | tac
10
9
8
7
6
5
4
3
2
1
$ seq 1 10 >zzzz1.txt
$ tac zzzz1.txt
10
9
8
7
6
5
4
3
2
1
$ seq 21 30 >zzzz2.txt
$ tac zzzz1.txt zzzz2.txt
10
9
8
7
6
5
4
3
2
1
30
29
28
27
26
25
24
23
22
21
$ _

FreeBSDMac OS Xにはtac(1)は入っていない。GNU coreutilsを導入するか「tail -r」で代用することになる。複数のファイルを扱う場合は「tail -r」よりも「tail -q -r」の方がtac(1)らしいかもしれない。

ややこしいことに、GNU coreutilsを使用しているLinuxでは「tail -r」は使用できない。オプション -r が存在しないのだ。

tac(1)を使用するシェルスクリプトを例えばLinuxMac OS Xとで使いたい場合、最初からtac(1)ではなく代用品で済ますことも考えられる。もっともシンプルなのはsed(1)による実装だろう。

#!/bin/sh
# @(#) substitutes for tac(1) or `tail -q -r`. Version 1.0.0

exec sed '1!G;h;$!d' ${@+"$@"}

sed(1)のスクリプトではなくシェルスクリプトである理由は、UbuntuMac OS Xとでsed(1)のパスが異なる*1上に、envを使おうとするとshebangの解析にて「/usr/bin/env sed -f」の「sed -f」を"sed -f"という1つの文字列としてパースしてしまい実行できない環境が多いからだ。

このsed(1)による実装も含めて、tac(1)の代用品を実装しようとすると、基本的には入力データをほぼ丸ごとメモリにコピーする実装になる。入力がローカルディスク上のファイルならseekして後ろから読むことができるが、標準入力や名前付きパイプでは不可能だ。

私はへそ曲がりなので、入力データを丸ごとメモリにコピーしなくても動作するtac(1)の代用品ができないか考えてみた。

で、できあがったのがコレ。

#!/bin/sh

readonly progname=`basename "$0"`

trap 'rm -f "$tmpfile"; exit 1' 1 2 15

tmpfile=`mktemp "$progname.$$.XXXXXXXXXX"`
if [ $? -ne 0 ]; then
    echo "$progname: cannot create temporary file" 1>&2
    exit 1
fi

last=`cat ${@+"$@"} | tee "$tmpfile" | wc -l`

seq $last -1 1              |
awk '{ print $0 "p" }'      |
ed "$tmpfile" 2>/dev/null

rm -f "$tmpfile"

入力を一時ファイルにコピーして、ed(1)で開いて、標準入力経由でed(1)に行表示用コマンドを流し込んでいる。

awk(1)を使用しているところが気に入らなかったので、別の方法を模索してみたのがコレ。

#!/bin/sh

readonly progname=`basename "$0"`

trap 'rm -f "$tmpfile"; exit 1' 1 2 15

tmpfile=`mktemp "$progname.$$.XXXXXXXXXX"`
if [ $? -ne 0 ]; then
    echo "$progname: cannot create temporary file" 1>&2
    exit 1
fi

cat ${@+"$@"} >"$tmpfile"

vim -e -s --noplugin "$tmpfile" << EOF
$
let b:maxline = line(".")
for b:i in range(1, b:maxline)
    print
    -1
endfor
q!
EOF

rm -f "$tmpfile"

しかし冷静に考えてみれば、ed(1)を使う方法もvim(1)を使う方法も内部的にはテキストファイルをメモリ上に丸読みしている気がするので、結局はsed(1)などによる代用品と変わらない。それどころか一時ファイルを使用している部分がボトルネックとなる可能性がある。

もしこれ以上頑張るとしたら、C言語システムコールを直叩きして、一時ファイルをseekするかmmap(2)を使うかして反転出力するぐらいだろうか?

*1:Ubuntuでは/bin/sedMac OS Xでは/usr/bin/sed