#!/bin/sh -ef

cmdcache_dir=
cmdcache_init()
{
	cmdcache_dir="$HOME/.cmdcache"
	[ -d "$cmdcache_dir" ] || mkdir -p "$cmdcache_dir" || return
	if [ "${RANDOM:-$$}" -lt 1024 ]; then
		find "$cmdcache_dir" -type f -mtime +33 -atime +33 -delete
		find "$cmdcache_dir" -depth -mindepth 1 -type d -print0 |
			xargs -r0 rmdir --ignore-fail-on-non-empty
	fi
}

c_st= c_hf= c_hd=
cmdcache_hash()
{
	c_st="$(stat -L -c '%i %s %Y' -- "$1")" || return
	set -- $c_st
	c_hf="i${1}s${2}m${3%.*}"
	c_hd=$(( ( $1 ^ $2 ^ ${3%.*} ) % 97 ))
}

cmdcache()
{
	if [ $# != 2 ]; then
		[ $# -gt 2 ] &&
			echo "cmdcache: too many arguments" ||
			echo "cmdcache: not enough arguments"
		pod2usage --exit=2 cmdcache ||:
		return 2
	fi >&2

	if [ -n "$CMDCACHE_DISABLE" ]; then
		"$@"
		return
	fi

	[ -n "$cmdcache_dir" ] || cmdcache_init || return 2

	local c_cmd="$1" c_file="$2"
	cmdcache_hash "$c_file" || return 2

	local c_dir="$cmdcache_dir/${c_cmd##*/}/$c_hd"
	[ -d "$c_dir" ] || mkdir -p "$c_dir" || return 2

	local c_cache="$c_dir/$c_hf"
	if [ -f "$c_cache" ]; then
		[ -s "$c_cache" ] && cat "$c_cache"
		return 0
	elif [ -f "$c_cache".gz ]; then
		zcat "$c_cache".gz
		return
	fi

	local c_rc=0 c_out="$c_cache.out$$"
	"$@" >"$c_out"; c_rc=$?

	cat "$c_out"
	if [ $c_rc = 0 ]; then
		local c_size="$(du -bk "$c_out" |cut -f1)"
		if [ "$c_size" -gt 4 ]; then
			gzip -nf "$c_out"
			mv -f "$c_out".gz "$c_cache".gz
		else
			mv -f "$c_out" "$c_cache"
		fi
	else
		rm -f "$c_out" "$c_cache" "$c_cache".gz
	fi
		
	local c_st0="$c_st"
	if cmdcache_hash "$c_file" && [ "$c_st0" = "$c_st" ]; then
		return $c_rc
	else
		echo "cmdcache: $*"
		echo "file $c_file has changed"
		return 2
	fi >&2
}

while getopts h c_opt; do
	case "$c_opt" in
		h) pod2usage --exit=0 "$0"; exit 0 ;;
		*) pod2usage --exit=2 "$0"; exit 2 ;;
	esac
done
shift "$((OPTIND-1))"; OPTIND=1

[ $# -eq 1 -a -z "$1" ] || cmdcache "$@"

: <<'__EOF__'

=head1	NAME

cmdcache - simple cache for command output

=head1	SYNOPSIS

B<cmdcache> [B<-h>] I<command> I<FILE>

=head1	DESCRIPTION

B<cmdcache> wraps I<command> and caches its output.  I<command> must
take exactly one argument, an existent I<FILE> (or possibly a
directory).  Previous I<command> output is reused when C<st_ino>,
C<st_size>, and C<st_mtime> of the I<FILE> match.

B<cmdcache> can be sourced by specifying null argument for the
I<command>.  Shell function with the same name is provided, and
I<command> can be a shell function, too.  Suggested usage is as follows:

	# implement custom command with a shell function
	my_command() { local f="$1"; study "$f"; }

	# grab cmdcache shell function
	. cmdcache ""

	# process arguments and cache the results
	for f; do cmdcache my_command "$f"; done

Upon successful I<command> completion, cache file is stored under
C<B<$HOME>/.cmdcache/I<command>> directory.  The directory has up to 97
subdirectories, due to C<mod 97> hash function.  That is, the designed
cache cacpacity is about 10k (100 files * 100 subdirs), and the capacity
does not impose any performance hit.

When I<command> return code is non-zero, cache file is not created, and
the return code of B<cmdcache> is that of the I<command>.  The return
code is 2 if an error occurred (e.g. if I<FILE> does not exist).

If I<FILE> has changed upon I<command> completion, the return code is 2.
This means that I<FILE> must not be modified by the I<command>.

=for comment
TODO: describe how cache is purged.
TODO: explain compression.

=head1	AUTHOR

Written by Alexey Tourbin <at@altlinux.org>.

=head1	COPYING

Copyright (c) 2005 Alexey Tourbin, ALT Linux Team.

This is free software; you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation;
either version 2 of the License, or (at your option) any later version.

=head1	SEE ALSO

stat(2),
ccache(1)

=cut

__EOF__
