文章を扱うコマンドをシェルスクリプトで作成する。作るコマンドは語尾のチェックコマンドとスペルチェックのコマンドだ。いずれのコマンドも既存のコマンドをうまく組み合わせて短いものを作る。
今回の内容ではGancarzのUNIX哲学の中でも特に「各プログラムが1つのことをうまくやるようにせよ。全てのプログラムはフィルタとして振る舞うようにせよ」を意識しておいてほしい。
ここでの作業環境はMac OS Xとし、GNU sed (gsed)とGNU awk (gawk)がインストールされているものとする。リスト1に環境を示す。
$ uname -a Darwin uedamac.local 12.4.0 Darwin Kernel Version 12.4.0: Wed May 1 17:57:12 PDT 2013; root:xnu-2050.24.15~1/RELEASE_X86_64 x86_64 $ gsed --version gsed (GNU sed) 4.2.2 (略) $ gawk --version GNU Awk 4.1.0, API: 1.0 Copyright (C) 1989, 1991-2013 Free Software Foundation. $
加工する文章はreStructuredTextという形式で記述されている原稿とする。reStructuredText形式のファイルの拡張子は.rstとなる。
$ ls *.rst 201201.rst 201210.rst 201306.rst 201202.rst 201211.rst 201306SPECIAL.rst 201203.rst 201212.rst 201307.rst (略) $
原稿にはリスト3のようにだいたい30字くらいで改行を入れてある。
$ tail -n 5 201302.rst lsとwcを使えば事足る。captiveでないので、なんとかなる。 今回は正直言いまして、 かなりエクストリームなプログラミングになってしまったので、 次回からはもうちょっとマイルドな話題を扱いたいと思う。 $
作成したコマンドなどはディレクトリSD_GENKOUの下にbinというディレクトリを作ってそこに置く事にする。
表記揺れの基本となる敬体(ですます調)と常体(である調)のチェックを行うシェルスクリプトを作る。
$ cat desumasu1 #!/bin/sh tmp=/tmp/$$ cat > $tmp-text desu="(です。|ます。|でした。|ました。|でしょう。|ません。)" da="(だ。|である。|ない。|か。)" grep -E "$desu" $tmp-text | wc -l | tr -d ' ' > $tmp-desu grep -E "$da" $tmp-text | wc -l | tr -d ' ' > $tmp-da echo "ですます" $(cat $tmp-desu) echo "だである" $(cat $tmp-da) rm -f $tmp-* exit 0 $
6行目で標準入力を$tmp-textに一度溜めている。8、9行目で正規表現を作る。自分の困らない範囲で列挙しておけばよいだろう。
$ cat ../*.rst | gsed 's/....。/\n&\n/g' | grep 。| gsed 's/。.*/。/' | sort -u (略) (縦) 軸。 (?) を。 ) を作れ。 ) を知る。 :私です。 $
実行するとリスト6にようになる。原稿は敬体で書かれているが1.5%程度常体で記載されている部分があるように見える。
$ cat *.rst | ./bin/desumasu1 ですます 2252 だである 32 $
常体がちょっと混ざっているようなのだが、次はどこを修正しなければならないのか知りたくなってくる。ここでは次のように新しいコマンドを作る。
$ cat ./bin/desumasu2 #!/bin/sh desu="です。|ます。|でしょう。|ません。" da="だ。|である。|ない。|か。" gawk '{print FILENAME ":" FNR ":" ,$0}' "$@" | gawk -v desu=$desu -v da=$da \ '$0~desu{print "+",$0}$0~da{print "-",$0}' $
7行目の"$@"はdesumasu2がもらったオプションをそのままgawkに渡すための方法だ。"$*"だと複数のファイル名がオプションに入っている場合にすべての引数が単一も文字列として扱われるためうまくいかない。たちえばリスト8の例では"201311.rst 201211.rst"が1つのファイル名だと解釈されるためcat がエラーを出す。
次に検索で引っ掛ける文字列は4、5行目でシェル変数として定義している。これを12行目でgawkに引き渡している。正規表現を変数に渡している。7行目のFNRは行番号が格納された変数。NR と違って読み込んだファイルごとの行番号が格納されている。この例のように「あるファイルの何行目」を出力するときに使用する。
$ cat ./bin/fail_sample #!/bin/sh cat "$*" $ chmod +x ./bin/fail_sample $ ./bin/fail_sample 201311.rst 201211.rst cat: 201311.rst 201211.rst: No such file or directory $
実際に使う場合はdesumasu2の出力からgrep "^-" で常体の行を抜き出し目で検査することになる。もしこれで分からなければファイル名と行番号が書いてあるので当該のファイルを開いて前後の文脈を見ればよい。
$ cat *.rst | ./bin/desumasu2 | tail -n 3 + -:13184: //--dont-suggestを指定すると、候補が出てきません。 + -:13195: エディタを開かなくてもどこに疑わしい単語があるかチェックできます。 + -:13197: エディタから独立させておくと、思わぬところで助けられることがあります。 $ ./bin/deathmath2 *.rst | tail -n 3 + 201311.rst:338: //--dont-suggestを指定すると、候補が出てきません。 + 201311.rst:349: エディタを開かなくてもどこに疑わしい単語があるかチェックできます。 + 201311.rst:351: エディタから独立させておくと、思わぬところで助けられることがあります。 $ cat *.rst | ./bin/deathmath2 | awk '{print $1}' | sort | uniq -c 2268 + 33 - $ ./bin/deathmath2 *.rst | grep "^-" | head -n 3 - 201202.rst:562: 寒さに負けず端末を叩いておられますでしょうか。 - 201202.rst:565: ドア用の close コマンドがないものか。 - 201202.rst:607: * プログラマの時間は貴重である。(略) $
リスト8のようにdesumasu2の出力をawk、sort、uniqで加工するとdesumasu1のような答えが得られる。このようにすることで1つのコマンドに1つの機能だけを持たせることができ、使い手がコマンドの機能を覚えるのが簡単、そしてほかのコマンドと組み合わせて処理をさせるといったことも効率よくできるようになる。
次に英単語のスペルチェックを行うスクリプトを作る。スペルチェッカーは通常エディタから読み出して使うが、ここではコマンド仕立てにする。
まずスペルチェッカGNU Aspellをインストールする。MacだとHomebrewでリスト9のようにインストールできる。
$ brew install aspell $
シェルスクリプトからAspellを使いたいので、対話形式ではなくフィルタとして使用する。-aを指定することでフィルタとして機能させることができる。
$ man aspell (略) pipe, -a Run Aspell in ispell -a compatibility mode. $
試しに使ってみよう。リスト11のように環境変数LANGをCなどに設定してから動作させる。
$ echo "All your base are berong to us." | LANG=C aspell -a @(#) International Ispell Version 3.1.20 (but really Aspell 0.60.6.1) (略) * & berong 25 18: Bering, bronc, belong, Behring, bearing, (略) //--dont-suggestを指定すると候補が出てこない。 $ echo "All your base are berong to us." | LANG=C aspell -a --dont-suggest @(#) International Ispell Version 3.1.20 (but really Aspell 0.60.6.1) (略) * # berong 18 (略) $
まず補助的なコマンドとして、疑わしいスペルのリストを表示するコマンドをリスト12のようなスクリプトを作る。aspellはバッククォートなどの記号類にも反応する事があり、また、日本語が入ると何が起こるかわからないので4行目のgsedで単語に使う文字だけ残してあとは空白に変換している。
$ cat ./bin/henspell-list #!/bin/sh gsed "s/[^a-zA-Z0-9']/ /g" "$@" | LANG=C aspell -a --dont-suggest | gawk '/^#/{print $2}' | sort -u $
リスト13のようにまともな単語も引っかかるが、これはAspellの辞書にこれらの単語を登録することで出なくなる。
$ ./bin/henspell-list 201311.rst FILENAME FNR (略) berong (略) $
辞書ファイルにはいくつかの種類があるが、今回のケースではリスト14のように1行目に「personal_ws-1.1 en 0」と書いて、あとは引っかかった正しい単語をひたすら書いていくと作れる。
$ head -n 5 ./bin/dict personal_ws-1.1 en 0 FILENAME FNR GENKOU Gancarz (略) $
これをhenspell-listに読み込ませるとよいということになる。
$ cat ./bin/henspell-list #!/bin/sh dict=$(dirname $0)/dict gsed "s/[^a-zA-Z0-9']/ /g" "$@" | LANG=C aspell -p "$dict" -a --dont-suggest | gawk '/^#/{print $2}' | sort -u $ ./bin/henspell-list 201311.rst berong da desumeth dirname zA $
次にこのコードを利用してもとの原稿のどこに変なスペルがありそうなのかを表示する。リスト16に作成したコマンドを示す。grepの-wオプションは単語の検索を行う。-nは出力に行番号を表示する指定、-f <FILE>で検索対象の文字列をFILEから読み込むとなっている。
$ cat ./bin/henspell #!/bin/sh tmp=/tmp/$$ if [ "$#" -eq 0 ] ; then cat | tee $tmp-stdin | $(dirname $0)/henspell-list > $tmp-list grep -w -n -f $tmp-list < $tmp-stdin else $(dirname $0)/henspell-list "$@" > $tmp-list grep -w -n -f $tmp-list "$@" fi rm $tmp-* exit 0 $
利用するときはリスト17のようにページャで受けて本当にスペルミスがないか探す事になるだろう。
$ ./bin/henspell 201311.rst | less 14:Macには、GNU sed( ``gdes`` )がインストールされているものとします。 87: da="(だ。|である。|ない。|か。)" ... 217:``deathmarch2`` がもらったオプションをそのまま ``awk`` ... $
今回はシェルスクリプトで文章チェックのためのコマンドを作った。このようにシェルスクリプトでコマンドを作ることを覚えると、1日かけていた作業が数秒で終わるという幸運なことに何回か巡り会うことができる。シェルスクリプトでコマンドを作ると他のコマンドも呼び出せるから、この方法はオススメだ。
Software Design 2013年11月号 上田隆一著、「テキストデータならお手のもの 開眼シェルスクリプト 【23】文章の表記揺れ/綴りをチェックする―コマンドを自作する時は単機能で」より加筆修正後転載