#!/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<URL> # # Names commencing with B<http://> or B<ftp://> # 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 0 ] || exit 1 [ -t 1 ] || 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<directory> # # 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<type> # # Specify the MIME I<type>. # Otherwise the type will be guessed from the file content. # -i) itype=$2; shift ;; # =item I<arg1-option> # # Supply an argument for I<arg1>, one of # B<-discard>, B<-end>, B<->I<n>. # If specified, # the view will be invoked via I<arg1>. # See arg1(1cs) for semantics. # This permits mailcap entries like this: # # image/jpeg; ah %s -end xv; gui # # which will correctly invoke I<xv> with the possibly modified filename I<ah> 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<y>" or "B<yes>' accepts this default and runs the viewer. # Entering "B<n>" or "B<no>" 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<n>" or "B<no>" 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<n>" or "B<no>" skips the save. # Entering "B<y>" or B"<yes>" 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<username> 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 E<lt>cs@zip.com.auE<gt> 18jun1998 #