0
\$\begingroup\$

This is a sort-of-useless "utility" I wrote to learn my way around shell programming better.

My concerns are:

  1. 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?
  2. Is the interface itself (the flags, arguments, etc) intuitive? Is the documentation clear?
  3. 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
  (( $? )) && return 1

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

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

  if [[ (-z "$interpreter") && (-n "$1") ]]; then
    interpreter=$(filename=$(basename "$1"); [[ "$filename" = *.* ]] && 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'" >&2
  fi

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

  if [[ -z "$1" ]]; then
    echo $shebang
  else
    gsed_avail=$(command which -s gsed && 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) && (-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
}
\$\endgroup\$
3
  • \$\begingroup\$ zparseopts -D t:=interpreter I:=inplace J::=inplace2 v=verbose -i=interactive Only problem for bash portability. \$\endgroup\$ Oct 26, 2015 at 0:57
  • \$\begingroup\$ Ah, I missed arrays. Is KSH_ARRAYS acceptable then? \$\endgroup\$ Oct 26, 2015 at 1:31
  • \$\begingroup\$ Oh and read. ouch. \$\endgroup\$ Oct 26, 2015 at 1:39

1 Answer 1

1
\$\begingroup\$

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))

    (( $? )) && return 1

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

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

    if [[ (-z "$interpreter") && (-n "$1") ]]; then
          interpreter=$(filename=$(basename "$1"); [[ "$filename" = *.* ]] && 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'" >&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) && (-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).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.