Search code examples
bashzsh

modify an existing function from .bashrc or .zshrc


There is a function defined in /usr/share/zsh/functions/Completion/Unix/_git

(( $+functions[_git-diff] )) ||
_git-diff () {
  local curcontext=$curcontext state line ret=1
  declare -A opt_args

  local -a diff_options diff_stage_options
  __git_setup_diff_options
  __git_setup_diff_stage_options

  _arguments -C -s $endopt \
    $* \
    $diff_options \
    '(--exit-code)--quiet[disable all output]' \
    $diff_stage_options \
    '(--cached --staged)--no-index[show diff between two paths on the filesystem]' \
    '(--cached --staged --no-index)'{--cached,--staged}'[show diff between index and named commit]' \
    '(-)--[start file arguments]' \
    '*:: :->from-to-file' && ret=0

  case $state in
    (from-to-file)
      # If "--" is part of $opt_args, this means it was specified before any
      # $words arguments. This means that no heads are specified in front, so
      # we need to complete *changed* files only.
      if [[ -n ${opt_args[(I)--]} ]]; then
        if [[ -n ${opt_args[(I)--cached|--staged]} ]]; then
          __git_changed-in-index_files && ret=0
        else
          __git_changed-in-working-tree_files && ret=0
        fi
        return ret
      fi

      # If "--no-index" was given, only file paths need to be completed.
      if [[ -n ${opt_args[(I)--no-index]} ]]; then
        _alternative 'files::_files' && ret=0
        return ret
      fi

      # Otherwise, more complex conditions need to be checked.
      case $CURRENT in
        (1)
          local files_alt='files::__git_changed-in-working-tree_files'
          if [[ -n ${opt_args[(I)--cached|--staged]} ]]; then
            files_alt='files::__git_changed-in-index_files'
          fi

          _alternative \
            'commit-ranges::__git_commit_ranges' \
            'blobs-and-trees-in-treeish::__git_blobs_and_trees_in_treeish' \
            $files_alt \
            'blobs::__git_blobs ' && ret=0
          ;;
        (2)
          # Check if first argument is something special. In case of committish ranges and committishs offer a full list compatible completions.
          if __git_is_committish_range $line[1]; then
            # Example: git diff branch1..branch2 <tab>
            __git_tree_files ${PREFIX:-.} $(__git_committish_range_last $line[1]) && ret=0
          elif __git_is_committish $line[1] || __git_is_treeish $line[1]; then
            # Example: git diff branch1 <tab>
            _alternative \
              'commits::__git_commits' \
              'blobs-and-trees-in-treeish::__git_blobs_and_trees_in_treeish' \
              'files::__git_tree_files ${PREFIX:-.} HEAD' && ret=0
          elif __git_is_blob $line[1]; then
            _alternative \
              'files::__git_cached_files' \
              'blobs::__git_blobs' && ret=0
          elif [[ -n ${opt_args[(I)--cached|--staged]} ]]; then
            # Example: git diff --cached file1 <tab>
            __git_changed-in-index_files && ret=0
          else
            # Example: git diff file1 <tab>
            __git_changed-in-working-tree_files && ret=0
          fi
          ;;
        (*)
          if __git_is_committish_range $line[1]; then
            # Example: git diff branch1..branch2 file1 <tab>
            __git_tree_files ${PREFIX:-.} $(__git_committish_range_last $line[1]) && ret=0
          elif { __git_is_committish $line[1] && __git_is_committish $line[2] } ||
              __git_is_treeish $line[2]; then
            # Example: git diff branch1 branch2 <tab>
            __git_tree_files ${PREFIX:-.} $line[2] && ret=0
          elif __git_is_committish $line[1] || __git_is_treeish $line[1]; then
            # Example: git diff branch file1 <tab>
            # Example: git diff branch -- f<tab>
            __git_tree_files ${PREFIX:-.} HEAD && ret=0
          elif __git_is_blob $line[1] && __git_is_blob $line[2]; then
            _nothing
          elif [[ -n ${opt_args[(I)--cached|--staged]} ]]; then
            # Example: git diff --cached file1 file2 <tab>
            __git_changed-in-index_files && ret=0
          else
            # Example: git diff file1 file2 <tab>
            __git_changed-in-working-tree_files && ret=0
          fi
          ;;
      esac
      ;;
  esac

  return ret
}

I just need to append

'files::__git_changed_files ${PREFIX:-.} HEAD' \

Above

'files::__git_tree_files ${PREFIX:-.} HEAD' && ret=0

Currently I have copy-pasted the new function in my .zshrc, and it is working.

However, I think there is a cleaner way to override existing function using .bashrc or .zshrc like the following:

_git-diff 2>/dev/null 
functions[_git-diff-orig]=$functions[_git-diff]  
_git-diff() {
    _git-diff-orig "$@"
    ... 
} 

But I am not sure how to do it. Can anyone please help. Just to be clear, I want to override the function using .bashrc or .zshrc so that it can be more portable.


Solution

  • In zsh, the value in the functions associative array is the normalized code text. This means you can use any of the usual text manipulation methods to modify it:

    foo() {
      echo before
      echo and after
    }
    
    foo
    # => before
    # => and after
    
    functions[foo]=${functions[foo]/echo before/echo before;echo during}
    
    foo
    # => before
    # => during
    # => and after
    
    functions foo
    # => foo () {
    # =>   echo before
    # =>   echo during
    # =>   echo and after
    # => }
    

    Interestingly, zsh will parse, validate, and normalize the code on assignment to the functions array - it's essentially the same process as declaring a function normally. That's why the output from functions foo has a newline, even though the string substitution used a semicolon.

    Adding these lines to ~/.zshrc should work for your example:

    current="'files::__git_tree_files \${PREFIX:-.} HEAD'"
    replacement="'files::__git_changed_files \${PREFIX:-.} HEAD' $current"
    functions[_git-diff]=${functions[_git-diff]/$current/$replacement}
    

    Additional ways to modify a function

    There are a number of ways to alter text in shell languages, and some of these may work better for more complex changes to functions. Note that more changes can make the process more fragile, since updates to the base code could trip up the patching process.

    Also note that the changes are applied to the code that is stored in the functions array; that may not match what is in the original source file.

    Using patch

    An obvious choice, since we're patching code. The patch utility modifies text based on the output from diff (usually diff -u). The utility can handle some whitespace differences, and some errors such as incorrect line numbers:

    plan() {
        forecast=${1}
        if [[ $forecast == sun ]]; then
            print "take sunscreen"
            print "wear hat"
        elif [[ $forecast == rain ]]; then
            print "take umbrella"
            print "carry raincoat"
        else
            print "stay home"
        fi
    }
    plan sun
    #=> take sunscreen
    #=> wear hat
    
    plan hail
    #=> stay home
    
    
    patchDiff='
    --- plan1
    +++ plan2
    @@ -4,3 +4,5 @@
            print "take sunscreen"
    +       print "apply sunscreen"
            print "wear hat"
    +       print "use sunglasses"
        elif [[ $forecast == rain ]]
    @@ -11,2 +13,3 @@
            print "stay home"
    +       print "and relax"
        fi'
    functions[plan]=$(print -- $patchDiff \
        | patch -ls -o >(cat) =(print -- $functions[plan]))
    
    plan sun
    #=> take sunscreen
    #=> apply sunscreen
    #=> wear hat
    #=> use sunglasses
    
    plan hail
    #=> stay home
    #=> and relax
    

    Using sed

    Using the sed stream editor, and building on the previous example:

    sedscript='s/print *\(.*\)/print ${(C):-\1}/'
    functions[plan]=$(print $functions[plan] | sed -e $sedscript)
    
    functions plan
    #=> plan () {
    #=>     forecast=${1} 
    #=>     if [[ $forecast == sun ]]
    #=>     then
    #=>         print ${(C):-"take sunscreen"}
    #=>         print ${(C):-"apply sunscreen"}
    #=>         print ${(C):-"wear hat"}
    #=>         print ${(C):-"use sunglasses"}
    #=>     elif [[ $forecast == rain ]]
    #=>     then
    #=>         print ${(C):-"take umbrella"}
    #=>         print ${(C):-"carry raincoat"}
    #=>     else
    #=>         print ${(C):-"stay home"}
    #=>         print ${(C):-"and relax"}
    #=>     fi
    #=> }
    
    plan rain
    #=> Take Umbrella
    #=> Carry Raincoat
    

    By line

    This doesn't require matching anything specific in the function. That's both good and bad - it's easy to implement, but it can break with even simple changes to the base function:

    # split the code into an array of lines (f)
    lines=("${(f)functions[plan]}")
    
    # add code in the middle
    newCode=(
        ${lines[1]}
        'if [[ $forecast == tornado ]]; then
            print "find shelter"
            print "go quickly"
            return
        fi'
        ${lines[2,-1]})
    
    # rejoin array with newlines (F)
    functions[plan]=${(F)newCode}
    
    functions plan
    #=> plan () {
    #=>     forecast=${1} 
    #=>     if [[ $forecast == tornado ]]
    #=>     then
    #=>         print "find shelter"
    #=>         print "go quickly"
    #=>         return
    #=>     fi
    #=>     if [[ $forecast == sun ]]
    #=>     then
    #=>         print ${(C):-"take sunscreen"}
    #=>         print ${(C):-"apply sunscreen"}
    #=>         print ${(C):-"wear hat"}
    #=>         print ${(C):-"use sunglasses"}
    #=>     elif [[ $forecast == rain ]]
    #=>     then
    #=>         print ${(C):-"take umbrella"}
    #=>         print ${(C):-"carry raincoat"}
    #=>     else
    #=>         print ${(C):-"stay home"}
    #=>         print ${(C):-"and relax"}
    #=>     fi
    #=> }
    
    plan tornado
    #=> find shelter
    #=> go quickly
    

    Using functions -c

    If the updated function can be based on a call to the initial function, then functions -c (in zsh 5.8 and later) can be used to create a copy to use in the new function:

    functions -c plan plan_orig
    plan() {
        print "Here's the list for $1:"
        plan_orig "$@"
    }
    
    plan rain
    #=> Here's the list for rain:
    #=> Take Umbrella
    #=> Carry Raincoat
    

    There is more information about modifying functions in this answer.