Help us understand the problem. What is going on with this article?

逆順出力 tac と tail -r

More than 5 years have passed since last update.

逆順出力 tac と tail -r

ポータブルじゃない問題点

ファイルの末尾から先頭に向かって行を出力したいとき(cat の逆)、tac コマンドという便利なものがあります。
名前も cat を逆さまにしたイカしたやつです。

$ tac file
333
222
111
$ seq 1 5 | tac
5
4
3
2
1

しかし実は難点があり、tac は Linux(GNU coreutils)にしかないことです。つまり Mac では使えません。その代わり、tail -r で代用できます。tail はファイル末尾から数行を正順で出力するコマンドですが、-rreverse)フラグによってそれを反転、要するに逆順に出力させることができます。しかし、これまた不幸なことに、BSD 系の tail でしか使用できません。Linux には -r フラグがないのです。つまり、Linux では使えません。

  • Linux では tactail -r は使えない)
  • Mac では tail -rtac は使えない)

ということは、ポータブルなシェルスクリプトが書けません。which などで条件分岐すれば書けなくもないですが、どちらも使えない状況ではどうしようもありません。

if which tac >/dev/null; then
    tac="tac"
else
    tac="tail -r"
fi

自分で実装

となると自分で書いたほうがはやいです。要件はファイル内容の逆順出力です。簡単です。

同じことを考えている人もいるようで、すでに様々な先行実装があります。

参考: How can I print lines from file backwards (without using “tac”)?

shell script

#!/bin/bash
# @(#) substitutes for tac(1) or `tail -q -r`
/bin/ex -s ${@+"$@"} <<-EOF
g/^/mo0
%p
EOF

ex を使ったシェルスクリプトです。ファイル名を引数に逆順出力を実現しています。つまり標準出力を受け付けません。

$ ./tac-ex file
333
222
111
$ 
$ cat file | ./tac-ex

sed

#!/bin/bash
# @(#) substitutes for tac(1) or `tail -q -r`
sed '1!G;h;$!d' ${@+"$@"}

sedのスクリプトではなくシェルスクリプトである理由は、

Ubuntu と Mac とで sed(1) のパスが異なる上に、env を使おうとすると shebang の解析にて /usr/bin/env sed -fsed -f"sed -f" という1つの文字列としてパースしてしまい実行できない環境が多いからだ。

しかし、この sed による tac 実装には問題点があります。

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

上の ex と同じ点です。

perl

#!/bin/bash
# @(#) substitutes for tac(1) or `tail -q -r`
perl -e 'print reverse <>' ${@+"$@"}

この方法は、ファイル名にも標準出力にも対応しています。ただし、このやり方は一端メモリーに全ファイルを読み込むので非効率ですね。

$ ./tac-perl file
333
222
111
$ 
$ cat file | ./tac-perl
333
222
111

冒頭のリンク(perl - tac を一行で)では、cpam モジュールなどを用いた高速実装も紹介されています。

awk

#!/bin/bash
# @(#) substitutes for tac(1) or `tail -q -r`
awk '{a[i++]=$0} END {for (j=i-1; j>=0;) print a[j--] }' ${@+"$@"}

これも、同様にファイル名にも標準出力にも対応しています。しかし、perl 同じ問題もあります。それを更に効率化したものが次の tac-awk です。

#!/bin/bash
# @(#) substitutes for tac(1) or `tail -q -r`
awk '
BEGIN {
    sort_exe = "sort -t \"\034\" -nr"
}

{
    printf("%d\034%s\n", NR, $0) |& sort_exe;
}

END {
    close(sort_exe, "to");

    while ((sort_exe |& getline var) > 0) {
        split(var, arr, /\034/);

        print arr[2];
    }
    close(sort_exe);
}' ${@+"$@"}

落とし所

様々な実装を見てきました。手軽さや汎用性(枯れている具合→どの環境でも使えるか)を考えると、

perl -e 'print reverse <>'

や、

awk '{a[i++]=$0} END {for (j=i-1; j>=0;) print a[j--] }'

くらいがちょうどいいかもしれません。perlawk も枯れていますし、行数は増えますが書き方次第ではメモリーを考慮した高速実装も出来ます。また、どちらもファイル名・標準入力に対応しているので tac らしい動きもバッチリです。

またシェルスクリプトとの親和性も高いので、簡単にスクリプト内に埋め込めます。

reverse() {
    perl -e 'print reverse <>' ${@+"$@"}
}

上のようにシェル関数化させておけば使いまわせて便利です。

$ cat file | reverse
333
222
111
mercari
フリマアプリ「メルカリ」を、グローバルで開発しています。
https://tech.mercari.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away