sortコマンド、基本と応用とワナ


sortコマンド、使いこなしてる?

UNIXの基本コマンドの一つsortコマンド。アナタはどれくらい使いこなしているか? 何にもオプションを付けずにsortと打ち込むくらいしか知らない、というなら、これを見て便利に使おう。

基本編. 各行を単なる1つの単語として扱う

sortコマンドの使い方には基本と応用がある。基本的な使い方は単純で、各行を1つの単語のように見なしてキャラクターコード順に並べるなどの使い方だ。

(オプションなし)……キャラクターコード順に並べる

$ cat <<EXAMPLE | sort
> perl
> ruby
> Perl
> Ruby
EXAMPLE
Perl  ← 註)
Ruby  ← キャラクターコード順なので
perl  ← 大文字から先に並ぶ
ruby  ←

-f……辞書順に並べる

$ cat <<EXAMPLE | sort -f
> perl
> ruby
> Perl
> Ruby
EXAMPLE
Perl  ← 註)
perl  ← 辞書順なので
Ruby  ← P,p,R,rの順で
ruby  ← 並ぶ

-n……整数順に並べる

$ cat <<EXAMPLE | sort -n
> 2
> 10
> -3
> 1
EXAMPLE
-3  ← 註)
1   ← 値の小さい順に並ぶ。
2   ← もし-nを付けないと
10  ← -3,1,10,2 の順に並ぶことになる(2と10の順番が狂ってしまう)

その他注意

マイナス記号は認識するが、プラス記号は認識しない。

-g……実数順に並べる(POSIX非標準)

$ cat <<EXAMPLE | sort -g
> +6.02e+23
> 1.602e-19
> -928.476e-26
> EXAMPLE
-928.476e-26 ← 註)
1.602e-19    ← 浮動小数点表記でも正しくソートする。
+6.02e+23    ← -nオプションと違い、+符号も認識する。

その他注意

単なる整数も正しくソートできるが、計算量が多くなるので、-nオプションで済むならその方がよい。

-r……降順に並べる(他オプションと併用可)

$ cat <<EXAMPLE | sort -gr
> +6.02e+23               ↑註1)他のオプションと組み合わせて使える
> 1.602e-19
> -928.476e-26
> EXAMPLE
+6.02e+23    ← 註2)
1.602e-19    ← 先程の-gオプションとは、
-928.476e-26 ← 順番が正反対になっている。

応用編. 複数の列から構成されるデータを扱う

sortコマンドの本領は、ここで紹介する使い方を覚えてこそ発揮される。SQLのORDER BYのように、第1ソート条件、第2ソート条件……、と指定できるのだ。強力である。

応用編では、2つのサンプルデータを例に紹介する。

サンプルデータ(1)…半角スペース1文字区切り

次のように、キャラクター初出年、性別、キャラクター名、の3列から構成される半角スペース区切りのデータがあったとしよう。

sample1.txt
1992 男 りょうおうき
1992 女 まさきささみじゅらい
2003 女 かわはらさき
2003 男 しらせあきら
1995 男 るみや
1995 女 あまのみさお

ちなみに列と列の間の半角スペースは1つでなければならない。2つのままだとデータによっては失敗するのだが、それについては「ワナ編」で説明しよう。

「名前順にソートせよ」

名前が1列目にあれば簡単(単にオプション無しのsortに渡すだけ)なのだが、このサンプルデータでは3列目にある。こういう時は、-k 3,3というオプションを付けてやる。つまり、-kオプションの後ろにソートしたい列番号をカンマ区切りで2つ書く。

$ sort -k 3,3 sample1.txt
1995 女 あまのみさお
2003 女 かわはらさき
2003 男 しらせあきら
1992 女 まさきささみじゅらい
1992 男 りょうおうき
1995 男 るみや

なぜ2つ付けるのかについてであるが、入門段階ではそーいうもんだと思って覚えておけばよい。(どうしても詳しく知りたい人はmanをどうぞ)

「名前を降順にソートせよ」

先程は名前をキャラクターコードの昇順にソートしたが、逆順にしたい場合はどうするか。答えは-k 3r,3である。つまり、最初の数字の直後に基本編で紹介したオプション文字を付ける。(これもとにかくそういうものだと覚えておけばよい)

$ sort -k 3r,3 sample1.txt
1995 女 あまのみさお
2003 女 かわはらさき
2003 男 しらせあきら
1992 女 まさきささみじゅらい
1992 男 りょうおうき
1995 男 るみや

ちなみに、もし名前が半角アルファベットで記述されていて、それをアルファベット順に並べたかったとするなら、-k 3fr,3と書けばよい。(fは基本編で出てきた「辞書順に並べる」オプションだ)

「性別→初出年逆順→名前の順でソートせよ」

複数のソート条件を指定する場合にはどうすればいいか。答えは、-kオプションを複数書く、である。つまりこの場合、-k 2,2 -k 1nr,1 -k 3,3だ。

$ sort -k 2,2 -k 1nr,1 -k 3,3 sample1.txt
2003 女 かわはらさき
1995 女 あまのみさお
1992 女 まさきささみじゅらい
2003 男 しらせあきら
1995 男 るみや
1992 男 りょうおうき

性別を昇順にすると「女」が先に来るのは、男女を音読みした場合の名前順で女の方が先だからである。また、初出年の降順ソート(-k1nr,1)で"n"を付けているのは、もし初出年が3桁以下だった場合でも正常に動作することを保証するためだ。

サンプルデータ(2)…/etc/passwd

もう一つのサンプルは/etc/passwdファイルだ。誰もが持ってるので試すの楽でしょ?

さて、とりあえず中身を覗いてみるとこんな感じになっている。

/etc/passwdの例(FreeBSD)
# $FreeBSD: release/9.1.0/etc/master.passwd 218047 2011-01-28 22:29:38Z pjd $
#
root:*:0:0:Charlie &:/root:/bin/csh
toor:*:0:0:Bourne-again Superuser:/root:
daemon:*:1:1:Owner of many system processes:/root:/usr/sbin/nologin
operator:*:2:5:System &:/:/usr/sbin/nologin
bin:*:3:7:Binaries Commands and Source:/:/usr/sbin/nologin
tty:*:4:65533:Tty Sandbox:/:/usr/sbin/nologin
kmem:*:5:65533:KMem Sandbox:/:/usr/sbin/nologin
 :

特長は、スペース区切りではなくてコロン区切りになっている点だ。あと、先頭にコメント行がついているが、これは正しくソートできないのでソートの直前にはgrep -v '^#'を挿んで取り除かなければならない。

「グループ番号→ユーザー番号の順でソートせよ」

ソート例は一つしかやらないが、sample1.txtを踏まえれば-kオプションについてはもう使い方はわかっているはずだ。グループ番号は第4列、ユーザー番号は第3列にあるのだから-k 4n,4 -k 3n,3とすればよい。

問題は区切り文字だ。列と列を区切る文字が半角スペース以外の場合には-tオプションを使う。 /etc/passwdはコロン区切りなので-t ':'と書く。

まとめると答えはこうだ。

$ cat /etc/passwd             |
> grep -v '^#'                | ←註)コメント行を除去する
> sort -k 4n,4 -k 3n,3 -t ':'
root:*:0:0:Charlie &:/root:/bin/csh
toor:*:0:0:Bourne-again Superuser:/root:
daemon:*:1:1:Owner of many system processes:/root:/usr/sbin/nologin
unbound:*:59:1:unbound dns resolver:/nonexistent:/usr/sbin/nologin
operator:*:2:5:System &:/:/usr/sbin/nologin
  :
kmem:*:5:65533:KMem Sandbox:/:/usr/sbin/nologin
nobody:*:65534:65534:Unprivileged user:/nonexistent:/usr/sbin/nologin

ワナ編. 区切り文字に潜むワナ2つ

おまちかねのワナ編。応用編のテクニックを使いこなすには、この2つのワナも覚えておかないとハマるぞ。

その1…半角スペース複数区切りのワナ

sample2.txt
1  B -
10 A -

ご覧のように第2列の位置を揃えるために、1行目の文字"B"の手前には半角スペースが2個挿入されている。このような例は、dfls -lpsなどのコマンド出力結果や、fstabなどの設定ファイルで身近に溢れている。

普通に第2列で昇順ソートしてみると……

このデータを第2列のキャラクターコード順にソートしたらどうなるか? 1行目と2行目が入れ替わってもらいたいところだが、入れ替わらないのだ。(少なくともGNU sort 8.4で実行したらそうなった)

$ sort -k 2,2 sample2.txt
1  B -
10 A -

理由は、列区切りルールがとても特殊であることに起因する。なんとsortコマンドはデフォルトでは、空白類(スペースやタブ)でない文字から空白類への切り替わり位置で列を区切り、しかも区切った文字列の先頭にその空白類があるものと見なす。つまり上記テキストの各列は、次の表の通りに解釈される。

第1列 第2列 第3列
1行目 1 __B _-
2行目 10 _A _-

(この表では便宜上、空白文字をアンダースコア"_"で記している)

なぜこのようなルールにされたのかは全くの謎だが、「それならば」と-tオプションを用い、列区切り文字は半角スペースである(-t ' ')と指定してもうまくいかない。この場合、対象となるテキストデータ中で半角スペースが複数個連続していると、その間に空文字の列があると見なされてしまうからだ。従って上記テキストは、1行目が第1列1の次に空文字の第2列があって4列から成ると解釈されてしまう。(次の表の通り)

第1列 第2列 第3列 第4列
1行目 1 B -
2行目 10 A -

どうすればいいのか

-bオプションを付ければよい。するとこの謎仕様は治まり、他の多くのUNIXコマンドと同様、連続する半角スペース全体が1つ列区切りと見なされる。その結果、半角スペースが列の値に含まれることがなくなるので、望みどおりのソートが行われる。

$ sort -b -k 2,2 sample2.txt
10 A -
1  B -

その2…全角スペース区切りのワナ

ロケールに関する環境変数(LC_*LANGなど)が設定してある環境で使っている人にはもう一つのワナが待ち構えているので注意しなければならない。

次のサンプルデータを見てもらいたい。これは、第1列に人名、第2列にかな、という構成の名簿データだ。注目すべきは苗字と名前の間には全角スペースが入っている点である。

sample3.txt
白瀬 慧 しらせ あきら
天野 美紗緒 あまの みさお
本木 紗英 もとき さえ

「名簿順にソートせよ」

普通に考えれば、-k 2,2だ。かなが第2列にあるのだから。ところが日本語ロケール(LANG=ja_JP.UTF-8など)になっているLinux環境で実行すると失敗する。

$ sort -k 2,2 sample3.txt
白瀬 慧 しらせ あきら
天野 美紗緒 あまの みさお
本木 紗英 もとき さえ

原因は、全角スペースも列区切り文字扱いされているということだ。

正しくやるには、環境変数を無効にする、もしくは-tオプションで区切り文字は半角スペースだと設定しなければならない。

具体的には、sortコマンド引数に-t ' 'と追記してやればよい。

$ sort -k 2,2 -t ' ' sample3.txt
天野 美紗緒 あまの みさお
白瀬 慧 しらせ あきら
本木 紗英 もとき さえ

おめでとう、これでアナタも今日からsortコマンドマスターだ。本当はここで説明していない機能が他にもあるが、それらについて知りたければ、man(こことかここ)を見るといいだろう。