2011/11/26

shell での while ループにおける問題

シェルスクリプトでコマンドの出力をループ処理する場合は パイプ | を使用するのが一般的な手法だと思うが、 パイプ | を使うとループが別なプロセスとして実行されるので ループ内部で設定したシェル変数がループ外部で参照できないという問題があり、 (存在するとすれば) shell script 業界では割と FAQ 的な問題だ。
何が問題かと言って posix で定義されていないから シェルの実装に依存しているのが一番の問題だったりするのだろう。
この問題に対しては、シェルビルトインの exec (1) を利用して ディスクリプタを複製して while ループの入力を標準入力にしてしまうのが 一番汎用的で柔軟性のある対応だと思うのだが、 シェルを愛する面々は 皆同じ様な苦労をしているのだ と改めて認識した。

当初は一時ファイルを作成する方法を考えついたのだが、 自分的にはなるべく一時ファイルを使いたくなかったので ファイルから入力する部分をバッククォートによるコマンド実行に置き換え等、 色々と試行錯誤した結果ヒアドキュメントを使う方法に落ち着いた。

# これでは正常に動作しなかった
exec 3<&0  0<`${command}`

while read line
    :
	

勿論一時ファイルを作成しても良いのだが、 ヒアドキュメントを利用する事で一時ファイルが不要になる分だけ 処理的に美しいかなと思う (完全に好みの問題)。
  1#!/bin/sh
  2# コマンドの出力をバッククォートしてヒアドキュメントとして使用する
  3
  4command="/path/to/command"
  5count=0
  6
  7# exec 3<&0: 標準入力 (FD0) を FD3 に複製
  8# exec 0<<EOF: ヒアドキュメントを標準入力として使用
  9exec 3<&0  0<<EOF
 10`${command}`
 11EOF
 12
 13while read line
 14do
 15    :
 16    count=`expr ${count} + 1`
 17done
 18
 19# exec 0<&3: FD3 に複製した標準入力 (FD0) を復帰
 20# exec 3<&-: FD3 をクローズ</i>
 21exec 0<&3 3<-
 22
 23echo ${count}
    

もう一点は子プロセスで親プロセスの変数値を変更する方法。
こちらはプロセスが別になってしまうとファイルを使うしかないので、 同じプロセス内で関数呼び出しとして対応する方法が汎用的ではないだろうか。

  1#!/bin/sh
  2# eval を利用して呼び元の変数を設定する
  3
  4default="デフォルト値"
  5
  6sub()
  7{
  8
  9    local   _val
 10
 11    :
 12    _val="設定したい値"
 13
 14    eval "${1}='${_val:-${default}}'"
 15
 16}
 17
 18value="元の値"
 19
 20sub value
 21
 22echo ${value}
    

ちなみに上のリンク元にある parent.sh の中の処理で ${tmp_file} の内容を for ループで eval しているが、 これはシェルビルトインの . もしくは source を利用して . ${tmp_file} としても同一の結果が得られるので楽だと思う。

  1#!/bin/sh
  2
  3g1="aa"
  4g2="bb"
  5
  6tmp_file=`mktemp /tmp/ps.XXXXXX`
  7export tmp_file
  8
  9./child.sh    # child.sh で、上で設定した変数 g1, g2 の値を変更する
 10
 11# for v in `cat $tmp_file`; do
 12#   eval $v
 13#   done
 14# ${tmp_file} の内容は <tt>name='value'</tt> 形式が保証されているので
 15# ファイル自体を `.' コマンドで読み込むだけで
 16# 行毎に eval を実行しなくてもシェル変数に代入された値が利用できる
 17
 18if [ -f ${tmp_file} ]
 19then
 20    . ${fmp_file}
 21    rm -f ${tmp_file}
 22fi
 23
 24echo $g1 #=> cc に変更された
 25echo $g2 #=> dd に変更された
    
ブログに直接コメントしたかったのだが、 はてなのアカウントが無いので申し訳ないがここで意見させて頂く。

2011/11/24

こどもの国

朝から好天に恵まれてぽかぽかな冬の休日を無駄にするのは勿体ないので、 かねてから気になっていた「こどもの国」に家族で出かけてみた。
行く前は「こどもの国」という名称から 『どうせ地方にありがちな子供向けのしょぼい遊園地の類だろう』等と 高をくくっていたのだが、実際に行ってみると大違い。 東京都と神奈川県にまたがる広大な敷地が、 自然の地形や環境上手に活かさて大人でも十分楽しめる遊園となっていて、 良い意味で期待が裏切られた。
あまりに広すぎるので一日では回りきれず 全体の半分程度を結構な駆け足で巡っただけなのだが、 それでも地面に自由に落書きできる広場や 本格的な牧場やポニーへの乗馬体験、 伊豆のサイクルスポーツセンターを彷彿とさせる様な 様々な変わり種自転車を集めた施設など盛りだくさんの内容だった。

Image: img_1475.jpg

母子で乗馬初体験

しかも牧場では定番のソフトクリームや 首都圏で随一生産されるらしい牛乳も楽しめる。

Image: img_1494.jpg

カーナンバー #27 の J.Alesiを彷彿とさせるドライビング

休日は道路も駐車場も混雑するだろうと電車で出かけたのだが、 2 度の乗り換えがあるが 1 時間かからない程度なので これからも是非出かけてみようと思う。

2011/11/16

メモリリークチェッカー

c で daemon 動作するサーバを作っていると malloc (3) などで動的に確保したメモリの free (3) 忘れによる いわゆるメモリリークを防ぐ事が非常に重要になってくる。
この様なメモリリークを防止するために役立つ 様々なツールが世の中には沢山存在しており それぞれ非常に有用ではあるのだが、 いざ利用しようと思うと割と面倒な作業が発生したり、 自分の求めている機能に対して明らかにオーバースペックだったりするので、 自分の用途に合わせて簡単に使える『俺様ツール』を作ってみた。

動作原理は簡単で malloc (3) や free (3) したアドレスを ログに出力して、 後からログを解析してメモリの確保と解放が対になっていない部分を 抽出するだけである。
そのためにまずは元となるソースコードを修正して、 malloc (3) や free (3) を独自に作成した wrapper 関数に置換えて 確保、解放したアドレスをログに出力する機能を組込む必要があるのだが、 malloc (3) などはエラー処理を含めて共通化するために あらかじめ wrapper 関数化されている場合が多いと思うので、 この部分はフォーマットを決めるだけで割りと簡単に追加できると思う。
今回は syslog (3) を利用して malloc (3)、strdup (3)、 free (3) で確保/解放するアドレスを出力した。

  1/*
  2 * malloc() の wrapper
  3 */
  4void    *mymalloc(size_t size)
  5{
  6
  7    void    *p;
  8
  9    if(!(p = malloc(size)))
 10        /* エラー処理 */;
 11    syslog(LOG_DEBUG, "malloc: %p\n", p);
 12
 13    return(p);
 14
 15}
 16
 17/*
 18 * strdup() の wrapper
 19 */
 20char    *mystrdup(const char *s)
 21{
 22
 23    char    *p = NULL;
 24
 25    if(s){
 26        if((p = strdup(s)))
 27            syslog(LOG_DEBUG, "strdup: %p\n", p);
 28        else
 29            /* エラー処理 */;
 30    }
 31
 32    return(p);
 33
 34}
 35
 36/*
 37 * free() の wrapper
 38 */
 39void    myfree(void *p)
 40{
 41
 42    if(p){
 43        free(p);
 44        syslog(LOG_DEBUG, "free: %p\n", p);
 45    }
 46
 47}
    

次に出力されたログの解析処理であるが、 確保したメモリのアドレスをキーとしたハッシュテーブルを利用したいので 入力行の解析やハッシュテーブルが簡単に利用できる言語として 今回はコマンドライン版の php (1) を利用してみた。
もちろん awk (1) を駆使したり perl (1) を利用しても問題ない。

  1<?php
  2    /*
  3     * Copyright (c) 2011 Mitzyuki IMAIZUMI, All rights reserved.
  4     *
  5     * $Id: memcheck.php 1677 2011-10-12 06:58:03Z mitz $
  6     */
  7
  8    /* 冗長指定 */
  9    if($argv[1] == "-v"){
 10        $verbose = 1;
 11        array_shift($argv);
 12    }
 13
 14    if($fp = fopen($argv[1], "r")){
 15        $line = 0;
 16        while($buf = fgets($fp, 1024)){
 17            $buf = ltrim($buf);
 18            $line++;
 19            /*
 20             * malloc か strdup か free を含む行の場合
 21             * コマンドを配列 $p[0] に、アドレスを配列 $p[1] に格納する
 22             */
 23            if(preg_match("/^(malloc|strdup|free):\s(.*)/", $buf, $p))
 24                /* 解放処理 */
 25                if($p[1] == "free")
 26                    /* 確保済みテーブルに解放アドレスが存在する場合 */
 27                    if($alloc[$p[2]]){
 28                        if($verbose)
 29                            printf("%s: % 8d -> % 8d\n", $p[2], $alloc[$p[2]], $line);
 30                        $alloc[$p[2]] = 0;
 31                    }
 32
 33                    /* 確保済みテーブルに解放アドレスが存在しない場合 */
 34                    else
 35                        printf("%s: unknown free at % 8d\n", $p[2], $line);
 36                else
 37                /* アドレスをキーとしたハッシュテーブルに行番号を格納 */
 38                    $alloc[$p[2]] = $line;
 39
 40        }
 41        fclose($fp);
 42
 43        foreach($alloc as $k => $v)
 44            if($v)
 45                printf("%s: NOT free (% 8d)\n", $k, $v);
 46    }
 47?>
    
このスクリプトを実行する事により malloc(3) もしくは strdup(3) で確保したメモリが解放されていない場合、 もしくは確保していないメモリを解放した場合が簡単に発見可能である。

これらは自分が必要とする機能のみを簡単に実装したものであり、 例えば解放していない事を意図しているメモリまで警告されてしまう等、 汎用的に使える事を目指したツールでは勿論ない。 ただ、日頃発生しうる面倒な作業がちょっとの工夫で 多少なりとも楽になるだろう例の一つとして公開してみた次第である。

2011/11/28 追記

awk (1) 版の方が汎用的なので簡単に作成してみた。 処理内容は上記 php (1) 版と同じである。
  1#!/bin/sh
  2
  3if [ "${1}" = "-v" ]
  4then
  5    verbose=true
  6    shift
  7fi
  8
  9awk '
 10    /^(malloc|strdup)/{
 11        alloc[$4] = NR;
 12    }
 13    /^free/{
 14        if(alloc[$4]){
 15            if("'${verbose:-false}'" == "true")
 16                printf("% 5d %s: % 5d\n", NR, $4, $alloc[$4]);
 17            alloc[$4] = 0;
 18        }
 19        else
 20            printf("% 5d %s: unknown free.\n", NR, $4);
 21    }
 22    END{
 23        for(i in alloc)
 24            if(alloc[i])
 25                printf("% 5d %s: NOT free.\n", alloc[i], i);
 26    }
 27' ${1} | sort ${2}
    


Copyright © Mitzyuki IMAIZUMI 2008,2009. All rights reserved.