Saturday, October 23, 2010

Emacs Tip #38: Automatically diff binary files using hexl-mode

I saw a question on Stack Overflow which referred to a non-free (as in beer) file comparison application. My first thought was, "people pay for this?" Sure enough, there are quite a few non-free file comparison tools. Which leads to the question, what do they offer that I'm not getting in Emacs (using ediff). From the table on Wikipedia, it appears not too much. I updated a couple of fields which weren't specified for Ediff, and noticed there was a column for doing a binary diff.

I rarely have the need to do a binary diff, but I was curious what tools did to support it. The two I looked at just showed a standard diff of files using a display somewhat like hexl-mode.

I figured adding support for Ediff to automatically switch to a diff with hexl-mode would be easy enough, and it was. Here's the code to use. Note: it currently prompts to see if you want to do the diff in hexl-mode, remove the call to y-or-n-p to have it done automatically.

Edited Oct 24 to split the code into two pieces of advice to avoid an error.

(defvar ediff-do-hexl-diff nil
"variable used to store trigger for doing diff in hexl-mode")
(defadvice ediff-files-internal (around ediff-files-internal-for-binary-files activate)
"catch the condition when the binary files differ

the reason for catching the error out here (when re-thrown from the inner advice)
is to let the stack continue to unwind before we start the new diff
otherwise some code in the middle of the stack expects some output that
isn't there and triggers an error"
(let ((file-A (ad-get-arg 0))
(file-B (ad-get-arg 1))
(condition-case err
(if ediff-do-hexl-diff
(let ((buf-A (find-file-noselect file-A))
(buf-B (find-file-noselect file-B)))
(with-current-buffer buf-A
(hexl-mode 1))
(with-current-buffer buf-B
(hexl-mode 1))
(ediff-buffers buf-A buf-B))
(error (error-message-string err)))))))

(defadvice ediff-setup-diff-regions (around ediff-setup-diff-regions-for-binary-files activate)
"when binary files differ, set the variable "
(condition-case err
(setq ediff-do-hexl-diff
(and (string-match-p "^Errors in diff output. Diff output is in.*"
(error-message-string err))
(string-match-p "^\\(Binary \\)?[fF]iles .* and .* differ"
(y-or-n-p "The binary files differ, look at the differences in hexl-mode? ")))
(error (error-message-string err)))))


Anonymous said...

Thank you for your effor, but I met a error :
Debugger entered--Lisp error: (wrong-type-argument overlayp nil)
ediff-overlay-put(nil face ediff-current-diff-A)
ediff-set-overlay-face(nil ediff-current-diff-A)
ediff-setup(# "/home/plato/tmp/DSC00010.JPG" # "/home/pl$
ediff-files-internal("/home/plato/tmp/DSC00010.JPG" "/home/plato/tmp/DSC00022.JPG" nil nil ediff-f$
ediff("/home/plato/tmp/DSC00010.JPG" "/home/plato/tmp/DSC00022.JPG")
eval((ediff "/home/plato/tmp/DSC00010.JPG" "/home/plato/tmp/DSC00022.JPG"))
call-interactively(repeat-complex-command nil nil)

After quit this backtrace, it works.

Unknown said...

I'll have to look into this, I can repeat it sometimes. I'm thinking it has to do with the fact that I'm switching from an error condition to a regular ediff, and some condition isn't being satisfied...

Unknown said...

The error has been fixed.

Anonymous said...

it is good, thank you!

Anonymous said...

Look at jojodiff
the -lr option is very useful to show where the differences are for binary files.