#!/bin/sh -u
#
# =head1 NAME
#
# apphelper - a generic wrapper for email attachments or web browser file helpers, offering both save and view choices
#
: ${TMPDIR:=/tmp}
: ${PAGER:=less}
: ${SAVEDIR:=$HOME/dl}
: ${APPHELPER_REMAPFILE:=$HOME/rc/remap}
: ${APPHELPER_SAVEMAP:="remap \"\$APPHELPER_REMAPFILE\" | tr '[A-Z_]' '[a-z-]'"}
: ${APPHELPER_SAVEMODE:=auto}
: ${APPHELPER_SAVEPOST:=''}
: ${APPHELPER_IDSET:=$HOME/var/apphelper}
: ${APPHELPER_DOVIEW:=''}
: ${APPHELPER_EXTRACT_GLOBS:=''}
: ${APPHELPER_OPTS:=''}
: ${APPHELPER_PREOPTS:=''}
: ${APPHELPER_LASTVALUE:=savedir}
cmd=`basename "$0"`
doterm=
itype=
savedir=
doview=$APPHELPER_DOVIEW
savemode=$APPHELPER_SAVEMODE
extract=
extract_globs=$APPHELPER_EXTRACT_GLOBS
termname=
pager=$PAGER
savepost=$APPHELPER_SAVEPOST
trace= ##set-x
arg1opts=
# =head1 SYNOPSIS
#
# apphelper [{-t|+t|-T title}] file [{-n|-y}]] [-d savedir] [-i type] [-p pager] [arg1opts...] viewer [viewer-args...]
#
usage="Usage: $cmd [{-t|+t|-T title}] file [{-n|-y}] [-d savedir] [-i type] [-p pager] [arg1opts...] \\
viewer [viewer-args...]
-t Run apphelper in a new terminal window.
+t Do not run apphelper in a new terminal window.
-T title Title for new terminal window. Implies -t.
Default: $termname
NB: -t or +t must preceed the file.
The file may be \"-\" to read it from standard input.
-b File basename, offered for saving.
-n Don't run the viewer at all.
-y Run viewer immediately.
-N Don't offer to save the file.
-Y Save the file without asking questions.
-A Auto-notsave if file already present.
-X Extract archive and run apphelper for each entry.
-x trace execution of subcommands.
-i type Specify MIME type of file.
-d savedir Save directory (from \$SAVEDIR: $savedir).
-p pager Pager (default viewer, from \$PAGER: $pager).
arg1opts Options as for arg1(1cs). Default: none.
If specified, the viewer is run via arg1."
unset tmpdir
trap 'rm -r "$tmpdir"' 0
tmpdir=`mkdirn "$TMPDIR/$cmd"` || exit 1
tmpfile=$tmpdir/$cmd$$
# =head1 DESCRIPTION
#
# Apphelper is a generic handler for a file.
# It was originally coded for use with netscape and mozilla
# which persist in directly opening files they have handlers for,
# offering no handy "save as?" accompaniment.
# This was particularly aggravating for PDF files,
# which I often want to keep around for later perusal.
#
# Apphelper offers to view the file with the named viewer
# and after viewing, to save the file in a location of your choice.
#
# =head2 Pre-File Options
#
# The following terminal related options must preceed the filename.
#
# =over 4
#
badopts=
# =item -t
#
# Open a fresh terminal window in which to run apphelper.
# Generally desirable when invoking inside a GUI web browser.
#
# =item +t
#
# Do not open a terminal window.
#
# =item -T I
#
# Specify the title of the fresh terminal in which to run apphelper.
# Implies the B<-t> option.
#
set -x
set -- $APPHELPER_PREOPTS ${1+"$@"}
set +x
ttyopts=
doterm=; [ -t 0 -a -t 1 ] || doterm=1
while [ $# -gt 0 ]
do
case $1 in
-t) doterm=1; ttyopts="$ttyopts $1" ;;
-T) termname=$2; shift; doterm=1 ;;
+t) doterm=; ttyopts="$ttyopts $1" ;;
*) break ;;
esac
shift
done
# =back
#
# =head2 File Designation
#
# After the terminal related pre-file options
# comes the file designation.
# Usually this will be a filename,
# however the following notations are also supported:
#
# =over 4
#
if [ $# = 0 ]
then
echo "$cmd: missing file" >&2
badopts=1
else
file=$1; shift
case "$file" in
# =item B<->
#
# Read data from stdin.
# This will be placed in a temporary file.
#
-)
if cat >"$tmpfile"
then
tmpfile=`fixexts -i "$itype" "$tmpfile"` || exit 1
file=$tmpfile
else
echo "$cmd: can't cat stdin to $tmpfile, aborting" >&2
badopts=1
fi
;;
# =item I
#
# Names commencing with B or B
# are taken to be URLs and fetched.
#
http://* | ftp://*)
exec $trace withurl -0 "$file" "$0" "$@"
;;
*)
# handed real filename
if [ ! -s "$file" ]
then
echo "$cmd: bad file?" >&2
ls -ld "$file" >&2
badopts=1
else
if [ $doterm ]
then
tmpfile=$tmpdir/`basename "$file"`
if cp "$file" "$tmpfile"
then tmpfile=`fixexts -i "$itype" "$tmpfile"` || exit 1
file=$tmpfile
else echo "$cmd: can't cp $file to $tmpfile, aborting" >&2
badopts=1
fi
fi
fi
;;
esac
#
# =back
#
# fork off a terminal if need be
if [ $doterm ]
then
[ -n "$DISPLAY" ] || { echo "$cmd: can't make new terminal: no \$DISPLAY!" >&2
exit 1
}
( [ -n "$termname" ] || termname="$cmd - $file"
exec $trace term -n "$termname" +a -e "$0" +t "$file" ${1+"$@"}
)
rm -f "$tmpfile"
exit 0
fi
[ -t 0 ] || exec /dev/tty; [ -t 1 ] || exit 1
# =head2 Post-File Options
#
# =over 4
#
set -- $APPHELPER_OPTS ${1+"$@"}
apphopts=
while [ $# -gt 0 ]
do
apphopt=$1
apphopt2=
case $1 in
# =item B<-d> I
#
# Specify a default directory in which to save files.
# Lacking this, use the value of the envvar B<$SAVEDIR>
# or B<$HOME/dl> if that is not set.
#
-d) apphopt2=$2 savedir=$2; shift ;;
# =item B<-n>
#
# No: do not run the viewer program.
# The default is to ask first.
#
-n) doview=0 ;;
# =item B<-y>
#
# Yes: always run the viewer program.
# The default is to ask first.
#
-y) doview=1 ;;
# =item B<-N>
#
# No: do not offer to save the file.
# The default is to ask.
#
-N) savemode=no ;;
# =item B<-Y>
#
# Yes: save the file without asking.
# The default is to ask.
#
-Y) savemode=yes ;;
# =item B<-a>
#
# Ask: ask to save the file even if the file's already there.
#
-a) savemode=ask ;;
# =item B<-A>
#
# Auto: don't ask to save the file if the file's already there.
# The default is to auto.
#
-A) savemode=auto ;;
# =item B<-X>
#
# Extract the file (if we know how)
# with the x(1) command
# and run apphelper on each component.
#
-X) extract=1 apphopt= ;;
# =item B<-x>
#
# Trace execution of subcommands.
#
-x) trace=set-x ;;
# =item B<-i> I
#
# Specify the MIME I.
# Otherwise the type will be guessed from the file content.
#
-i) itype=$2; shift ;;
# =item I
#
# Supply an argument for I, one of
# B<-discard>, B<-end>, B<->I.
# If specified,
# the view will be invoked via I.
# See arg1(1cs) for semantics.
# This permits mailcap entries like this:
#
# image/jpeg; ah %s -end xv; gui
#
# which will correctly invoke I with the possibly modified filename I may use.
#
-stdin|-end|-[0-9]*)
arg1opts="$arg1opts $1"
;;
--) shift; break ;;
-?*) echo "$cmd: unrecognised option: $1" >&2; badopts=1 ;;
*) break ;;
esac
shift
apphopts="$apphopts $apphopt $apphopt2"
done
fi
# =back
#
# default to "v" for the command
[ $# = 0 ] && set v '%s'
[ $badopts ] && { echo "$usage" >&2; exit 2; }
if [ $extract ]
then
mtype=`file2mime "$file"` || mtype=
[ "$mtype" = application/octet-stream ] && mtype=
[ -n "$mtype" ] || mtype=`ext2mime "$file"`
if [ -n "$mtype" ] \
&& action=`mailcap -s "$file" "$mtype" unpack !gui`
then
case "$file" in
/*) absfile=$file ;;
*) absfile=`pwd`/$file ;;
esac
xdir=`mkdirn "$TMPDIR/$cmd$$.x"` || exit 1
(
cd "$xdir" || exit 1
$trace x "$absfile"
set --
sep=
# ugly workaround for weird bash glob/quoting bug:-(
if [ -n "$extract_globs" ]
then
for glob in $extract_globs
do
set -- ${1+"$@"} $sep -name "$glob"
sep=-o
done
else
set -- ${1+"$@"} -name '*'
fi
$trace find . -type f -size +0 \( "$@" \) -exec $trace ifnewfile -v -discard {} "$0" +t {} $apphopts -A ';'
)
rm -rf "$xdir"
exit
fi
fi
file2=`fixexts -i "$itype" "$file"` && file=$file2
fbase=`basename "$file"`
case "$fbase" in
=\?*\?Q\?*\?= )
# deuglify the font spec
fdir=`dirname "$file"`
nfbase=`unfontise "$fbase"`
nfile=$fdir/$nfbase
ln "$file" "$nfile" || exit 1
fbase=$nfbase
file=$nfile
;;
esac
export file fbase
file "$file" | noesc; echo
# =head1 OPERATION
#
# =head2 Viewing The File
#
# Apphelper first asks whether to view the file,
# offering the supplied view command in [square brackets]
# as the default.
# Pressing return or entering "B" or "B' accepts this default and runs the viewer.
# Entering "B" or "B" skips running the viewer.
# Other answers are taken to be the name (and optional arguments) of a viewing program.
#
# If the viewer has a nonzero exit code (failure)
# then this is reported and the user is prompted for an alternative viewing program.
# Pressing return or entering "B" or "B" skips the second attempt.
# Other answers are taken to be the name (and optional arguments) of a viewing program.
#
if [ -z "$doview" ]
then
necho "View [$*]? "
read viewit || exit 0
case "$viewit" in
''|y|yes) doview=1 ;;
n|no) doview=0 ;;
*) set -- $viewit "$file"
doview=1 ;;
esac
fi
if [ "$doview" = 1 ]
then
# transmute %s into the (possibly renamed by fixexts or remap) file and run viewer
(
echo "$0: view args=[$*]" >&2
first=1
for arg
do
[ $first ] && { set --; first=; }
if [ "x$arg" = 'x%s' ]
then set -- ${1+"$@"} "$file"
else set -- ${1+"$@"} "$arg"
fi
done
[ -n "$trace" ] && set -- $trace ${1+"$@"}
[ -n "$arg1opts" ] && set -- arg1 $arg1opts -- "$file" ${1+"$@"}
set -x
exec "$@"
) \
|| { echo "failed: $* $file"
viewit=`readline 'View with? '` || exit 0
case "$viewit" in
''|n|no) ;;
*) $viewit "$file" ;;
esac
}
fi
# =head2 Saving The File
#
# After the view phase
# apphelper offers to save the file
# with the default pathname in [square brackets].
# Pressing return or entering "B" or "B" skips the save.
# Entering "B" or B"" saves the file under the supplied filename.
# Entering a string commencing with a bang (`!')
# performs a shell escape, running the tail of the string in the user's shell.
# Occurences of bangs in the command are replaced by the last command
# run from this instance of apphelper.
# Occurences of percents (`%') in the command are replaced by the filename.
#
# Other answers are taken to be a different filename under which to save the file.
#
# If the file is to be saved
# then the save filename is examined.
# If it starts with a tilde ("~")
# then the leading B<~> or B<~>I is replaced with the approriate
# home directory path.
# Relative pathnames are taken to be relative to the default save directory.
#
# If there is a percent ("%") present
# then the rightmost precent is replaced with the basename of the attachment name.
# This is useful for repairing truncated filenames on attachments.
#
# If the filename points at a directory
# then the file is saved inside that directory
# with the basename of the attachment name.
#
# If the filename ends in a dash ("-")
# then the basename of the attachment name is appended.
# This is useful for attaching a prefix to some poorly chosen
# generic attachment name (like "10.pdf").
#
# If after all this the target filename already exists
# then the two files are compared.
# If they are the same
# then no action is taken.
# If they differ
# the then new file is copied onto the existing with via cp(1)
# with the B<-i> option,
# leaving it up to cp(1) to ask questions.
#
# offer remapped name if inferrable
remaptmp=$TMPDIR/$cmd-remap$$
if printf "%s\n" "$fbase" | eval "$APPHELPER_SAVEMAP" >"$remaptmp"
then
exec 3<&0 0<"$remaptmp"
if read -r oldname \
&& read -r newname \
&& [ -n "$newname" ] \
&& [ "x$oldname" != "x$newname" ]
then
case "$newname" in
*/*)
savedir=`expr "x$newname" : 'x\(.*\)/.*'`
newname=`expr "x$newname" : 'x.*/\(.*\)'`
case "$savedir" in /*) ;; *) savedir=$SAVEDIR/$savedir ;; esac
[ -n "$newname" ] && fbase=$newname
;;
?*)
fbase=$newname
;;
esac
fi
exec 0<&3 3<&-
fi
rm -f "$remaptmp"
# see if we've saved this before
if oldsave=`fileloc "$file"` \
&& cmp -s "$file" "$oldsave"
then
savedir=`dirname "$oldsave"` || exit 1
fbase=`basename "$oldsave"` || exit 1
fi
# no guesses? offer last save directory
if [ -z "$savedir" ]
then
savedir=`lastvalue "$APPHELPER_LASTVALUE" 2>/dev/null`
: ${savedir:=$SAVEDIR}
fi
if [ "x$savemode" = xauto ]
then
trysave=$savedir/$fbase
if [ -s "$trysave" ] && cmp -s "$file" "$trysave"
then echo "This file already saved:"
ls -ld "$trysave"
savemode=no
else savemode=ask
fi
fi
case "$savemode" in
yes) saveas=$savedir/$fbase ;;
no) saveas= ;;
*)
repeat=1
lastshcmd=
while [ $repeat ]
do
repeat=
trysave=$savedir/$fbase
if [ -s "$trysave" ]
then
if cmp -s "$file" "$trysave"
then echo "Exists, same data."
else echo "Different data."
# pick a similar name
case "$fbase" in
*.*) fext=`expr "x$fbase" : 'x.*\.\(.*\)'`
fpre=`basename "$fbase" ".$fext"`
;;
*) fpre=$fbase
if ftype=`file2mime "$file"`
then fext=`mimeext "$ftype"` || fext=dat
else fext=dat
fi
;;
esac
n=2
while trysave2=$savedir/$fpre$n.$fext
[ -s "$trysave2" ]
do n=`expr $n + 1`
done
trysave=$trysave2
fi
fi
prompt="Save [$trysave]? "
saveas=`readline -d "$savedir" "$prompt"` || exit 0
case "$saveas" in
\!*) repeat=1
shcmd=`expr "x$saveas" : 'x! *\(.*\)'`
case "$file" in
/*) absfile=$file ;;
*) absfile=`pwd`/$file ;;
esac
( [ -d "$savedir/." ] && cd "$savedir"
case "$shcmd" in
'') ${SHELL:-sh} ;;
*[%!]*)
qfile=`shqstr "$absfile"`
shcmd=`echo "$shcmd" | sed "s!$lastshcmdg"`
lastshcmd=$shcmd
shcmd=`echo "$shcmd" | sed "s%$qfileg"`
"${SHELL:-sh}" -xc "$shcmd"
;;
*) "${SHELL:-sh}" -xc "$shcmd"
;;
esac
)
;;
''|n|no) saveas= ;;
y|yes) saveas=$trysave ;;
[/~]*) ;;
*) saveas=$savedir/$saveas ;;
esac
done
;;
esac
[ -n "$saveas" ] || exit 0
case "$saveas" in
\~*) saveas=`untilde "$saveas"` ;;
esac
# %foo -> filefoo
case "$saveas" in
*%*) pre=`expr "x$saveas" : 'x\(.*\)%.*'`
post=`expr "x$saveas" : 'x.*%\(.*\)'`
saveas=$pre$fbase$post
;;
esac
# dir -> dir/file
# foo- -> foo-file
case $saveas in
*/|*-) saveas=$saveas$fbase ;;
*/. | */..) saveas=$saveas/$fbase ;;
esac
case `basename "$saveas"` in
*.*) ;;
*/$fbase) ;;
*) saveas=$saveas/$fbase ;;
esac
[ -d "$saveas/." ] && saveas=$saveas/$fbase
parent=`dirname "$saveas"`
[ -d "$parent/." ] \
|| if ask "mkdir $parent"
then $trace mkdir -p "$parent" || exit 1
else exit 0
fi
# stash savedir
( cd "$parent" || exit 1
pwd | lastvalue "$APPHELPER_LASTVALUE" -
)
xit=1
if { [ -f "$saveas" -a -s "$saveas" ] && cmp -s "$file" "$saveas"; } \
|| $trace cp -i "$file" "$saveas"
then
xit=0
$trace fileloc -a "$saveas" || xit=1
if [ -n "$savepost" ]
then
if qsaveas=`shqstr "$saveas"`
then
$trace sh -c "$savepost $qsaveas" || xit=1
else
xit=1
fi
fi
fi
exit $xit
# =head1 EXAMPLE
#
# My B<.mailcap> file contains this line for JPEG files:
#
# image/jpeg; apphelper %s -y xv
#
# which causes mutt(1),
# on pressing enter on a JPEG attachment,
# to hand JPEG files to apphelper
# which runs xv(1) immediately
# and then offers a save option.
#
# This mailcap line:
#
# application/pdf;apphelper %s -y xpdf
#
# causes mozilla to run xpdf(1) to view PDF files
# and then to offer a save option.
#
# =head1 AUTHOR
#
# Cameron Simpson Ecs@zip.com.auE 18jun1998
#