読者です 読者をやめる 読者になる 読者になる

余白の書きなぐり

aueweのブログ

vimをパイプにする

Vim シェルスクリプト

この記事は Vim Advent Calendar 2016 (その2) の3日目の記事です。

ノーマルモード以外をパイプとして使う際の情報を追記しました (2016/12/04)

UNIXのテキスト処理

UNIXでテキストを自動整形する際、パイプ機能は欠かせない。

$ cat a.txt
1 hoge
2 piyo
3 fuga

$ cat a.txt |sed 's/piyo/foo/' |grep '2'
2 foo

シェル上で | というパイプ記号を使ってコマンドを次々繋げることで、複雑なテキスト処理をこなすわけだ。 パイプは便利だが、テキストエディタをパイプとして使う人はあまり見かけない。 テキストエディタ=対話的 という常識があるため、パイプのような自動処理とは相性が悪いと思われているのだろう。 しかし今日はあえて、シェルスクリプトワンライナーの中にvimを埋め込み、パイプとして静的に使ってみたい。

vimをパイプとして使う

vimを普通に使って冒頭の$ cat a.txt |sed 's/piyo/foo/'を行うには、ノーマルモードでjwCfooと入力すればよい。 このjwCfooという呪文をシェル上で使用するには以下のようにすれば良い。

# 最も普通の方法
$ cat a.txt |vim -es +'norm jwCfoo' +%p +q! /dev/stdin

# あるいは
$ cat a.txt |ex -s +'norm jwCfoo' +%p +q! /dev/stdin

# このやり方は Vim: Reading stdin... 問題が生じる
$ cat a.txt |vim - -es +'norm jwCfoo' +%p +q! |sed '1d'

とすればよい。一番上のワンライナーの説明をしておくと

  • vim -eexと等価で、vimを非対話(exモード)で起動するという意味。
  • -sはサイレントモードで起動し、標準出力を汚さないというオプション。
  • +'norm jwCfoo'normは、ノーマルモードのコマンドを使うという意味。
  • jwCfoovimの呪文。中にESCを入れるにはCtrl+vしてESCを押す。
  • +%p はファイルの全内容を標準出力に表示するexコマンド。
  • +q!vimを強制終了するexコマンド。馴染み深い。
  • /dev/stdinは読み込み先を標準入力 (cat a.txt) にするという意味。

三番目のワンライナー/dev/stdinの代わりにハイフン(標準入力のシンボル)を使っていて、スマートに見える。 ただしこの方法では、標準出力の冒頭にVim: Reading from stdin...というクッソうざい文字列が勝手に挿入される。 こいつを消すには、直後にシェルコマンド|sed '1d'等を噛ます必要がある。

外部コマンドにしよう

以下のようなvipeコマンドを作っておくと、シェル上でvimの呪文が使いやすくなる。

# .bashrcや.zshrcに書き込む
vipe () {
  COMMAND=$(echo "$*")
  # コロン':'でESC入力を代替する場合はコメントを外す。^[はCtrl+vしてESC押して入力
  # COMMAND=$(echo "$*" |sed -e 's/:/^[/g')
  vim - -es +":norm gg" +":norm $COMMAND" +:%p +:q! |sed '1d'
}

使い方は以下のような感じ。

$ cat a.txt
1 hoge
2 piyo
3 fuga

$ cat a.txt |vipe jwCfoo |grep '2'
2 foo

# ^[はESC文字で、Ctrl+Vした後にESCを押して入力する
$ cat a.txt |vipe Abar^[oxxxx
1 hogebar
xxxx
2 piyo
3 fuga

# コマンドに空白文字を含む場合は'か"でくくる
$ cat a.txt |vipe "A bar baz"
1 hoge bar baz
2 piyo
3 fuga

シェル上でvimのマクロ機能を使う

vimには超便利なマクロ機能が存在する。 簡単に説明すると、ノーマルモードにおける一連の操作をマクロ文字列として記録再生する機能で、 qaと押すことでレジスタaに記録し、@aと押すことで再生できる。 上述のjwCfooという呪文はvimのマクロの一種といえるだろう。

vimを対話的に使って (qaを使って) 記録したマクロを表示するには、ノーマルモードで"apと押せば良い。 こうして表示されたレジスタaの中身は、vipeコマンドの引数として食わせることが出来る。

$ cat a.txt |vipe "レジスタの中身を書き込む"

これで「vimのマクロを使いまわしたいな〜」なんて場合も安心ですね!

参考

vim -esとした時に表示されるVim: Reading from stdin...問題についてはコチラ。 vimソースコードを修正してくれ~

追記

ブコメで良い指摘をいただきました。

vimをパイプにする - 余白の書きなぐり

Vimをパイプで使うのめっちゃ便利!ただ、なんでnormalコマンド限定の書き方にしたんだろ?

2016/12/04 01:23

確かに記事中ではパイプの対象をvimのノーマルモードに限定していました。 もしvimのビジュアルモードが使いたければ、以下のようにvを入れれば良いです。

$ cat a.txt
1 hoge
2 piyo
3 fuga

$ cat a.txt |vipe lvjhd
1 piyo
3 fuga

矩形選択が使いたければ、vの代わりにCtrl+vを二回連打すれば良いですね。

もしvimコマンドラインモードを使ってテキストの整形がしたければ...まてよ、vimの親はviでその親はedで、ストリーム版のedはsed... なので、vimの叔父にあたるsedを使うと良いでしょう。

コマンドラインモードを使ったファイルの作成や書き込みは、vipeでむりやり実装するより、シェルのリダイレクション機能を使う方が良いと思います。 「どうしてもvipeでファイルを上書きしたいんやああ」という方がいらっしゃれば、

vim - -es +":norm gg" +":norm $COMMAND" +:%p +:q! |sed '1d'

:q!の部分を":wq! hoge.txt"に変更するなど、うまく調整してください~