Neovim小ネタ - バッファを開き直さずにsudo権限で保存する

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では多い気がしますね。


参考