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

### 追記

その後、`optex` だけを使ってより汎用的なインタフェースで実装しました。今から使うんだったら、こちらの方がいいと思います。

- [MS Office ドキュメントや PDF ファイルを grep したり diff したりする - Qiita](https://qiita.com/kaz-utashiro/items/23fd825bd325240592c2)

### 追記2

greple の -Mmsdoc モジュールは、1.04 (2020-09-23) から optex -Mtextconv のコードを利用するように変更されています。なので、今はどちらを使っても同じ結果になります。optex を使うと、指定したすべてのファイルを変換してからコマンドを実行するので、多くのファイルを指定すると時間がかかります。`-Mmsdoc` モジュールの場合は、都度変換しながら処理します。

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

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

# grep する

正確には grep ではなく [greple](https://github.com/kaz-utashiro/greple) 用の [msdoc](https://github.com/kaz-utashiro/greple-msdoc) というモジュールを作った。`greple` モジュールは `-M` オプションで指定する。

```
$ greple -Mmsdoc 経費 kisairei.docx
```
<img width="762" alt="スクリーンショット.png" src="https://qiita-image-store.s3.amazonaws.com/0/36551/3753fe7a-6122-489c-3f4d-71faa68ff5e7.png">

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

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

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

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

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

<img width="762" alt="スクリーンショット 2018-06-29 17.53.34.png" src="https://qiita-image-store.s3.amazonaws.com/0/36551/0ad5c6a5-12da-e983-d1b3-c771f9f5ad7c.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` というオプションを実装してある[^--dump]。テキスト部分だけを抜き出しているので、フォーマット情報は皆無だが、大体の意味はわかる。このように、word の場合は、パラグラフの間に空行が入る。

[^--dump]: 実際にはこう。<pre>option --dump --le &sub{} --need 0 --all --epilogue 'sub{exit(0)}'</pre>`--le` で `&sub{}` で空リストを返す関数、つまり何もマッチしないパターンを定義しているが `--need 0` を指定しているので、マッチしなくても表示する。`\A` を避けたのは、空文字列にマッチしても、エスケープシークエンスは出力する仕様になっているためだ。`--nocolor` にしてもそれは防げるが、こうすることでパターンを追加すれば着色して表示することができる。最初の色が消費されてしまうのがイマイチだが、そこは目を瞑った。マッチしないので `grep` と同様に `exit(1)` する。`git diff` の `textconv` コマンドは正常終了しなければならないので、苦肉の策で `--epilogue` で `exit(0)` している。


<img width="762" alt="スクリーンショット" src="https://qiita-image-store.s3.amazonaws.com/0/36551/e9e59ef4-11e6-be8a-aa0e-c5940af673a7.png">

# less する

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

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

しかし、いつも設定しておくわけにもいかない。ということで、[optex](https://github.com/kaz-utashiro/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 をインデントして表示する。

<img width="898" alt="スクリーンショット" src="https://qiita-image-store.s3.amazonaws.com/0/36551/c704f124-d91f-3d03-fa23-31d3b874c40b.png">

```
$ optex --ln less
```

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

```
$ less --ms kisairei.docx
```

<img width="682" alt="スクリーンショット" src="https://qiita-image-store.s3.amazonaws.com/0/36551/fd3f1d19-cdd1-9c84-d0c8-485c86aef91c.png">


もちろん、alias しても ok。

# ホントに grep する

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

でも、`bash` のプロセス置換機能を使えば、引数にコマンドを指定することができる。

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

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

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

たとえば `~/.optex.d/default.rc` に、次のような設定をする[^move]。

[^move]: ここで `$<move>` が必要なのには少々解説が必要だ。`optex` というか `Getopt::EX` モジュールがオプションを展開する時には、展開後の先頭にある `-M` で始まるオプションをモジュール指定として取り扱うのだが、この時展開した結果だけが処理に渡され、残りの引数があっても無視される。`optex::msdoc` モジュールは、その後に続く引数を操作するので、そこに対象が含まれないと操作対象にならないのだ。`$<move>` は、それに続くすべての引数をそこに移動して展開する効果を持っている。実は、自分自身、これがうまく動かずにかなり悩んだ。

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

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

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

<img width="762" alt="スクリーンショット" src="https://qiita-image-store.s3.amazonaws.com/0/36551/3719b44a-3551-2ddf-a4d2-65892667e9c3.png">

パターンを複数指定した場合の `grep` と `greple` の挙動には違いがあり、`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](https://github.com/kaz-utashiro/sdif-tools) に通した結果だ。`cdif` は `--mecab` オプション付きで実行されているので、mecab が分割した単位で色付けされている。

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

[^cdif]: これについては随分昔だが[記事を書いた](http://utashiro.hatenablog.com/entry/2013/12/28/114121)。もう5年も前なので、状況は変わっているかもしれない。素敵なツールがあったら教えて欲しい。cdif の欠点は、無茶苦茶速いわけではないところだ。

<img width="762" alt="スクリーンショット" src="https://qiita-image-store.s3.amazonaws.com/0/36551/198554de-df54-1d98-1d39-4fc6931c57b5.png">

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

```
$ alias diff="optex diff -Mmsdoc"
```

# git でも diff する

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

```toml:~/.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` に次のような設定が入る。手で編集してもいい。

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

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

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


<img width="1242" alt="スクリーンショット" src="https://qiita-image-store.s3.amazonaws.com/0/36551/bb26be10-8298-be7e-de79-ad4cfa9e6cee.png">

## (2) external diff command を使う

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

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

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

```toml:~/.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 でエラーになるためだ。

<img width="682" alt="スクリーンショット 2018-07-01 00.21.47.png" src="https://qiita-image-store.s3.amazonaws.com/0/36551/9ec0fa9b-cb93-3861-8d35-b2715feb5bd9.png">

# インストール

## cpanminus

CPAN から cpanminus 自身でインストールするのがよさそうだ。一般ユーザ権限で実行すると `~/perl5` にインストールするかと訊かれるので、言われた通りにする。管理者権限でインストールすると、どこに何が入ったのかわからなくなる。

```
$ curl -sL http://cpanmin.us | perl - App::cpanminus
```

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

```:~/.profile
export PATH=${HOME}/perl5/bin:${PATH}
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` オプションが必要。`LESS` と `LESSANSIENDCHARS` という環境変数を設定する。後者は、若干一般的ではない端末制御シークエンスを使っているため[^el]。

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

```:~/.profile
export LESS=-cR
export LESSANSIENDCHARS=mK
```

## sdif, cdif

`sdif` や `cdif` を使いたい場合は [App::sdif](https://github.com/kaz-utashiro/sdif-tools) をインストールする[^sdif-tools]。

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

```
$ 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 を使っている場合は、自動的に調整される[^terminal]。それ以外の黒系のターミナルを使っている人は sdif のマニュアルの COLOR セクションを読んでほしい。読むのが面倒な人は、とりあえず TERM_BGCOLOR という環境変数を 000 に設定して試してみるといい。

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

追記： 2019.01 にリリースした 4.8.0 から iTerm2 にも対応している。

追記： 2020.04 にリリースした 4.14.0 から、モジュールを Getopt::EX::termcolor に移行した。また、XTerm 互換の端末をサポートしている。

```:~/.profile
export TERM_BGCOLOR=000
```
<img width="762" alt="スクリーンショット" src="https://qiita-image-store.s3.amazonaws.com/0/36551/9c5d60fe-d091-aea5-3aeb-58a48d6f06d9.png">

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

<img width="762" alt="スクリーンショット 2018-07-03 11.35.10.png" src="https://qiita-image-store.s3.amazonaws.com/0/36551/0e752bd9-965f-299d-693f-39a829fafc46.png">

# SEE ALSO

## optex

``optex`` については以前 Qiita に書いてあったが、最新版に合わせてアップデートした。

- [optex であらゆる UNIX コマンドのオプションをカスタマイズする](https://qiita.com/kaz-utashiro/items/2df8c7fbd2fcb880cee6)

## Pandoc & Apache Tika

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

- [Pandoc](https://pandoc.org/)
- [Apache Tika](https://tika.apache.org/)
  - [Apache Tikaでコンテンツを読み込んでみる](https://qiita.com/yocifico/items/092215c27f5204e22480)

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'
```
<img width="682" alt="スクリーンショット" src="https://qiita-image-store.s3.amazonaws.com/0/36551/4ebfa3ad-1405-8a53-cb81-e51d35d1895a.png">

<img width="682" alt="スクリーンショット" src="https://qiita-image-store.s3.amazonaws.com/0/36551/188f50af-bbfe-8bc2-7111-042bec4cf914.png">

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

# FEEDBACK

-  https://kitani3.blogspot.com/2018/07/greple-appgreplemsdoc-microsoft-office.html
  - この方の使い方だったら tika の方がいいような気がするんだけど
  - あと、みなさん、コマンドを使う前にマニュアルを読まないんですかね。
  - PowerPoint のページをソートされるようにしました。いずれ、optex のモジュールを使うように書き換えようと思います。[2020-09]