Last active
April 25, 2024 17:57
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/sh | |
set -eu | |
set -x | |
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 "$cmd2" # 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