Vim
Vim2Day 18

Vimと外部コマンドをつなぐ!コマンド(仮称)

この記事はVimのアドベントカレンダー(その2)の18日目の記事です。

Vimを使っているとき、
「あの外部コマンドの出力結果が欲しい」、
「この範囲をあの外部コマンドで処理したい」、
「Vimを抜けるのが面倒だがちょっと外部コマンドを実行したい」、
といったときは無いでしょうか。

そういったときに要望を叶える機能がVimにはあります。
それが!コマンド(仮称)1です。

この記事ではこの!コマンドについて紹介します。

!がつく4つのコマンド

:!

http://vim-jp.org/vimdoc-ja/various.html#:!cmd

外部コマンドを実行するコマンドです。
mkdirfirefoxといった単体で実行する外部コマンドを実行したいときに使います。

" cssというディレクトリを作成する
:!mkdir css

" 今開いているファイルをFirefoxで開く(%は後述する特殊文字で今開いてるファイルに置き換えられます)
:!firefox %

よく使う外部コマンドがあるなら以下のような設定をvimrcに一時的に書くと楽かもしれません。

" F5で今開いているJavaのプログラムをコンパイルし実行する
nnoremap <F5> :<C-u>javac %; java %:r<CR>

" F6で今開いているJavaのプログラムをコンパイルする
nnoremap <F6> :<C-u>javac %<CR>

:{range}!

http://vim-jp.org/vimdoc-ja/change.html#filter

バッファの中身を外部コマンドで処理するコマンドです。
{range}には.%'<,'>などの範囲を指定します。
sedawkといったパイプで繋いで使うコマンドを使ってテキストを処理したい際に使います。

" カーソル行に書いた数式を計算結果に書き換える
:.!bc

" カーソル行の文字列をbase64に変換する
:.!ruby -r base64 -nle 'puts Base64.encode64($_)'

" 選択範囲にある全ての数字をその数字に1を足した数字へと書き換える
:'<,'>!perl -pe's/\d+/$&+1/ge'

" 管理者権限を得て現在のファイルに書き込みを行う
:%!sudo tee %

範囲は直接指定してもいいのですが、このコマンドに入るためのキーマップが用意されているのでそれらを使うとより気軽にフィルタコマンドを利用できると思います。

モード キーマップ 内容
ノーマルモード !{motion} カーソル行から{motion}で指定した範囲までを外部コマンドで処理する
ノーマルモード !! カーソル行を外部コマンドで処理する
ビジュアルモード ! 選択範囲を外部コマンドで処理する

またコマンドはシェルによって処理されるため、パイプやリダイレクト、コマンド置換、プロセス置換といった機能も使うことが可能で、以下のようなことも可能です。

" httpbinで取得したuuidを書き出す
:.!curl -s httpbin.org/uuid | jq -r .uuid

" カスミガセキ・ジグラットの階数を書き出す
:.!seq 700 | awk '\!/4|9/' | wc -l

" ファイル内の頻出行トップ10を書き出す
:%!sort | uniq -c | sort -rn | head -10 | sed 's/^ *[0-9] //'

:r!

http://vim-jp.org/vimdoc-ja/insert.html#:r!

外部コマンドの出力結果をバッファに読み込むコマンドです。

datemanなどの入力無しで扱うコマンドの出力結果が欲しいときには使います。

" 今日の日付を読み込む
:r!date +%Y-%m-%d

" curlのマニュアルを読み込む
:r!man curl

" 自身のグローバルIPアドレスを取得する
:r!curl -s ifconfig.io

:w !

http://vim-jp.org/vimdoc-ja/editing.html#:w_c

外部コマンドの標準入力にバッファの中身を流し込んで実行するコマンドです。
w!を繋げてはだめで:w !といったように間に空白を挟む必要がある点に注意(例えば:w!shとすると今開いているファイルがshという名前で保存されます)

bashperlなど標準入力から読んだスクリプトを実行できるコマンドを実行する際に使います。
こちらを改めて使わずとも:!:{range}!で済むケースも多いかもですが。

" 今開いているファイルに書いたシェルスクリプトを実行する
:w !sh

" 今開いているファイルに書いたRubyのスクリプトを実行する
:w !perl

特殊文字

http://vim-jp.org/vimdoc-ja/cmdline.html#cmdline-special

!コマンドを使う際に便利なもの……でかつ、気をつけないといけないものとして、特殊文字というものがあります。

特殊文字は、%:rで拡張子を省いたファイル名が得られたり、%:p:hでファイルのあるディレクトリの絶対パスが得られたりと便利なものなのですが、
引数のどこかで%#を使ってハマることのほうが多い気がします。
何か実行結果がおかしいと思ったら以下の文字を疑ってみてください。

特殊文字 展開内容
% カレントファイル名を展開します
# 代替ファイル名を展開します
! 前回実行したコマンドを展開します
<なになに> なになににはcwordcWORDが入り、それによって展開内容が変わります

いずれも以下のようにバックスラッシュ(Windowsだと円マーク)を文字の前につけることでエスケープできます。

文字 展開内容
% カレントファイル名
\% %
\\% \%
\<cword> <cword>

また、エスケープせずに特殊文字を意図して使う場合、今度は%<cfile>をそのまま使うとシェルのメタ文字がエスケープされない点に注意。
基本的には%:S<cfile>:Sといったように、特殊文字の末尾に:Sをつければ大丈夫ですが、
<cword><cWORD>につけても:Sが無視される、シェルがfishのようにエスケープの仕方が違うシェルだといけない、などあるので、
エッジケースをカバーしないといけない場面では他の手段を利用したほうがいいと思います。
逆にいえば展開する内容が分かっていて、手打ちで使うだけ、vimrcの中で定義した関数の中で使うだけ、という感じであれば付けても付けなくても。

" カーソル上にあるファイル名のファイルを作成する
:!touch <cfile>:S

" 今開いているファイルを削除する
:!rm %:p:S

外部コマンドを扱う他の手段

!コマンドはどちらかというとユーザが直接使うのに向いていて、
Vim scriptの中で使う分には不都合な部分があります(DOS窓が出る、入出力にバッファが必要、同期的に実行してしまう、など)

そのため、Vimでは外部コマンドを利用するための手段が別途用意されています。
Vim scriptを書く際には以下の機能を利用することになると思います。

http://vim-jp.org/vimdoc-ja/eval.html#system()
http://vim-jp.org/vimdoc-ja/channel.html#job

また、Vimで作業するより、シェルで作業するほうがやりやすい場合もあるでしょう。
その際には以下の機能を利用することになると思います。

http://vim-jp.org/vimdoc-ja/starting.html#CTRL-Z
http://vim-jp.org/vimdoc-ja/various.html#:sh
http://vim-jp.org/vimdoc-ja/terminal.html#:terminal

全部を使わないといけないということはなくて、目的にあった、自分の作業の仕方にあったものを使えば大丈夫です。
何かしら不満が出てきたら上記の機能を試してみるぐらいがいいのかなと。

余談

!コマンドはVimに限らずedやviでも使えます。

http://pubs.opengroup.org/onlinepubs/9699919799/utilities/ed.html
http://pubs.opengroup.org/onlinepubs/9699919799/utilities/vi.html

Vimは無いがviとPerlはある……という場面がもしあれば重宝すると思います。


  1. 仮称なのは、その、このコマンド群の名称を見つけられなかったため……(もし知っている方がいたら教えてほしいです)