
Neovim小ネタ - バッファを開き直さずにsudo権限で保存する
/etc/nginx/nginx.conf を編集して、いざ保存しようとしたら...
E45: 'readonly' option is set (add ! to override)
あるあるですね。
sudoで開き直すのは面倒。編集した内容が消えてしまう。
今回は、編集中のバッファをそのままsudo権限で保存する方法を紹介します。
Vimでの解決策:sudo.vim
Vimを使っていた頃は、sudo.vim という便利なプラグインがありました。
:w sudo:%
これだけで、現在のバッファをsudo権限で保存できました。
しかし、Neovimではこのプラグインが動作しません。
なぜNeovimではsudo.vimが使えないのか
理由は、Neovimがtty(端末)を持たない設計だからです。
ttyとは
tty(TeleTYpewriter)は、ユーザーとシステムの間で入出力を行う端末デバイスです。
# 現在のttyを確認
$ tty
/dev/ttys001
ターミナルでコマンドを実行するとき、キーボード入力やパスワードの入力は、このttyを通じて行われます。
sudoとttyの関係
sudo コマンドは、パスワードの入力を求める際にttyを使用します。
$ sudo cat /etc/shadow
Password: ▌ # ← ttyからパスワードを読み取る
これはセキュリティ上の理由です。パスワードをパイプやファイルから読み取ると、コマンド履歴やプロセスリストにパスワードが残る可能性があります。
Neovimとttyの問題
Vimは内部でシェルを起動する際、親プロセスのttyを継承します。そのため、:!sudo ... のようなコマンドでパスワード入力が可能でした。
一方、Neovimは非同期処理を重視した設計になっており、内部のジョブ(:! や system())はttyを持ちません。
-- Neovimで実行すると...
vim.fn.system('sudo cat /etc/shadow')
-- → sudo: a terminal is required to read the password
このため、sudo.vimのような「内部でsudoを呼び出す」プラグインは動作しないのです。
解決策:sudo teeを使う
ttyがなくても動作する方法があります。sudo tee を使う方法です。
基本的な考え方
# バッファの内容を標準入力としてsudo teeに渡す
echo "ファイルの内容" | sudo tee /path/to/file > /dev/null
tee コマンドは、標準入力を受け取ってファイルに書き込みます。この方法なら、対話的なパスワード入力は不要です。
ただし、sudoersで NOPASSWD を設定しているか、事前に sudo -v でパスワードをキャッシュしておく必要があります。
Neovimでの実装
以下の設定を init.lua または設定ファイルに追加します。
-------------------------------------------------------------------------------
-- Sudo で保存(パスワードなし版)
-- sudoers で NOPASSWD が設定されている前提
-------------------------------------------------------------------------------
-- SudoWrite コマンド
vim.api.nvim_create_user_command('SudoWrite', function()
local filepath = vim.fn.expand('%:p')
local cmd = string.format('sudo tee %s > /dev/null', vim.fn.shellescape(filepath))
-- バッファの内容を sudo tee に渡す
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
local content = table.concat(lines, '\n')
local result = vim.fn.system(cmd, content .. '\n')
-- 結果を確認
if vim.v.shell_error == 0 then
vim.bo.modified = false
vim.notify('File saved with sudo', vim.log.levels.INFO)
else
vim.notify('Failed to save: ' .. result, vim.log.levels.ERROR)
end
end, { desc = 'Write file with sudo (NOPASSWD)' })
-- <Space>r で sudo 保存
vim.keymap.set('n', '<Space>r', ':SudoWrite<CR>', {
noremap = true,
silent = false,
desc = 'Sudo write current file'
})
コードの解説
1. ファイルパスの取得
local filepath = vim.fn.expand('%:p')
%:p で現在のバッファのフルパスを取得します。
2. コマンドの組み立て
local cmd = string.format('sudo tee %s > /dev/null', vim.fn.shellescape(filepath))
shellescape() でファイルパスをエスケープし、シェルインジェクションを防ぎます。
3. バッファ内容の取得と送信
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
local content = table.concat(lines, '\n')
local result = vim.fn.system(cmd, content .. '\n')
nvim_buf_get_lines()で全行を取得- 改行で結合してコンテンツを作成
system()の第2引数で標準入力として渡す
4. 結果の処理
if vim.v.shell_error == 0 then
vim.bo.modified = false
vim.notify('File saved with sudo', vim.log.levels.INFO)
else
vim.notify('Failed to save: ' .. result, vim.log.levels.ERROR)
end
- 成功したら
modifiedフラグをクリア(未保存マークを消す) - エラーがあれば通知
使い方
コマンドで実行
:SudoWrite
キーマップで実行
<Space>r を押すだけ。
Press: <Space>r
→ "File saved with sudo"
前提条件:sudoersの設定
この方法は、NOPASSWD が設定されているか、直前に sudo -v でパスワードをキャッシュしている必要があります。
NOPASSWDの設定例
sudo visudo
# 特定のユーザーに対してteeのみNOPASSWD
username ALL=(ALL) NOPASSWD: /usr/bin/tee
# または全コマンドに対して(セキュリティに注意)
username ALL=(ALL) NOPASSWD: ALL
一時的なキャッシュ
# 事前にパスワードを入力しておく
sudo -v
# その後15分間(デフォルト)はパスワード不要
注意点
セキュリティ
NOPASSWD を設定する場合は、セキュリティリスクを理解した上で行ってください。ローカル開発環境では便利ですが、サーバー環境では慎重に。
新規ファイル
この方法は、既存ファイルの上書きを想定しています。新規ファイルを作成する場合は、ディレクトリに書き込み権限があるか確認してください。
バックアップ
設定ファイルを編集する前に、バックアップを取ることをおすすめします。
sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak
まとめ
- Vimの
sudo.vimはNeovimでは動作しない(ttyの問題) sudo teeを使えば、プラグインなしでsudo保存が可能- カスタムコマンドとキーマップで快適に操作できる
地味だけど、一度設定しておくと本当に便利と思う機会がvimでは多い気がしますね。