複数のファイルの中身を検索して、条件に一致した行だけ消す方法


目次

ピクトリンク事業部開発部インフラ課の「アドベントカレンダー3記事目で若干ネタ切れの気配を感じ始めたエンジニア」の橋本です。

むしろ12月中はもろびとがこぞってアドベントカレンダーするので、6月くらいにやったほうが目立てるんじゃないかなとか現実逃避しています。梅雨明けとか讃えましょうよ。

 

本記事は フリューAdvent Calendar 2018 の12/19の分です。

今回は、複数のファイルに横断検索をかける方法、ヒットした情報だけを消す方法をご紹介しようと思います。

どういう時に便利?

いろんな場面で活用できる余地のあるテクニックだと思いますが、実例をあげるとメーリングリストの管理などに便利に使えます。

mlsample1

このように、ひとつのメーリングリストにユーザ名を連ねる形で運用している場合を想像してみてください。

社内の関係者に一斉に情報が行き渡るよう、いい感じにメーリングリストが構築されているような環境ですね。

 

この状態で、「桐島、弊社やめるってよ」と言われた場合にリストの管理者がやらないといけないのは、

mlsample2

ファイルの中に「桐島」が含まれるものを全て探し、

mlsample3

このように「桐島」を削除した状態で上書き保存する必要があるはずです。

 

この例だとファイルは3つだけですが、これが20も30もあったら大変面倒なうえに、そのうち作業ミスも発生しかねません。

 

これをスクリプト一発で終えることができたら幸せですよね!

実際のスクリプト

以下の例は「ファイル名が.qmail-から始まるファイルから対象者を探して消す」スクリプトです。

#!/bin/sh

##
# 実行例) ./qmail_hitman.sh 対象ユーザ名
# TARGET_PATHにある.qmail-* ファイルに含まれる、対象ユーザを一括で消去します。
##

TARGET_PATH="./"

cd ${TARGET_PATH}

# 対象ユーザ名
target=${1}

# 対象ユーザ名が含まれるファイルのリスト
files=($(grep -l "&${target}$" .qmail-*))

# 確認して消す関数
function ConfirmDelete() {
  echo "Please Enter: [y or n]"

  read input

  # 入力がないのにEnterされた場合
  if [ -z $input ] ; then
    ConfirmDelete

  # 消さないよって言われた場合
  elif [ $input = 'no' ] || [ $input = 'NO' ] || [ $input = 'n' ] ; then
    echo "ok, suspend."
    exit 2

  # 消してって言われた場合
  elif  [ $input = 'yes' ] || [ $input = 'YES' ] || [ $input = 'y' ] ; then
    echo "ok, I'll delete."

    # filesの数だけ繰り返す
    for file in ${files[@]}; do
      echo "processing...${file}"
      # 対象となるファイルからターゲットユーザの行を削除する
      sed -i -e "/^&${target}$/d" ${file}
    done

    # おわったよ!
    echo "done."
    exit 0

  else
    # ちゃんと入力して!
    ConfirmDelete
  fi
}

# ヒットしたファイルを表示.
echo "target file list:"
echo ${files[@]}
echo ""

# 対象となるファイルがなかった場合
if [ 0 -ge $(echo ${files[@]} | wc -w) ] ; then
  # ヒットするファイルがなかったよ.
  echo "${target} is not found for qmail."
  exit 1
# 対象となるファイルが1つ以上あった場合
else
  echo "Want to delete?"
  ConfirmDelete
fi

以下のようにして使います。例のごとく実行権限付与を忘れずに。

$ ./qmail_hitman kirishima.nobuo

実働部分の解説

「本当に消していいの?」と聞く部分が長いだけで、中身は至ってシンプル。

grep -l "&${target}$" .qmail-*

これで探して、出てきたファイルのリストを

for file in ${files[@]}; do
  sed -i -e "/^&${target}$/d" ${file}
done

これで対象者を削除して上書き保存しているだけです。グッバイ桐島。また会う日まで。

 

対象となるファイル名の.qmail-* があまりにも長すぎるとオーバーフローを起こしてしまう可能性があります。その場合はxargsを使うなど調整してみてください。

おわりに

今回はメーリングリストの管理を例にしましたが、

「複数にまたがったファイルから条件に合致するものだけを探す」

「ヒットしたものを置き換える、削除する」

などの操作は、ときおり発生するものです。

 

一回だけで済む場合はこの限りではありませんが、何度もやらないといけない可能性がある場合は、少しでも楽に運用できるようにしていきたいものですね。