6

This question already has an answer here:

I have a function in my .bashrc to automatically sudo to open files that aren't writable by me:

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

When the file needs sudo, it works fine. When it doesn't, it recursively calls this function and uses 100% of 1 CPU until I C-C.

From this answer I see there are a few options, all of which I've tried. One actually works:

'vim' "$1" #fails
\vim "$1" #fails
command vim "$1" #Works!

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

(I know this is a duplicate, but it was very hard to find my answer on SO/SE with the current question titles, so I wanted to post a question with a title that I and others could've found by google searching)

marked as duplicate by muru, G-Man, Anthony Geoghegan, Jeff Schaller, schily Jul 11 '18 at 12:27

This question has been asked before and already has an answer. If those answers do not fully address your question, please ask a new question.

7

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`
2

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.

1

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
      }

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