Search code examples
bashaliaszsh

"alias method chain" in Bash or Zsh


This is (or was, at least) a common pattern in Ruby, but I can't figure out how to do it in Zsh or Bash.

Let's suppose I have a shell function called "whoosiwhatsit", and I want to override it in a specific project, while still keeping the original available under a different name.

If I didn't know better, I might try creating an alias to point to whoosiwhatsit, and then create a new "whoosiwhatsit" function that uses the alias. Of course that work, since the alias will refer to the new function instead.

Is there any way to accomplish what I'm talking about?


Solution

  • Aliases are pretty weak. You can do this with functions though. Consider the following tools:

    #!/usr/bin/env bash
    
    PS4=':${#FUNCNAME[@]}:${BASH_SOURCE}:$LINENO+'
    
    rename_function() {
      local orig_definition new_definition new_name retval
      retval=$1; shift
      orig_definition=$(declare -f "$1") || return 1
      new_name="${1}_"
      while declare -f "$new_name" >/dev/null 2>&1; do
        new_name+="_"
      done
      new_definition=${orig_definition/"$1"/"$new_name"}
      eval "$new_definition" || return
      unset -f "$orig_definition"
      printf -v "$retval" %s "$new_name"
    }
    
    # usage: shadow_function target_name shadowing_func [...]
    # ...replaces target_name with a function which will call:
    # shadowing_func target_renamed_to_this number_of_args_in_[...] [...] "$@"
    shadow_function() {
      local shadowed_func eval_code shadowed_name shadowing_func shadowed_func_renamed
      shadowed_name=$1; shift
      shadowing_func=$1; shift
      rename_function shadowed_func_renamed "$shadowed_name" || return
      if (( $# )); then printf -v const_args '%q ' "$@"; else const_args=''; fi
      printf -v eval_code '%q() { %q %q %s "$@"; }' \
        "$shadowed_name" "$shadowing_func" "$shadowed_func_renamed" "$# $const_args" 
      eval "$eval_code"
    }
    

    ...and the following example application of those tools:

    whoosiwhatsit() { echo "This is the original implementation"; }
    
    override_in_directory() {
      local shadowed_func=$1; shift
      local override_cmd_len=$1; shift
      local override_dir=$1; shift
      local -a override_cmd=( )
      local i
      for (( i=1; i<override_cmd_len; i++)); do : "$1"
        override_cmd+=( "$1" ); shift
      done
      : PWD="$PWD" override_dir="$override_dir" shadowed_func="$shadowed_func"
      : override_args "${override_args[@]}"
      if [[ $PWD = $override_dir || $PWD = $override_dir/* ]]; then
          [[ $- = *x* ]] && declare -f shadowed_func >&2 # if in debugging mode
          "${override_cmd[@]}"
      else
          "$shadowed_func" "$@"
      fi
    }
    
    ask_the_user_first() {
      local shadowed_func=$1; shift;
      shift # ignore static-argument-count parameter
      if [[ -t 0 ]]; then
        read -r -p "Press ctrl+c if you are unsure, or enter if you are"
      fi
      "$shadowed_func" "$@"
    }
    
    shadow_function whoosiwhatsit ask_the_user_first
    
    shadow_function whoosiwhatsit \
      override_in_directory /tmp echo "Not in the /tmp!!!"
    
    shadow_function whoosiwhatsit \
      override_in_directory /home echo "Don't try this at home"
    

    The end result is a whoosiwhatsit function that asks the user before it does anything when its stdin is a TTY, and aborts (with different messages) when run under either /tmp or /home.


    That said, I don't condone this practice. Consider the above provided as an intellectual exercise. :)