標籤:blog http 使用 os strong io 檔案 資料
在Linux上工作的朋友很可能遇到過這樣一種情況,當你用Vim編輯完一個檔案時,運行:wq
儲存退出,突然蹦出一個錯誤:
E45: ‘readonly‘ option is set (add ! to override)
這表明檔案是唯讀,按照提示,加上!
強制儲存::w!
,結果又一個錯誤出現:
"readonly-file-name" E212: Can‘t open file for writing
檔案明明存在,為何提示無法開啟?這錯誤又代表什麼呢?查看文檔:help E212
:
For some reason the file you are writing to cannot be created or overwritten.The reason could be that you do not have permission to write in the directoryor the file name is not valid.
原來是可能沒有許可權造成的。此時你才想起,這個檔案需要root許可權才能編輯,而當前登陸的只是普通使用者,在編輯之前你忘了使用sudo
來啟動Vim,所以才儲存失敗。於是為了防止修改丟失,你只好先把它儲存為另外一個臨時檔案temp-file-name
,然後退出Vim,再運行sudo mv temp-file-name readonly-file-name
覆蓋原檔案。
目錄
- 解決方案
- Vim中執行外部命令
- 命令的另一種表示形式
- %的意義
- tee的作用
- 命令執行之後
- 更簡單的方案:映射
- 另一種思路
- 寫在結尾
但這樣操作過於繁瑣。而且如果只是想暫存此檔案,還需要接著修改,則希望保留Vim的工作狀態,比如編輯曆史,buffer狀態等等,該怎麼辦?能不能在不退出Vim的情況下獲得root許可權來儲存這個檔案?
解決方案
答案是可以,執行這樣一條命令即可:
:w !sudo tee %
接下來我們來分析這個命令為什麼可以工作。首先查看文檔: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} isexecuted like with ":!{cmd}", any ‘!‘ is replaced withthe 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中執行外部命令
接下來是一個歎號!
,它表示其後面部分是外部命令,即sudo tee %
。文檔中說的很清楚,這和直接執行:!{cmd}
是一樣的效果。後者的作用是開啟shell執行一個命令,比如,運行:!ls
,會顯示當前工作目錄下的所有檔案,這非常有用,任何可以在shell中執行的命令都可以在不退出Vim的情況下運行,並且可以將結果讀入到Vim中來。試想,如果你要在Vim中插入當前工作路徑或者當前工作路徑下的所有檔案名稱,你可以運行:
:r !pwd或:r !ls
此時所有的內容便被讀入至Vim,而不需要退出Vim,執行命令,然後拷貝粘貼至Vim中。有了它,Vim可以自由的操作shell而無需退出。
命令的另一種表示形式
再看前面的文檔:
Execute {cmd} with [range] lines as standard input
所以實際上這個:w
並未真的儲存當前檔案,就像執行:w new-file-name
時,它將當前檔案的內容儲存到另外一個new-file-name
的檔案中,在這裡它相當於一個另存新檔,而不是儲存。它將當前文檔的內容寫到後面cmd
的標準輸入中,再來執行cmd
,所以整個命令可以轉換為一個具有相同功能的普通shell命令:
$ cat readonly-file-name | sudo tee %
這樣看起來”正常”些了。其中sudo
很好理解,意為切換至root執行後面的命令,tee
和%
是什麼呢?
%的意義
我們先來看%
,執行:help cmdline-special
可以看到:
In Ex commands, at places where a file name can be used, the followingcharacters have a special meaning. These can also be used in the expressionfunction expand() |expand()|.%Is replaced with the current file name. *:_%* *c_%*
在執行外部命令時,%
會擴充成當前檔案名稱,所以上述的cmd
也就成了sudo tee readonly-file-name
。此時整個命令即:
$ cat readonly-file-name | sudo tee readonly-file-name
注意:在另外一個地方我們也經常用到%
,沒錯,替換。但是那裡%
的作用不一樣,執行:help :%
查看文檔:
Line numbers may be specified with:*:range* *E14* *{address}*{number}an absolute line number...%equal to 1,$ (the entire file) *:%*
在替換中,%
的意義是代表整個檔案,而不是檔案名稱。所以對於命令:%s/old/new/g
,它表示的是替換整篇文檔中的old為new,而不是把檔案名稱中的old換成new。
tee的作用
現在只剩一個痛點: tee。它究竟有何用?維基百科上對其有一個詳細的解釋,你也可以查看man page。下面這幅圖很形象的展示了tee
是如何工作的:
ls -l
的輸出經過管道傳給了tee
,後者做了兩件事,首先拷貝一份資料到檔案file.txt
,同時再拷貝一份到其標準輸出。資料再次經過管道傳給less
的標準輸入,所以它在不影響原有管道的基礎上對資料作了一份拷貝並儲存到檔案中。看中間部分,它很像大寫的字母T,給資料流動增加了一個分支,tee
的名字也由此而來。
現在上面的命令就容易理解了,tee
將其標準輸入中的內容寫到了readonly-file-name
中,從而達到了更新唯讀檔案的目的。當然這裡其實還有另外一半資料:tee
的標準輸出,但因為後面沒有跟其它的命令,所以這份輸出相當於被拋棄。當然也可以在後面補上> /dev/null
,以顯式的丟棄標準輸出,但是這對整個操作沒有影響,而且會增加輸入的字元數,因此只需上述命令即可。
命令執行之後
運行完上述命令後,會出現下面的提示:
W12: Warning: File "readonly-file-name" has changed and the buffer was changed in Vim as wellSee ":help W12" for more info.[O]K, (L)oad File:
Vim提示檔案更新,詢問是確認還是重新負載檔案。建議直接輸入O
,因為這樣可以保留Vim的工作狀態,比如編輯曆史,buffer等,撤消等操作仍然可以繼續。而如果選擇L
,檔案會以全新的檔案開啟,所有的工作狀態便丟失了,此時無法執行撤消,buffer中的內容也被清空。
更簡單的方案:映射
上述方式非常完美的解決了文章開始提出的問題,但畢竟命令還是有些長,為了避免每次輸入一長串的命令,可以將它映射為一個簡單的命令加到.vimrc
中:
1 " Allow saving of files as sudo when I forgot to start vim using sudo.2 cmap w!! w !sudo tee > /dev/null %
這樣,簡單的運行:w!!
即可。命令後半部分> /dev/null
在前面已經解釋過,作用為顯式的丟掉標準輸出的內容。
另一種思路
至此,一個比較完美但很tricky的方案已經完成。你可能會問,為什麼不用下面這樣更常見的命令呢?這不是更容易理解,更簡單一些嗎?
:w !sudo cat > %
重新導向的問題
我們來分析一遍,像前面一樣,它可以被轉換為相同功能的shell命令:
$ cat readonly-file-name | sudo cat > %
這條命令看起來一點問題沒有,可一旦運行,又會出現另外一個錯誤:
/bin/sh: readonly-file-name: Permission deniedshell returned 1
這是怎麼回事?不是明明加了sudo
麼,為什麼還提示說沒有許可權?稍安勿躁,原因在於重新導向,它是由shell執行的,在一切命令開始之前,shell便會執行重新導向操作,所以重新導向並未受sudo
影響,而當前的shell本身也是以普通使用者身份啟動,也沒有許可權寫此檔案,因此便有了上面的錯誤。
重新導向方案
這裡介紹了幾種解決重新導向無許可權錯誤的方法,當然除了tee
方案以外,還有一種比較方便的方案:以sudo
開啟一個shell,然後在該具有root許可權的shell中執行含重新導向的命令,如:
:w !sudo sh -c ‘cat > %‘
可是這樣執行時,由於單引號的存在,所以在Vim中%
並不會展開,它被原封不動的傳給了shell,而在shell中,一個單獨的%
相當於nil
,所以檔案被重新導向到了nil
,所有內容丟失,儲存檔案失敗。
既然是由於%
沒有展開導致的錯誤,那麼試著將單引號‘
換成雙引號"
再試一次:
:w !sudo sh -c "cat > %"
成功!這是因為在將命令傳到shell去之前,%
已經被擴充為當前的檔案名稱。有關單引號和雙引號的區別可以參考這裡,簡單的說就是單引號會將其內部的內容原封不動的傳給命令,但是雙引號會展開一些內容,比如變數,逸出字元等。
當然,也可以像前面一樣將它映射為一個簡單的命令並添加到.vimrc中:
1 " Allow saving of files as sudo when I forgot to start vim using sudo.2 cmap w!! w !sudo sh -c "cat > %"
注意:這裡不再需要把輸出重新導向到/dev/null
中。
寫在結尾
至此,藉助Vim強大的靈活性,實現了兩種方案,可以在以普通使用者啟動的Vim中儲存需root許可權的檔案。兩者的原理類似,都是利用了Vim可以執行外部命令這一特性,區別在於使用不同的shell命令
轉貼,原文地址:feihu.me/blog/2014/vim-write-read-only-file/