読者です 読者をやめる 読者になる 読者になる

ここ最近よく使ってるものと、そのときにはまった落とし穴

bash

うちの職場には自分ともう1人(Aさんとしよう)ツールを書く人がいる。 Aさんはperl派で、ぼくはbash派。perlのいいところはいろいろ柔軟にできるところで、 bashのいいところはワンライナーでほいほい書けるところだと思う。 問題はどちらで書いても保守できる人がいないところ。

それは置いといて。bashでコマンドライン引数をいじってるときにはまった罠。 コマンドライン引数がいくつあるかを表すのに、bashだと$#っていう特別な変数が割り当てられてる。 今回やりたかったのは、引数の数を2以上にして、最初の1個以外をオプションとして使うようなコマンド。 例えばこんな感じ。grep -eの数を増やしたいわけ。

#!/bin/bash

cat $2 | grep -e $2 -e $3 -e $4 > $1

どうしようかなと考えた結果がこれ。

#!/bin/bash

num=$#
args=($@)
GREPARG=""
for i in $(seq 2 $((num-1)))
do
  GREPARG="${GREPARG} -e ${args[$i]}"
done

cat $2 | grep $GREPARG > $1

美しくないのがnum=$#とargs=($@)なんだけど、 こいつらをそのまま使おうとするとなぜか動かない。 うーん、別の何かと認識されてるのかな・・・

(9/11追記)
bashでも配列のスライスが取れるらしい。 記法はこんな感じ。start_indexからlength分の配列を切り出す。

${A[@]:start_index:length}

lengthを配列の長さ以上にしても、エラーにならずに配列の要素数までで止まる。
参考:http://stackoverflow.com/questions/1335815/how-to-slice-an-array-in-bash

書き直すとこうなる。美しくなったね。

#!/bin/bash

GREPARG=""
# $@の3番目($3)から配列末尾まで。
# 長さ指定の部分ははみ出しても問題無さそう。配列要素分だけでおしまいになる。
for i in ${@:3:$#}
do
  GREPARG="${GREPARG} -e $i"
done

cat $2 | grep $GREPARG > $1
expect

teratermを卒業して、expectを使うようになった。 何十ものホストに同時にログインして、同じコマンドを叩かなければ ならないときは特に便利。踏み台を経由する必要があっても、expectなら大丈夫。 ここに至るまでの過程はこんな感じ。

  1. teratermのブロードキャストコマンドで叩く(時間とリソースが食われまくる上に、コピペできないせいでミス誘発)
  2. teratermのマクロをバッチファイルで呼びまくる(ミスはないけどリソースが食われまくる。ポータビリティかけらもない)
  3. forループでexpectをぐるぐる回す(作業用サーバががんばってくれるので自分にダメージないし、ログインできれば使える)

expectはこんな感じで書く。基本的にはシェルスクリプトに埋め込んで使ってるので、 いささかバックスラッシュが面倒ではある。 流行りのGNU Parallelを使うべきなのかもしれないなあと思うけども、ひとまず手が足りてるのでこれでOKとしてる。

#!/bin/bash

# 踏み台にするサーバと目的地のサーバ
fumidai="fumidai.hoge.com"
main=("main1.hoge.com" "main2.hoge.com" "main3.hoge.com" "main4.hoge.com")

# ログファイルに名前をつけるのが面倒なので、日付を付けておく
now=`date +"%Y%m%d-%H%M%S"`

# ログが貯まるまで待ってから採取
sleeptime=10

# forループ+かっこ+&でなんちゃって並列。ログはちゃんと残しておく。
for i in $seq(0 3)
do
  (expect -c "
    set timeout 60
    spawn ssh $fumidai
    # まずは踏み台を経由
    expect \"user@fumidai ~ $\"
    # mainサーバへ。よく\nを忘れて途中で止まってる・・・
    send   \"ssh ${main[$i]}\n\"
    # expect -- \"プロンプト\"にすると、以降を文字列とみなしてくれるので、->みたいなプロンプトでも動く。
    expect -- \"->\"
    send   \"log_start\n\"
    sleep $sleeptime
    expect -- \"->\"
    send   \"log_get\n\"
    expect -- \"->\"
    send   \"logout\n\"

    # 踏み台に戻る
    expect \"user@fumidai ~ $\"
    send   \"logout\n\"
  " >> ${main[$i]}$now".log" )&
done

expect使っててこの前はまった罠。なぜか無限に同じコマンドを叩かなきゃいけない場面に出くわしたときに、 while使ったんだけど、while内ではusleep使えないらしい。sleepなら大丈夫だった。 要はこんな感じ。

#!/bin/bash

# 踏み台にするサーバと目的地のサーバ
fumidai="fumidai.hoge.com"
main=("main1.hoge.com" "main2.hoge.com" "main3.hoge.com" "main4.hoge.com")

# ログファイルに名前をつけるのが面倒なので、日付を付けておく
now=`date +"%Y%m%d-%H%M%S"`

# コマンド間の間隔調整
sleeptime=1

for i in $seq(0 3)
do
  (expect -c "
    set timeout 60
    spawn ssh $fumidai
    # まずは踏み台を経由
    expect \"user@fumidai ~ $\"
    # mainサーバへ。よく\nを忘れて途中で止まってる・・・

    send   \"ssh ${main[$i]}\n\"

    while {1} {
      expect -- \"->\"
      send   \"log_print\n\"
      # usleep はダメだけど、sleepならOK
      sleep $sleeptime
    }

    # 踏み台に戻る
    expect \"user@fumidai ~ $\"
    send   \"logout\n\"
  " >> ${main[$i]}$now".log" )&
done