What’s the diff?

Last week, Rob Griffiths—late of Mac OS X Hints and currently of Many Tricks—asked a really tough question on the Keyboard Maestro forum:

I’m trying to run a very simple shell script:

  cd /tmp
  echo "$KMVAR_var1" > file1.txt
  echo "$KMVAR_var2" > file2.txt
  diff file1.txt file2.txt > diff_result.txt

I don’t actually want the diff results in a file, I want them in a variable returned by the shell script action. But because that was failing, I tried the above to write it to a file instead. But it still failed (with the generic failed in shell script message).

Rob learned that the macro failed only when diff was the last command in the shell script. Adding a final innocuous command, like echo "foo" to the end of it got rid of the error. And of course, the script—without the echo "foo" line—worked just fine when run from the command line.

How can this be? My first thought was that the error had something to do with interactive vs. noninteractive shells, but that led nowhere. So I made my own version of Rob’s macro and changed the last line from diff to comm:

KM Griffdiff

The shell script in the last action is

cd ~/Desktop/griffdiff
echo "$KMVAR_InstanceVar1" > file1.txt
echo "$KMVAR_InstanceVar2" > file2.txt
comm file1.txt file2.txt

This worked fine. I think of comm and diff as being similar, so success with comm and failure with diff was a real stumper. There were no clues in diff’s man page. I put the problem aside to think about in the evening.

As luck would have it, when I returned to the problem I was using my iPad, so when I decided to review the man page again, I used this online version instead of the one included with macOS. And there, down at the end of the DESCRIPTION section, was the answer:

FILES are 'FILE1 FILE2' or 'DIR1 DIR2' or 'DIR FILE' or 'FILE
DIR'.  If --from-file or --to-file is given, there are no
restrictions on FILE(s).  If a FILE is '-', read standard input.
Exit status is 0 if inputs are the same, 1 if different, 2 if
trouble.

Emphasis mine. I went back to look at the error message Keyboard Maestro gave when the shell script action ended with diff. Without the timestamps it was

Execute macro “Griffdiff” from trigger Editor
Action 222451 failed: Task failed with status 1
Task failed with status 1. Macro “Griffdiff” cancelled (while executing Execute Shell Script).

The “Task failed with status 1” message was not—as I had previously thought—giving me a status code generated by Keyboard Maestro itself. Instead, KM was just passing along the status code it had received from the shell. diff had returned an exit code of 1 because the inputs were different. Keyboard Maestro then interpreted the nonzero exit code as an error and bailed out. So everything was working just as it was supposed to.

But that didn’t fix Rob’s problem. Luckily, I remembered that I’d run into a situation some time ago in which I had to turn off Keyboard Maestro’s normal error handling. I did it by changing the “Failure Aborts Macro” and “Notify on Failure” settings in the action’s gear menu from ✔︎ to ✖︎.

KM gear menu

With those two changes, the macro ran fine. Now I had two questions:

  1. How had I missed the exit status stuff when I looked at the diff’s man page earlier?
  2. Why does diff return a nonzero exit code when it does exactly what you want?

The answer to the first question was easy: Here’s what macOS Catalina’s man diff says at the end of the DESCRIPTION section:

FILES  are  `FILE1  FILE2'  or `DIR1 DIR2' or `DIR FILE...' or `FILE...
DIR'.  If --from-file or --to-file is given, there are no  restrictions
on FILES.  If a FILE is `-', read standard input.

Nothing about exit status in that paragraph or anywhere else. According to the copyright notice, Catalina’s diff man page was written in 2002 (way to keep on top of things, Apple!). The online version was updated in 2019.

I’m not sure about the answer to the second question, but my guess is that it works that way so diff can be used in if statements or those short circuit statements with && or || you often see in shell scripts, like

diff file1.txt file2.txt && echo "Identical files"

where the part after the && is executed only if the part before it returns a zero (success) exit code. Still, I found it surprising. diff is probably used most often on files that are known to be different—it’s weird that using it that way produces an exit code that typically indicates failure.

By the way, if you’re wondering about the exit status of a command, you can learn what it is by running

echo $?

immediately after the command. This works in both bash and zsh.