Vim的”用sudo写”技巧是如何工作的?

How does the vim “write with sudo” trick work?

许多人可能已经看到允许您在需要根权限的文件上写入的命令,即使您忘记用sudo打开vim:

1
:w !sudo tee %

问题是我不知道这里到底发生了什么。

我已经想好了:w是为了这个

1
2
3
4
5
6
                                                        *:w_c* *:write_c*
:[range]w[rite] [++opt] !{cmd}
                        Execute {cmd} with [range] lines as standard input
                        (note the space in front of the '!').  {cmd} is
                        executed like with":!{cmd}", any '!' is replaced with
                        the previous command |:!|.

所以它将所有行作为标准输入传递。

!sudo tee部分使用管理员特权调用tee

为了让所有人理解,%应该输出文件名(作为tee的参数),但在帮助中找不到此行为的引用。

有人能帮我解剖这个命令吗?


:w !sudo tee %中…

%表示"当前文件"

正如尤金Y所指出的,%确实意味着"当前文件名"。VIM中的另一个用途是替换命令。例如,:%s/foo/bar的意思是"在当前文件中,用bar替换出现的foo",如果在键入:s之前突出显示一些文本,您会看到突出显示的行取代了%作为替换范围。

:w没有更新你的文件

这个技巧的一个令人困惑的部分是,您可能认为:w正在修改您的文件,但它不是。如果您打开并修改file1.txt,然后运行:w file2.txt,它将是一个"另存为";file1.txt不会被修改,但当前的缓冲区内容将被发送到file2.txt

您可以用shell命令代替file2.txt来接收缓冲区内容。例如,:w !cat只显示内容。

如果vim不是用sudo access运行的,那么它的:w不能修改受保护的文件,但是如果它将缓冲区内容传递给shell,那么shell中的命令可以用sudo运行。在这种情况下,我们使用tee

了解TEE

对于tee,将tee命令想象成正常bash管道情况下的T形管道:它将输出定向到指定的文件,并将其发送到标准输出,然后由下一个管道命令捕获。

例如,在ps -ax | tee processes.txt | grep 'foo'中,进程列表将写入一个文本文件并传递给grep

1
2
3
4
5
6
7
8
9
10
11
     +-----------+    tee     +------------+
     |           |  --------  |            |
     | ps -ax    |  --------  | grep 'foo' |
     |           |     ||     |            |
     +-----------+     ||     +------------+
                       ||  
               +---------------+
               |               |
               | processes.txt |
               |               |
               +---------------+

(使用asciiFlow创建的图表。)

有关更多信息,请参阅tee手册页。

三通

在您的问题描述的情况下,使用tee是一种黑客行为,因为我们忽略了它的一半功能。sudo tee写入我们的文件,并将缓冲区内容发送到标准输出,但我们忽略了标准输出。在本例中,我们不需要将任何内容传递给另一个管道命令;我们只是使用tee作为另一种编写文件的方法,这样我们就可以使用sudo来调用它。

让这个把戏简单点

你可以将这个添加到你的.vimrc中,使这个技巧易于使用:只需输入:w!!

1
2
" Allow saving of files as sudo when I forgot to start vim using sudo.
cmap w!! w !sudo tee > /dev/null %

> /dev/null部分明确地丢弃了标准输出,因为正如我所说,我们不需要将任何内容传递给另一个管道命令。


在执行的命令行中,%代表当前文件名。这在:help cmdline-special中有记录:

1
2
3
In Ex commands, at places where a file name can be used, the following
characters have a special meaning.
        %       Is replaced with the current file name.

正如您已经发现的,:w !cmd将当前缓冲区的内容传输到另一个命令。tee所做的是将标准输入复制到一个或多个文件,以及复制到标准输出。因此,:w !sudo tee % > /dev/null在作为根目录的同时,有效地将当前缓冲区的内容写入当前文件。另一个可用于此目的的命令是dd

1
:w !sudo dd of=% > /dev/null

作为快捷方式,您可以将此映射添加到您的.vimrc中:

1
2
" Force saving files that require root permission
cnoremap w!! w !sudo tee > /dev/null %

通过上述方法,您可以键入:w!!将文件保存为根文件。


这也很有效:

1
:w !sudo sh -c"cat > %"

这是受到@nathan long评论的启发。

注意事项:

必须使用"而不是',因为我们希望在传递给shell之前扩展%


:w—写一个文件。

!sudo—调用shell sudo命令。

tee—使用tee重定向的写入(vim:w)命令的输出。%只是当前文件名,即/etc/apache2/conf.d/mediawiki.conf。换句话说,tee命令作为根目录运行,它接受标准输入并将其写入由%表示的文件。但是,这将再次提示重新加载文件(单击l以加载VIM本身的更改):

教程链接


被接受的答案涵盖了这一切,所以我将给出另一个我用来记录的快捷方式的例子。

添加到你的etc/vim/vimrc~/.vimrc中:

  • cnoremap w!! execute 'silent! write !sudo tee % >/dev/null' edit!

在哪里?

  • cnoremap:告诉vim以下快捷方式将在命令行中关联。
  • w!!:捷径本身。
  • execute '...':执行以下字符串的命令。
  • silent!:安静地运行
  • write !sudo tee % >/dev/null:操作问题,在NULL中增加了消息的重定向,以生成一个干净的命令。
  • edit!:这个技巧是关键所在:它还调用edit命令重新加载缓冲区,然后避免缓冲区等消息发生更改。是如何在这里编写管道符号来分隔两个命令的。

希望它有帮助。其他问题请参见:

  • 超级用户:强制vim写入文件


我想建议另一种方法来解决"在打开我的文件时忘记写sudo"问题:

我发现,如果文件所有者是root,那么有条件的vim命令可以执行sudo vim,而不是接收permission denied,并且必须键入:w!!,这样做更为优雅。

这是很容易实现的(甚至可能有更优雅的实现,我显然不是bash专家):

1
2
3
4
5
6
7
8
function vim(){
  OWNER=$(stat -c '%U' $1)
  if [["$OWNER" =="root" ]]; then
    sudo /usr/bin/vim $*;
  else
    /usr/bin/vim $*;
  fi
}

而且效果很好。

这是一种比vim更以bash为中心的方法,因此并非每个人都喜欢它。

当然:

  • 有些用例会失败(当文件所有者不是root,但需要sudo,但函数可以编辑)
  • 使用vim只读取一个文件是没有意义的(就我而言,我使用tailcat只读取小文件)

但我发现这带来了更好的开发用户体验,这是imho在使用bash时容易忘记的。-)