やりたいこと
Vim等で編集してる時に、気になったら編集中のファイルを変更、 とすれば大体の場合は良いのですが、 特にGitやSubversionで管理してるパッケージなんかで ファイルごとにその都度変更してコミットしたりしている時、 インデントなどを変更してしまうとdiffをした時にその部分が大量に出てきてしまいます。
コマンドでこれらの変更を無視することも出来ますが 1、 ぱっと見で分かりやすくしたいので、 一度全体で変更して全体での変更点を作っておいたほうが良いかな、と思います。
そこでまず全部変換してみよう、と一つ一つ開いてVimで整形して、とやろうと思ったら あまりに大量にあって面倒だったのでコマンドラインから処理できる様にしたい、と 2。
Vimでの設定
まず、Vimの中で適当に編集出来る様に設定を行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
|
追記: 2014/07/05
上のままcall
を使って呼ぶのはちょっと格好悪いので、
1 2 3 4 5 |
|
みたいに、一度関数を別名で定義した後に、 コマンドとして定義した方が良いかと。
これなら、あとで:IndentAll
とするだけで呼べます。
他のも変更したのは下の.vimrcにあります。
追記ここまで
最初に
- Tabをスペース2つで表示(
tabstop
、retab
時にこの数に変換)。 - 自動インデントの数を2スペースに(
shiftwidth
)。 - C++等で
private
などの表示を字下げなしに(cinooptions
)。
を設定しています。 この辺りは言語や好み依存で。
その後に関数の定義をしていて、
- IndentAll(): 開いているファイル全体を
=
を使って再インデント。 - DeleteSpace(): 開いてるファイル全体で行末の余計なスペース削除。
- AlignCode():
retab
に加えて上の二つの関数を実行。 - AlignAllbuf(): バッファにあるファイル全てに
AlignCode()
を実行後、すべて保存し終了。
と言うもの。
IndentAll
とDeleteSpace
ではマーク(mx
)を使って元の位置に戻るようにしてますが、
もしx
をマークとして良く使う場合には違う文字に割り当てた方が良いです。
DeleteSpace()
については行末に空白がある行を探してその行で
s/ \+$//g<CR>
を実行、と言った事をしてます。
最初にG$
してファイルの末尾に行ってsearch
でw
オプションを使うことで
ファイルの先頭へwrapした状態でその次にある(つまりファイルの本当の先頭から探して該当する)箇所
を探し、 それ以降はW
を使って最後まで行った時にwrapしない(先頭に戻らない)、
というふうに検索することでファイルの先頭行に該当行がある場合でも
すべて変換出来るようにしています。(:h search
参照)
これで、ファイルを開いてる状態で、
:call AlignCode()
とすれば
追記: 2014/07/05
もしくは上に追記したようにコマンド定義して
:AlignCode
とすれば
追記ここまで
- タブをスペースに変換
- インデントの調整
- 行末スペースの削除
が行われます。
また、いくつかファイルを同時に開いてる状態で
:call AlignAllbuf()
を行えば全てのファイルを調整した上で保存し、Vimを終了します。
最後によく使う行末スペース削除については<Leader><Space>
にMapしています。
ノーマルモードではファイル全体を、
ビジュアルモードでは選択範囲のスペースを削除します。
ノーマルモードでは以前は
nn <Leader><Space> :s/<Space>\+$//g<CR>
の様に直接コマンドを定義していましたが、 下に書くようにこれだと該当箇所が無いとエラーが出て止まるので、 特に関数のところではそれを避ける様に上の様にしてあります。
ビジュアルモードの方はその辺り修正するのが面倒なのと 1回きりのコマンドであれば特にエラーが出ても問題が無いので 上の様に直接入れ替えコマンドを書いています。
コマンドラインから実行
コマンドを関数化したので、-c
オプションで呼ぶことで直接開いて終了させます。
$ vim -c "silent call AlignAllBuf()" A.cxx B.cxx >&/dev/null
これでA.cxx、B.cxxに対して調整を行えます。
追記: 2014/06/25
メッセージが大量にあるときに途中で出力が多すぎて止まってしまって
Retrunなどを押さないと進まなくなってしまうので、
これを抑制するためにsilent
コマンドを追加。
さらに、これにり出力を/dev/null
に出しても通常は困らないのでそのように。
追記ここまで
追記: 2014/07/05
これなんかもコマンド定義をしておけば
$ vim -c "silent AlignAllBuf" A.cxx B.cxx >&/dev/null
と、コマンドを節約出来ます。 以下の他も全て同様。
追記ここまで
これを使って、あるパッケージ(ディレクトリ)内(e.x. Package1 Package2)にあるファイルすべてを変換したいときは、
$ dirs=("dir1" "dir2")
$ orig_ifs=$IFS
$ IFS=$'\n'
$ files=($(find "${dirs[@]}" -name "*.cxx" -or -name "*.h" |grep -v .svn|grep -v .git))
$ IFS=orig_ifs
$ vim -c "silent call AlignAllBuf()" "${files[@]}">&/dev/null
としてあげれば、現在居るディレクトリにある二つのディレクトリ:Package1、Package2
にある全ての*.cxx
、*.h
ファイルの変更を行えます
3
4
。
ハマりどころ/Tips
-s/-S
最初、簡単なVimスクリプトを用意して-s {scriptin}
オプションを使って実行する事を考えていました。
-s
はVimスクリプトを読み込んで実行してくれるオプションで、
Vimを起動後に{scriptin}
で指定したファイルの内容をそのまま手で打ち込んだ様に実行してくれる
オプションです。
1
|
|
こんな感じのスクリプトを作っておいて、
$ vim -s align.vim A.cxx
みたいに実行しようと思ったのですが、 .vimrcの中で、
" Swap colon <-> semicolon
no ; :
no : ;
の様に;と;を交換していたため、上のスクリプトの実行は
;call...
の様に理解されてしまい上手く行きませんでした。
-s
で呼ぶときは.vimrcが読まれたあとで実行されてMapとかもすべて有効になるようです。
なのでMapとかを沢山使ってると、
-s
でファイルを読み込んで実行する際には結構問題が起きやすいと思います。
ここで一つ、このHelpが良くわからない所があって、
:h -s
で見ると
-s {scriptin} The script file "scriptin" is read. The characters in the
file are interpreted as if you had typed them. The same can
be done with the command ":source! {scriptin}". If the end
of the file is reached before the editor exits, further
characters are read from the keyboard. Only works when not
started in Ex mode, see |-s-ex|. See also |complex-repeat|.
{not in Vi}
となってるんですが、
途中の:source! {scriptin}
と一緒だ、と言うのは間違い?
もう一つ、-S {file}
というものがあって、こちらは
-S {file} The {file} will be sourced after the first file has been read.
This is an easy way to do the equivalent of:
-c "source {file}"
It can be mixed with "-c" arguments and repeated like "-c".
The limit of 10 "-c" arguments applies here as well.
{file} cannot start with a "-".
{not in Vi}
こんな感じで、こちらは素直に.vimrcの後に続いて読み込まれるVimスクリプト、 だと思えば良い感じで、こちらが起動後に
:source! {file}
とするのと同じ事になってると思います。
こちらを使えば
1
|
|
みたいに直接call
するファイルを書いて
$ vim -S align.vim A.cxx
とすればMapの問題を関係なく上手く行くと思います。
ただ、やりたいことの大半は関数に押し込めてしまって.vimrcで設定して
しまえば与えるコマンドは1行で済むので、
わざわざファイルを用意しなくても
-c
で簡単に出来る事にその後気づいたのでいずれにしろこれらは必要なし、と。
文字の置き換え時に該当箇所が無いことがある
DeleteSpace()
の所で、必ず空白があるのであれば関数の中身は単純に
%s/ \+$//g<CR>
だけで良いのですが、もしファイルに該当箇所がない場合には これだとエラーになってしまってその後の処理が実行されなくなります。
この関数を含めて複数連続して実行するときに困るので、 上の様に該当箇所を探してー、みたいなことを行っています。
normalコマンド
コマンドラインから(=Vimスクリプトの記述で)ノーマルモードのキー入力を行いたいときは、
の普通のキー入力はnormal ...
で行えます。
ただ、これだとマップが効いてしまうので、それを避けるように
normal! ...
としておいた方が通常は安全。
(ただ、下に書くように:
でコマンドモードに入ることはこちらでは上手く出来ませんが)
executeコマンド
normal
コマンドだと特殊文字(<ESC>
だとか<CR>
だとか<C-o>
だとか)が打てないので、
これを回避するためにexecute
コマンドが使えます。
execute
コマンドは一度与えられた文字列を評価してから実行するので、
<ESC>
など、Mapで使う様なキーをきちんとそのキー入力をして理解してくれます。
なので、インサートモードなんかも
execute "normal ixxx\<ESC>"
と書くと実行出来ます(特殊文字のところにはバックスラッシュ(\
)が必要)。
コマンドモードで行うことを指定するときには直接
let i = 3
execute "buffer" i
のように指定できます。
この様に変数を使えるので色々便利です
(最初、上のbuffer
を使ってる部分でbuffer i
みたいにi
を直接使おうとして
ちょっとハマった。。。)
上の様に複数の引数を与えるとスペースで区切られた(buffer 3
)形に評価され、
もしa . b
の様に間に.
を入れるとスペースなしで連結されます。
<C-o>
を使って戻る
上ではmx
と'x
を使ってマークを使った移動をしていますが、最初、
<C-o>
を使って移動前部分に戻る、みたいなことをやっていました。
これだと、
normal mxgg=G'x
delmarks x
の部分を
execute "normal gg=G\<C-o>\<C-o>"
な感じでかけますが(gg
とG
で2回移動してるため2回戻る)、
もし最初にファイルの先頭に居ると、
最初のgg
が移動にカウントされずに<C-o>
を余計に行ってもう一つ前に戻ってしまいます。
新しいファイルを開いたばかりでこれを行うと、一つ前のファイルに戻ってしまったり
結構やっかりなので、
マークできちんと場所を認識するのが最善です。
-
Gitなら
$ git diff -b # (= --ignore-space-change)
SVNなら
$ svn diff -x -b
でインデント量などを無視できます。
スペースの変更すべてを無視するには
Gitなら
$ git diff -w # (= --ignore-all-space)
SVNなら
$ svn diff -x -w
と
w
の方を使います。GitHubなんかでも、diffの結果ページに
?w=1
を付けると空白を無視して表示してくれます。 -
タブだけの変換であれば
expand
コマンドを使えば$ expand -i -t 2 a.cxx
等とするとタブをスペースに変換出来ます。 (
-i
は行頭以外のタブを無視、-t
でタブをいくつのスペースに変換するか指定、デフォルトは8。) -
svnかgitが自明な場合は最後の
grep -v
は必要なものだけで良いですが。 また、除きたいファイルがある場合はさらに$()
内に|grep -v ignore.h
みたいに加えてあげればOK。 ↩ -
追記: 2014/06/25
for
文が冗長だったのを直したのと、空白文字を含むディレクトリやファイルがある場合への 対処を加えて上の様に複数コマンドで。 加えて上にも書いてるslient
も追加。元のコマンドは以下の様な感じ。
$ for d in Package1 Package2;do do vim -c "silent call AlignAllBuf()" $(find ${d}/ -name "*.cxx" -or -name "*.h" |grep -v .svn|grep -v .git) >&/dev/null;done
追記ここまで