Skip to content

Instantly share code, notes, and snippets.

@wchargin
Last active September 13, 2023 05:39
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wchargin/b90b38df77c5843ba26ae66aa5484e41 to your computer and use it in GitHub Desktop.
Save wchargin/b90b38df77c5843ba26ae66aa5484e41 to your computer and use it in GitHub Desktop.
difmap: diff files under a Unix filter, like difmap -c 'jq .' f1 f2
#!/bin/sh
set -eu
usage() {
cat <<'EOF'
difmap: diff two files after mapping them through a shell filter
Examples:
# How do the plaintexts of these encrypted files differ?
difmap -c 'openssl enc -d -pbkdf2 -aes-256-ctr -pass env:KEY' a.enc b.enc
# How has this minified JSON changed since my last commit?
difmap -c 'jq -S .' -g @ path/to/file.json
# How does this string change if I remove all the diacritics?
printf 'r\xc3\xa9sum\xc3\xa9\n' | difmap -c cat -c 'iconv -t ascii//TRANSLIT' -
# What string literals do these two binary files have in common?
difmap -d 'comm -12' -c 'strings -n8 | sort -u' /usr/bin/vi /usr/bin/emacs
Usage:
Diff two files:
difmap <old-file> <new-file> [<flags>...]
Diff a file between a Git tree(-ish) and the working tree:
difmap -g <git-ref> <file> [<flags>...]
Diff a file between two Git trees:
difmap -g <old-git-ref> -g <new-git-ref> <file> [<flags>...]
Diff two files at a Git tree:
difmap -g <git-ref> <old-file> <new-file> [<flags>...]
Diff two files at different Git trees:
difmap -g <old-git-ref> <old-file> -g <new-git-ref> <new-file> [<flags>...]
Diff a file against itself, under different filters:
difmap <file> -c <cmd1> -c <cmd2> [<flags>...]
Diff stdin against itself (read only once), under different filters:
difmap -c <cmd1> -c <cmd2> - [<flags>...]
Flags:
-c CMD apply `sh -c "$CMD"` to each file before diffing
-d CMD use $CMD as diff program (e.g.: "nvim -d")
-- further flags passed to diff program
EOF
}
die_usage() {
printf >&2 'difmap: fatal: %s\n' "$@"
printf '\n'
usage >&2
exit 1
}
parse_args() {
diff=
gitold=
gitnew=
cmd1=
cmd2=
fold=
fnew=
argn=$#
while [ $# -gt 0 ]; do
arg="$1"
shift
case "$arg" in
-g)
if [ $# = 0 ]; then die_usage '-g: missing argument'; fi
ref="$(git rev-parse --short=12 --verify "$1")" && shift
if [ -z "$ref" ]; then die_usage '-g: empty argument'; fi
if [ -z "$gitold" ]; then gitold="$ref"
elif [ -z "$gitnew" ]; then gitnew="$ref"
else die_usage '-g: too many'; fi
;;
-c)
if [ $# = 0 ]; then die_usage '-c: missing argument'; fi
cmd="$1" && shift
if [ -z "$cmd" ]; then die_usage '-c: empty argument'; fi
if [ -z "$cmd1" ]; then cmd1="$cmd"
elif [ -z "$cmd2" ]; then cmd2="$cmd"
else die_usage '-c: too many'; fi
;;
-d)
if [ $# = 0 ]; then die_usage '-d: missing argument'; fi
if [ -z "$1" ]; then die_usage '-d: empty argument'; fi
diff="$1" && shift
;;
--)
break
;;
-?*)
die_usage "${arg}: unknown flag"
;;
*)
if [ -z "$arg" ]; then die_usage '<file>: empty argument'; fi
if [ -z "$fold" ]; then fold="$arg"
elif [ -z "$fnew" ]; then fnew="$arg"
else die_usage '<file>: too many'; fi
;;
esac
done
if [ -z "$fold" ]; then die_usage 'too few sources'; fi
if [ -z "$fnew" ] && [ -z "$gitold" ]; then fnew="$fold"; fi
if [ -z "$cmd1" ]; then cmd1=cat; fi
if [ -z "$cmd2" ]; then cmd2="$cmd1"; fi
nshift=$(( argn - $# ))
}
main() {
parse_args "$@"
shift "$nshift"
tmpdir=
trap cleanup EXIT
tmpdir="$(mktemp -d)"
forcelabelold=
forcelabelnew=
if [ "$fold" = - ] || [ "$fnew" = - ]; then
cat >"${tmpdir}/stdin"
if [ "$fold" = - ]; then forcelabelold="$fold" && fold="${tmpdir}/stdin"; fi
if [ "$fnew" = - ]; then forcelabelnew="$fnew" && fnew="${tmpdir}/stdin"; fi
elif [ "$fold" = "$fnew" ] && [ -p "$fold" ]; then
cat "$fold" >"${tmpdir}/pipe"
forcelabelold="$fold"
forcelabelnew="$fnew"
fold="${tmpdir}/pipe"
fnew="${tmpdir}/pipe"
fi
readold >"${tmpdir}/old"
readnew >"${tmpdir}/new"
if [ -n "$forcelabelold" ]; then labelold="$forcelabelold"; fi
if [ -n "$forcelabelnew" ]; then labelnew="$forcelabelnew"; fi
if [ "$cmd1" != cat ]; then labelold="$labelold | $cmd1"; fi
if [ "$cmd2" != cat ]; then labelnew="$labelnew | $cmd2"; fi
exec ${diff:-diff -u --color=auto --label "${labelold}" --label "${labelnew}"} \
"${tmpdir}/old" "${tmpdir}/new" "$@"
}
filter() {
sh -c "$1"
}
readold() {
labelold= # output variable
if [ -n "$gitold" ]; then
labelold="${gitold}:${fold}"
git show "${gitold}:${fold}" | sh -c "$cmd1"
else
labelold="${fold}"
cat -- "$fold" | sh -c "$cmd1"
fi
}
readnew() {
labelnew= # output variable
if [ -n "$gitnew" ]; then
labelnew="${gitnew}:${fnew:-${fold}}"
git show "${gitnew}:${fnew:-${fold}}" | sh -c "$cmd2"
elif [ -n "$gitold" ]; then
if [ -n "$fnew" ]; then
labelnew="${gitold}:${fnew}"
git show "${gitold}:${fnew}" | sh -c "$cmd2"
else
labelnew="${fold}"
cat -- "$fold" | sh -c # diff Git ref against working tree
fi
else
labelnew="${fnew:-${fold}}"
cat -- "${fnew:-${fold}}" | sh -c "$cmd2"
fi
}
cleanup() {
if [ -n "${tmpdir}" ]; then
rm -f "${tmpdir}"/*
rmdir "${tmpdir}"
fi
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment