Bash shadow a command - function with same name as command

The problem is that the first two options are designed only to deal with aliases. They are not special redirecting operators that can sense if you have a function or command with the same name. All they do is prevent expansion, which is what an alias tries to do

alias v='sudo vim'
v x.txt
#automatically expands "v" to "sudo vim" and then does the rest of the command
# v x.txt ==> sudo vim x.txt

Bash just tries to expand the first word of a command using the list of aliases it knows about (which you can get with alias). Aliases don't take arguments, and can only be the first word (space-separated; vim x.txt won't expand into sudo vimim x.txt using the alias above) in a command to expand properly.

However, expansion never happens to things in single quotes: echo '$USER' will print out a literal $USER and not what the variable stands for. Also, bash expands \x to be x (mostly). These aren't extra added-in ways specifically to escape an alias, they're just part of how bash expansion was written. Thus, you can use these methods to let an alias shadow a command and still have access to the actual command.

alias ls='ls -la'
ls foo.txt     #will expand into ls -la foo.txt
\ls foo.txt    #will expand into ls foo.txt
'ls' foo.txt   #same as above
'ls foo.txt'   #same as above

However, they don't stop functions. A function doesn't need expansion to work, it is called with its name.

ls () {
    echo "not an alias"
}
alias ls='echo "an alias"'

ls foo.txt          #will echo "an alias"
\ls foo.txt         #will echo "not an alias"
'ls' foo.txt        #will echo "not an alias"
command ls foo.txt  #will actually run `ls`

Why do the other options not work as I would expect them to?

Because there are some details in the way a command line gets executed.

The basic concept is that the first word of a command line is the command that the shell will search and try to execute, in this order:

  1. The command line is split on metacharacters (mostly):

    metacharacters
    A character that, when unquoted, separates words. One of the following:
    | & ; ( ) < > space tab newline

  2. If the first unquoted word match an alias word, it is replaced by the alias definition. To avoid loops, this is executed only once if the new first word match an alias being expanded. This makes this work without loops:

    $ alias ls='ls -la'
    $ ls dir               # will expand to `ls -la dir`
    

    Note that: If the last character of the alias value is a blank, then the next command word following the alias is also checked for alias expansion.

  3. If the (resulting) first word is an expansion (like a $var), it is replaced (and subject to IFS word splitting (and globing) if not quoted):

    $ var='echo -e '
    $ $var test '\twords'            # will expand to `echo -e test words`
    test    words
    
  4. The resulting first word after above expansions (after optional variable assignments) will be searched in this order:

    • special builtins (only in POSIX mode)
    • functions
    • builtins
    • hashed commands (read help hash)
    • external commands (searched in $PATH dirs)

    The first match found will be executed.

The order may be changed by (for test):

  • To bypass just alias, use \test or any other kind of expansion.
  • To ignore functions and aliases, use command test.
  • To execute a builtin, use builtin test.
  • To execute an external application, use an explicit path: /bin/test.

So, a function defined as:

$ ls(){ ls; }

Will be called in an infinite loop. As also an script which calls the same first word. Something like:

#!/bin/bash
$0 "$@"

Will re-execute the same script in a loop until the kernel breaks out (if the kernel in use has a limit of successive calls to scripts).

The order will be shown by running type -a:

$ test(){ echo test function; }
$ alias test=test
$ type -a
test is aliased to `test'
test is a function
test ()
{
    echo test function
}
test is a shell builtin
test is /usr/bin/test

The function (as defined in the question) will only bypass the alias with \vim but not the function. To bypass alias and functions, use command. This function should do what you need:

vim() {
        if [ -w "$1" ]; then
            command vim "$1"
        else
            sudo HOME="$HOME" bash -c 'command vim "$@"' _ -u ~/.vimrc "$1"
        fi
      }

If you have the function call the full path to the vim binary, you won't get the recursive loop. Also, using the -H option to sudo sets $HOME.

vim() {
    if [ -w "$1" ]; then
        command vim "$1"
    else
        sudo env HOME="$HOME" \vim -u ~/.vimrc "$1"
    fi
}

Thanks. I hadn't thought about this, but now I have it in my .bashrc, too.


EDITED: updated calling command vim and sudo env instead of sudo -h. this way there is no loop to stay in.