LoginSignup
6
6

More than 5 years have passed since last update.

ファイルの各行にコールバック関数を適用する汎用関数

Last updated at Posted at 2016-01-10

業務のシェルスクリプトで、ファイルの各行についてほげほげ~な処理をすることはよくあると思いますが、std::for_each()的な「リスト型の各要素についてコールバック関数をマッピングする」メソッドのファイル版があったらな~と思い、実装してみました。

for_each_line関数の仕様

第1引数で指定したファイルの各行について、第2引数に指定したコールバック関数を適用します。その際、事前にコメント行やコメント部分、空行は事前に除外することで、コールバック関数には有効行のみが渡るようにしました。

また、ファイルの読み込みには、ファイルが巨大サイズの場合を考慮してwhile readを使用しています。

libsample.sh
#!/bin/bash

#
# Apply callbackfn to each line in file
#
for_each_line ()
{
    local in_file="$1"
    local callbackfn="$2"

    while read line; do
        local line="${line//#*/}"
        [[ -z "${line}" ]] && continue
        "${callbackfn}" "${line}"
    done <"${in_file}"
}

for_each_line関数の使い方

すごくすっきりした!コールバック万歳。

1行のsplitについてはプロセスforkを抑止するために、awkcutなどの外部コマンドは使わないようにしています。

sample.sh
#!/bin/bash
set -u

. ./lib/libsample.sh

#
# Callback function applied to each line in file
#
callbackfn()
{
    set -f; set -- $1; set +f
    [[ $# -ne 3 ]] && return
    echo 1=$1 2=$2 3=$3
}

for_each_line "./sample.lst" "callbackfn"
実行結果
$ cat ./sample.lst
#
# sample.lst
#

100 aaa xxx
200 bbb #yyy
300 ccc zzz

$ ./sample.sh
1=100 2=aaa 3=xxx
1=300 2=ccc 3=zz

コマンドの実行結果を読み込む版

ファイルを読み込むのではなく、コマンドラインの標準出力結果を一時ファイルを媒介せずに各行処理したい場合の汎用関数です。第1引数には、結果にコールバック関数を適用したいコマンドラインを文字列リテラルで指定します。

libsample.sh
#
# Apply callbackfn to each line in command execution result
#
for_each_line_cmdresult()
{
    local cmdline="$1"
    local callbackfn="$2"

    while read line; do
        [[ -z "${line}" ]] && continue
        "${callbackfn}" "${line}"
    done < <(${cmdline})
}
sample.sh
callbackfn()
{
    #TODO : 1行についての処理をここに書く
}

# カレント配下の全ファイルについて、コールバック関数を適用する
for_each_line_cmdresult "find ./ -type f" "callbackfn"

ワンシェルで完結させるコードスニペット

使い捨てのスクリプトを書くのに、いちいち外部モジュールを作るのは逆に手間なので、ワンスクリプトで記述するコードも貼りつけておきます。findawkの実行結果からの読み込みは、Process Substitution(プロセス置換)という機能を使っています。

sample.sh
#!/bin/bash

#awkでファイルのコメント部分と空行を削除した結果を1行ずつ処理
    while read line; do
        set -f; set -- ${line}; set +f
        [[ $# -ne 3 ]] && continue
        echo "1=$1 2=$2 3=$3"
    done < \
<(cat ./sample.lst | awk '! /^[ \t]*(#|$)/ {sub("#.*$",""); print}')
sample.sh
#!/bin/bash

#カレント配下にあるすべてのファイルパスについて処理
while read line; do
    #TODO : 1行についての処理をここに書く
done < <(find ./ -type f)

参考サイト様

ありがとうございます。

シェルスクリプトの中で1行ずつ変数を分割する際には、cutとかawkとか余計なプロセスを起動せずsetを使って分割した方が効率的(双六工場日誌)

巨大なファイルをシェルスクリプトで読む時@arc279さん)

パイプ出力を現在のシェル上のwhileに喰わせる上手いやり方@kawazさん)

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6