Linux环境下linux虚拟机,倘若直接使用VI/VIM命令编辑没有更改权限的文件时,保存的时侯才会提示用户难以进行保存操作,通常的解决方式只能是关掉文件重新以sudo权限打开该文件编辑后再保存(前提是用户具有sudo权限)。虽然,在VI/VIM模式下通过一些简单的命令,能够在不关掉当前文件的情况下达到保存文件的目的。

方式一

关于%!sudotee%>/dev/null这条命令的说明如下

此命令是把当前文件(即%)作为stdin传给sudotee命令来执行。

方式二

在Linux上工作的同事很可能碰到过这样一种情况,当你用Vim编辑完一个文件时,运行:wq保存退出,忽然蹦出一个错误:

E45:'readonly'optionisset(add!tooverride)

这表明文件是只读的,根据提示,加上!强制保存::w!,结果又一个错误出现:

“readonly-file-name”E212:Can'topenfileforwriting

文件明明存在,为什么提示未能打开?这错误又代表哪些呢?查看文档:helpE212:

Forsomereasonthefileyouarewritingtocannotbecreatedoroverwritten.

Thereasoncouldbethatyoudonothavepermissiontowriteinthedirectory

orthefilenameisnotvalid.

原先是可能没有权限导致的。此时你才想起,这个文件须要root权限能够编辑,而当前登录的只是普通用户,在编辑之前你忘了使用sudo来启动Vim,所以才保存失败。于是为了避免更改遗失,你只得先把它保存为另外一个临时文件temp-file-name,之后退出Vim,再运行sudomvtemp-file-namereadonly-file-name覆盖原文件。

但这样操作过分冗长。并且假如只是想暂存此文件,还须要接着更改,则希望保留Vim的工作状态,例如编辑历史,buffer状态等等,该如何办?能不能在不退出Vim的情况下获得root权限来保存这个文件?

保存命令是什么_保存命令的快捷键是什么_linux vi保存命令

解决方案

答案是可以,执行这样一条命令即可:

:w!sudotee%

接出来我们来剖析这个命令为何可以工作。首先查看文档:help:w,向上滚动一点可以看见:

	*: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 |:!|.
The default [range] for the ":w" command is the whole buffer (1,$)

把这个使用方式对应上面的命令,如下所示:

:       w               !sudo tee %
|       |               |  |
:[range]w[rite] [++opt] !{cmd}

我们并未指定range,参见帮助文档最下边一行,当range未指定时,默认情况下是整个文件。据悉,这儿也没有指定opt。

Vim中执行外部命令

接出来是一个感叹号!,它表示其前面部份是外部命令,即sudotee%。文档中说的很清楚,这和直接执行:!{cmd}是一样的疗效。前者的作用是打开shell执行一个命令,例如,运行:!ls,会显示当前工作目录下的所有文件,这十分有用,任何可以在shell中执行的命令都可以在不退出Vim的情况下运行,而且可以将结果读入到Vim中来。试想,假如你要在Vim中插入当前工作路径或则当前工作路径下的所有文件名,你可以运行:

:r!pwd或:r!ls

此时所有的内容便被读入至Vim,而不须要退出Vim,执行命令,之后拷贝粘贴至Vim中。有了它,Vim可以自由的操作shell而无需退出。

命令的另一种表示方式

再看后面的文档:

Execute{cmd}with[range]linesasstandardinput

所以实际上这个:w并未真的保存当前文件,如同执行:wnew-file-name时,它将当前文件的内容保存到另外一个new-file-name的文件中,在这儿它相当于一个另存为,而不是保存。它将当前文档的内容讲到前面cmd的标准输入中,再来执行cmd,所以整个命令可以转换为一个具有相同功能的普通shell命令:

$catreadonly-file-name|sudotee%

这样看上去”正常”些了。其中sudo挺好理解,意为切换至root执行前面的命令,tee和%是哪些呢?

%的意义

我们先来看%,执行:helpcmdline-special可以看见:

InExcommands,atplaceswhereafilenamecanbeused,thefollowing

charactershaveaspecialmeaning.Thesecanalsobeusedintheexpression

functionexpand()|expand()|.

%Isreplacedwiththecurrentfilename.*:_%**c_%*

在执行外部命令时,%会扩充成当前文件名,所以上述的cmd也就成了sudoteereadonly-file-name。此时整个命令即:

$catreadonly-file-name|sudoteereadonly-file-name

注意:在另外一个地方我们也常常用到%,没错,替换。并且哪里%的作用不一样,执行:help:%查看文档:

Linenumbersmaybespecifiedwith:*:range**E14**{address}*

{number}anabsolutelinenumber

%equalto1,$(theentirefile)*:%*

在替换中,%的意义是代表整个文件,而不是文件名。所以对于命令:%s/old/new/g,它表示的是替换每篇文档中的old为new,而不是把文件名中的old换成new。

tee的作用

现今只剩一个难点:tee。它到底有何用?维基百科上对其有一个详尽的解释,你也可以查看manpage。下边这幅图很形象的展示了tee是怎样工作的:

ls-l的输出经过管线传给了tee,前者做了两件事,首先拷贝一份数据到文件file.txt,同时再拷贝一份到其标准输出。数据再度经过管线传给less的标准输入,所以它在不影响原有管线的基础上对数据作了一份拷贝并保存到文件中。看上图中间部份,它很像小写的字母T,给数据流动降低了一个分支,tee的名子也由此而至。

如今里面的命令就容易理解了,tee将其标准输入中的内容讲到了readonly-file-name中,进而达到了更新只读文件的目的。其实这儿也许还有另外一半数据:tee的标准输出,但由于前面没有跟其它的命令,所以这份输出相当于被抛弃。其实也可以在前面补上>/dev/null,以显式的遗弃标准输出,而且这对整个操作没有影响,但是会降低输入的字符数,因而只需上述命令即可。

命令执行以后

运行完上述命令后,会出现下边的提示:

W12:Warning:File”readonly-file-name”haschangedandthebufferwaschangedinVimaswell

See”:helpW12″formoreinfo.

[O]K,(L)oadFile:

Vim提示文件更新,寻问是确认还是重新加载文件。建议直接输入O,由于这样可以保留Vim的工作状态,例如编辑历史,buffer等,撤销等操作一直可以继续。而假如选择L,文件会以全新的文件打开,所有的工作状态便遗失了,此时未能执行撤销,buffer中的内容也被清空。

更简单的方案:映射

上述方法十分完美的解决了文章开始提出的问题,但其实命令还是有些长,为了防止每次输入一长串的命令,可以将它映射为一个简单的命令加到.vimrc中:

“AllowsavingoffilesassudowhenIforgottostartvimusingsudo.

cmapw!!w!sudotee>/dev/null%

这样,简单的运行:w!!即可。命令后半部份>/dev/null在上面早已解释过,作用为显式的扔掉标准输出的内容。

另一种思路

至此,一个比较完美但很tricky的方案早已完成。你可能会问linux删除命令,为何不用下边这样更常见的命令呢?这不是更容易理解,更简单一些么?

:w!sudocat>%

重定向的问题

我们来剖析一遍,像上面一样,它可以被转换为相同功能的shell命令:

$catreadonly-file-name|sudocat>%

这条命令看上去一点问题没有,可一旦运行,又会出现另外一个错误:

/bin/sh:readonly-file-name:Permissiondenied

shellreturned1

这是如何回事?不是明明加了sudo么,为何还提示说没有权限?稍安勿躁,缘由在于重定向,它是由shell执行的,在一切命令开始之前,shell便会执行重定向操作,所以重定向并未受sudo影响,而当前的shell本身也是以普通用户身分启动,也没有权限写此文件,因而便有了前面的错误。

重定向方案

这儿介绍了几种解决重定向无权限错误的方式,其实不仅tee方案以外linux vi保存命令,还有一种比较便捷的方案:以sudo打开一个shell,之后在该具有root权限的shell中执行含重定向的命令,如:

:w!sudosh-c'cat>%'

但是这样执行时,因为单冒号的存在,所以在Vim中%并不会展开,它被原封不动的传给了shell,而在shell中,一个单独的%相当于nil,所以文件被重定向到了nil,所有内容遗失,保存文件失败。

既然是因为%没有展开造成的错误,这么试着将单冒号'换成双冒号”再试一次:

:w!sudosh-c”cat>%”

成功!这是由于在将命令传到shell去之前,%早已被扩充为当前的文件名。有关单冒号和双冒号的区别可以参考这儿,简单的说就是单冒号会将其内部的内容原封不动的传给命令,而且双冒号会展开一些内容,例如变量,通配符字符等。

其实linux vi保存命令,也可以像上面一样将它映射为一个简单的命令并添加到.vimrc中:

“AllowsavingoffilesassudowhenIforgottostartvimusingsudo.

cmapw!!w!sudosh-c”cat>%”

注意:这儿不再须要把输出重定向到/dev/null中。

写在结尾

至此,利用Vim强悍的灵活性,实现了两种方案,可以在以普通用户启动的Vim中保存需root权限的文件。二者的原理类似,都是借助了Vim可以执行外部命令这一特点,区别在于使用不同的shell命令。假如你还有其它的方案,欢迎给我留言。

(全文完)

Author

这篇优质的内容由TA贡献而来

刘遄

《Linux就该这么学》书籍作者,RHCA认证架构师,教育学(计算机专业硕士)。

发表回复