初歩のシェルスクリプトで遊ぶ[ぬかみそフォントの制作サポート(14)]

簡易な文字種類を数えるシェルスクリプト『暖簾』

何をするスクリプト

  • テキストファイルを突っ込むと、使われている文字の種類をリストにする。
  • 文字の種類ごとに、テキストファイルを比較する。
    • 片方にだけ使われている文字は?
    • 両方に使われている文字は?
    • 異体字を同じ文字だと考えて、片方に使われている漢字と、もう片方に使われている漢字に、共通する漢字は?

たとえば、テキストファイルが2個あって、片方には「味噌」、もう片方には「み噌󠄀」とだけ書かれてたとします。この共通の文字は、「噌󠄀噌」です、と出る。


スクリプトの現物は……まだ出してないけどいいや。どうせ誰も使わん。手直ししなくなった頃に、気が向いたら出しときます。

基本処理

『utf8テキスト』『コードリスト』『ビットリスト』

あいお
12354	0
12356	0
12362	0
1
1
0
0
1
いうえお
12356	0
12358	0
12360	0
12362	0
0
1
1
1
1

元のテキストは、普通に読めるutf8のテキストファイルです。このテキストを、unicodeの番号に変換して、1行1文字ずつ、文字の種類をリストにします。処理のために10進数で。
ビットリスト、と名付けたファイルは、見てのとおり「0」「1」の並びです。「1」は文字が有る。「0」は文字が無い。行は、1行目から「あいうえお」を意味してる。
これらの文字データは、それぞれテキストファイルです。

「文字テキスト」→「コードリスト」→「ビットリスト」
「文字テキスト」←「コードリスト」←「ビットリスト」

というように、相互変換ができます。文字をコードに変えて、コードをビットに変えて、ビットをコードに変えて、コードを文字に戻す。



たとえば2つのファイルに共通の文字は何か、を調べたいならば、ビットリストをpasteコマンドで繋げます。

()  10
()  11
()  01
()  01
()  11

「あいうえお」は、分かりやすくするための追記です。

行が「11」ならば、両方のテキストに共通して有る文字を意味する。
「10」ならば、左のファイルに有り、右のファイルに無い文字、を意味する。

もし左のファイルが「人名漢字」で、右のファイルが「JIS第一水準」ならば、「11」で検索すると、「人名漢字かつ、第一水準の漢字」が抜き出せます。

pasteで繋げるビットリストファイルは、3つでも同様にできる、のが利点です。たとえば「110」で検索すると、二つに共通し、もう一つに無い文字、なんてのも抜き出せます。

漢字の異体字も扱う

味噌
21619	0
22092	0
0
1
1
0
み噌󠄀
12415	0
22092	917760
1
0
0
1

Code_total

12415	0
21619	0
22092	0
22092	917760

「味噌」と「み噌󠄀」の例です。異体字セレクタがついている「噌󠄀」には、異体字セレクタの数字がコードの2列目に付いてます。
もしさっきのように、ビットの「10」で検索したならば、異体字と基底文字は別の文字として調べることができる。これはこれで必要です。
では、異体字を同じ文字と見なして、共通文字を抜き出すにはどうするか。
今度はビットのファイルをpasteではなくて、コードのファイルをjoinします。

22092 0 917760

共通のキー、つまり基底文字のunicode番号で、一行にまとめて抜き出せました。
この1行を2行に分けて書き出せば、「噌噌󠄀」になります。
もしビットの「1」「0」で共通文字を探していたら、「共通文字は無し」になる。

利点

上に「Code_total」というのがありますが、これは処理中のテキストファイルの全ての文字をコードにしたものです。このtotalを基準にして、ビットリストの「1」「0」を決めています。16進数と文字を書き加えると以下になる。

12415	0	307f	021619	0	5473	022092	0	564c	022092	917760	564c	e0100	噌󠄀

「1」「0」を並べたファイルをpasteでくっつける、というのが最初の思い付きだったんですが、では、行がどの文字を示すかをどうやって決めるか。
最初は異体字を扱わないつもりだったもんで、とりあえず行番号をUnicodeのコード番号にして試作しました。これだと、コードリストが要りません。
TTEditで扱える範囲なら、せいぜい20万行もあれば足ります。基底文字だけなら、こんなのでも十分でした。

しかし、漢字の異体字を扱うには、ちょっと困るわけです。漢字の異体字セレクタはVS17~VS256までもあるし、これらに1行1文字を当てはめるわけにもいかない。
そういう理由から、「〇行目の文字は~である」を決めるファイルを、可変で作る仕様に変えました。

Unicodeの番号を、そのまま行番号とするように、行と文字の対応を固定する方式と比べて優れてるのは、面倒なことを考えなくても済む、って点です。

  • 手持ちのテキストファイルの「比較」ができればよいので、Unicodeの最大コード番号は何番だろうか、漢字は何番から何番だろうか、異体字セレクタは何種類使われているだろうか、などと考えなくていい。
  • 文字の種類が少ないなら、行数も少なくて済む。速い。
  • 「全ての文字の種類のコードリスト」を作るのが面倒そうな気がするが、実はめちゃくちゃ簡単。「cat」でテキストファイルを繋げてしまえばいい。

もともと「文字テキスト→コードリスト」の変換スクリプト自体が、sortコマンドとuniqコマンドを使って、並び替えと重複削除をしています。なので、「これら全てのテキストファイルに使われている文字、すべてのコード」が欲しいのなら、単純にcatで繋げて、一気にスクリプトに突っ込めば済みます。

シェルスクリプトで、文字をコードに変換するのが以下です。
iconvでutf32に変換してから、odでダンプリストに変えてます。異体字セレクタが来たら、直前の文字と並べて出力。
複雑な絵文字とかには対応できませんが、それはそれとして。

#!/bin/sh
#set -x
# ========================================================================================
# ■ 「暖簾」基底文字とVSを符号化 文字集合のリストを作成する_Unicodeベースで
# 
# ・基底文字と異体字セレクタのリストを作成する。
# 
# ・異体字セレクタはIVSのみ扱う。 (VS以外は無視)
#   U+E0100 - U+E01EF  (VS17-VS256)
#    917760 -  917999
# ・SVSも入れるが、IVSと同様の処理しかしない。
#    U+FE00 - U+FE0F (VS1-VS16)
#     65024 -  65039
# ・濁点と半濁点 、は検討したけどやめた。
#    U+3099 U+309A
# 
# 
# ▼ 入力形式
# 漢字のIVS異体字を含むutf-8テキスト
# stdinから入力する。
# 絵文字は基本的に扱えない。
# 
# ▼ 出力形式
# codeDec  vs
#  12378	0
#  12415	0
#  20518	917760
#  20677	0
#  20900	0
#  20900	917760
#  20958	917764
#  21255	917761
# 
# Unicode10進 
#             「0」は基底文字のみ
#             「1」以上は基底文字直後に表記された数値のvsが有る
# 
# Unicodeの最大
# 10ffff = 1114111
# 
# 
# 
# 
# ========================================================================================



# ▼ 入力のダンプリスト化とソート、重複文字データを除去
{
# タブと改行は削除する。半角空白は残す。
tr -d '\n\t\r'

} | {
# utf32leに変換して、1行4バイト、10進数、一文字幅で出力
# 4バイトの値を10進数で直接に出力
iconv -f utf8 -t utf32le | od -tu4 -An -v -w4 #--endian=little

} | {
# 前コードが0ならば空と見なす
preCode='0'

while read codeDec ; do

if test "${preCode}" -eq 0 ; then
# もし前コードが空ならば、基底文字が入ってきているはずなので
# いったん基底文字を保存する
preCode="${codeDec}"
continue

elif test "${codeDec}" -ge 917760 && test "${codeDec}" -le 917999 ; then
# 前コードが存在し、
# IVSが入ってきたならば
# 前コードは基底文字と見なして出力、空(=0)にセット
# VSは16進数出力してはならない(sortでVSの順番が狂う)

printf '%d\t%d\n' "${preCode}" "${codeDec}"
preCode='0'
continue

elif test "${codeDec}" -ge 65024 && test "${codeDec}" -le 65039 ; then
# 同様にSVSが入ってきたならば

printf '%d\t%d\n' "${preCode}" "${codeDec}"
preCode='0'
continue


else
# 前コードが存在し、
# 基底文字が入ってきたならば、
# 前コードはvs無し基底文字と見なし出力し、
# いったん現基底文字を保存する

printf '%d\t%d\n' "${preCode}" '0'
preCode="${codeDec}"
continue

fi


done

# 最終入力文字は単独基底文字、またはvs
# もしvsならば、既に出力されており、preCode=0にセットされている
# 単独基底文字ならば、preCodeに保存されている

if test "${preCode}" -ne 0 ; then
printf '%d\t%d\n' "${preCode}" '0'

fi


} | {
# 10進数でバージョンソートし、重複除去
sort -V | uniq

} 

exit 0