注目の記事

Vimで変態テキスト処理!シェルコマンドを使い倒す

Vimから外部のシェルコマンドを実行して出力結果を得たり、バッファ内のテキストの変換を行う方法を紹介しています。
2018.09.19

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

こんにちは、データインテグレーション部の平野です。

私はテキストエディタにVimを使用しています。 Vimは敷居が高いと言われますが、ある程度慣れてくると普通のエディタとは明らかに異なる、Vimらしい編集方法がだんだんと身についてくるものです。 今回はVimから外部のシェルコマンドを実行してテキスト編集する手段についてご紹介します。

なお、Vimには色々なプラグインが公開されておりますが、ここで紹介する方法はあくまでもVimのオリジナル機能ですので、その場ですぐ試すことができます。 (lsコマンド等にはPATHが通っているという前提です)

カレントディレクトリのファイル一覧を取得したい

Vimでテキストを編集していて、カレントディレクトリのファイル一覧を挿入したい、というようなことがあります(よね?)。

スマートでない例

やり方は様々ありますが、以下のようなやり方をしていませんか?

  1. ctrl+zでシェルに戻る
  2. lsで欲しい情報を出力させる
  3. マウスで選択してクリップボードに入れる
  4. fgでVimに戻り、クリップボードから貼り付ける

もちろんこれでもできますが、手数も多いし、一度シェルに戻るのは面倒なことが多いです。

そこで、Vim内部からシェルコマンドが実行できることを利用しようとするのですが、試しにコマンドモード(ノーマルモードで:を押した状態)で

:!ls    # :はコマンドモードに入った時のもの。 !はシェルコマンドを実行するという意味

としてみます。 すると確かにlsの結果は表示されるのですが、Press ENTER or type command to continueと出て、Enterを押してみるとlsの出力結果は泡と消えてしまいます。 Enterを押す前にクリップボードに保存して、というのも手ですがさすがにちょっと間抜けです。

シェルコマンドの結果を直接持ってくる方法

コマンドモードに入る前にV(shift+v)を押してから:を押してみましょう。 そこでやはり!lsを入力します。 この時コマンドラインには以下のように表示されているはずです。

:'<,'>!ls

この状態でEnterで実行すると、無事lsの出力結果をVimの編集画面に持ってくることが出来ます。 1

行選択からコマンドを実行した際の挙動

いきなりVを打てと言いましたが、これは行選択モードに入るという動作になります。 次に行選択モード中に:を押すと、:'<,'>と表示され、これは選択行に対するコマンドモードになります2。 そして選択行に対するコマンドモードで実行されるコマンドは以下のような挙動になります。

  1. 選択された行の内容を標準入力として受け取って実行
  2. 実行した結果の出力(標準出力、標準エラー出力共に)を選択された行に上書き

擬似的に書くなら以下のような感じでしょうか。あくまでもイメージです。

echo '[選択行の内容]' | [コマンド] > [選択行の場所] 2>&1

lsは標準入力は特に使用しないので上記のように適当なところでVを押してから実行することで、 lsの結果を標準出力で受け取るという結果だけを得たわけです。

さて、さらっと書きましたが、標準入力が受け取れるということは、その内容によっていろいろなことができそうです。

一部の行だけを対象にしてテキスト変換

標準入力としてコマンドに渡して出力を得られるということは、文字列の変換ができるということです。 そして選択した行だけが対象となりますから、ファイル全体ではなく部分の変換ができます。 例えば以下のような感じです。

  • 対象の行だけ選択して:'<,'>!sort 3
    • 選択した行だけを対象としてソート
  • 対象の行だけ選択して:'<,'>!tac
    • 選択した行だけ逆順にする
  • 対象の行だけ選択して:'<,'>!perl aaa.pl
    • 選択した行だけをperlスクリプトで変換させる

最後の例のように、定型的に行いたい処理があればそれをスクリプト化しておくことで、 ファイル全体ではなく指定行だけを対象とした変換処理が簡単にできます。 一部の行だけをプログラム的に変換させたいことは時々あるので、それを切り貼りしないで直感的に操作できるのは嬉しいです。

なお注釈にも書いていますが、選択行を対象とした変換処理が使えるのはシェルコマンドに限った話ではないです。 というより、「基本的にはVimコマンドを使うが、!をつけると特別にシェルコマンドも使える」と考えるのが自然ですね。 とは言え、多くの人にとってはVim内コマンドよりも、シェルのコマンドの方が馴染みが深いでしょうから、ありがたく!を付けて使うのがオススメです。
例えば、数値でソートしたいときVimコマンドのsortを使った場合、どうするかご存知でしょうか? sort -nとしてもエラーになってしまいます。答えとしてはsort nという簡単なものですが、いちいち調べないといけないのは嫌ですよね。 普段からsort -nを使っているなら、わざわざVim内コマンドのやり方を覚える必要はないでしょう。

ちなみに全行に適用するときには、全行を選択しないでも

:%!sort

のように'<,'>の代わりに%を指定します。

コマンドランチャのように使う

話は変わって、コマンドラインで以下のようなコマンドが実行できるのはご存知でしょうか?

echo "pwd" | sh

標準入力で渡されたpwdという文字列をshで処理する。つまり、pwdとだけ書いて実行するのと一緒です。 さて、標準入力として渡して実行する、ということは上記の枠組みでも同じことができるということになります。
pwdとだけ書かれている行を選択して、:'<,'>!shとすれば、その行がカレントディレクトリのパスで上書きされます。

このコマンドは結構便利なのですぐに呼び出せるようにキーバインドを設定しています。 私は<Space> を押した後 <Enter>というキーバインドで設定しているので、.vimrcに以下のように書きます。

vnoremap <Space><CR> :!sh<CR>    # 行選択中に実行
nnoremap <Space><CR> V:!sh<CR>   # 行選択していない状態から実行

これを利用すると、単純にコマンドが並んだシェルスクリプトをコマンドランチャのように扱うことができます。 1行選択して実行して、を繰り返すこともできますし、複数行を選択して一気に実行することもできます。

実用編

上記で行ったことの実例として、カレントディレクトリ以下のファイルのリネームを行ってみます。 まずfindコマンドを使ってファイル一覧を得ます。

find . -type f
./animal/cat
./animal/elephant
./color/blue
./color/red
./food/alcohol/beer
./food/alcohol/sake
./food/beef
./food/bread

ここでファイル名の先頭文字を大文字にして、拡張子.jpgをつけたいとします。
まずはawkを使ってファイルパスを2度繰り返します。

:%!awk '{print $1,$1}'
./animal/cat ./animal/cat
./animal/elephant ./animal/elephant
./color/blue ./color/blue
./color/red ./color/red
./food/alcohol/beer ./food/alcohol/beer
./food/alcohol/sake ./food/alcohol/sake
./food/beef ./food/beef
./food/bread ./food/bread

次にmvを先頭につけます。これでmvでリネームするための骨子ができました。

mv ./animal/cat ./animal/cat
mv ./animal/elephant ./animal/elephant
mv ./color/blue ./color/blue
mv ./color/red ./color/red
mv ./food/alcohol/beer ./food/alcohol/beer
mv ./food/alcohol/sake ./food/alcohol/sake
mv ./food/beef ./food/beef
mv ./food/bread ./food/bread

最後にmvの行き先がリネーム後の名前になるように編集します。 階層の深さが異なる部分があるのでちょっと編集が面倒ですがめげずにやります。4

mv ./animal/cat ./animal/Cat.jpg
mv ./animal/elephant ./animal/Elephant.jpg
mv ./color/blue ./color/Blue.jpg
mv ./color/red ./color/Red.jpg
mv ./food/alcohol/beer ./food/alcohol/Beer.jpg
mv ./food/alcohol/sake ./food/alcohol/Sake.jpg
mv ./food/beef ./food/Beef.jpg
mv ./food/bread ./food/Bread.jpg

これでリネームスクリプトができましたので、あとは各行を実行します。 最初の1,2行は一行ずつ実行してみて、問題がなさそうなら残りの全行を選択して実行します。

ファイル一覧の取得からスクリプトの組み立てまでサッと行うことができました(Vimの習熟度にも依りますが。。。) 変に繰り返しロジックを書くよりも処理の内容が一目瞭然に確認できるので、安全性の面でもなかなか優れたやり方だと思います。

まとめ

Vimで、シェルコマンドの結果を編集画面(バッファ)に貼り付ける方法と、 選択した行だけをシェルコマンドで変換する方法を示し、 その応用としてコマンドランチャとしてコマンドを実行するような使い方をご紹介しました。

Vimに慣れていない方は、最初のシェルコマンドの結果を貼り付けるというやり方だけでも参考にして頂ければ幸いです。 そして慣れてきたら行選択からの変換など試してみて頂ければと思います。


  1. :r!lsとしても良いのですが、後続の説明のように、行選択から行なった方が汎用性が高いです 
  2. 本当はコマンドモードにそんな区別はありませんが、わかりやすさ優先ということで 
  3. !sortではなく、sort(Vimのコマンド)を使っても良いです 
  4. 一発でやるなら:%normal $T/~A.jpgなどでできます