各ファイルの行数をCSVにまとめるコマンドライン

各ファイルの行数を(スプレッドシートに読み込ませるような)CSV ファイルにまとめるコマンドラインのメモです。

例として、JavaScript のライブラリ Three.js のソースファイルの行数を調べてみます。
Three.js のソースを含むアーカイブは公式サイトから入手できます。

- http://threejs.org/

# コマンドラインでCSV作成
## 準備
Three.js のアーカイブを展開して、そのディレクトリに移動します。

    $ unzip mrdoob-three.js-r69-0-gca14c79.zip
    $ cd mrdoob-three.js-ca14c79/

## ファイルを再帰的に検索する
ディレクトリを再帰的に検索する find コマンドを使って調べます。
また、ファイルの行数を調べるには wc コマンドに「-l」オプションをつけて使います。
ここで調べたいソースファイルは src ディレクトリ以下にあります。
また、ソースファイル(JavaScript)の拡張子は「.js」です。

    $ find src -name '*.js' -exec wc -l {} \;

こんな感じで出力されます。行数とファイル名が出力されてます。

    180 src/Three.js
    62 src/cameras/Camera.js
    79 src/cameras/CubeCamera.js
    57 src/cameras/OrthographicCamera.js
    141 src/cameras/PerspectiveCamera.js
    177 src/core/BufferAttribute.js
    927 src/core/BufferGeometry.js
                :

## ソートする
行数(1カラム目)で並び替えたいので sort コマンドを使います。
行数は数値として扱うので「-n」オプションをつけます。また、降順にしたいので「-r」オプションも付けます。

    $ find src -name '*.js' -exec wc -l {} \; | sort -nr

こんな感じに出力されます。行数順に降順にソートされてます。

    6449 src/renderers/WebGLRenderer.js
    1330 src/renderers/shaders/ShaderLib.js
    1009 src/core/Geometry.js
    963 src/math/Matrix4.js
    927 src/core/BufferGeometry.js
    818 src/math/Vector3.js
               :

## 行数の合計を計算する
行数の合計をつけたくなったので、awk を使います。
awk は入力ファイルのテキストを処理する一種のプログラミング言語です。
「-e」オプションで awk へのスクリプトを指定します。
コマンドラインが１行に収まらないので「\」で行末をエスケープして行を継続しています。
awk スクリプト中の改行は awk で問題なく処理されるのでエスケープしていません。

    $ find src -name '*.js' -exec wc -l {} \; | sort -nr | awk -e '\
    >         {count += $1 ; print}
    >     END {print count,"合計"}'

出力の最後がこんな感じになります。

           :
    11 src/extras/geometries/CubeGeometry.js
    10 src/extras/curves/ArcCurve.js
    10 src/core/Face4.js
    1 src/renderers/shaders/ShaderChunk.js
    34103 合計

# カラム位置を逆転して見出し行をつける
見出し行をつけます。awk で処理してみます。
ついでに「行数」と「ファイル名」の位置を逆転します。
(awk スクリプトの2〜3行目の print の引数が変更されているのに注意してください)

    $ find src -name '*.js' -exec wc -l {} \; | sort -nr | awk -e '\
    >     BEGIN {print "ファイル名","行数"}
    >           {count += $1 ; print $2, $1}
    >     END   {print "合計",count}'

出力の最初がこんな感じになります。

    ファイル名 行数
    src/renderers/WebGLRenderer.js 6449
    src/renderers/shaders/ShaderLib.js 1330
    src/core/Geometry.js 1009
    src/math/Matrix4.js 963
             :

# カンマ区切りにする
CSV なので区切りは「,」(カンマ)にしておきましょう。

awk の print は引数を" "でつなげて出力します。
また、文字列は並べておくだけで連結されます。(「+」などを使用しない)
なので、他の言語とはちょっと変わった感じになります。
（変化が微妙なので注意してください）

    $ find src -name '*.js' -exec wc -l {} \; | sort -nr | awk -e '\
    >     BEGIN {print "ファイル名,行数"}
    >           {count += $1 ; print $2 "," $1}
    >     END   {print "合計," count}'

出力はこんな感じになります。

    ファイル名,行数
    src/renderers/WebGLRenderer.js,6449
    src/renderers/shaders/ShaderLib.js,1330
    src/core/Geometry.js,1009
          :
    src/extras/curves/ArcCurve.js,10
    src/core/Face4.js,10
    src/renderers/shaders/ShaderChunk.js,1
    合計,34103

(※ heliac2000 さんにご指摘いただきました。OFS=x で print の区切り文字が指定できるそうです。詳しくはコメント欄をご覧ください)

# シェルスクリプトにする
完成したようなので、シェルスクリプトにしておきます。
1番目の引数で検索したいディレクトリを指定する仕様にします。

ついでに、出力の１カラム目に番号をつけるように改造しました。
awk の NR は特殊な変数で入力行番号が格納されています。

```bash:count-lines.sh
#!/bin/bash
PATH=/usr/bin:/bin

dir=${1:-.}

find "$dir" -name '*.js' -exec wc -l {} \; | sort -nr | awk -e '\
    BEGIN {print "番号,ファイル名,行数"}
          {count += $1 ; print NR "," $2 "," $1}
    END   {print "-,合計," count}'
```

こんな感じで使います。

    $ bash count-line.sh src >lines.csv

lines.csv への出力は、こんな感じです。

    番号,ファイル名,行数
    1,src/renderers/WebGLRenderer.js,6449
    2,src/renderers/shaders/ShaderLib.js,1330
    3,src/core/Geometry.js,1009
    4,src/math/Matrix4.js,963
        :
    160,src/extras/curves/ArcCurve.js,10
    161,src/core/Face4.js,10
    162,src/renderers/shaders/ShaderChunk.js,1
    -,合計,34103


# 円グラフにしてみた
lines.csv を Google スプレッドシートにインポートして円グラフにした結果です。

![lines-graph.png](https://qiita-image-store.s3.amazonaws.com/0/60624/a79223aa-3107-98fe-9a95-cea8d3189478.png)


# 付記
awk のスクリプトは以下のような構造になっています。

    パターン アクション
    パターン アクション
           :

上のスクリプトでは、パターンは「BEGIN」と「END」、アクションは「{」「}」でくくられた部分です。
入力テキストのパターンにマッチした行についてアクションが実行されます。
パターンが省略された場合は、どんな行でもマッチします。
「BEGIN」と「END」は特殊なパターンで、それぞれファイル入力の前と後に(擬似的に)マッチします。

## トップ10のみ詳細にカウントする
上のスクリプトをトップ10のみ詳細にカウントするように改造してみます。
トップ11以降は「その他」としてまとめます。

```bash:count-lines-top10.sh
#!/bin/bash
PATH=/usr/bin:/bin

dir=${1:-.}

find "$dir" -name '*.js' -exec wc -l {} \; | sort -nr | awk -e '\
    BEGIN    {print "番号,ファイル名,行数"}
             {count += $1 }
    NR <= 10 {print NR "," $2 "," $1}
    NR >  10 {n += 1; m += $1}
    END      {print "-,その他(" n ")," m; print "-,合計," count}'
```

出力はこんな感じになります。

    番号,ファイル名,行数
    1,src/renderers/WebGLRenderer.js,6449
    2,src/renderers/shaders/ShaderLib.js,1330
    3,src/core/Geometry.js,1009
    4,src/math/Matrix4.js,963
    5,src/core/BufferGeometry.js,927
    6,src/math/Vector3.js,818
    7,src/core/Object3D.js,784
    8,src/extras/core/Path.js,675
    9,src/extras/geometries/ExtrudeGeometry.js,673
    10,src/math/Vector4.js,657
    -,その他(152),19818
    -,合計,34103

改造のバリエーションですが、ファイル行数が一定値を超えたものまで詳細にカウントする場合は、以下のようになります。

```bash
#!/bin/bash
PATH=/usr/bin:/bin

dir=${1:-.}

find "$dir" -name '*.js' -exec wc -l {} \; | sort -nr | awk -e '\
    BEGIN     {print "番号,ファイル名,行数"}
              {count += $1 }
    $1 >= 500 {print NR "," $2 "," $1}
    $1 <  500 {n += 1; m += $1}
    END       {print "-,その他(" n ")," m; print "-,合計," count}'
```