Bizarre failure of shell script 'diff'

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).

It works fine if I run those commands directly in Terminal. But here's where it gets weird; if I do the following…

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

…then it works fine: The file is created, and contains the proper results. It doesn't matter what the next command is; ls or cd or whatever—just anything at all. But when diff is the last line in the shell script, it errors out.

I am completely befuddled by this. I did work around it, as I didn't want to have to write a file to disk and then read it back, by doing this:

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

I save that result to a variable, and it works perfectly. But why the heck is it failing on diff if it's the last command?

-rob.

I just ran a simplified test on Catalina using this action:

And that ran as expected, delivering the output to a window. I tried a few other commands (cd /tmp and ls on that) and there were no surprises. I looked at the Gear options in the action and didn't find any clue that would explain your results.

Sorry all I can offer is a contradictory data point.

I need to do some more testing, but haven't had the time. My suspicion is it's something about the two $KMVAR values I'm passing in—they're both multi-row entries, so maybe that's (somehow??) throwing something off. But if so, I wouldn't expect any version of what I'm trying to do would work, yet it does with the extra line (or the echo redirect).

-rob.

The answer lies in how diff works. When diff finds a difference between the two files, its exit code is 1. Nonzero exit codes typically indicate an error, so when Keyboard Maestro sees that exit code, it bails out. If you look through the Engine.log file, you'll see something like

Task failed with status 1

which is true but not very helpful if you don't know diff's exit codes.

The workaround is to click on the gear icon in the upper left corner of your Execute Shell Script action and change the settings on "Failure Aborts Macro" and "Notify on Failure" from âś“ to Ă—. That should get you the output you expect.

20220120-212443

I assume diff returns a nonzero exit code when there are differences between the files so it can be used as a test for file equality. But it is surprising, given that most people use it to find differences they know are there.

By the way, you can avoid temporary files by using process substitution which is available in zsh and later versions of bash (e.g., the bash you can get from Homebrew). A shell script like

#!/bin/zsh
diff <(echo "$KMVAR_InstanceVar1") <(echo "$KMVAR_InstanceVar2")

will diff the contents of the two KM variables with no need to create and clean up temp files.

4 Likes

When diff finds a difference between the two files, its exit code is 1. Nonzero exit codes typically indicate an error, so when Keyboard Maestro sees that exit code, it bails out.

OMG I never would have figured that out, thanks! (I'm always hesitant to disable failure abort/notify until I've fully debugged my scripts.) And thanks for the zsh tip; never knew about process substitution, which seems incredibly useful.

-rob.

Despite the authoritative tone of my answer, I found the exit code only after going down other rabbit holes first. Also, the Mac man page for diff says nothing about exit codes, but I luckily found other man pages online that do say what a status of 1 means. That’s when I finally realized what KM’s error message meant.

2 Likes

@drdrang, thank for the heads up (and link) regarding process subsitution.

I'm far from a shell expert, and since it seems like you are one, maybe you can tell me how this differs from something I've used before—and if my memory serves me correctly, this syntax goes way back to when I was a pup hacking on HP-UX: $(some command or some commands separated by a semicolon). Here's an example:

@drdrang may have a more thorough answer than I, but here's the simple explanation: This creates a shell variable named KMTMPPATH containing the output of the commands within the parentheses.

-rob.

The $(command) construct is also called a process substitution, but I've only seen it used the way you did: to assign a variable. I assume that's the reason it uses the dollar sign syntax. I always think of it as a replacement for backticks, which seem to be frowned upon nowadays.

The <(command) construct creates a named pipe, which acts like a file when it's used as an argument to another command. It's especially useful when a command (like diff or comm) takes two file arguments.

As for putting multiple commands inside the parentheses, that's a new one on me. I wouldn't have considered doing that if I hadn't seen your code. Now I have a new tool.

2 Likes

@drdrang, thanks for the additional information—appreciate your insight!

Here's another for the list, which makes sense when I think about it, but I didn't think about it before getting frustrated :). If you use grep -c to count things, the exit code may cause debugging issues.

Consider a file, file1, that contains this...

foo
bar
baz
baz
bin

...then grep -c works like this, showing its exit code:

$ grep -c 'baz' file1; echo $?
2
0

$ grep -c 'blah' file1; echo $?
0
1

The 1 exit code typically means error, but in the case of grep, it means "no lines found." So if you count something and grep doesn't find anything, it exits with 1 (even though it returns the correct count of zero) ... and as with diff, that can cause difficulties in Keyboard Maestro if you leave the Execute Shell Script action at its default settings (abort and notify on failure).

Documenting here in case it trips anyone else up.

-rob.