#!/bin/ksh
# @(#) conglom.ksh 93/11/23
# 93/11/23 John H. DuBois III (john@armory.com)
# 93/11/26 If no match, mention almost-matching files

name=${0##*/}
Usage="Usage: $name [-bdhsv] filename [filename ...] [destination]"

alias istrue="test 0 -ne"
alias isfalse="test 0 -eq"

typeset -i backup=0 remove=1 verbose=1 database=0

while getopts :bdhsv opt; do
    case $opt in
    h)
	echo \
"$name: conglomerate files.
$name appends the contents of files to other already existing files and then
removes the files whose contents were appended.
$Usage
If the destination is a directory, the contents of each file is appended to a
file with the same name in the directory.  If the directory does not exist or a
file with the same name does not exist in the directory, the operation fails
and the source file is not removed.  The destination can be forced to be a
directory by ending it with a '/'.  If the destination is a file, the contents
of all of the other files is appended to it.  If the destination does not
exist, the operation fails and the source files are not removed.
If only one filename is given, a file database is used to find the destination.
The last component of the filename is searched for in the database; if one and
only one name is found that ends with that component (other than a name that
refers to the source file itself), it is used as the destination. Relative
filenames in the database are taken to be relative to the user's home
directory.  If no match is found, an error message is printed.  The default
database is the file \".files\" in the user's home directory.  The environment
variable CGFILES overrides the default.
Options:
-b: A backup of each destination file is made; the name is its original name
    with a '-' attached.  Any file that exists with that name is overwritten.
-d: Use the file database to find the destination for each source file.
    All arguments are taken to be source files.  
-h: Print this help.
-s: Save.  The source file is not removed.
-v: Verbose.  The operations performed are printed."
	exit 0
	;;
    b)
	backup=1
	;;
    d)
	database=1
	;;
    s)
	remove=0
	;;
    v)
	verbose=1
	;;
    +?)
	print -u2 "$name: options should not be preceded by a '+'."
	exit 1
	;;
    ?) 
	print -u2 "$name: $OPTARG: bad option.  Use -h for help."
	exit 1
	;;
    esac
done
 
# Usage: ProcDest <destfile>
# Process a destination file.  Check that it can be appended to,
# and make a backup of it if backup is true.
# Return nonzero on failure.
# Globals used: backup verbose
function ProcDest {
    typeset DestFile=$1
    if [ ! -f "$DestFile" ]; then
	print -u2 "$name: $DestFile: no such file."
	return 1
    fi
    if [ ! -w "$DestFile" ]; then
	print -u2 \
	"$name: $DestFile: not writable."
	return 1
    fi
    if istrue backup; then
	if cp "$DestFile" "$DestFile-"; then :; else
	    print -u2 "$name: $DestFile: could not copy to $DestFile-"
	    return 1
	fi
	istrue verbose && print -u2 "Copied $DestFile to $DestFile-"
    fi
}

# Usage: FindMatch <filename>
# Sets global DestMatch to matching filename from db
# Returns nonzero on error
# Uses global vars db, HOME
function FindMatch {
    typeset file tail=${1##*/} IFS ChkFile=$1 dest alldest DestFound file
    typeset -i numdest=0

    OIFS=$IFS
    IFS="
"
    for file in $(grep -e "$tail\$" "$db"); do
	# grep may give extra files due to tail having e.g. '.' in it,
	# and because we don't want to put / in front of $tail in the grep
	# expression since bare files in home dir may be in database, nor
	# do we want to use egrep since there would be even more opportunities
	# for metacharacter misinterpretation...  so do a further test
	if eval [[ \"$file\" = "?(*/)$tail" ]]; then
	    # Convert relative path in database to absolute path from home
	    [[ $file != /* ]] && dest="$HOME/$file" || dest=$file
	    # Skip if it's the same file as the source file
	    if [[ "$ChkFile" -ef "$dest" ]]; then :; else
		let numdest+=1
		DestFound=$dest
		alldest="$alldest
$dest"
	    fi
	fi
    done
    IFS=$OIFS
    if isfalse numdest; then
	print -u2 "$name: No matching file for $tail found in $db."
	CloseFiles=
	egrep -ie "(^|/)$tail(\\.z|\\.gz)?\$" "$db" | while read file; do
	    [[ $file != /* ]] && file="$HOME/$file"
	    [[ "$file" -ef "$dest" ]] || CloseFiles="$CloseFiles
$file"
	done
	[ -n "$CloseFiles" ] && print -u2 "Note: did find:$CloseFiles"
	return 1
    fi
    if [ numdest -gt 1 ]; then
	print -u2 \
	"$name: Found $numdest files ending in $tail in $db:$alldest"
	return 1
    fi
    DestMatch=$DestFound
    return 0
}

# Usage: CheckDest <destfile>
# Test destfile & set it up to have file(s) appended
# Global DirDest is set to 1 if destination is a directory, 0 if a file
# Globals used: DirDest remove
# Return value is 0 for success, 1 for error
function CheckDest {
    typeset dest=$1
    if [[ -d "$dest" || "$dest" = */ ]]; then
	DirDest=1
	if [ ! -d "$dest" ]; then
	    print -u2 "$name: $dest is not a directory."
	    return 1
	fi
	if [ ! -x "$dest" ]; then
	    print -u2 "$name: $dest: cannot access."
	    return 1
	fi
    else
	DirDest=0
	DestFile=$dest
	if ProcDest "$DestFile"; then :; else
	    istrue remove && print -u2 "Source file(s) not removed."
	    return 1
	fi
    fi
    return 0
}

# remove args that were options
let OPTIND=OPTIND-1
shift $OPTIND

if [ $# -eq 0 ]; then
    print -u2 "$Usage\nUse -h for help."
    exit
fi

typeset -i DirDest nsource numfiles=0

[ $# -eq 1 ] && database=1

if istrue database; then
    [ -n "$CGFILES" ] && db=$GCFILES || db=$HOME/.files
    if [ ! -f "$db" -o ! -r "$db" ]; then
	print -u2 "$name: $db: cannot read."
	exit 1
    fi

    for file; do
	if FindMatch "$file"; then
	    if ProcDest "$DestMatch"; then
		if istrue DirDest; then
		    print -u2 \
"$name: Destination $DestMatch is a directory.
Skipping source file $file."
		else
		    SourceFiles[numfiles]=$file
		    DestFiles[numfiles]=$DestMatch
		    let numfiles+=1
		fi
	    fi
	else
	    print -u2 "Skipping source file $file."
	fi
    done
else
    if [ $# -lt 2 ]; then
	print -u2 "$Usage\nUse -h for help."
	exit
    fi
    set -A args -- "$@"
    nsource=$#-1
    DestFile=${args[nsource]}
    unset args[nsource]
    if CheckDest "$DestFile"; then :; else
	print -u2 Aborting.
	exit 1
    fi
    DestDir=$DestFile
    for file in "${args[@]}"; do
	if istrue DirDest; then
	    DestFile="$DestDir/$file"
	    if ProcDest "$DestFile"; then :; else
		istrue remove && print -u2 \
	    "Appending of $file to $DestFile not attempted; $file not removed."
		continue
	    fi
	fi
	SourceFiles[numfiles]=$file
	DestFiles[numfiles]=$DestFile
	let numfiles+=1
    done
fi

typeset -i filenum=0

while [ filenum -lt numfiles ]; do
    file=${SourceFiles[filenum]}
    DestFile=${DestFiles[filenum]}
    let filenum+=1
    if [ ! -f "$file" ]; then
	print -u2 "$name: $file: no such file."
	continue
    fi
    if [ ! -r "$file" ]; then
	print -u2 "$name: $file is not readable."
	continue
    fi
    if cat "$file" >> "$DestFile"; then 
	istrue verbose && print -u2 "Concatenated $file to $DestFile"
	if istrue remove; then
	    rm "$file"
	    istrue verbose && print -u2 "Removed $file"
	fi
    else
	print -u2 "$name: concatenation failed."
	istrue remove && print -u2 "Source file $file not removed."
    fi
done
