Bash 影子命令 - 与命令同名的函数

Bash 影子命令 - 与命令同名的函数

我有一个功能.bashrc可以自动 sudo 打开我不可写的文件:

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

当文件需要 sudo 时,它可以正常工作。如果没有,它会递归调用此函数并使用 1 个 CPU 的 100%,直到 I CC。

从这个答案我看到有几个选项,我都尝试过。一个实际上有效:

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

为什么其他选项不能像我期望的那样工作?

(我知道这是重复的,但是很难用当前的问题标题在 SO/SE 上找到我的答案,所以我想发布一个带有我和其他人可以通过谷歌搜索找到的标题的问题)

答案1

别名前面的任何字符都会阻止别名触发:

alias ls='ls -la'
ls foo.txt     #will run ls -la foo.txt
\ls foo.txt    #will run ls foo.txt
'ls' foo.txt   #will run ls foo.txt
'ls foo.txt'   #will run ls foo.txt

但是,这不会停止函数,command如果您创建同名的函数,则需要引用底层内置函数。

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`

解释

问题是前两个选项只能处理别名。它们不是特殊的重定向运算符,可以感知您是否有同名的函数或命令。他们所做的就是在别名前面放置一些内容,因为只有当别名是管道或命令的第一部分时才会扩展

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 只是尝试使用它知道的别名列表(您可以使用 获得alias)来扩展命令的第一个单词。别名不接受参数,并且只能是第一个单词(空格分隔;vim x.txt 惯于扩展为sudo vimim x.txt在命令中使用上面的别名来正确扩展。

但是,单引号中的内容永远不会发生扩展:echo '$USER'将打印出文字$USER而不是变量所代表的内容。另外,bash 扩展\xx(大部分)。这些并不是专门为了转义别名而额外添加的方法,它们只是 bash 扩展编写方式的一部分。因此,您可以使用这些方法让别名隐藏命令,但仍然可以访问实际命令。

答案2

为什么其他选项不能像我期望的那样工作?

因为命令行的执行方式有一些细节。

基本概念是命令行的第一个单词是 shell 将搜索并尝试执行的命令,在此命令:

  1. 命令行按元字符(大部分)进行分割:

    元字符
    未加引号时分隔单词的字符。以下之一:
    | &; ( ) < > 空格 制表符 换行符

  2. 如果第一个未引用的word 匹配一个别名单词,它被别名定义替换。为了避免循环,仅执行此操作一次如果新的第一个单词与正在扩展的别名匹配。这使得这个工作没有循环:

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

    请注意: 如果别名值的最后一个字符为空白,则还会检查别名后面的下一个命令字是否有别名扩展。

  3. 如果(生成的)第一个单词是扩展(如 a $var),则它会被替换(如果未引用,则受 IFS 分词(和通配)影响):

    $ var='echo -e '
    $ $var test '\twords'            # will expand to `echo -e test words`
    test    words
    
  4. 上述扩展后(可选变量赋值后)生成的第一个单词将按以下顺序搜索:

    • 特殊内置函数(仅在 POSIX 模式下)
    • 功能
    • 内置函数
    • 散列命令(阅读帮助散列)
    • 外部命令(在$PATH目录中搜索)

    将执行找到的第一个匹配项。

顺序可以通过以下方式更改(用于测试):

  • 要绕过别名,请使用\test或任何其他类型的扩展。
  • 要忽略函数和别名,请使用command test.
  • 要执行内置函数,请使用builtin test.
  • 要执行外部应用程序,请使用显式路径:/bin/test

因此,函数定义为:

$ ls(){ ls; }

将在无限循环中调用。也是调用相同第一个单词的脚本。就像是:

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

将在循环中重新执行相同的脚本,直到内核崩溃(如果正在使用的内核对连续调用脚本有限制)。

该顺序将通过运行显示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

该函数(如问题中所定义)只会绕过别名,\vim但不会绕过该函数。要绕过别名和函数,请使用命令。这个函数应该可以满足您的需要:

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

答案3

如果您让函数调用vim二进制文件的完整路径,您将不会得到递归循环。另外,使用-Hsudo 选项设置 $HOME。

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

谢谢。我没有想到这一点,但现在我.bashrc也有了它。


编辑: 更新了调用command vimsudo env而不是sudo -h.这样就没有循环可停留。

相关内容