rcmdnk's blog
Last update

Mastering Software Project Requirements: A Framework for Successful Planning, Development & Alignment

昔に作ったコードとか人が作ったコードを再編集しようとした時に、 インデントやらタブ文字やら色々と一度整理してから書き直したいときに 複数ファイルをまとめてコマンド一つで再編集する方法について。

Sponsored Links

やりたいこと

Vim等で編集してる時に、気になったら編集中のファイルを変更、 とすれば大体の場合は良いのですが、 特にGitやSubversionで管理してるパッケージなんかで ファイルごとにその都度変更してコミットしたりしている時、 インデントなどを変更してしまうとdiffをした時にその部分が大量に出てきてしまいます。

コマンドでこれらの変更を無視することも出来ますが 1、 ぱっと見で分かりやすくしたいので、 一度全体で変更して全体での変更点を作っておいたほうが良いかな、と思います。

そこでまず全部変換してみよう、と一つ一つ開いてVimで整形して、とやろうと思ったら あまりに大量にあって面倒だったのでコマンドラインから処理できる様にしたい、と 2

Vimでの設定

まず、Vimの中で適当に編集出来る様に設定を行います。

.vimrc
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
set tabstop=2      " Width of <Tab> in view
set shiftwidth=2   " Width for indent
set softtabstop=0  " Disable softtabstop function
set cinoptions=g0  " g0: No indent for private/public/protected
set autoindent     " autoindent

function! IndentAll()
  normal mxgg=G'x
  delmarks x
endfunction

function! DeleteSpace()
  normal mxG$
  let flags = "w"
  while search(" $", flags) > 0
    s/ \+$//g
    let flags = "W"
  endwhile
  'x
  delmarks x
endfunction

function! AlignCode()
  retab
  call IndentAll()
  call DeleteSpace()
endfunction

function! AlignAllBuf()
  for i in  range(1, bufnr("$"))
    if buflisted(i)
      execute "buffer" i
      call AlignCode()
      update
      bdelete
    endif
  endfor
  quit
endfunction

" remove trail spaces for all
nn <Leader><Space>  :call DeleteSpace()<CR>

" remove trail spaces at selected region
xn <Leader><Space>  :s/<Space>\+$//g<CR>

追記: 2014/07/05

上のままcallを使って呼ぶのはちょっと格好悪いので、

1
2
3
4
5
function! s:indent_all()
  normal mxgg=G'x
  delmarks x
endfunction
command! IndentAll call s:indent_all()

みたいに、一度関数を別名で定義した後に、 コマンドとして定義した方が良いかと。

これなら、あとで:IndentAllとするだけで呼べます。

他のも変更したのは下の.vimrcにあります。

rcmdnk/dotfiles/.vimrc

追記ここまで

最初に

  • Tabをスペース2つで表示(tabstopretab時にこの数に変換)。
  • 自動インデントの数を2スペースに(shiftwidth)。
  • C++等でprivateなどの表示を字下げなしに(cinooptions)。

を設定しています。 この辺りは言語や好み依存で。

その後に関数の定義をしていて、

  • IndentAll(): 開いているファイル全体を=を使って再インデント。
  • DeleteSpace(): 開いてるファイル全体で行末の余計なスペース削除。
  • AlignCode(): retabに加えて上の二つの関数を実行。
  • AlignAllbuf(): バッファにあるファイル全てにAlignCode()を実行後、すべて保存し終了。

と言うもの。

IndentAllDeleteSpaceではマーク(mx)を使って元の位置に戻るようにしてますが、 もしxをマークとして良く使う場合には違う文字に割り当てた方が良いです。

DeleteSpace()については行末に空白がある行を探してその行で s/ \+$//g<CR>を実行、と言った事をしてます。

最初にG$してファイルの末尾に行ってsearchwオプションを使うことで ファイルの先頭へ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.cxxB.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

としてあげれば、現在居るディレクトリにある二つのディレクトリ:Package1Package2 にある全ての*.cxx*.hファイルの変更を行えます 3 4

ハマりどころ/Tips

-s/-S

最初、簡単なVimスクリプトを用意して-s {scriptin}オプションを使って実行する事を考えていました。 -sはVimスクリプトを読み込んで実行してくれるオプションで、 Vimを起動後に{scriptin}で指定したファイルの内容をそのまま手で打ち込んだ様に実行してくれる オプションです。

align.vim
1
:call AlignAllBuf()

こんな感じのスクリプトを作っておいて、

$ 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}

とするのと同じ事になってると思います。

こちらを使えば

align.vim
1
call AlignAllBuf()

みたいに直接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>"

な感じでかけますが(ggGで2回移動してるため2回戻る)、 もし最初にファイルの先頭に居ると、 最初のggが移動にカウントされずに<C-o>を余計に行ってもう一つ前に戻ってしまいます。 新しいファイルを開いたばかりでこれを行うと、一つ前のファイルに戻ってしまったり 結構やっかりなので、 マークできちんと場所を認識するのが最善です。

Sponsored Links
  1. 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を付けると空白を無視して表示してくれます。

    GitHub Secrets

  2. タブだけの変換であればexpandコマンドを使えば

    $ expand -i -t 2 a.cxx
    

    等とするとタブをスペースに変換出来ます。 (-iは行頭以外のタブを無視、-tでタブをいくつのスペースに変換するか指定、デフォルトは8。)

  3. svnかgitが自明な場合は最後のgrep -vは必要なものだけで良いですが。 また、除きたいファイルがある場合はさらに$()内に|grep -v ignore.h みたいに加えてあげればOK。

  4. 追記: 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
    

    追記ここまで

Sponsored Links

« sentakuにEmacsキーバインドを実装した Macでcronを使う時の注意点 »