Print a "shebang" line, or prepend it to a file This is a sort-of-useless "utility" I wrote to learn my way around shell programming better.

My concerns are:


Is the code readable? Could it be more efficient, or just simpler? The logic ends up being a little complicated and I have a lot of nested if statements. Should I use 4 tabs instead of 2?
Is the interface itself (the flags, arguments, etc) intuitive? Is the documentation clear?
How easily this could be ported to Bash? As far as I remember, the only Zsh-specific feature I use here is zparseopts, but I've been casually using Zsh for a while and I could have forgotten some of the inconsistencies between shells.




# shebang [-iv] [-t interpreter] [-I extension] [-J [extension]] [filename]
#
# If no filename is given, print a shebang. If a filename is given, append a shebang to the file and print its contents. If a filename is given and the -I or -J options are specified, append a shebang to the file in place.
#
# OPTIONS
#   -i
#     Interactive mode; ask for confirmation first.
#   -I
#     Modify "filename" in place (as in `sed -i`) using specified extension. Extension is mandatory.
#   -J
#     Same as -I, but extension is mandatory only if the string "gsed" cannot be found in the output of `which sed`. Overridden by -I.
#   -t
#     Specify an interpreter, e.g. `shebang -t zsh` produces "#!/usr/bin/env zsh"
#   -v 
#     Print (to stderr) the interpreter being used.

shebang () {
  local interpreter
  local inplace
  local inplace2
  local verbose
  local interactive
  local input_shebang
  local shebang
  local continue
  local sed_command
  local gsed_avail
  zparseopts -D t:=interpreter I:=inplace J::=inplace2 v=verbose -i=interactive
  (( $? )) &amp;&amp; return 1

  if [[ -n $1 ]]; then
    input_shebang=$(sed -n '1 { /^#!/ p; }' "$1")
    if (( $? )); then
      echo "Unable to read $1." &gt;&amp;2
      return 1
    fi
    if [[ -n "$input_shebang" ]]; then
      echo "$1 already has a shebang." &gt;&amp;2
      return 1
    fi
  fi

  if [[ (-z "$interpreter") &amp;&amp; (-z "$1") ]]; then
    echo "The -t option is mandatory if no argument is supplied." &gt;&amp;2
    return 1
  fi

  if [[ (-z "$interpreter") &amp;&amp; (-n "$1") ]]; then
    interpreter=$(filename=$(basename "$1"); [[ "$filename" = *.* ]] &amp;&amp; echo "${filename##*.}" || echo '')  # grab extension
    interpreter=${interpreter:#(* *|* | *)}  # "extensions" with whitespace probably aren't legit
    interpreter=${interpreter:-sh}  # assume sh if no extension and no -t option
  else
    interpreter="$interpreter[2]"
  fi

  shebang="#!/usr/bin/env $interpreter"

  if [[ -n $verbose ]]; then
    echo "Using interpreter '$interpreter'" &gt;&amp;2
  fi

  if [[ -n $interactive ]]; then
    read -q "continue?Shebang will be '$shebang'. Ok? y/n: "
    [[ $continue == n ]] &amp;&amp; return 1
  fi

  if [[ -z "$1" ]]; then
    echo $shebang
  else
    gsed_avail=$(command which -s gsed &amp;&amp; echo 1)
    echo $gsed_avail

    if [[ -n $gsed_avail ]]; then
      sed_command="1 i\\$shebang\n"
      if [[ -n $inplace ]]; then
        gsed "${inplace/-I/-i}" "$sed_command" "$1"
      elif [[ -n $inplace2 ]]; then
        gsed "${inplace2/-J/-i}" "$sed_command" "$1"
      else
        gsed "$sed_command" "$1"
      fi
    else
      sed_command='1 i\
REPLACEME\n'  # need to use single quotes to preserve the line break
      sed_command=${sed_command/REPLACEME/$shebang}  # work around the single quotes
      if [[ -n $inplace ]]; then
        sed "${inplace/-I/-i}" "$sed_command" "$1"
      elif [[ (-n $inplace2) &amp;&amp; (-z ${inplace2#-J}) ]]; then
        echo "'-J' was given without an argument, but 'gsed' is unavailable. Specify an explicit extension or use -I."
      elif [[ -n ${inplace2#-J} ]]; then
        sed "${inplace2/-J/-i}" "$sed_command" "$1"
      else
        sed "$sed_command" "$1"
      fi
    fi
  fi
}
Is the code readable? Can this code be simpler?


Basically readable, although I prefer early returns and things like that.

Ported to bash but testing limited:

# shebang [-iv] [-t interpreter] [-I extension] [-J [extension]] [filename]
#
# If no filename is given, print a shebang. If a filename is given, append a
# shebang to the file and print its contents. If a filename is given and the -I
# or -J options are specified, append a shebang to the file in place.
#
# OPTIONS
#   -i
#    Interactive mode; ask for confirmation first.
#   -I
#    Modify "filename" in place (as in `sed -i`) using specified extension.
#   Extension is mandatory.
#   -J
#    Same as -I, but extension is mandatory only if the string "gsed" cannot
#    be found in the output of `which sed`. Overridden by -I.
#   -t
#    Specify an interpreter, e.g. `shebang -t zsh` produces "#!/usr/bin/env zsh"
#   -v 
#    Print (to stderr) the interpreter being used.

shebang () {
    local inplace inplace2 verbose interactive
    local interpreter input_shebang shebang continue
    local sed_command gsed_avail _nextopt _line _readbool
    local OPT OPTIND OPTARG
    # HACK: shift manually when encounters J
    while getopts 't:I:viJ' OPT; do case "$OPT" in
        i)  interactive=1;;
        I)  inplace="$OPTARG";;
        J)  eval "nextopt=\$$OPTIND";
            case "$_nextopt" in
                -*) inplace2='-J';;
                *)  inplace2="-J $_nextopt"; shift;;
            esac;;
        t)  interpreter="$OPTARG";;
        v)  verbose=1;;
    esac; done
    shift $((OPTIND-1))

    (( $? )) &amp;&amp; return 1

    if [[ -n $1 ]]; then
        IFS='' read -r _line &lt; "$1"
        if (( $? )); then
            echo "Unable to read $1." &gt;&amp;2
            return 1
        fi
        # if echo "$_line" | grep ^#!; then
        if [[ "$_line" =~ ^\#! ]]; then
            echo "$1 already has a shebang." &gt;&amp;2
            return 1
        fi
    fi

    if [[ (-z "$interpreter") &amp;&amp; (-z "$1") ]]; then
          echo "The -t option is mandatory if no argument is supplied." &gt;&amp;2
          return 1
    fi

    if [[ (-z "$interpreter") &amp;&amp; (-n "$1") ]]; then
          interpreter=$(filename=$(basename "$1"); [[ "$filename" = *.* ]] &amp;&amp; echo "${filename##*.}")  # grab extension
          interpreter=${interpreter/#* *}  # "extensions" with whitespace probably aren't legit
          : ${interpreter:=sh}  # assume sh if no extension and no -t option
    fi

    if [[ "$ZSH_VERION" ]]; then
        # reads one character.
        _sb_read1(){ read -k 1 "$@"; }
    else
        # ksh/bash
        _sb_read1(){ read -n 1 "$@"; }
    fi

    shebang="#!/usr/bin/env $interpreter"

    if [[ -n $verbose ]]; then
        echo "Using interpreter '$interpreter'" &gt;&amp;2
    fi

    if [[ -n $interactive ]]; then
        while true; do
            printf "continue? Shebang will be '$shebang'. Ok? y/n: "
            # ah, zsh/bash have different ideas on read.
            _sb_read1 _readbool
            echo
            case "$_readbool" in
                [nN])   return 1;;
                [yY])   break;;
            esac
        done
    fi

    if [[ -z "$1" ]]; then
          echo $shebang
          return 0
    fi

    if type gsed; then
        sed_command="1 i\\$shebang\n"
        if [[ -n $inplace ]]; then
            gsed "${inplace/-I/-i}" "$sed_command" "$1"
        elif [[ -n $inplace2 ]]; then
            gsed "${inplace2/-J/-i}" "$sed_command" "$1"
        else
            gsed "$sed_command" "$1"
        fi
    else
        sed_command=$'1i\n'$shebang'\n'
        if [[ -n $inplace ]]; then
            sed "${inplace/-I/-i}" "$sed_command" "$1"
        elif [[ (-n $inplace2) &amp;&amp; (-z ${inplace2#-J}) ]]; then
            echo "'-J' was given without an argument, but 'gsed' is unavailable. Specify an explicit extension or use -I."
        elif [[ -n ${inplace2#-J} ]]; then
            sed "${inplace2/-J/-i}" "$sed_command" "$1"
        else
            sed "$sed_command" "$1"
        fi
    fi
}


Using that grep for if gives extra ksh compatibility (untested).