summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xbin/dv556
-rwxr-xr-xbin/wd239
2 files changed, 795 insertions, 0 deletions
diff --git a/bin/dv b/bin/dv
new file mode 100755
index 0000000..0737295
--- /dev/null
+++ b/bin/dv
@@ -0,0 +1,556 @@
+#!/bin/sh
+
+# Local dv (aka sim)
+
+dv_version='dv-0.23 Copyright 2013-2014 Marc Vertes, Philippe Bergheaud'
+unset CDPATH
+export LC_ALL=C IFS='
+'
+
+anc() {
+ usage 'anc [Version]' 'print ancestor' && return
+ getdv .dv || die no item
+ case $1 in
+ (""|.|$PWD)
+ [ "$Opt_N" ] && Opt_N=$(($Opt_N - 1))
+ [ "$Opt_N" ] && info anc $R2 $Opt_N || echo $R2
+ ;;
+ (*) info anc $1 ${Opt_N:-1};;
+ esac
+}
+
+cache() {
+ isremote "$1" || return
+ o=$1 _d=$cacheprefix/$1; pd=${_d%/*}; ppd=${pd%/*} t=$pd/.${_d##*/}
+ [ -d "$ppd" ] || mkdir -p "$ppd"
+ [ -L "$ppd/.dv." ] || ln -s . "$ppd/.dv."
+ [ -f "$_d/.dv" ] && return
+ [ -d "$pd" ] || mkdir -p "$pd"
+ set -- $pd/*; shift $(($# - 1))
+ [ -d "$1" ] && ropt="--link-dest=../${1##*/}" || ropt=
+ rsync -aDS $ropt "$o/" "$t" && mv "$t" "$_d"
+}
+
+client() {
+ usage 'client [Version]' 'print clients' && return
+ getdv .dv || die no item
+ case $1 in
+ (""|.|$PWD)
+ while getpdir .dv ..
+ do
+ cd "$R1"
+ getdv ".dv"
+ [ "$OptM" ] && echo "$R2 -> $PWD" || echo "$R2"
+ ((Opt_N = Opt_N-1)) || break
+ done ;;
+ (*) info cli $1 ${Opt_N:-9999} | sort -u ;;
+ esac
+}
+
+clone() {
+ usage 'clone [-A Author] [-m Msg] Version [Dir]' \
+ 'Copy version to dir' && return
+ [ "$1" ] || die missing argument
+ [ "$3" ] && die too many arguments
+ getpdir .dv . && root=$R1 && getdv $root/.dv && lib=$R1 item=${R2%/*}
+ new_version=${1##*/}
+ if [ "$new_version" = "$1" ]
+ then
+ getdv .dv && lib=$R1 new_item=${R2%/*} || die no item
+ else
+ v=${1%/$new_version}
+ new_item=${v##*/}
+ [ "$new_item" = "$v" ] || new_lib=${v%/$new_item}
+ fi
+ [ "$PWD" = "$root" ] && [ "$item" != "$new_item" ] &&
+ die "already a clone of $item"
+ lib=${lib:-$new_lib}
+ [ "$lib" ] || die no lib
+ [ "$new_lib" -a "$new_lib" != "$lib" ] && die "already in lib: $lib"
+ if [ "$new_version" = 0 ] # Init item in lib
+ then
+ initlib "$lib"
+ getdvinfo "$lib" || die "could not lock $lib"
+ mkdir -p /tmp/dv.$$/$new_item/0
+ dv_write $new_item/0 $new_item/0 >/tmp/dv.$$/$new_item/0/.dv
+ { cat /tmp/dv.$$/$new_item/0/.dv; echo; } >>/tmp/dv.$$/.dvinfo.0
+ ln -f /tmp/dv.$$/.dvinfo.0 /tmp/dv.$$/.dvinfo.1
+ rsync -a /tmp/dv.$$/ "$lib"
+ fi
+ if [ ! "$2" ] # Use the item name as default directory (a la git)
+ then
+ set -- "$1" "${1%/*}"
+ set -- "$1" "${2##*/}"
+ echo cloning into "$2"
+ fi
+ [ ! -d "$2" ] && mkdir "$2"
+ cd "$2" || die cannot chdir to "$2"
+ # Rename existing supplier directories to allow supplier symlinks
+ find . -name .dv | while read l
+ do
+ l=${l%/.dv} && [ "$l" = "." ] && continue
+ mv "$l" "$l.dvold"
+ done
+ # Sync from lib, starting from most client item, up to last supplier
+ d=. s=$new_item/$new_version
+ while [ "$d" ]
+ do
+ [ -L "$d" ] && {
+ rm -f "$d"
+ [ -d "$d.dvold" ] && mv "$d.dvold" "$d"
+ }
+ cache "$lib/$s" && pref=$cacheprefix/ || pref=
+ rsync -aDS "$pref$lib/$s/" "$d"
+ dv_header "$d" Anc "$s"
+ dv_header "$d" Lib "$lib"
+ # Find next symlink pointing to a supplier
+ d= l=
+ eval "$(find . -type l | while read -r l
+ do
+ f=$(readlink "$l")
+ case $f in
+ (*/.dv./*) echo "d=\"$l\" s=\"${f#*/.dv./}\""
+ break;;
+ esac
+ done)"
+ done
+}
+
+desc() {
+ usage 'desc [Version]' 'print descendants' && return
+ getdv .dv || die no item
+ case $1 in
+ (""|.|$PWD) ;;
+ (*) info desc $1 ;;
+ esac
+}
+
+die() { echo "$0: fatal: $@" >&2; exit 1; }
+
+diff() {
+ usage 'diff [-x pat] [-F File] [V1 [V2]]' 'Print differences' && return
+ getpdir .dv && getdv "$R1/.dv" || die not in a clone
+ lib=$R1 item=${R2%/*} old=${R2#*/}
+ [ "$lib" = "" ] && [ -L "../../.dv." ] && lib=${PWD%/*/*}
+ if [ "$2" ]
+ then
+ case $2 in
+ (.) new=. new_is_clone=1 ;;
+ (*) new=$lib/$item/$2 ;;
+ esac
+ cache "$new" && np=$cacheprefix/ || np=
+ old=$lib/$item/$1
+ elif [ "$1" ]
+ then
+ case $1 in
+ (.) new=. new_is_clone=1 ;;
+ (*) new=$lib/$item/$1 ;;
+ esac
+ cache "$new" && np=$cacheprefix/ || np=
+ getdv "$np$new/.dv" && old=${R1:-$lib}/$R2
+ else
+ new=$PWD new_is_clone=1
+ cache "$new" && np=$cacheprefix/ || np=
+ getdv "$np$new/.dv" && old=${R1:-$lib}/$R2
+ fi
+ cache "$old" && op=$cacheprefix/ || op=
+ [ "$Opts" ] || printf "old %s\nnew %s\n" "$old" "$new"
+ # Itemized diff for all suppliers, deepest first
+ cd "$np$new" && for f in $(find . -type f -name .dv | sort -r)
+ do
+ d=${f%/.dv}
+ [ "$d" = . ] && prefix= || prefix=${d#./}/
+ getdv "$f" && cache "$lib/$R2"
+ [ "$new_is_clone" ] && dest=$np$d || dest=$prefix$d
+ diffdir "$op$lib/$R2" "$dest"
+ Optx="$Optx --exclude=${d##*/}"
+ done
+}
+
+# Usage: diffdir oldpath newpath
+# Print differences between 2 directories
+diffdir() {
+ [ -f "$2/.dvignore" ] && xf=--exclude-from=$2/.dvignore || xf=
+ rsync -aDSniv $xf --delete --exclude=".dv.*/" $Optx "$2/" "$1" |
+ awk -v OptF="${OptF#./}" -v prefix=$prefix '
+ NF == 0 {exit}
+ NR < 2 || /\/*\.dv$/ || /\/$/ {next}
+ # Match an itemized status for all versions of rsync -i
+ $1 !~ /^[<>ch.*][fdLDS+][.+?cstpoguaxz]+$/ {next}
+ {key = $1; file = substr($0, length(key) + 2)}
+ OptF && OptF != file {next}
+ key == "*deleting" {print "deleted " prefix file; next}
+ substr(key, 3, 7) == "+++++++" {print "created " prefix file; next}
+ { # Avoid false positive if only mtime is changed.
+ of = "'$1'/" file; gsub("'\''", "'\'\\\\\'\''", of)
+ nf = "'$2'/" file; gsub("'\''", "'\'\\\\\'\''", nf)
+ if (substr(key, 2, 1) == "L") { # Symlink
+ src = target = file
+ sub(/.* -> /, "", target);
+ sub(/ -> .*/, "", src);
+ "readlink '$2'/" src | getline otarget
+ if (target != otarget)
+ print "changed " src
+ } else if (system("cmp -s '\''" of "'\'\ \''" nf "'\''"))
+ print "changed " file
+ }'
+ # Or: scan key for file/link size, checksum or permission change
+ # rsync -c is required
+ #key ~ /[cps]/ { print "changed " prefix file }'
+}
+
+dv_write() {
+ cat <<- EOT
+ From $(username)
+ Date: $(date +"%F %T %z")
+ Version: $1
+ Anc: $2
+ EOT
+
+ [ "$OptA" ] && echo "Author: $OptA"
+ printf "\n%s\n" "$Optm"
+}
+
+dv_header() {
+ awk -v val="$3" '/^'$2':/ {print "'$2': " val; done = 1; next}
+ NF == 0 && done == 0 {print "'$2': " val; done = 1}
+ {print}' $1/.dv >$1/.dv.$$ && mv $1/.dv.$$ $1/.dv
+}
+
+# Usage: getdv path
+# Return lib in R1, anc in R2, version in R3
+getdv() {
+ R1= R2=
+ while read -r line
+ do
+ case $line in
+ ("Lib: "*) R1=${line#Lib: };;
+ ("Anc: "*) R2=${line#Anc: };;
+ ("Version: "*) R3=${line#Version: };;
+ esac
+ done <$1
+ [ "$R1" -o "$R2" ]
+}
+
+# Usage: getdvinfo lib
+# Get exclusive write access to a dvlib, for new version commit
+getdvinfo() {
+ [ -d /tmp/dv.$$ ] || mkdir /tmp/dv.$$
+ [ "$Optn" ] && { rsync "$1/.dvinfo.1" /tmp/dv.$$/.dvinfo.0; return; }
+ t=0
+ for i in 1 2 3 4 5
+ do
+ rsync --remove-source-files "$1/.dvinfo.0" /tmp/dv.$$/ && break
+ t=$(($t + $i)) && sleep $t
+ done
+ test -f /tmp/dv.$$/.dvinfo.0
+}
+
+putdvinfo() {
+ [ "$Optn" ] && return
+ ln -f /tmp/dv.$$/.dvinfo.0 /tmp/dv.$$/.dvinfo.1
+ rsync -a /tmp/dv.$$/.dvinfo.[01] "$1/"
+ isremote "$1" && cp /tmp/dv.$$/.dvinfo.0 "$cacheprefix/$1/"
+}
+
+# Usage: getpdir file [path]
+# Return absolute parent dir of path or PWD containing file
+getpdir() {
+ R1="$([ -d "${2:-.}" ] && cd "${2:-.}" && pwd)"
+ while [ "$R1" ]
+ do
+ [ -f "$R1/$1" ] && return || R1=${R1%/*}
+ done
+ return 1
+}
+
+# Usage: getprd file [path]
+# Return relative path to parent directory containing file
+getprd() {
+ R2="$(cd "${2:-.}" && pwd)"; R2=${R2%/*} R1=..
+ while [ "$R2" ]
+ do
+ [ -f "$R2/$1" ] && return || R2=${R2%/*} R1=$R1/..
+ done
+ return 1
+}
+
+info() {
+ usage info && return
+ case $2 in
+ (*:*) lib=${2%/*/*} ;;
+ (*) getpdir .dv && getdv $R1/.dv && lib=$R1 || die no dvlib found
+ esac
+ case $2 in # $2 specifies:
+ (*/*/*) v=${2%/*/*}; v=${2#$v/} ;; # lib/item/version
+ (*/*) v=$2 ;; # item/version
+ (*) v=${R2%/*}/$2 ;; # version
+ esac
+ isremote "$lib" && {
+ p=$cacheprefix
+ [ -d "$p/$lib" ] || mkdir -p "$p/$lib"
+ [ "$Optl" ] || rsync -a "$lib/.dvinfo.1" "$p/$lib/"
+ } || p=
+ info_query $1 $v $3 <$p/$lib/.dvinfo.1
+}
+
+info_query()
+{
+ awk -v arg0=$1 -v arg1=$2 -v arg2=$3 '
+ /^From / {
+ if (v) msg[v] = var["Body"]
+ delete var
+ var["From"] = substr($0, 6)
+ header = 1
+ next
+ }
+ header == 1 && NF == 0 {
+ v = var["Version"]; a = var["Anc"]; s = var["Sup"]
+ if (a != v) {
+ anc[v] = a; desc[a] = desc[a] ? desc[a] " " v : v
+ }
+ n = split(s, as)
+ for (i = 1; i <= n; i++) {
+ cli[as[i]] = cli[as[i]] ? cli[as[i]] " " v : v
+ }
+ sup[v] = s; msg[v] = var["Msg"]; date[v] = var["Date"]
+ if (var["Root"]) root[v] = var["Root"]
+ header = 0
+ next
+ }
+ header == 1 {
+ i = index($0, ":")
+ var[substr($0, 1, i-1)] = substr($0, i+2)
+ next
+ }
+ header == 0 {
+ if ($0 ~ />+From /) sub(/>/, "")
+ var["Body"] = var["Body"] ? var["Body"] "\n" $0 : $0
+ }
+ END {
+ if (v) msg[v] = var["Body"]
+ if (arg0 == "graph") anc_graph()
+ else if (arg0 == "anc") query_anc(arg1, arg2)
+ else if (arg0 == "sup") query_sup(arg1, arg2)
+ else if (arg0 == "cli") query_cli(arg1, arg2)
+ else if (arg0 == "log") print msg[arg1]
+ else if (arg0 == "from") print from[arg1]
+ else if (arg0 == "date") print date[arg1]
+ else if (arg0 == "desc") printl(desc[arg1])
+ }
+ function anc_graph() {
+ print "digraph G {"
+ for (v in anc) print "\"" anc[v] "\" -> \"" v "\""
+ print "}"
+ }
+ function query_anc(v, n) { while (n-- > 0) v = anc[v]; print v }
+ function query_cli(v, n) {
+ while (n-- > 0 && v != "") {
+ num = split(v, av); v = ""
+ for (i = 1; i <= num; i++) {
+ printl(cli[av[i]])
+ v = v ? v " " cli[av[i]] : cli[av[i]]
+ }
+ }
+ }
+ function query_sup(v, n) {
+ while (n-- > 0 && v != "") {
+ num = split(v, av); v = ""
+ for (i = 1; i <= num; i++) {
+ printl(sup[av[i]])
+ v = v ? v " " sup[av[i]] : sup[av[i]]
+ }
+ }
+ }
+ function printl(l, i, n) {
+ n = split(l, al)
+ for (i = 1; i <= n; i++)
+ if (root[al[i]])
+ print al[i] " -> '"$PWD/"'" root[al[i]]
+ else
+ print al[i]
+ }'
+}
+
+initlib() {
+ rsync --list-only "$1/.dv." >/dev/null 2>&1 && return
+ mkdir -p /tmp/dv.$$
+ ln -s . /tmp/dv.$$/.dv.
+ >/tmp/dv.$$/.dvinfo.0
+ >/tmp/dv.$$/.dvinfo.1
+ chmod g+w /tmp/dv.$$/.dvinfo.[01]
+ rsync -a /tmp/dv.$$/ "$1"
+}
+
+isremote() { case $1 in (*:*) return;; esac; return 1; }
+
+help_all() {
+ printf "$dv_version\nUsage: dv command [options] [args]\n"
+ Opth=1; for c in $Cmdlist; do $c; done
+}
+
+newversion() {
+ [ "$OptV" ] && R1=$OptV && return
+ case $OptB in
+ ('') R1=$1 ;;
+ (*[-/]*) die 'illegal character [-/] in branch name' ;;
+ (*[0-9.]) die 'illegal end character [0-9.] in branch name' ;;
+ (*) case $1 in (*-${OptB}[1-9]*) R1=$1;; (*) R1=$1-${OptB}0;; esac ;;
+ esac
+ R2=${R1##*[!0-9]}
+ optb=$Optb
+ [ "$optb" ] && R3=.1 optb=${optb%b} || R3= R1=${R1%$R2}$(($R2 + 1))
+ while [ "${optb}" ]; do optb=${optb%b} R3=.0$R3; done
+ R1=$R1$R3
+ while grep -q "^Version: $item/$R1\$" /tmp/dv.$$/.dvinfo.0
+ do
+ R2=${R1##*[!0-9]}
+ [ "$R2" ] && R1=${R1%$R2}$(($R2 - 1)).1 || R1=${R1}1
+ done
+}
+
+patch() {
+ usage 'patch [-x pat] [-F File] [Dir|Version]' \
+ 'Print patch diff from ancestor' && return
+ diff "$@" | awk -v wdflag="-v${OptD:+3}${OptN:+1}${OptO:+2}" '
+ { key = $1; file = substr($0, length(key) + 2) }
+ key == "old" { old = (file ~ /:/ ? "'$cacheprefix'/" : "") file; next }
+ key == "new" { new = (file ~ /:/ ? "'$cacheprefix'/" : "") file; next }
+ {
+ of = old "/" file; gsub("'\''", "'\'\\\\\'\''", of)
+ nf = new "/" file; gsub("'\''", "'\'\\\\\'\''", nf)
+ system("diff -Naup '\''" of "'\'\ \''" nf "'\''; echo")
+ }'
+}
+
+save() {
+ usage 'save [-bln] [-A Author] [-m msg] [-x pat] [-B Branch|-V Version] [Dir]'\
+ 'Save dir into a new version' && return
+ [ "$2" ] && die 'too many arguments'
+ getpdir .dv "$1" || die "not in a clone: ${1:-$PWD}" && root=$R1
+ cd "$root"
+ getdv .dv && lib=$R1 && getdvinfo "$lib"
+ # Process supplier subdirs from the deepest up to "." (most client)
+ for d in $(find . -type f -name .dv | sort -r)
+ do
+ d=${d%/.dv}
+ cd "$root/$d"
+ OptF= save1 "$d" && nanc=$R1 && echo $nanc
+ [ "$d" = . ] && continue # not a supplier, done
+
+ # Replace supplier subdir by symlink in lib
+ getprd .dv && s=$R1/../.dv./$nanc
+ t=${d##*/}; t=.dv.$t
+ mv "$root/$d" "$root/${d%/*}/$t"
+ ln -s "$s" "$root/$d"
+ done
+ putdvinfo "$lib"
+ # Restore supplier subdirs and remove links to lib, deepest first
+ find . -type d -name ".dv.*" | sort -r | while read d
+ do
+ t=${d##*/.dv.}; t=${d%/*}/$t
+ rm -f "$t" && mv "$d" "$t"
+ done
+}
+
+save1() {
+ getdv .dv && lib=$R1 anc=$R2 && item=${anc%/*} old=${anc#*/}
+ [ "$(diffdir "$lib/$anc" .)" ] || { R1=$anc; return; }
+ newversion $old && new=$item/$R1
+ [ "$Optn" ] || dv_write "$new" "$anc" >.dv
+ # Compute list of direct suppliers, for caching in .dvinfo.0
+ lsup=$(find . -type l | while read -r l
+ do
+ case $l in (*/.dv.*) continue ;; esac
+ f=$(readlink "$l")
+ case $f in (*/.dv./*) printf "%s " ${f#*/.dv./} ;; esac
+ done)
+ [ ! "$Optn" ] && [ "$lsup" ] && dv_header . Sup "$lsup"
+ { cat .dv; echo; } >>/tmp/dv.$$/.dvinfo.0
+ [ -f ".dvignore" ] && xf=--exclude-from=.dvignore || xf=
+ rsync -aDS$Optn$Optv --link-dest=../$old $xf --exclude=".dv.*/" \
+ ./ "$lib/$new"
+ R1=$new
+ [ "$Optn" ] && return
+ isremote "$lib" && rsync -aDS --link-dest=../$old --exclude=".dv.*/" \
+ ./ "$cacheprefix/$lib/$new"
+ dv_header . Anc "$new"
+ dv_header . Lib "$lib"
+}
+
+supplier() {
+ usage 'supplier [Version]' 'print suppliers' && return
+ getdv .dv || die no item
+ case $1 in
+ (""|.|$PWD) # create a temporary dvinfo file, query it
+ V=$R2; find . -name .dv |
+ while read dv
+ do
+ [ "$dv" = "./.dv" ] && continue
+ # get the supplier
+ getdv "$dv"; v=$R2; dv=${dv%/.dv}
+ # get the client
+ getpdir .dv "${dv%/*}"; getdv "$R1/.dv"
+ # declare the client as supplier
+ printf "From \nVersion: %s\nSup: %s\n" "$v" "$R2"
+ [ "$OptM" ] && echo "Root: ${dv#./}"; echo
+ done | info_query cli $V ${Opt_N:-9999} ;;
+ (*) info sup $1 ${Opt_N:-9999} ;;
+ esac | sort -u
+}
+
+usage() { [ "$Opth" ] && printf " %-34s %s\n" "$1" "$2"; }
+
+username() {
+ case $(uname -s) in
+ (Darwin) id -P | awk -v FS=: '{print $8}' ;;
+ (*) awk -v FS=[:,] '/^'$USER':/ {print $5; exit}' /etc/passwd ;;
+ esac
+}
+
+wdiff() {
+ usage 'wdiff [-DNO] [-x pat] [-F File] [Dir|Version]' \
+ 'Print word diffs from ancestor' && return
+ diff "$@" |
+ awk -v wdflag="-v${OptD:+3}${OptN:+1}${OptO:+2}" '
+ { key = $1; file = substr($0, length(key) + 2) }
+ key == "old" { old = (file ~ /:/ ? "'$cacheprefix'/" : "") file; next }
+ key == "new" { new = (file ~ /:/ ? "'$cacheprefix'/" : "") file; next }
+ {
+ of = old "/" file; gsub("'\''", "'\'\\\\\'\''", of)
+ nf = new "/" file; gsub("'\''", "'\'\\\\\'\''", nf)
+ system("wd " wdflag " '\''" of "'\'\ \''" nf "'\''; echo")
+ }' | less -FCimnqGrX -j5 -h0 +/'\[0'
+}
+
+cacheprefix=$HOME/.cache/dv
+Cmdlist='anc client clone desc diff info patch save supplier wdiff'
+while getopts :nvV opt # Parse global options
+do
+ case $opt in
+ (V) echo "$dv_version"; exit ;;
+ (*) help_all; exit ;;
+ esac
+done
+shift $((OPTIND - 1))
+[ $1 ] && C=$1 && shift 1 || { help_all; exit 1; }
+for c in $Cmdlist
+do
+ case $c in
+ ($C|_$C) cmd=$c; break;;
+ ($C*) [ $cmd ] && die ambiguous command $C || cmd=$c;;
+ esac
+done
+while getopts :0123456789A:bB:F:hl:m:MnNOsvV:x: opt # Parse command options
+do
+ case $opt in
+ ([0-9]) Opt_N=$Opt_N$opt;;
+ ([bhlnMNOsv]) eval Opt$opt=\${Opt$opt}$opt ;;
+ ([ABFmV]) eval Opt$opt=\$OPTARG ;;
+ (x) Optx="$Optx --exclude=$OPTARG" ;;
+ (*) Opth=1; $cmd; exit 1;;
+ esac
+done
+shift $(($OPTIND - 1))
+trap "rm -rf /tmp/dv.$$" EXIT
+[ "$cmd" ] || die "no command \"$C\"" && $cmd "$@"
diff --git a/bin/wd b/bin/wd
new file mode 100755
index 0000000..74e67de
--- /dev/null
+++ b/bin/wd
@@ -0,0 +1,239 @@
+#!/bin/sh
+#
+# Copyright (c) 2007, 2008 Oligem.com. All rights reserved.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+# wd, word diff
+
+usage()
+{
+ echo 'NAME
+ wd - word differences
+SYNOPSIS
+ wd [-123NvV] [-w|-x|-y|-z Mark] Oldfile Newfile
+OPTIONS
+ -1 inhibit output of deleted words
+ -2 inhibit output of inserted words
+ -3 inhibit output of common words
+ -N treat absent files as empty
+ -O Optimal markup. Default marks are redundant, but
+ make less(1) happy for colorizing.
+ -V print version and exit
+ -v print a header line with old and new filenames.
+ -w Mark mark beginning of old word
+ -x Mark mark end of old word
+ -y Mark mark beginning of new word
+ -z Mark mark end of new word
+AUTHORS
+ Philippe Bergheaud and Marc Vertes' >&2
+
+ exit 2
+}
+
+istextfile()
+{
+ diff /dev/null "$1" 2>/dev/null | awk '/differ/ {exit 1} {exit 0}'
+}
+
+opt1=0 opt2=0 opt3=0
+red=""; red="$red[01;31m"
+blue=""; blue="$blue[01;34m"
+green=""; green="$green[01;35m"
+white=""; white="$white[00m"
+opth=$green optw=$blue optx=$white opty=$red optz=$white
+
+while getopts :123L:NOuVvw:x:y:z: opt
+do
+ case $opt in
+ ([123NOuv]) eval opt$opt=1 ;;
+ ([Lwxyz]) eval "opt$opt=\"$OPTARG\"" ;;
+ (V) echo wd-0.6; exit ;;
+ (*) usage ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+[ $# -eq 2 ] || usage
+
+if [ $opt1 -eq 0 -a $opt2 -ne 0 ]
+then
+ # show old: swap files and colors
+ old_file=$2 new_file=$1
+ opt2=0 opt1=1 opty=$optw optz=$optx
+else
+ old_file=$1 new_file=$2
+fi
+
+[ -f "$old_file" ] || { [ "$Optn" ] && old_file=/dev/null || exit; }
+[ -f "$new_file" ] || { [ "$OptN" ] && new_file=/dev/null || exit; }
+istextfile $new_file || exit
+
+old_word_file=/tmp/old_word_file.$$
+trap 'rm -f $old_word_file' EXIT
+
+tr -s '[:blank:]' '\n' <$old_file >$old_word_file
+tr -s '[:blank:]' '\n' <$new_file |
+diff -B --new-group-format='n %dF %dL
+' --old-group-format='o %df %dl
+' --line-format= "$old_word_file" - |
+awk -v optv="$optv" -v new_file="$new_file" -v old_file="$old_file" \
+ -v optO="$optO" -v opt1=$opt1 -v opt2=$opt2 -v opt3=$opt3 -v opth="$opth" \
+ -v optw="$optw" -v optx="$optx" -v opty="$opty" -v optz="$optz" '
+function print_tblank(file, blank1, display, resume)
+{
+ if (!display) return
+ if (tblank ~ /\n/) {
+ if (prev_file != file && resume) {
+ if (blank1)
+ printf("%s%s", tblank, blank1)
+ else
+ printf(" ")
+ } else
+ printf("%s%s", tblank, blank1)
+ } else {
+ if (prev_file && prev_file != file)
+ printf(" ")
+ else
+ printf("%s%s", tblank, blank1)
+ }
+ prev_file = file
+ tblank = ""
+}
+
+function print_word(file, begin, end, blank, word, display, bmark, emark)
+{
+ bmark_printed = 0
+ emark_toprint = 0
+ while (W[file] != end) {
+ if (i[file] > nf[file]) {
+ if (getline line <file == 0) {
+ print_tblank(file, "", display, 0)
+ exit # end of file, exit
+ }
+ split(line, blank, "[^ ]+") # anti-split
+ if (tblank || nf[file] != -1)
+ print_tblank(file, blank[1], display, 0)
+
+ if (!blank[1] || nf[file] == -1)
+ i[file] = 1 # initialize the for loop
+ else
+ i[file] = 2 # line starts with blank
+ nf[file] = split(line, word, "[ ]+")
+ if (nf[file] && word[nf[file]] == "")
+ nf[file]-- # line ends with blank
+ } else
+ print_tblank(file, "", display, 1)
+
+ # redundant per line marks (for less(1) colorizing)
+ if (! optO) {
+ bmark_printed = 0
+ emark_toprint = 0
+ }
+ for (; i[file] <= nf[file]; i[file]++) {
+ if (++W[file] < begin)
+ continue
+ if (display) {
+ if (! bmark_printed) {
+ if (emark_toprint) {
+ printf("%s", emark)
+ emark_toprint = 0
+ }
+ printf("%s", bmark)
+ bmark_printed = 1
+ }
+ printf("%s", word[i[file]])
+ emark_toprint = 1
+ if (W[file] == end)
+ tblank = blank[i[file] + (blank[1] ? 0 : 1)]
+ else
+ printf("%s", blank[i[file] + (blank[1]? 0 : 1)])
+ }
+ if (W[file] == end) {
+ i[file]++
+ break
+ }
+ }
+ # redundant per line marks
+ if (! optO && emark_toprint) {
+ printf("%s", emark)
+ emark_toprint = 0
+ }
+ if (display && i[file] > nf[file]) {
+ if (W[file] == end)
+ tblank = tblank "\n"
+ else {
+ print ""
+ tblank = ""
+ }
+ }
+ if (W[file] == end)
+ break
+ }
+ if (emark_toprint)
+ printf("%s", emark)
+ w[file] = end + 1
+}
+
+function same_group(begin, end)
+{
+ print_word(old_file, w[old_file], w[old_file] + end - begin,
+ oblank, oword, 0, "", "")
+ print_word(new_file, begin, end, nblank, nword, !opt3, "", "")
+}
+
+function old_group(begin, end)
+{
+ if (begin > w[old_file])
+ same_group(w[new_file], w[new_file] + begin - w[old_file] - 1)
+ print_word(old_file, begin, end, oblank, oword, !opt1, optw, optx)
+}
+
+function new_group(begin, end)
+{
+ if (begin > w[new_file]) same_group(w[new_file], begin - 1)
+ print_word(new_file, begin, end, nblank, nword, !opt2, opty, optz)
+}
+
+BEGIN {
+ # index of old word read, to be printed
+ W[old_file] = 0; w[old_file] = 1
+
+ # index of new word read, to be printed
+ W[new_file] = 0; w[new_file] = 1
+
+ # initialize the for loop on old lines
+ i[old_file] = 0; nf[old_file] = -1
+
+ # initialize the for loop on new lines
+ i[new_file] = 0; nf[new_file] = -1
+
+ oblank[1] = ""; oword[1] = "" # old blank and old word arrays
+ nblank[1] = ""; nword[1] = "" # new blank and new word arrays
+
+ # trailing blank, replaced by a space after the last word
+ # of an old group when followed by a word on the same line
+ tblank = ""
+ prev_file = "" # previous file
+ if (optv) print opth "### wd " optw old_file " " opty new_file optx
+}
+/^n/ { new_group($2, $3) } # display common and new words
+/^o/ { old_group($2, $3) } # display common and old words
+END { # display common words, up to the end of file
+ if ((!opt1 || !opt2) && opt3)
+ print ""
+ print_word(new_file, w[new_file], -1, nblank, nword, !opt3, "", "")
+}' |
+less -CimqGrX -j9 -h0 +/'\[01'; echo -n ''
+echo -n '' # force color reset