#!/bin/zsh # # tailsh - runs a file through sh, ignoring lines before # the line reading "#!". Useful if someone mails you a shell # script and you're too lazy to save to a temp file, strip the stuff # from the top, chmod it to be executable, run the script and # delete the temporary file. # # It works on things other than /bin/sh. What it actually # does is pass the lines following the first #! line into the # command named on the #! line. # # Usage: # tailsh [-p path] [-c command] [-o options] [file . . .] # # Where # -p path means tailsh will perform a cd to path before running # each file. Files given as parameters to tailsh # are intelligently renamed so that they can still be found # if relative paths were given. # -c command overrides the command to run the script through. # useful if the script was written by someone who puts # /bin/sh in a different place. This switch overrides # *all* files given on the line, even though each file # may have a different command in its #! line # -o options supplies options to the file, making it believe # they were supplied as command-line options. # This really only works with scripts that invoke # a shell of some sort with any reliability. All it # does is put them after the temp file name it creates. # Importantly, it works with sh and zsh. # file is the name of a file to be sourced in this fashion. # If no files are supplied, tailsh will run the entire # standard input as if it were a single file. tailsh is # more efficient in stdin mode. # # Examples: # tailsh -o '-c' part1of3.shar part2of3.shar part3of3.shar # Run each of the three shar files you were just mailed. # Depending on what your version of shar is like it should # completely decode the whole thing. # # Written by Deborah Pickett # # To do: Maybe find a way of doing the stdin style on a filename # provided. On the other hand, it's more general if you can supply # a filename rather than rely on stdin. The present compromise # is probably best. Redirect from stdin if you can, otherwise # name a file. The down side is that it doesn't appear to work for # substituted files, i.e., <(some command). Use =(some command) # instead. # Also need a better mechanism for passing perceived parameters; # the -o switch isn't very portable. # local variables local neg sws file line linenum opt newpath overridecmd passopts\ usage="Usage: $0:t [-p path] [-c command] [-o options] file . . ." \ excl=! # fetch options while getopts :p:c:o: opt do case $opt in 'p') newpath=$OPTARG ;; 'c') overridecmd=$OPTARG ;; 'o') passopts=$OPTARG ;; ':') echo $usage >&2 ; return 1 ;; '?') echo $usage >&2 ; return 1 ;; esac done # put options behind us shift $[ $OPTIND - 1 ] # extendedglob and shwordsplit need to be set thusly [[ -o extendedglob ]] || neg=1 setopt extendedglob [[ -o shwordsplit ]] && sws=1 unsetopt shwordsplit # tailsh proper if [[ $# -ne 0 ]] then # files specified - read them in one at a time for file do # if file doesn't start with "/" it was relative and needs $PWD # prepended so that it still can be found when -c was used. if [[ "$newpath" != "" && "$file[1]" != "/" ]] then file=$PWD/$file fi # check for existence of file if [[ ! -r $file ]] then echo "${0:t}: $file: no such file" >&2 continue fi # find first line containing a #! # This sed script outputs the line number followed by the command # itself. It exits after the first #! is found; this avoids # duplicates. It's simpler to extract it from an array, thus # the surrounding brackets. # (This looks hideous and there's good reason. # Owing to what I think is a misfeature of zsh's handling of # ! characters, I can't quote any as "\!" or '\!' because it # is treated as ! in a zsh function if nobanghist is off, # \! in a zsh function if nobanghist is on, # and \! in a zsh script regardless of the settings of nobanghist. # I solve this by setting a shell variable to contain the # exclamation mark. [This has probably been solved in zsh version 3, # but I haven't changed it because this way we get backwards # compatibility.] ) line=($(sed -n '/#'$excl'/{=;s/#'$excl' *//p;q;}' < $file)) if [[ "$line" != "" ]] then # there was a #! - find its line number. linenum=${line[1]} # cd to the path given by -c if it was specified if [[ "$newpath" != "" ]] then cd $newpath fi # run the command. "eval" is used in case the command # was somthing like "cat >foo" and the shell needs to # see it. It also helps sidestep that annoying ! feature. eval ${overridecmd:-${line[2,-1]}} \ =(tail +$[$linenum+1] $file) ${=passopts} # cd back to the old path to make it look like nothing happened. if [[ "$newpath" != "" ]] then cd $OLDPWD fi else # file didn't have a #! - ignore it. echo "${0:t}: $file: no #"\!" line" >&2 fi done else # no files named - must be wanting stdin mode. linenum=0; # keep reading lines till something happens. while read -r -u0 line do if [[ $line[1,2] = \#\! ]] then # found a #! - flag the fact we have found it. linenum=1 if [[ "$newpath" != "" ]] then cd $newpath fi # duplicate our stdin and give it to the command. # If no command is supplied (i.e. the line was #! and nothing # else), it's a null command, which defaults to calling # cat(1) implicitly. Just an interesting side-effect. eval ${overridecmd:-${line##\#\! #}} "<&0" "${=passopts}" if [[ "$newpath" != "" ]] then cd $OLDPWD fi # and break out of the loop. break fi done if [[ $linenum = 0 ]] then # No #! line - output warning message. echo "${0:t}: stdin: no #"\!" line" >&2 fi fi # restore option settings [[ "$neg" = 1 ]] && unsetopt extendedglob [[ "$sws" = 1 ]] && setopt shwordsplit