テキストでないファイルのdiff(差分)をとる方法

AppleScriptスクリプト言語なんだけど、ファイルに保存するときはAppleScript形式にコンパイル(変換)される。その結果、AppleScriptエディタでは人間が理解できるコードに見えるのに、普通のテキストエディタで開くとこんな状態。

その実態は、AppleScript独自のバイナリファイルなのだ。バイナリファイルの痛いところは、diffを実行してもそのままでは差分がとれないこと。どうしても差分をとりたいと思うのなら、いったんAppleScriptファイルを開いて、テキストファイルとして保存したファイルに対してdiffを実行するしかない。

しかし、それでは手間がかかりすぎる。手軽にdiffできないと、いずれ使わなくなる。そんな訳で「AppleScriptはバイナリファイルなのだから、しょうがない」と以前から諦めていたところがあった。(Gitのあの素晴らしい仕組みを知るまでは)ところが、Gitにはファイルアトリビュートとテキストコンバートという設定がある。

# .gitattirbutesあるいは.git/info/attributesの設定
*.scpt    diff=applescript
# git configの設定
$ git config diff.applescript.textconv osadecompile

このように設定しておくと、.scpt形式のAppleScriptファイルに対してdiffを実行したときに、事前にosadecompileを実行した結果に対してdiffしてくれるのだ。osadecompileは.scpt形式のAppleScriptファイルをテキストに変換してくれるOSX独自のコマンド。つまり、テキストに変換されたAppleScriptの差分がとれるのだ!やっていることは、手作業でテキストに変換してdiffを実行しているのと同じことなんだけど、一度設定してしまえば、そのリポジトリでは普通にdiffを実行するだけで、当然のようにAppleScriptの差分がとれてしまうところが素晴らしい。

そのような感動的に素晴らしい情報が書籍「入門Git」には さらり と書いてある。あまりにも さらり と書いてあるので、なかなか気付けなかった...。実は、何度も読み直している中でようやく気付いたのだ。この辺りの話題は14章ファイルアトリビュートの中で、数ある便利な一つの機能(P248バイナリファイルの比較)として紹介されている。その他にもファイルアトリビュート+何らかの設定により、素晴らしく便利な機能を手に入れることができるのだ。*1

一旦diffの恩恵を経験すると、差分をとれない環境というのがとても不安に思えてくる。あの、MacBook ProRetinaモデルのデスクトップピクチャに採用された写真家のKent Shiraishi氏も、常時モニタを2台並べて写真の色合いのチェックをしているそうである。曰く、自分の感覚なんて当てにならない、正確な色を再現するためには完璧にデジタル調整されたモニタを並べて、その違いを感じて現像するそうである。つまり、色の差分を見ているのだ!差分を知ることはとても大切。写真でも、テキストでも。

という訳で、あらゆるものを可能な限り手軽にdiffできる環境を作っておきたい、と常に思っている。だから、もちろん前回作り始めたdropbox_diff.shでも、AppleScriptの差分をとれるようにしてみた。さらに、テキストに変換するコマンドさえあれば、あらゆるものの差分をとれるようになる。(リッチテキストJPEG(exifだが)の差分だってとれる。PDF表計算ソフトのファイルだって、工夫すればどうにかなるかもしれない。)

convert_diffプロジェクトの開始

DropboxやGitリポジトリの中ではAppleScriptの差分が見られるようになった。そうなると、それ以外の環境でも手軽にdiffしたくなるのが人情。ならば、dropbox_diff.shに組み込んだテキスト変換機能の部分だけを切り出して、diffコマンドのラッパーを作ってしまえばいいのだ。.scptなどの特定の拡張子のファイルだったら、テキストコンバートしてdiffする、それ以外はそのままdiffする。そんな動作をするconvert_diff.shを作ってみた。

  • とりあえず、.scpt、.rtf、.jpgの拡張子に対して、テキスト変換するようにしてみた。
#!/bin/bash

convert() {
  if [ "${PATH1##*.}" = "scpt" ] && [ "${PATH2##*.}" = "scpt" ]; then
    echo -n 'osadecompile'
  elif [ "${PATH1##*.}" = "rtf" ] && [ "${PATH2##*.}" = "rtf" ]; then
    echo -n 'textutil -convert txt -stdout'
  elif [ "${PATH1##*.}" = "jpg" ] && [ "${PATH2##*.}" = "jpg" ]; then
    echo -n 'exiftool'
  else
    echo -n 'cat'
  fi
}

OPTION=("$@")
PATH1=${OPTION[$(($# - 2))]}
PATH2=${OPTION[$(($# - 1))]}
unset OPTION[$(($# - 1))]
unset OPTION[$(($# - 2))]
if [ -f "$PATH1" ] && [ -f "$PATH2" ] && [ "`convert`" != 'cat' ]; then
  diff "${OPTION[@]}" <(`convert` "$PATH1") <(`convert` "$PATH2")
  echo -e "\n\`diff ${OPTION[@]} <(`convert` \"$PATH1\") <(`convert` \"$PATH2\")\`\n"
else
  diff "$@"
fi
$ diff -u file1.scpt file2.scpt
  • テキストに変換してからdiffするには、以下のように修正すればいいのだ。
$ diff -u <(osadecompile file1.scpt) <(osadecompile file2.scpt)

カラー表示

  • そういえば、Gitのdiffではカラー表示が見やすかった。
  • 変更箇所が目立っていると、感覚的に漏れなくチェックできる。

そうだ、色を付けよう!

$ brew install colordiff
  • 使い方はdiffの代わりにcolordiffを実行するか、
$ colordiff file1 file2
  • diffの出力とパイプで接続して、色付けする。
$ diff -u file1 file2|colordiff
  • 既存のdiffの機能をすべて使いたいので、後者のパイプで接続してみた。
  • colordiffは、パイプで接続したときの動作をサポートしていないようだ。
$ echo 'hello'|diff -u - ~/Desktop/hello.txt
 ...中略...
 -hello
 +hello world!!
$ echo 'hello'|colordiff -u - ~/Desktop/hello.txt
 ...中略...
 +hello world!!
  • しかし、パイプを利用すると、巨大なファイルのdiffで問題が生じるかもしれない。
  • 自分の環境では、約26万字の文字数制限があるのだ。
$ sysctl -A kern.argmax
kern.argmax: 262144
  • 自分の使い方では今のところ、それほど巨大なファイルをdiffする需要はないので、
  • 約26万文字の制限があることを念頭に置きながら、パイプで接続してみた。
#!/bin/bash

convert() {
  if [ "${PATH1##*.}" = "scpt" ] && [ "${PATH2##*.}" = "scpt" ]; then
    echo -n 'osadecompile'
  elif [ "${PATH1##*.}" = "rtf" ] && [ "${PATH2##*.}" = "rtf" ]; then
    echo -n 'textutil -convert txt -stdout'
  elif [ "${PATH1##*.}" = "jpg" ] && [ "${PATH2##*.}" = "jpg" ]; then
    echo -n 'exiftool'
  else
    echo -n 'cat'
  fi
}

OPTION=("$@")
PATH1=${OPTION[$(($# - 2))]}
PATH2=${OPTION[$(($# - 1))]}
unset OPTION[$(($# - 1))]
unset OPTION[$(($# - 2))]
if [ -f "$PATH1" ] && [ -f "$PATH2" ] && [ "`convert`" != 'cat' ]; then
  diff "${OPTION[@]}" <(`convert` "$PATH1") <(`convert` "$PATH2")|colordiff
  echo -e "\n\`diff ${OPTION[@]} <(`convert` \"$PATH1\") <(`convert` \"$PATH2\")\`\n"
else
  diff "$@"|colordiff
fi
  • 差分
$ dropbox_diff.sh convert_diff.sh
 @@ -21,10 +21,10 @@
  if [ -f "$PATH1" ] && [ -f "$PATH2" ] && [ "`convert`" != 'cat' ]; then
  unset OPTION[$( ($# - 1) )]
  unset OPTION[$( ($# - 2) )]
 -  diff "${OPTION[@]}" <(`convert` "$PATH1") <(`convert` "$PATH2")
 +  diff "${OPTION[@]}" <(`convert` "$PATH1") <(`convert` "$PATH2")|colordiff
    echo -e "\n\`diff ${OPTION[@]} <(`convert` \"$PATH1\") <(`convert` \"$PATH2\")\`\n"
  else
 -  diff "$@"
 +  diff "$@"|colordiff
  fi


これでAppleScriptなども手軽にdiffできるようになった!快適。快適。


改良バージョン

  • convert_diff.shは、次に日記でより汎用的に使えるように、以下のように修正された。
#!/bin/bash

convert() {
  case $1 in
    *.scpt ) echo -n "osadecompile" ;;
    *.rtf )  echo -n "textutil -convert txt -stdout" ;;
    *.jpg )  echo -n "exiftool" ;;
    * )      echo -n "cat";;
  esac
}

OPTION=("$@")
PATH1=${OPTION[$(($# - 2))]}
PATH2=${OPTION[$(($# - 1))]}
unset OPTION[$(($# - 1))]
unset OPTION[$(($# - 2))]

# 一方がディレクトリだったら、他方のファイル名を補う
if [ -d "$PATH1" ] && [ -f "$PATH2" ]; then
  PATH1="$PATH1/`basename "$PATH2"`"
elif [ -f "$PATH1" ] && [ -d "$PATH2" ]; then
  PATH2="$PATH2/`basename "$PATH1"`"
fi

# 両方ともディレクトリだったら、変換せずにdiffを実行する
if [ -d "$PATH1" ] && [ -d "$PATH2" ]; then
  echo "\`diff" "$@""|colordiff'"
  diff "$@"|(`which colordiff`||`which cat`)
else
  echo "\`diff ${OPTION[@]} <(`convert "$PATH1"` \"$PATH1\") <(`convert "$PATH2"` \"$PATH2\")|colordiff'"
  diff "${OPTION[@]}" <(`convert "$PATH1"` "$PATH1") <(`convert "$PATH2"` "$PATH2")|(`which colordiff`||`which cat`)
fi


colordiff 追加情報

  • 設定ファイル(自分好みの色などを設定できる)
    • /etc/colordiffrc(自分でmakeした場合)
    • /usr/local/etc/colordiffrc(Homebrewでインストールした場合)
# Example colordiffrc file for dark backgrounds
#
# Set banner=no to suppress authorship info at top of
# colordiff output
banner=no
# By default, when colordiff output is being redirected
# to a file, it detects this and does not colour-highlight
# To make the patch file *include* colours, change the option
# below to 'yes'
color_patches=no
# 
# available colours are: white, yellow, green, blue,
#                        cyan, red, magenta, black,
#                        darkwhite, darkyellow, darkgreen,
#                        darkblue, darkcyan, darkred,
#                        darkmagenta, darkblack
#
# Can also specify 'none', 'normal' or 'off' which are all
# aliases for the same thing, namely "don't colour highlight
# this, use the default output colour"
#
plain=off
newtext=blue
oldtext=red
diffstuff=magenta
cvsstuff=green
  • 上記オリジナルの設定ファイルをホームフォルダにコピーして、自分好みに修正するのだ。
$ cp /usr/local/etc/colordiffrc ~/.colordiffrc
  • 自分の環境でGitデフォルト風に見える設定
banner=no
color_patches=no
#diff_cmd=diff #コメントアウトしないとエラーになってしまった
plain=off
newtext=darkgreen
oldtext=darkred
diffstuff=cyan
cvsstuff=red

*1:入門Gitは、読み直す度に新たな発見がある。たぶん、すべての機能の解説が平等に網羅されているのだと思う。そのため、Gitをある程度知らないと読みにくいのだが、Gitをさらに便利に使いたいと思ったときには、そこには問題解決のヒントがたくさんあるのだ。タイトルが入門となっているが、これはGitの原典なのである。