226 lines
6.9 KiB
Bash
Executable file
226 lines
6.9 KiB
Bash
Executable file
#!/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
|
|
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" -- "$@")
|
|
|
|
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=$(tempfile)
|
|
difffile=$(tempfile)
|
|
|
|
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'"
|