schedule2021-03-23

【bash】ディレクトリ内のファイルの行数をCSV出力する

ディレクトリの中身のファイルをリスト化して操作できると色々と便利です。 例としてディレクトリ内のファイルの行数をCSV出力してみながら、bashの解説をしていきます。

Contents

目的

Apacheのログファイルのファイル名とそのファイルの行数のCSVを出力するbashを考えてみる。

▼ディレクトリの中身はこんな感じ。 Apacheのログとそのバックアップのディレクトリがあるとする。 ログは1週間ごとに切り分けてあり、古いファイルは圧縮している。

/var/log/httpd/
├── access_log
├── access_log-20210228.gz
├── access_log-20210307.gz
├── access_log-20210314.gz
├── access_log-20210321.gz
├── error_log
├── error_log-20210228.gz
├── error_log-20210307.gz
├── error_log-20210314.gz
├── error_log-20210314.gz
├── error_log-20210321.gz
└── backup  # ディレクトリ
    ├── 202012
    ├── 202101
    ├── 202102
    └── 202103

以降で順に説明しながら目的を実行するスクリプトを書いていく。

1. ディレクトリ内のファイルとフォルダを出力する

#!/bin/bash

DIR=/var/log/httpd

# ディレクトリ内のファイルのリストを操作
for path in $DIR/*; do
  # パスを出力する
  echo $path
done
実行結果
/var/log/httpd/access_log
/var/log/httpd/access_log-20210228.gz
/var/log/httpd/access_log-20210307.gz
/var/log/httpd/access_log-20210314.gz
/var/log/httpd/access_log-20210321.gz
/var/log/httpd/error_log
/var/log/httpd/error_log-20210228.gz
/var/log/httpd/error_log-20210307.gz
/var/log/httpd/error_log-20210314.gz
/var/log/httpd/error_log-20210321.gz
/var/log/httpd/backup

for path in /path/to/dir/*; doと書くとディレクトリ内の一層目のファイルとフォルダの リストをfor文で扱うことができる。

2. ディレクトリとファイルを判定する

if [ -d "$path" ]; thenでディレクトリか判定できる。

#!/bin/bash

DIR=/var/log/httpd

for path in $DIR/*; do
  if [ -d "$path" ]; then
    # ディレクトリの場合スキップする
    continue
  fi
  echo $path
done

▼ファイルのみ出力する

実行結果
/var/log/httpd/access_log
/var/log/httpd/access_log-20210228.gz
/var/log/httpd/access_log-20210307.gz
/var/log/httpd/access_log-20210314.gz
/var/log/httpd/access_log-20210321.gz
/var/log/httpd/error_log
/var/log/httpd/error_log-20210228.gz
/var/log/httpd/error_log-20210307.gz
/var/log/httpd/error_log-20210314.gz
/var/log/httpd/error_log-20210321.gz

早めに例外で切り上げるため-dでディレクトリか判定しました。 他にもファイルやシンボリックリンクなど判定できます。

3. パスからファイル名を取得する

FILE_NAME=${path##*/}でパスからファイル名を取得出来る。

#!/bin/bash

DIR=/var/log/httpd

for path in $DIR/*; do
  if [ -d "$path" ]; then
    # ディレクトリの場合スキップする
    continue
  fi
  # echo $path
  FILE_NAME=${path##*/}
  echo $FILE_NAME
done
実行結果
access_log
access_log-20210228.gz
access_log-20210307.gz
access_log-20210314.gz
access_log-20210321.gz
error_log
error_log-20210228.gz
error_log-20210307.gz
error_log-20210314.gz
error_log-20210321.gz

パターンマッチより${変数##パターン}とすると先頭から最長一致した部分を取り除く。 つまり、${path##*/}ではパスの一番後ろの/までが取り除かれるため、ファイル名が残る。

パターンマッチの仕様

${変数#パターン}   # 先頭から最短一致した部分を取り除く
${変数##パターン}  # 先頭から最長一致した部分を取り除く
${変数%パターン}   # 末尾から最短一致した部分を取り除く
${変数%%パターン}  # 末尾から最長一致した部分を取り除く

参考: パス文字列からファイル名などを抜き出す

4. ファイルの行数を変数に代入する

wc -lでファイルの行数がわかる。 この結果を変数に代入して扱えるようする。

#!/bin/bash

file=/var/log/httpd/access_log
LINE=$(wc -l < $file)

echo $LINE
実行結果
173543

また、行数を取得する際に条件付けもしてみる。

4.1. grepで条件付けした行数を変数に代入する

#!/bin/bash

# grepで条件付け
file=/var/log/httpd/access_log
LINE=$(cat $file | grep POST | wc -l)

echo "POST:"$LINE
実行結果
POST:26

4.2. 一時ファイルに保存した行数を変数に代入する

#!/bin/bash

# 一時ファイルに保存する
# GETを含む and 200を含まない
cat /var/log/httpd/access_log \
      | grep GET \
      | grep -v "200" \
      > tmp.txt
LINE=$(wc -l < "tmp.txt")
echo "GET and not in 200:"$LINE
実行結果
GET and not in 200:76

4.3. 圧縮ファイルの行数を変数に代入する

#!/bin/bash

# 圧縮ファイル
file=/var/log/httpd/access_log-20210228.gz
LINE=$(zcat $file | wc -l)

echo $LINE
実行結果
413180

5. ログファイルのファイル名とそのファイルの行数のCSVを出力する

目的の処理を実装する。

#!/bin/bash

DIR=/var/log/httpd

echo "filename,line"
for path in $DIR/*; do
  if [ -d "$path" ]; then
    # ディレクトリの場合スキップする
    continue
  fi
  # echo $path
  FILE_NAME=${path##*/}

  # 拡張子
  ext=${FILE_NAME##*.}
  if [ "$ext" == "gz" ]; then
    # 圧縮ファイル
    LINE=$(zcat $path | wc -l)
  else
    LINE=$(cat $path | wc -l)
  fi
  echo $FILE_NAME","$LINE
done
実行結果
filename,line
access_log,178389
access_log-20210228.gz,413180
access_log-20210307.gz,411489
access_log-20210314.gz,413381
access_log-20210321.gz,422814
error_log,16686
error_log-20210228.gz,41686
error_log-20210307.gz,44184
error_log-20210314.gz,44154
error_log-20210321.gz,43002

いい感じですね。