Perl
grep
diff
docx
pptx

Word や PowerPoint のファイルを grep したり diff したりする

MS のドキュメントを端末で操作する

マイクロソフトという会社は別に嫌いではないが、ソフトウェアさえ作ってくれなければいいのになあと思う。そうは言っても Word の資料を扱わなければならないことはあり、読めと言われれば仕方がないので嫌でもビューアーとかアプリを立ち上げるわけだが、古い資料を検索したり、バージョンの違うドキュメントを比較したりしようとした途端に息が詰まる。これをなんとかしようという話。

grep する

正確には grep ではなく greple 用の msdoc というモジュールを作った。greple モジュールは -M オプションで指定する。

$ greple -Mmsdoc 経費 kisairei.docx

スクリーンショット.png

複数の検索ワードを指定できるので「経費」と「換算」という単語を含む行を探したければ、こんな風に使う。

$ greple -Mmsdoc '経費 換算' kisairei.docx

ちなみに、こうしても同じだ。

$ greple -Mmsdoc -e 経費 -e 換算 kisairei.docx

-p オプションをつけるとパラグラフ単位で表示する。word のドキュメントは1パラグラフが1行なので、内容は変わらないが境界はわかりやすくなる。

スクリーンショット 2018-06-29 17.53.34.png

特定の単語を含まない行を見たければ、こんな風にすると、上の例であれば最初のパラグラフのみが出力される。

$ greple -Mmsdoc '経費 換算 -物品調達' kisairei.docx
$ greple -Mmsdoc -e 経費 -e 換算 -v 物品調達 kisairei.docx

PowerPoint の場合はページ境界に空行が入るので、パラグラフ単位で検索すると、大体ページ単位の出力が得られる。

cat する

さて、検索はできるようになった。ファイル全体を表示させることもオプションを工夫すればできる。

$ greple -Mmsdoc '\A' --all kisairei.docx

\A は、データの先頭なので必ずマッチする。--all を指定するとデータ全体をブロックとして扱うので、結果的にファイルの内容が全部表示される。App::Greple::msdoc モジュールには、これと同じような動作をする --dump というオプションを実装してある1。テキスト部分だけを抜き出しているので、フォーマット情報は皆無だが、大体の意味はわかる。このように、word の場合は、パラグラフの間に空行が入る。

スクリーンショット

less する

もちろん --dump の結果をパイプで送れば less で見ることはできるが、LESSOPEN という環境変数を使うと直接ファイルを指定することができる。

$ LESSOPEN="| greple -Mmsdoc --dump %s" less kisairei.docx

しかし、いつも設定しておくわけにもいかない。ということで、optex を使うことにする。~/.optex.d/less.rc というファイルに、こんな設定をする。

~/.optex.d/less.rc
option --ms  -Mutil::setenv(LESSOPEN="| greple -Mmsdoc --dump %s")
option --msx -Mutil::setenv(LESSOPEN="| greple -Mmsdoc --dump --indent %s")

これで --ms--msx というオプションが使えるようになり、こんな風に実行できる。

$ optex less --ms kisairei.docx

ちなみに --msx というオプションを指定すると greple -Mmsdoc--indent オプションをつけて実行して、XML をインデントして表示する。

スクリーンショット

$ optex --ln less

を実行すると、~/.optex.d/bin の下に lessoptex のシンボリックリンクを作成する。ここを PATH に入れて less を実行すると、自動的に optex 経由で実行される。なので less コマンドに、--ms--msx という新たなオプションが追加されたように見える。

$ less --ms kisairei.docx

スクリーンショット

もちろん、alias しても ok。

ホントに grep する

grepleless には、入力を制御する仕組みがあるので、上のような対応ができる。しかし普通のコマンドにはそんなインタフェースは用意されていない。

でも、bash の process substitution を使えば、引数にコマンドを指定することができる。

$ grep 経費 <(greple -Mmsdoc --dump kisairei.docx)

しかし、いちいち入力するのは面倒なので、ファイル名を見て、自動的にデータを変換してくれるとうれしい。

というわけで、これと同等の動きをする optex のモジュールを作った。App::optex::msdoc を使うと、ファイル名を見て、それを変換したデータを読むためのパスにコマンド引数を差し替える。

たとえば ~/.optex.d/default.rc に、次のような設定をする2

~/.optex.d/default.rc
option --msdoc -Mmsdoc $<move>

こうすると、optex 経由で実行する、すべてのコマンドで --msdoc というオプションが使えるようになる。ほら、grep だってできた。

$ grep --msdoc -e 経費 -e 換算 kisairei.docx

スクリーンショット

パターンを複数指定した場合の grepgreple の挙動には違いがあり、grep はどれかのパターンを含む行をすべて出力するのに対して、greple はすべてのパターンを含む行だけを出力する。また、grep はすべてのパターンを同じ色で表示するが greple は異なる色を使う。

diff する

optex::msdoc を使った場合、引数で指定したファイルをすべて変換した後にコマンドが実行される。だから多くのファイルを同時に指定して grep したり less したりする用途には向かない。その点 diff で指定するファイルは2つに決まっている。

実際のところ optex::msdoc モジュールは、ファイル名を見て必要な時にのみ処理をするので、不要な場合に使ってもオーバーヘッドはほとんどない。diff コマンドで常に有効にしたければ、~/.optex.d/diff.rc に default を設定する。

~/.optex.d/diff.rc
option default --msdoc

こうすれば、

$ optex diff kisairei-H25.docx kisairei-H26.docx

で word ファイルをテキストベースで比較することができる。下は、この出力を cdif に通した結果だ。cdif--mecab オプション付きで実行されているので、mecab が分割した単位で色付けされている。

word ドキュメントの diff 出力は、パラグラフ単位になってしまうので、ツールのサポートがないと、どこが変更されたのかを判別しにくい。diff 出力をハイライトするコマンドや環境はいくつもあるが、日本語をちゃんと処理できるものは、今の所 cdif 以外に知らない3

スクリーンショット

これで、さらに diffoptex のシンボリックリンクをパスに入れてあれば、単に diff するだけでいいのだが、間接的に実行される diff コマンドも置き換えられてしまうことになるので、バックエンドで大量の diff を実行する場合にはオーバーヘッドが問題になる。というか、この例の cdif がまさに影響を受ける。使いたければ alias などを使って対話型シェルでのみで有効にした方がいいだろう。その場合は、default を設定するのではなく alias に含めたらいいだろう。

$ alias diff="optex diff -Mmsdoc"

git でも diff する

git diff を実行できるようにするためには、~/.config/git/attributes ファイルに以下のような設定をして、ファイル拡張子に対する属性を定義する。

~/.config/git/attributes
*.docx   diff=msdoc
*.pptx   diff=msdoc
*.xlsx   diff=msdoc

(1) textconv を使う

git の設定で、textconv を指定する。

$ git config --global diff.msdoc.textconv "greple -Mmsdoc --dump"

こうすると ~/.gitconfig に次のような設定が入る。手で編集してもいい。

~/.gitconfig
[diff "msdoc"]
        textconv = greple -Mmsdoc --dump

この状態で git diff を実行すると、ここで指定したフィルターを通した結果を比較する。自分の場合、.gitconfig をこう設定してあるので、出力は sdif を通して表示される。

~/.gitconfig
[pager]
        log  = sdif -n | less -cR
        show = sdif -n | less -cR
        diff = sdif -n | less -cR

スクリーンショット

(2) external diff command を使う

git diff するには、textconv ではなく、command を設定する方法もある。

$ git config --global diff.msdoc.command "optex --exit 0 diff --msdoc -u --git-external-diff"

~/.gitconfig の内容はこうなる。

~/.gitconfig
[diff "msdoc"]
        command = optex --exit 0 diff --msdoc -u --git-external-diff

--git-external-diff というオプションは、App::optex::msdoc で設定されている。

App/optex/msdoc.pm
##
## GIT_EXTERNAL_DIFF is called with 7 parameters:
##    path old-file old-hex old-mode new-file new-hex new-mode
##    0    1        2       3        4        5       6
##
option --git-external-diff $<copy(1,1)> $<copy(4,1)> $<remove>

コメントにあるように、git の外部コマンドは7つのパラメータと共に実行され、その2番目と5番目に新旧のファイル名が指定されている。このオプションは、その2つを取り出している。optex--exit 0 というオプションが指定してあるのは、こうしてコマンドを正常終了しないと git でエラーになるためだ。

スクリーンショット 2018-07-01 00.21.47.png

インストール

cpanminus

まずは cpanm コマンド (cpanminus) が必要だ。Mac だったら brew でインストールできる。

$ brew install cpanminus

そうでなければ、apt コマンドとかを使うのだと思う。https://qiita.com/debug-ito/items/7caaecf6988870973438 などを参考にしてほしい。

ローカルにインストールするのなら、PATH などを設定する。

export PATH=${PATH}:${HOME}/perl5/bin
export PERL5LIB=${HOME}/perl5/lib/perl5:${PERL5LIB}
export MANPATH=${HOME}/perl5/man:${MANPATH}

それ以外に以下の条件が必要。

  • /dev/fd
  • unzip コマンド
  • perl5.014 以上

greple, optex

以上の環境を作るためには

  • App::Greple
  • App::Greple::msdoc
  • App::optex
  • App::optex::msdoc

という4つをインストールする必要があるが、App::optex::msdoc は他の3つに依存しているので、これをインストールすれば全部が入るはずだ。

$ cpanm App::optex::msdoc

自分の ~/.greplerc は、出力をパイプに渡した時にも着色するように、こう設定してある。

~/.greplerc
option default --color=always

greple の出力を less で見るのには -R オプションが必要。LESSLESSANSIENDCHARS という環境変数を設定する。後者は、若干一般的ではない端末制御シークエンスを使っているため4

export LESS=-cR
export LESSANSIENDCHARS=mK

sdif, cdif

sdifcdif を使いたい場合は App::sdif をインストールする5

$ cpanm App::sdif

cdif--mecab オプションを有効にするには、~/.cdifrc を設定する。もちろん mecab コマンドがインストールされている必要がある。

~/.cdifrc
option default --mecab

sdif には、3種類のテーマ色のようなものが定義してある。デフォルトは green で、それ以外に cmy と mono が使える。cmy を使いたければ、~/.sdifrc を次のように設定する。

~/.sdifrc
option --light --cmy
option --dark  --dark-cmy

端末の背景色

バックグラウンドが白系のターミナルを使っている場合は、デフォルトの設定でうまく表示できるはずだ。また、Apple Terminal を使っている場合も、自動的に調整される6。それ以外の黒系のターミナルを使っている人は sdif のマニュアルの COLOR セクションを読んでほしい。読むのが面倒な人は、とりあえず BRIGHTNESS という環境変数を 0 に設定して試してみるといい。

export BRIGHTNESS=0

スクリーンショット

明るい端末用の設定のままだとこんな風に見える。このままでも使えなくはないが、なんかケバい。

スクリーンショット 2018-07-03 11.35.10.png

SEE ALSO

本格的に変換したければ、そのようなツールを使うべきだ。ただ、どれも結構時間はかかるので、気軽に grep するような気分にはならないと思う。App::Greple::msdoc は、手抜きな分だけ高速に動作する。

greple で、これらを入力フィルタとして使うのは簡単だ。~/.greplerc に、こんな設定を書いておくと --pandoc, --tika というオプションが使えるようになる。pandoc は、どうやら pptx と xlsx という入力形式を理解しないようだ。tika は、標準入力からどんな形式も受け付けるところがすごい。ただ、手元の環境では変なエラーが出る。

~/.greplerc
option --pandoc \
    --if '/\.docx$/:pandoc -f docx -t plain'

option --tika \
    --if '/\.(docx|pptx|xlsx)$/:tika --text'

スクリーンショット

スクリーンショット

それにしても、結果が随分と違う。実行速度については、tika は遅すぎるが、pandoc だったら高速なマシンなら耐えられるかもしれない。


  1. 実際にはこう。

    option --dump --le &sub{} --need 0 --all --epilogue 'sub{exit(0)}'
    --le&sub{} で空リストを返す関数、つまり何もマッチしないパターンを定義しているが --need 0 を指定しているので、マッチしなくても表示する。\A を避けたのは、空文字列にマッチしても、エスケープシークエンスは出力する仕様になっているためだ。--nocolor にしてもそれは防げるが、こうすることでパターンを追加すれば着色して表示することができる。最初の色が消費されてしまうのがイマイチだが、そこは目を瞑った。マッチしないので grep と同様に exit(1) する。git difftextconv コマンドは正常終了しなければならないので、苦肉の策で --epilogueexit(0) している。 
  2. ここで $<move> が必要なのには少々解説が必要だ。optex というか Getopt::EX モジュールがオプションを展開する時には、展開後の先頭にある -M で始まるオプションをモジュール指定として取り扱うのだが、この時展開した結果だけが処理に渡され、残りの引数があっても無視される。optex::msdoc モジュールは、その後に続く引数を操作するので、そこに対象が含まれないと操作対象にならないのだ。$<move> は、それに続くすべての引数をそこに移動して展開する効果を持っている。実は、自分自身、これがうまく動かずにかなり悩んだ。 

  3. これについては随分昔だが記事を書いた。もう5年も前なので、状況は変わっているかもしれない。素敵なツールがあったら教えて欲しい。cdif の欠点は、無茶苦茶速いわけではないところだ。 

  4. 具体的にはパラグラフ境界を示すマークだ。BLOCKEND の色指定は /WE になっていて、/W は白のバックグラウンドを表す。E{EL} と同じで、Erase Line の意味。つまりバックグラウンド色で行末まで塗りつぶす効果がある。通常の色指定のシークエンスは m で終わるが EL は K で終わる。 

  5. これで sdif, cdif, watchdiff という3つのスクリプトがインストールされる。watchdiff は、一定間隔で指定したコマンドを実行して、変更のあった部分を強調表示する。 

  6. AppleScript を使って Terminal の背景色を取得し、RGB から輝度を計算して 0-100 の数値に変換し、50以上なら --light そうでなければ --dark というオプションを設定している。See perldoc -m App::sdif::autocolor::Apple_Terminal