#!/bin/bash

version="0.34"
program=$(basename $0)

NEW=""		# If there are more than $NEW % new lines, skip update
OLD=""		# If there are more than $OLD % deleted lines, skip update
FILE=""
verbose=""
silent=""

# ----------------------------------------------------------------------
function usage() {
cat <<EOF
NAME
    $program -  conditionally update target file.

SYNOPSIS
    $program [OPTIONS] FILE

DESCRIPTION
    $program reads input from a pipe or file and saves it to a target
    (FILE) if there are changes. If the new content is the same as the
    old, the target is left untouched. By default, the target is also
    left untouched if the new content is empty. There are options to
    also abstain from applying an update if the changes are too large,
    and to back up the previous version.

    The purpose is to handle files with dynamically generated content in
    such a manner that timestamps don't change if the content doesn't change,
    and mistakes in content generation doesn't unnecessarily propagate to
    the target.
    
OPTIONS
EOF
    if   [ "$(uname)" = "Linux" ]; then
        egrep "^[	]+[-][A-Za-z| -]+\*?\)[	]+[A-Za-z].+#" $0 | tr -s "\t|" "\t," | sed -r -e 's/\)[ \t]+([A-Z]+)="\$2"[^#]*#/=\1\t/' -e 's/\)[^#]*#/\t/'
    else
        egrep "^[	]+[-][A-Za-z| -]+\*?\)[	]+[A-Za-z].+#" $0 | sed 's/\|.*"\$2"[^#]*#/	/'| sed -E 's/\|.*\)[^#]*#/	/' 
    fi
    cat <<EOF

AUTHOR
    Henrik Levkowetz <henrik@levkowetz.com>
EOF
exit
}


# ----------------------------------------------------------------------
function note() {
    if [ -n "$verbose" ]; then
	echo -e "$program: $*"
    fi
}

# ----------------------------------------------------------------------
function warn() {
    [ "$QUIET" ] || echo -e "$program: $*"
}

# ----------------------------------------------------------------------
function err() {
    echo -e "$program: $*" > /dev/stderr
}

# -----------------------------------------------------------------------------
function leave() {
   errcode=$1; shift
   if [ "$errcode" -ge "2" ]; then warn "$*"; else note "$*"; fi
   if [ -f "$tempfile" ]; then rm $tempfile; fi
   if [ -f "$difffile" ]; then rm $difffile; fi
   if [ "$errcode" = "1" -a "$RESULT" = "0" ]; then exit 0; else exit $errcode; fi
}

# ----------------------------------------------------------------------
# Set up error trap
trap 'leave 127 "$program($LINENO): Command failed with error code $? while processing '$origfile'."' ERR

# exit with a message if a command fails
set -e

# ----------------------------------------------------------------------
#	Get any options
#

# Default values
PAT="\$path\$base.%Y-%m-%d_%H%M"
RESULT="0"
QUIET=""

# Based on the sample code in /usr/share/doc/util-linux/examples/parse.bash.gz
if   [ "$(uname)" = "Linux" ]; then
    GETOPT_RESULT=$(getopt -o bc:ef:hn:o:p:qrvV --long backup,maxchg:,empty,file:,help,maxnew:,maxold:,prefix:,report,quiet,verbose,version  -n "$program" -- "$@")
else
    GETOPT_RESULT=$(getopt bc:ef:hn:o:p:qrvV "$@")
fi 

if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi

note "GETOPT_RESULT: $GETOPT_RESULT"
eval set -- "$GETOPT_RESULT"

while true ; do
	case "$1" in
		-b|--backup)	backup=1;	shift ;;	# Back up earlier versions by creating a backup file
		-c|--maxchg)	CHG="$2";	shift 2 ;;	# Limit on percentage of changed lines
		-e|--empty)	empty=1;	shift ;;	# Permit the update to be empty (default: discard)
		-f|--file)	FILE="$2";	shift 2 ;;	# Read input from FILE instead of standard input
		-h|--help)	usage;		shift ;;	# Show this text and exit
		-n|--maxnew)	NEW="$2";	shift 2 ;;	# Limit on percentage of new (added) lines
		-o|--maxold)	OLD="$2";	shift 2 ;;	# Limit on percentage of old (deleted) lines
		-p|--pat*)	PAT="$2";	shift 2 ;;	# Backup name base ('$path$base.%Y%m%d_%H%M')
		-q|--quiet)	QUIET=1;	shift;;		# Be less verbose
		-r|--result)	RESULT=1;	shift ;;	# Return 1 if update not done
		-v|--verbose)	verbose=1;	shift ;;	# Be more verbose about what's happening
		-V|--version)	echo -e "$program\t$version"; exit;;	# Show version and exit
		--) shift ; break ;;					
		*) echo "$program: Internal error, inconsistent option specification." ; exit 1 ;;
	esac
done

if [ $CHG ]; then OLD=$CHG; NEW=$CHG; fi

if [ $# -lt 1 ]; then echo -e "$program: Missing output filename\n"; usage; fi

origfile=$1
tempfile=$(mktemp)
difffile=$(mktemp)

if [ -e "$origfile" ]; then
    cp -p $origfile $tempfile		# For ownership and permissions
    cat $FILE > $tempfile
    [ "$FILE" ] && touch -r $FILE $tempfile
    # This won't work if we don't have sufficient privileges:
    #chown --reference=$origfile $tempfile
    #chmod --reference=$origfile $tempfile
else
    cat $FILE > $origfile
    [ "$FILE" ] && touch -r $FILE $tempfile
    leave 0 "Created file '$origfile'"
fi

origlen=$(wc -c < $origfile)
newlen=$(wc -c < $tempfile)

if [ $origlen = 0 -a $newlen = 0 ]; then
    rm $tempfile
    leave 1 "New content is identical (and void) - not updating '$origfile'."
fi
if [ $newlen = 0 -a -z "$empty" ]; then
    leave 1 "New content is void - not updating '$origfile'."
fi

diff $origfile $tempfile > $difffile || [ $? -le 1 ] && true # suppress the '1' error code on differences
difflen=$(wc -l < $difffile)
if [ $difflen = 0 ]; then
    leave 1 "New content is identical - not updating '$origfile'."
fi

if [ "$OLD" -o "$NEW" ]; then

    if [ "$NEW" ]; then maxnew=$(( $origlen * $NEW / 100 )); fi
    if [ "$OLD" ]; then maxdel=$(( $origlen * $OLD / 100 )); fi

    newcount=$(grep "^> " $difffile | wc -c)
    outcount=$(grep "^< " $difffile | wc -c)
    delcount=$(grep "^! " $difffile | wc -c)
    delcount=$(( $outcount + $delcount ))
    rm $difffile

    if [ "$OLD" ]; then
	if [ "$delcount" -ge "$maxdel" ]; then
	    cp $tempfile $origfile.update
	    leave 2 "New content has too many removed lines ($delcount/$origlen)\n - not updating '$origfile'.\nNew content placed in '$origfile.update' instead"
	fi
    fi
    if [ "$NEW" ]; then
	if [ "$newcount" -ge "$maxnew" ]; then
	    cp $tempfile $origfile.update
	    leave 2 "New content has too many added lines ($newcount/$origlen)\n - not updating '$origfile'.\nNew content placed in '$origfile.update' instead"
	fi
    fi
fi

if [ "$backup" ]; then

    path=${origfile%/*}
    name=${origfile##*/}
    base=${name%.*}
    ext=${origfile##*.}

    if [ "$ext" = "$origfile" ]; then
	ext=""
    elif [ ! "${ext%/*}" = "$ext" ]; then
	ext=""
    else
	ext=".$ext"
    fi

    if [ "$path" = "$origfile" ]; then
	path=""
    else
	path="$path/"
    fi

    ver=1
    backfile=$(eval date +"$PAT")
    backpath="${backfile%/*}"
    if [ "$backpath" = "$backfile" ]; then
	backpath="."
    fi
    if [ ! -d $backpath ]; then
	if [ -e $backpath ]; then
	    leave 3 "The backup path '$backpath' exists but isn't a directory"
	else
	    mkdir -p $backpath
	fi
    fi
    while [ -e "$backfile,$ver$ext" ]; do
       ver=$(( $ver+1 ))
    done
    note "Saving backup: $backfile,$ver$ext"
    cp -p "$origfile" "$backfile,$ver$ext"
    chmod -w "$backfile,$ver$ext" || true
fi

if ! mv $tempfile $origfile; then cp -p $tempfile $origfile; fi
leave 0 "Updated file '$origfile'"