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

この記事は[Vimのアドベントカレンダー(その2)](https://qiita.com/advent-calendar/2017/vim2)の18日目の記事です。

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

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

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


# !がつく4つのコマンド

## :!

[http://vim-jp.org/vimdoc-ja/various.html#:!cmd](http://vim-jp.org/vimdoc-ja/various.html#:!cmd)

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

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

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

よく使う外部コマンドがあるのであれば、以下のような設定をvimrcに書くと作業がしやすくなります。

```vim
" 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](http://vim-jp.org/vimdoc-ja/change.html#filter)

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

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

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

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

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

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

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

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

```vim
" 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!](http://vim-jp.org/vimdoc-ja/insert.html#:r!)

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

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

```vim
" 今日の日付を読み込む
: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](http://vim-jp.org/vimdoc-ja/editing.html#:w_c)

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

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

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

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

# 特殊文字

[http://vim-jp.org/vimdoc-ja/cmdline.html#cmdline-special](http://vim-jp.org/vimdoc-ja/cmdline.html#cmdline-special)

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

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

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


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

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


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

```vim
" カーソル上にあるファイル名のファイルを作成する
:!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/eval.html#system())
[http://vim-jp.org/vimdoc-ja/channel.html#job](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/starting.html#CTRL-Z)
[http://vim-jp.org/vimdoc-ja/various.html#:sh](http://vim-jp.org/vimdoc-ja/various.html#:sh)
[http://vim-jp.org/vimdoc-ja/terminal.html#:terminal](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]: 仮称なのは、その、このコマンド群の名称を見つけられなかったため……(もし知っている方がいたら教えてほしいです)