Oops, I Did It Again — Diffing My Bash Functions to Find My Mistakes
Yesterday, I had a small failure. I’m the kind of person who makes many small mistakes but also often feels quite confident that things…
Yesterday, I had a small failure. I’m the kind of person who makes many small mistakes but also often feels quite confident that things aren’t my fault. Long story short, I had made a bad edit in one of my Bash scripts, and as a result, the script stopped working properly. Because I can be stubbornly overconfident at times, I didn’t check whether something had changed in my code when the problem appeared. Instead, I started asking others, “Hey, did you change anything on your side?”
After everyone told me they hadn’t made any changes, I finally started looking at my own code and commit history. That’s when I discovered that several commits back, I had accidentally deleted a single line, which was exactly why the script wasn’t working correctly.
Once I told everyone it was my mistake (it’s honestly so cathartic to take responsibility), I started thinking about how I could do a quick check next time this happens. I came up with the idea of a script where you provide a function name and a filename, and it prints the diff between the function in the current file and its versions in previous commits.
The script
I won’t get into too much detail, but essentially, this script takes a function name and a filename as command line arguments. It uses sed to locate the function in previous commits and extracts those versions into temporary files under /tmp/funcdiff. Then, it compares (diffs) the current version of the function in your working directory against the extracted versions from the commit history.
#!/usr/bin/env bash
set -e
# Check arguments
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <function_name> <filename>"
exit 1
fi
FUNC_NAME="$1"
FILE_NAME="$2"
# Create temp directory
TMP_DIR=/tmp/funcdiff
mkdir -p "$TMP_DIR"
# Get last 2 commits
commits=($(git log --oneline | tail -n2 | cut -d " " -f1))
# Extract each commit's function body
for c in "${commits[@]}"; do
date=$(git show -s --format=%ci "$c")
echo "Extracting function '$FUNC_NAME' from commit $c ($date)..."
git show "$c":"$FILE_NAME" | sed -n "/^function $FUNC_NAME/,/^}/p" > "$TMP_DIR/${FUNC_NAME}_${c}.txt"
done
# Extract current working directory version
sed -n "/^function $FUNC_NAME/,/^}/p" "$FILE_NAME" > "$TMP_DIR/${FUNC_NAME}_working.txt"
# Diff each commit version vs working directory
for c in "${commits[@]}"; do
date=$(git show -s --format=%ci "$c")
echo
echo "===== Diff between commit $c ($date) and working directory ====="
if ! diff -u "$TMP_DIR/${FUNC_NAME}_${c}.txt" "$TMP_DIR/${FUNC_NAME}_working.txt"; then
echo "(above are differences or no output if identical)"
fi
done
# Cleanup
rm -rf "$TMP_DIR"The output of the script looks something like this. Note that in this version, it only works with Bash scripts where functions are declared using the function keyword:
./diff_function.sh say_hello ./main.sh
Extracting function 'say_hello' from commit 832d90a (2025-07-16 04:05:32 +0300)...
Extracting function 'say_hello' from commit 9ec285e (2025-07-16 04:04:13 +0300)...
===== Diff between commit 832d90a (2025-07-16 04:05:32 +0300) and working directory =====
--- /tmp/funcdiff/say_hello_832d90a.txt 2025-07-16 04:49:52.431245400 +0300
+++ /tmp/funcdiff/say_hello_working.txt 2025-07-16 04:49:52.527050100 +0300
@@ -1,4 +1,4 @@
function say_hello() {
local name="$1"
- echo "Hello, world! ${name}"
+ echo "Hello ${name}"
}
(above are differences or no output if identical)
===== Diff between commit 9ec285e (2025-07-16 04:04:13 +0300) and working directory =====
--- /tmp/funcdiff/say_hello_9ec285e.txt 2025-07-16 04:49:52.494891900 +0300
+++ /tmp/funcdiff/say_hello_working.txt 2025-07-16 04:49:52.527050100 +0300
@@ -1,3 +1,4 @@
function say_hello() {
- echo "Hello, world!"
+ local name="$1"
+ echo "Hello ${name}"
}
(above are differences or no output if identical)As we can see, this approach lets us clearly see how the function evolves across each commit without the noise of unrelated code we don’t care about. It focuses only on the specific function you’re interested in, making it much faster to track down when and how a change was introduced.
Moral of the story
Never assume it’s always someone else’s fault — sometimes it’s just you, a stray git commit, and one innocent-looking missing line conspiring to ruin your day. When in doubt, diff it out. Because nothing beats a script that can travel back in time, pluck out exactly the function you broke, and show you precisely how you did it — so you can stop interrogating your teammates and start interrogating your past self.