2014/06/19

Amazon AWS 上でホスト情報を取得する

Amazon AWS で ELB + AutoScaling を利用している場合など、 IP アドレスが頻繁に変わってしまうので、 グローバルIP、プライベートIP を取得して一覧表示するスクリプト。
ホスト名は Name タグの Value に格納されている必要がある。
aws コマンドは EC2 インスタンスで Amazon Linux を起動した場合は 標準でインストールされている筈。
EC2 インスタンスに適切な role が設定されていれば aws はそのまま動作するが、 role が設定されていない場合でも設定する事で利用可能になる。

引数でホスト名 (Name タグで指定された値) が指定された場合、 デフォルトではグローバル IP のみを出力するが -p オプションでプライベートIP のみを表示する。

  1#!/bin/sh
  2
  3# リージョン
  4region="--region ap-northeast-1"
  5# 出力はテキスト形式
  6output="--output text"
  7# 出力項目を指定 (ホスト名(=タグ)、インスタンスID、パブリックIP、プライベートIP
  8base="--query Reservations[*].Instances[*]."
  9# Name タグの値表示
 10name="Tags[?Key==\`Name\`].Value,"
 11# インスタンスID、パブリックIP、プライベートIP
 12default="${name}InstanceId,PublicIpAddress,PrivateIpAddress" 
 13# フィルタ指定 (running 状態のみ出力)
 14filter="--filter Name=instance-state-name,Values=running"
 15
 16# aws コマンド
 17aws="/usr/bin/aws ec2 describe-instances ${region} ${output}"
 18
 19# メイン関数
 20function    doit()
 21{
 22
 23    local   _filter _query
 24
 25    if [ -n "${1}" ]
 26    then
 27        _filter="${filter} Name=tag:Name,Values=${1}"
 28        _query=${base}[${3}${2}]
 29    else
 30        _filter=${filter}
 31        _query=${base}[${default}]
 32    fi
 33
 34    ${aws} ${_filter} ${_query}
 35
 36}
 37
 38# オプション解析
 39if opt=`getopt p $*`
 40then
 41    set -- ${opt}
 42    while [ -n "${1}" ]
 43    do  
 44        case "${1}" in
 45            -p )
 46                target=PrivateIpAddress;;
 47            -- )
 48                shift
 49                break;;
 50        esac
 51        shift
 52    done
 53else
 54    echo "Usage: ${0##*/} [-p][host [... host]" 1>&2
 55    exit 255
 56fi
 57
 58if [ $# -eq 1 ]
 59then
 60    doit ${1} ${target:-PublicIpAddress}
 61elif [ $# -gt 0 ]
 62then
 63    for i in $*
 64    do
 65        doit ${i} ${target:-PublicIpAddress} ${name}
 66    done | awk '{ if(NR % 2) ip = $0; else printf "%s   %s\n", $0, ip }'
 67else
 68    doit | awk '{ if(NR % 2) ip = $0; else printf "%s   %s\n", $1, ip }'
 69fi 
    

2013/11/23 追記

複数ホスト名が指定された場合はホスト名も表示する様変更。

2014/05/23

checkinstall で rpm パッケージの作成がエラーになる場合

rpm パッケージを手軽に作成するために checkinstall は非常に便利だが、 現行の checkinstall にはインストール対象となるソフトウェアに ファイルに対するシンボリックリンクが含まれると rpm パッケージが作成できなくなる不具合がある様だ。

この不具合は checkinstall が rpm パッケージ用の spec ファイルを生成する際に、 ターゲットとなるオブジェクトがディレクトリかシンボリックリンクなら 無条件にディレクトリと仮定して %dir と出力しているために発生している。 そのために、例えばインストール時に libhoge.so.X.Ylibhoge.so にシンボリックリンクする様なソフトウェアの rpm パッケージを作成しようとするとエラーとなってしまう。

そこでターゲットなるオブジェクトがシンボリックリンクの場合、 リンク先を確認してディレクトリだった場合のみ %dir を出力する様にするパッチを作成した。

*** /usr/sbin/checkinstall~ 2014-05-21 12:29:30.000000000 +0900
--- /usr/sbin/checkinstall  2014-05-23 23:51:09.131081380 +0900
***************
*** 2428,2434 ****
  # Prepare directories to be included in the .spec file
  mv ${TMP_DIR}/newfiles ${TMP_DIR}/newfiles.tmp
  cat ${TMP_DIR}/newfiles.tmp | while read line; do
!    [ -d "${BUILD_DIR}/${line}" -o -L "${BUILD_DIR}/${line}" ] && echo -n "%dir " >> ${TMP_DIR}/newfiles
     echo "\"/${line}\"" >> ${TMP_DIR}/newfiles
  done
  
--- 2428,2435 ----
  # Prepare directories to be included in the .spec file
  mv ${TMP_DIR}/newfiles ${TMP_DIR}/newfiles.tmp
  cat ${TMP_DIR}/newfiles.tmp | while read line; do
!    [ -d "${BUILD_DIR}/${line}" ] && echo -n "%dir " >> ${TMP_DIR}/newfiles
!    [ -L "${BUILD_DIR}/${line} -a -d `ls -l ${BUILD_DIR}/${line} | awk '$0=$NF'` ] && echo -n "%dir" >> ${TMP_DIR}/newfiles
     echo "\"/${line}\"" >> ${TMP_DIR}/newfiles
  done
    

/usr/sbin/checkinstall にこのパッチを適用する事で シンボリックリンク先がディレクトリの場合のみ %dir を出力する様に変更される。
シンボリックリンクがネストしている場合は正しく動作しないので あくまでも暫定的なパッチだが、 取りあえず rpm パッケージを作成した場合にはそれなりに有用だと思う。
一応 checkinstall の配布元の bugzilla にはパッチを添えて報告してある。

ちなみに checkinstall で rpm の作成がエラーになる場合は 殆どが自動生成された spec ファイルが問題な場合なので、 --review-spec オプションを指定して checkinstall を実行する事で、 rpm パッケージの作成処理を実行する前に 自動生成された spec ファイルを編集できるので問題を回避できる事が多い。

# checkinstall --review-spec</i> 
    

2014/05/22

Amazon Linux AMI 2014.03.1 (64bit) への checkinstall 導入

Amazon AWS の EC2 インスタンスで動作している Amazon Linux AMI 2014.03.1 (Linux ****** 3.10.40-50.136.amzn1.x86_64 #1 SMP Tue May 13 21:35:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux) への checkinstall の導入時にエラーとなった場合の対処方法。

Amazon AWS の EC2 インスタンスに checkinstall コマンドを導入しようとして、 ネットなどの情報に基づいて以下のコマンドを実行した所コンパイルエラーが発生した。

$ git clone http://checkinstall.izto.org/checkinstall.git
$ cd checkinstall
$ vi Makefile checkinstall checkinstallrc-dist installwatch/Makefile
$ make
    :
    :
make -C installwatch
make[1]: ディレクトリ `/home/mitz/checkinstall/installwatch' に入ります
gcc -Wall -c -D_GNU_SOURCE -DPIC -fPIC -D_REENTRANT -DVERSION=\"0.7.0beta7\" installwatch.c
installwatch.c:2960:5: error: conflicting types for ‘readlink’
 int readlink(const char *path,char *buf,size_t bufsiz) {
     ^
In file included from installwatch.c:41:0:
/usr/include/unistd.h:831:16: note: previous declaration of ‘readlink’ was here
 extern ssize_t readlink (const char *__restrict __path,
                ^
installwatch.c:3098:5: error: conflicting types for ‘scandir’
 int scandir( const char *dir,struct dirent ***namelist,
     ^
In file included from installwatch.c:49:0:
/usr/include/dirent.h:255:12: note: previous declaration of ‘scandir’ was here
 extern int scandir (const char *__restrict __dir,
            ^
installwatch.c:3714:5: error: conflicting types for ‘scandir64’
 int scandir64( const char *dir,struct dirent64 ***namelist,
     ^
In file included from installwatch.c:49:0:
/usr/include/dirent.h:278:12: note: previous declaration of ‘scandir64’ was here
 extern int scandir64 (const char *__restrict __dir,
            ^
make[1]: *** [installwatch.o] エラー 1
make[1]: ディレクトリ `/home/mitz/checkinstall/installwatch' から出ます
make: *** [all] エラー 2
$
    
ざっと調べてみると glibc のバージョン情報を格納していると思われるマクロ GLIBC_MINOR の値が不正で readlink()、 及び内部で定義している scandir()scandir64() のプロトタイプ宣言が正しく展開されていない様だったので、 他にもいくつか発生したコンパイルエラーを修正して 取りあえずコンパイルしてインストール可能なパッチを作成した。
本来は Amazon LINUX 上でも glibc のバージョンを正しく取得できる様にするべきなのだが、 諸処の事情でそこまで修正する余裕がないので暫定的な対策のみとなっている。

  1diff -rc checkinstall/installwatch/installwatch.c checkinstall~/installwatch/installwatch.c
  2*** checkinstall/installwatch/installwatch.c    2014-05-21 12:41:51.659879736 +0900
  3--- checkinstall~/installwatch/installwatch.c   2014-05-21 12:41:16.107577033 +0900
  4***************
  5*** 99,105 ****
  6  static int (*true_xstat)(int,const char *,struct stat *);
  7  static int (*true_lxstat)(int,const char *,struct stat *);
  8  
  9! #if(GLIBC_MINOR >= 10)
 10  
 11  static int (*true_scandir)(   const char *,struct dirent ***,
 12                int (*)(const struct dirent *),
 13--- 99,105 ----
 14  static int (*true_xstat)(int,const char *,struct stat *);
 15  static int (*true_lxstat)(int,const char *,struct stat *);
 16  
 17! #if 1
 18  
 19  static int (*true_scandir)(   const char *,struct dirent ***,
 20                int (*)(const struct dirent *),
 21***************
 22*** 130,136 ****
 23  static int (*true_open64)(const char *, int, ...);
 24  static struct dirent64 *(*true_readdir64)(DIR *dir);
 25  
 26! #if(GLIBC_MINOR >= 10)
 27  static int (*true_scandir64)( const char *,struct dirent64 ***,
 28                int (*)(const struct dirent64 *),
 29                int (*)(const struct dirent64 **,const struct dirent64 **));
 30--- 130,136 ----
 31  static int (*true_open64)(const char *, int, ...);
 32  static struct dirent64 *(*true_readdir64)(DIR *dir);
 33  
 34! #if 1
 35  static int (*true_scandir64)( const char *,struct dirent64 ***,
 36                int (*)(const struct dirent64 *),
 37                int (*)(const struct dirent64 **,const struct dirent64 **));
 38***************
 39*** 2545,2551 ****
 40  }
 41  
 42  FILE *fopen(const char *pathname, const char *mode) {
 43!   FILE *result;
 44    instw_t instw;
 45    int status=0;
 46  
 47--- 2545,2551 ----
 48  }
 49  
 50  FILE *fopen(const char *pathname, const char *mode) {
 51!   FILE *result = NULL;
 52    instw_t instw;
 53    int status=0;
 54  
 55***************
 56*** 2956,2962 ****
 57    return result;
 58  }
 59  
 60! #if (GLIBC_MINOR <= 4)
 61  int readlink(const char *path,char *buf,size_t bufsiz) {
 62    int result;
 63  #else
 64--- 2956,2962 ----
 65    return result;
 66  }
 67  
 68! #if 0
 69  int readlink(const char *path,char *buf,size_t bufsiz) {
 70    int result;
 71  #else
 72***************
 73*** 3096,3103 ****
 74  }
 75  
 76  int scandir(  const char *dir,struct dirent ***namelist,
 77!       int (*select)(const struct dirent *),
 78! #if (GLIBC_MINOR >= 10)
 79        int (*compar)(const struct dirent **,const struct dirent **)    ) {
 80  #else
 81        int (*compar)(const void *,const void *)    ) {
 82--- 3096,3103 ----
 83  }
 84  
 85  int scandir(  const char *dir,struct dirent ***namelist,
 86!       int (*filter)(const struct dirent *),
 87! #if 1
 88        int (*compar)(const struct dirent **,const struct dirent **)    ) {
 89  #else
 90        int (*compar)(const void *,const void *)    ) {
 91***************
 92*** 3114,3124 ****
 93      /* We were asked to work in "real" mode */
 94    if( !(__instw.gstatus & INSTW_INITIALIZED) ||
 95        !(__instw.gstatus & INSTW_OKWRAP) ) {
 96!       result=true_scandir(dir,namelist,select,compar);
 97        return result;
 98    }
 99  
100!   result=true_scandir(dir,namelist,select,compar);
101  
102    return result;
103  }     
104--- 3114,3124 ----
105      /* We were asked to work in "real" mode */
106    if( !(__instw.gstatus & INSTW_INITIALIZED) ||
107        !(__instw.gstatus & INSTW_OKWRAP) ) {
108!       result=true_scandir(dir,namelist,filter,compar);
109        return result;
110    }
111  
112!   result=true_scandir(dir,namelist,filter,compar);
113  
114    return result;
115  }     
116***************
117*** 3712,3719 ****
118  }
119  
120  int scandir64(    const char *dir,struct dirent64 ***namelist,
121!       int (*select)(const struct dirent64 *),
122! #if (GLIBC_MINOR >= 10)
123        int (*compar)(const struct dirent64 **,const struct dirent64 **)    ) {
124  #else
125        int (*compar)(const void *,const void *)    ) {
126--- 3712,3719 ----
127  }
128  
129  int scandir64(    const char *dir,struct dirent64 ***namelist,
130!       int (*filter)(const struct dirent64 *),
131! #if 1
132        int (*compar)(const struct dirent64 **,const struct dirent64 **)    ) {
133  #else
134        int (*compar)(const void *,const void *)    ) {
135***************
136*** 3730,3740 ****
137      /* We were asked to work in "real" mode */
138    if( !(__instw.gstatus & INSTW_INITIALIZED) ||
139        !(__instw.gstatus & INSTW_OKWRAP) ) {
140!       result=true_scandir64(dir,namelist,select,compar);
141        return result;
142    }
143  
144!   result=true_scandir64(dir,namelist,select,compar);
145  
146    return result;
147  }     
148--- 3730,3740 ----
149      /* We were asked to work in "real" mode */
150    if( !(__instw.gstatus & INSTW_INITIALIZED) ||
151        !(__instw.gstatus & INSTW_OKWRAP) ) {
152!       result=true_scandir64(dir,namelist,filter,compar);
153        return result;
154    }
155  
156!   result=true_scandir64(dir,namelist,filter,compar);
157  
158    return result;
159  }     
    
2014/05/22 に git clone した checkinstall のソースに このパッチを適用してコンパイル、動作する事は確認できている。
Amazon Linux で checkinstall の導入に困っている方の参考になればと思い公開してみる。

2014/03/27

LDAP 登録用スクリプト

以前、某社内のユーザ管理を ldap 化した際に作成した ldap 登録用の ldif ファイルを生成するスクリプト。
漢字の名字と名前を入力すると kakashi を利用して ひらがなの名字、名前、ローマ字の名字、名前を自動的に取得し ldap 登録用の ldif ファイルを生成して ldap 登録まで自動で行う。
当初は csv 型式のファイルから必要な情報を取得して 連続で ldap に登録するバッチスクリプトとして作成したのだが、 初期登録が完了した後はたまに発生するユーザの追加処理にのみ使用するので 対話的に実行するコマンドに作り直した。

漢字をひらがな/ローマ字に変換するために kakashinkf が必要になる。
漢字のよみは自動では完全には取得できないので変換した値を確認する様にしている。 ローマ字の氏名は私の趣味で名字は全て大文字、名前はキャピタライズとしている。

  1#!/bin/sh
  2# All rights reserved, copyright (C) 2014, Mitzyuki IMAIZUMI
  3#
  4
  5# ldap 用設定
  6passwd="password"
  7org="dc=example,dc=com"
  8
  9# コマンド定義
 10id="`/usr/bin/id -u`"
 11kakasi=/usr/bin/kakasi
 12slappasswd="/usr/sbin/slappasswd -h {md5}"
 13toutf="/usr/bin/nkf -w"
 14toeuc="/usr/bin/nkf -e"
 15ldapadd="/usr/bin/ldapadd -x -D cn=Manager,${org}"
 16ldappasswd="/usr/bin/ldappasswd -x -D cn=Manager,${org}"
 17ldapsearch="/usr/bin/ldapsearch -LL -x -D cn=Manager,${org}"
 18
 19# uid 格納ファイル
 20uidnum=/etc/uid
 21
 22# OU 選択肢
 23oulist="Board Sales Develop Affair"
 24
 25# 入力用タイトル
 26_uid="ユーザID"
 27_sn="漢字名字"
 28_gn="漢字名前"
 29_hsn="名字よみ"
 30_hgn="名前よみ"
 31_tel="内線番号"
 32_ou="所属"
 33
 34#
 35# 汎用入力処理
 36#   $1: 入力された値を格納する変数名
 37#   $2: 必須フラグ
 38#
 39getans()
 40{
 41
 42    title=`eval "echo \\$_$1"`
 43    var=$1
 44    require=${2:-true}
 45
 46    while true
 47    do
 48        echo -n "${title} を入力して下さい: "
 49        read ans
 50
 51        if [ -z "${ans}" ]
 52        then
 53            if ${require}
 54            then
 55                echo "${title} の入力は必須です"
 56            else
 57                echo "${titoe} なし"
 58                break
 59            fi
 60        else
 61            break
 62        fi
 63    done
 64
 65    eval "$var=${ans}"
 66            
 67}
 68
 69#
 70# カナ確認/入力処理
 71#   $1: 入力された値を格納する変数名
 72#
 73getkana()
 74{
 75
 76    title=`eval "echo \\$_$1"`
 77    var=$1
 78
 79    echo -n "${title} は \"${2}\" で正しいですか?([y]/n): "
 80    read ans
 81    if [ "${ans}" = "n" ]
 82    then
 83        getans ${var}
 84    else
 85        eval "$var=${2}"
 86    fi
 87
 88}
 89
 90#
 91# 所属選択処理
 92#   $1: 入力された値を格納する変数名
 93#
 94getou()
 95{
 96
 97    title=`eval "echo \\$_$1"`
 98    var=$1
 99
100    while true
101    do
102        i=1
103        set ${oulist}
104        for o
105        do
106            echo "${i} ) ${o}"
107            i=`expr ${i} + 1`
108        done
109        echo -n "${title} を選択して下さい: "
110        read ans
111        if [ "${ans}" -ge 1 -a "${ans}" -lt $i ]
112        then
113            eval "$var=\${$ans}"
114            break
115        else
116            echo "入力が不正です"
117        fi
118    done
119
120}
121
122#
123# UID 取得処理
124#
125getuid()
126{
127
128    num=`cat ${uidnum}`
129    expr ${num} + 1 > ${uidnum}
130
131}
132
133#
134# People 用 ldif ファイルの生成
135#
136ladduser()
137{
138
139    cat <<-EOF | ${toutf}
140        dn:             uid=${uid},ou=People,${org}
141        uid:            ${uid}
142        objectclass:    posixAccount
143        objectclass:    top
144        objectclass:    shadowAccount
145        objectclass:    inetOrgPerson
146        cn:             ${sn} ${gn}
147        cn:             ${hsn} ${hgn}
148        cn:             ${rgn} ${rsn}
149        sn:             ${rsn}
150        givenname:      ${rgn}
151        mail:           ${uid}@example.com
152        o:              Example, Inc.
153        ou:             ${ou}
154        loginShell:     /bin/bash
155        uidNumber:      ${num}
156        gidNumber:      ${num}
157        homeDirectory:  /home/${uid}
158        gecos:          ${rgn} ${rsn}
159EOF
160    test -n "${tel}" && echo "telephoneNumber:  ${tel}"
161    echo
162
163}
164
165#
166# Group 用 ldif ファイルの生成
167#
168laddgroup()
169{
170
171    cat <<-EOF | ${toutf}
172        dn:             cn=${uid},ou=Group,${org}
173        objectclass:    posixGroup
174        objectclass:    top
175        cn:             ${uid}
176        userPassword:   {crypt}x
177        gidNumber:      ${num}
178        memberUid:      ${uid}
179
180EOF
181
182}
183
184#
185# ID が既に使用済みか確認する
186#   $1: ID
187#
188existldap()
189{
190
191    if ! egrep -q "^${1}:" /etc/passwd
192    then
193        return `${ldapsearch} uid=${1} -w ${passwd} | grep uid | wc -l`
194    else
195        return 1
196    fi
197
198}
199
200#
201# メイン処理
202#
203if [ "${id}" != "0" ]
204then
205    echo "root にて実行して下さい" 1>&2
206    exit 255
207fi
208
209# 情報入力
210while true
211do
212    while true
213    do
214        getans "uid"
215        if existldap ${uid}
216        then
217            break
218        fi
219        echo  "${uid} は既に使用されています" 1>&2
220    done
221    getans "sn"
222    getans "gn"
223    gettel "tel" false
224    getkana "hsn" "`echo ${sn} | ${toeuc} | ${kakasi} -JH | ${toutf}`"
225    getkana "hgn" "`echo ${gn} | ${toeuc} | ${kakasi} -JH | ${toutf}`"
226    getou "ou"
227
228    echo
229    for i in uid sn gn hsn hgn tel
230    do
231        eval "echo \$_$i: \$$i"
232    done
233
234    echo -n "この情報で正しいですか?([y]/n): "
235    read ans
236    if [ "${ans}" != "n" ]
237    then
238        break
239    fi
240done
241
242# ローマ字表記の名字取得(全て大文字)
243rsn=`echo ${hsn} | ${toeuc} | ${kakasi} -Ha -Ka -Ja -Ea -ka | tr '[a-z]' '[A-Z]'`
244# ローマ字表記の名前取得(キャピタライズ)
245rgn=`echo ${hgn} | ${toeuc} | ${kakasi} -Ha -Ka -Ja -Ea -ka |  awk '{ print toupper(substr($0, 1, 1)) substr($0, 2, length($0) - 1) }'`
246getuid
247# People 登録
248ladduser | ${ldapadd} -w ${passwd}
249# Group 登録
250laddgroup | ${ldapadd} -w ${passwd}
251# パスワード自動生成
252${ldappasswd} uid=${uid},ou=People,${org} -w ${passwd}
    

あまり需要がないとは思うけど折角なので公開してみる。

2014/03/25

キャピタライズ

英単語の先頭を大文字に置換する処理をキャピタライズというが、 このキャピタライズ処理を unix 標準のコマンドで実装しようという話。
置換と言えばまず思い浮かぶのが sed (1) なのだが、 GNU 拡張された sed (1) の場合は簡単に置換が可能なのだが、 FreeBSD の標準の sed (1) では簡単にいかない。

GNU sed の場合
$ echo "foo" | sed 's/\(.\)\(.*\)/\U\1\L\2/g'
Foo
            
GNU 拡張された sed (1) には \U/L という 演算子があるので大文字、小文字変換が簡単に可能である。

FreeBSD 標準の sed の場合
$ echo "foo" | sed 's/\(.\)\(.*\)/\U\1\L\2/g'
UfLoo
            
FreeBSD 標準の sed (1) だと正しく変換されない。

awk を利用した場合
sed (1) ではなく awk (1) を利用すれば FreeBSD でもキャピタライズ処理は簡単にできる。
$ echo "foo" | awk '{ print toupper(substr($0, 1, 1)) substr($0, 2, length($0) - 1) }'
Foo
            
awk (1) には大文字変換関数が実装されているので、 切り出した1文字目を大文字変換すれば簡単に実現できる。
ここまではシェル芸っぽくワンライナーでそれなりに綺麗に実現可能…かな?

FreeBSD で sed を利用した場合
どうしても FreeBSD の sed (1) を利用したい場合、 tr (1) も併用する事にはなるが以下の処理で可能だ。
$ eval `echo "foo" | sed 's/\(.\)\(.*\)/\/bin\/echo -n \1 | tr "[a-z]" "[A-Z]"; echo \2/g'`
Foo
            
sed (1) でtr (1) を利用した置換スクリプトを出力して eval (1) で実行した結果を表示しているのだが もはやネタとしか思えなくもない。
良い子は真似しないで下さい(笑

2013/11/23

Exif 情報から GPS の緯度/経度情報を削除する

例によって こちら のブログをネタに 他人の褌で相撲を取る記事です。
元記事では Exif 情報を操作する時に、 バイナリを直接操作するのではなく od (1) を利用してバイナリをテキスト型式に変換し、 テキスト操作した後で再度バイナリ型式に戻すという 正統的な手法でのアプローチをしています。
しかし dd (1) を利用すると シェルスクリプトからバイナリを操作できるので、 バイナリ型式の画像を直接操作してしまうというアプローチを取ってみます。

画像ファイルに格納されている Exif 情報を簡単に解析して GPS 情報を格納している領域 (GPSInfoIFD) から 緯度/経度情報だけをゼロに置換する処理となっています。

流石にシェル芸では無理なのでシェルスクリプトです。

  1#!/bin/sh
  2
  3#
  4# Endian を考慮して 4 バイト数字を取得する
  5#   一度 od(1) で 16 進数に変換しバイト並びを修正した後に 10 進数に変換する
  6#
  7#   $1: ファイル
  8#   $2: オフセット
  9#
 10get4Byte()
 11{
 12
 13    if [ ${endian} = "MM" ]
 14    then
 15        printf "%d" `dd if="${1}" bs=1 skip=${2} count=4 2> /dev/null | od -x |
 16            sed -n "s/^0000000 *\(..\)\(..\)[   ]*\(..\)\(..\)/0x\2\1\4\3/p"`
 17    else
 18        printf "%d" `dd if="${1}" bs=1 skip=${2} count=4 2> /dev/null | od -x |
 19            sed -n "s/^0000000 *\(..\)\(..\)[   ]*\(..\)\(..\)/0x\1\2\3\4/p"`
 20    fi
 21        
 22
 23}
 24
 25#
 26# Endian を考慮して 2 バイト数字を取得する
 27#   一度 od(1) で 16 進数に変換しバイト並びを修正した後に 10 進数に変換する
 28#
 29#   $1: ファイル
 30#   $2: オフセット
 31#
 32get2Byte()
 33{
 34
 35    if [ ${endian} = "MM" ]
 36    then
 37        printf "%d" `dd if="${1}" bs=1 skip=${2} count=2 2> /dev/null | od -x |
 38            sed -n "s/^0000000 *\(..\)\(..\)/0x\2\1/p"`
 39    else
 40        printf "%d" `dd if="${1}" bs=1 skip=${2} count=2 2> /dev/null | od -x |
 41            sed -n "s/^0000000 *\(..\)\(..\)/0x\1\2/p"`
 42    fi
 43
 44}
 45
 46#
 47# 任意の長さの文字列を取得する
 48#
 49#   $1: ファイル
 50#   $2: オフセット
 51#   $3: 文字列長
 52#
 53getString()
 54{
 55
 56    dd if="${1}" bs=1 skip=${2} count=${3} 2> /dev/null
 57
 58}
 59
 60#
 61# GPSInfoIFD の解析を行い緯度/経度情報をクリアする
 62#
 63#   $1: ファイル
 64#   $2: GPSInfoIFD のタグ数
 65#   $3: GPSInfoIFD のオフセット位置
 66#
 67gps()
 68{
 69
 70    local   i   j   offset  tag val
 71
 72    i=0
 73
 74    while [ ${i} -lt ${2} ]
 75    do
 76        # GPSInfoIFD のタグ情報
 77        offset=`expr ${3} + 14 + \( ${i} \* 12 \)`
 78        tag=`get2Byte "${1}" ${offset}`
 79        # タグ種類
 80        #   2: 緯度
 81        #   4: 経度
 82        if [ ${tag} -eq 2 -o ${tag} -eq 4 ]
 83        then
 84            # データの数とデータのオフセットを取得
 85            # 緯度/経度情報は値のタイプに RATIONAL(8バイト長) が指定されている
 86            num=`get4Byte "${1}" \`expr ${offset} + 4\``
 87            val=`get4Byte "${1}" \`expr ${offset} + 8\``
 88
 89            # 情報を削除するためにオフセットから 8 * データの数をゼロクリア
 90            dd if=/dev/zero of="${1}" bs=1 conv=notrunc \
 91                seek=`expr ${val} + 12` count=`expr ${num} \* 8` 2> /dev/null
 92        fi
 93        i=`expr $i + 1`
 94    done
 95
 96}
 97
 98#
 99# 0th IFD データの解析処理
100#
101#   $1: ファイル
102#   $2: 0th IFD のタグ数
103#
104ifd()
105{
106
107    local   i   offset  count   position
108
109    i=0
110
111    while [ ${i} -lt ${2} ]
112    do
113        # 0th IFD のタグ情報
114        offset=`expr 22 + \( ${i} \* 12 \)`
115        # GPSInfoIFDPointer の場合はオフセットとタグ数を取得
116        if [ `get2Byte "${1}" ${offset}` -eq 34853 ]
117        then
118            position=`get4Byte "${1}" \`expr ${offset} + 8\``
119            count=`get2Byte "${1}" \`expr ${position} + 12\``
120
121            gps "${1}" ${count} ${position}
122        fi
123        i=`expr $i + 1`
124    done
125
126}
127
128# 処理開始
129if [ "`getString "${1}" 6 4`" = "Exif" ]
130then
131    endian=`getString "${1}" 12 2`
132
133    ifd "${1}" `get2Byte "${1}" 20`
134fi
    

Image: before.jpg

処理前
Image: after.jpg

処理後
GPS 情報が削除されており緯度/経度がそれぞれゼロになっています。

2013/11/11

grep -o

少々前の話ではあるが、某ブログ にて grep -o について言及する記事が出ていた。
非常に便利そうな機能ではあるのだが、 POSIX (IEEE Std 1003.1, 2013 Edition) 準拠ではなく独自拡張の機能の様なので POSIX の範囲での実現方法を考えてみた。

例えば

$ grep -o 'href="[^"]*"'
    
$ sed -n 's/.*\(href="[^"]*"\).*/\1/p'
    
で代用可能なので、
$ cat << EOF | sed -n 's/.*\(href="[^"]*"\).*/\1/p'
<!DOCTYPE html>
<html>
    <body>
        <a href="http://aho.aho" target="_blank">あほ</a>
        <img src="aho.jpg" alt="aho">エロい画像</a>
    </body>
</html>
EOF
href="http://aho.aho"
    
という結果が得られる。
素晴らしい! POSIX 準拠なコマンドで POSIX 非準拠をぶち破ったぞ!
論破! 論破! 論破!

ただし grep (1) の -E オプションは POSIX に準拠しているが、 sed (1) の -E オプションは POSIX に定義されていないので、
$ grep -o -E '(src|href)="[^"]*"'
    
には対応できない。
FreeBSD の sed (1) など -E オプションが利用できれば
$ sed -n -E 's/.*((src|href)="[^"]*").*/\1/p'
    
とする事で対応できるのだが、
$ cat << EOF | sed -n -E 's/.*((src|href)="[^"]*").*/\1/p'
<!DOCTYPE html>
<html>
    <body>
        <a href="http://aho.aho" target="_blank">あほ</a>
        <img src="aho.jpg" alt="aho">エロい画像</a>
    </body>
</html>
EOF
href="http://aho.aho"
src="aho.jpg"
    
POSIX の範疇を逸脱した時点で grep -o を使えば良いとなってしまう。

残念っ!

2013/11/23 追記

頑張る必要は全くありません(笑)
普段は GNU の便利なオプションも使っていますし、 POSIX 準拠していないコマンドやオプションも使ってます。
純粋に楽しいからやってるだけです。

2013/10/02

国別フィルタの自動生成ツール

bsdhack.org ドメインは FreeBSD でサーバを構築しているのだが、 特定の国からのアタックや迷惑メイルが非常に多いので ipfw (8) によるフィルタリングを利用して 指定した国からのパケットを拒否する設定を自動生成するツールを作成した。
APNIC から IPアドレスの割当リストを取得して、 IPアドレスを CIDR 型式に修正した上で ipfw (8) のコマンドラインパラメタを自動生成している。
スクリプトは FreeBSD の ipfw (8) 向けだが、 出力部分を適宜修正する事で Linux の iptables (8) 向けの 設定も出力可能だと思う。

  1#!/usr/bin/perl
  2use Socket;
  3
  4# IP アドレス割当リスト取得 URL
  5$url = "http://ftp.apnic.net/stats/apnic/delegated-apnic-latest";
  6
  7# FreeBSD の ipfw (8) のルール番号
  8if(@ARGV < 1){
  9    $rule = 2000;
 10}
 11else{
 12    $rule = $ARGV[0];
 13}
 14
 15# 拒否対象の国コード
 16@country = ('KR', 'CN');
 17
 18foreach $i (@country){
 19    $country{$i} = 1;
 20}
 21
 22if(open(IN, "fetch -q -o - $url|")){
 23    # 拒否対象の国の場合は IP アドレスを保存
 24    while(<IN>){
 25        if(/^apnic\|(..)\|ipv4\|(\d+.\d+.\d+.\d)\|(\d+)/){
 26            if($country{$1}){
 27                $table{inet_aton($2)} = $3;
 28            }
 29        }
 30    }
 31    close(IN);
 32
 33    # 取得した IP 割当リストから開始アドレスと個数を取得
 34    foreach $net (sort keys %table){
 35        $addr = unpack('N', $net);
 36        $num = $table{$net};
 37        while($num == $num[0] && ($addr ^ $addr[0]) == $num){
 38            shift @addr;
 39            shift @num;
 40            $addr &= ~$num;
 41            $num <<= 1;
 42        }
 43        unshift(@addr, $addr);
 44        unshift(@num, $num);
 45    }
 46
 47    # IP アドレスと個数を CIDR 型式に変換して出力
 48    while (@addr){
 49        for($num = pop(@num), $mask = 32; $num > 1; $num >>= 1, $mask--){}
 50        $filt = inet_ntoa(pack('N', pop(@addr))) . "/$mask";
 51        # FreeBSD の ipfw(8) 向け出力
 52        print "\${fwcmd} add $rule deny ip from $filt to any in recv \${wan}\n";
 53        # Linux の iptables(8) 向け出力
 54        # print "\${iptables} -A INPUT -s $filt -j DROP\n";
 55        $rule++;
 56    }
 57}
 58
 590;
    

FreeBSD の場合は WAN 側のインタフェイス名が '${wan}' 変数に、 iptables コマンドの実行ファイル名が '${ipfw}' 変数に それぞれ格納されている事を前提としたスクリプトになっているので、 利用する際は適宜変更しながら使って下さい。

2013/09/18

main 関数の書き方

こちらの記事に触発されて、 C で同じお題のプログラム (echo (1) のサブセット)を書いてみた。

main には殆ど処理を記述せずに パラメタ解析や割り込みなどの初期設定のみをしてから、 実際のメイン処理(この場合は echo)を実行するというスタンスは同じです。
ただし、今回の例で言えば getopt(3) を利用して オプション解析を済ませた(実際に echo 出力する)パラメタ群のループは main 側で行う方が好きかな。

  1#include    <stdio.h>
  2#include    <unistd.h>
  3
  4void    echo(const char *p, int nonl)
  5{
  6
  7    /*
  8     * ここは以下の様にすると printf(3)、putchar(3) の呼び出しを
  9     * printf(3) の呼び出しのみに変更できソースもスッキリする。
 10     * 1 文字の出力を '%s' で変換する無駄が微妙に気になるのだが、
 11     * どちらかと言えば改行を出力する事の方が多いと思うので
 12     * putchar(3) の呼び出しがない方が良いのか
 13
 14     printf("%s%s", p, nonl ? "" : "\n");
 15
 16     */
 17
 18    printf("%s", p);
 19    if(!nonl)
 20        putchar('\n');
 21
 22}
 23
 24int     main(int argc, char *argv[])
 25{
 26
 27    int     i,
 28            nonl = 0;
 29
 30    while((i = getopt(argc, argv, "n")) != EOF){
 31        switch(i){
 32            case 'n':
 33                nonl = 1;
 34                break;
 35
 36            default:
 37                break;
 38        }
 39    }
 40
 41    for(i=optind; i<argc; i++)
 42        echo(argv[i], nonl);
 43
 44    exit(0);
 45
 46}
    

2013/08/02

シェルスクリプトでループ

シェルスクリプトでループ処理をする場合、 最も多用するのは while だと思う。
良く見かけるのは次の様な書き方だろう。

    :
while true
do
    処理
    if 条件
    then
        break
    fi
done
    :
    
勿論この書き方でも問題なく動作するが、 条件がコマンドの戻り値の場合は以下の方が簡潔に記述できる。
コマンドの戻り値が 0 (正常終了の場合) ループを続けたい場合。
    :
while コマンド
do
    処理
done
    :
    
コマンドの戻り値が 0 以外(正常終了以外の場合) ループを続けるのであれば while の替わりに until が利用できる。
例えば再起動中のリモートサーバに ssh (1) 接続したい場合、 以下の方法で繰り返し接続を試行する事ができる。
until ssh -o 'ConnectTimeout 5' リモートサーバ
do
    sleep 10
done
    
ssh (1) が成功するとリモートサーバから exit した時点で until の条件が真になるのでループを抜ける事ができる。


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