BASH | ||
---|---|---|
HOME |
サンプルスクリプトを通して Bash の変数,配列,展開,代入などの基本的な使い方を解剖。上記リファレンスマニュアルと併せて読むと理解しやすいだろう。項目の冒頭にサンプルスクリプトファイルへのリンクが張ってあるものでは、その下に表してあるのは補足を加えたスクリプトの出力となっている。
##### test of variable ##### VAR=onetwothree VARに値を代入 $VAR is: onetwothree echoしてみる。変数の参照は `$変数名' で ${#VAR} is: 11 スカラーの場合 `${#変数名}' とすると変数の値の文字数が得られる ## parameter substitutions ## 置換テスト `:=' のテスト。左辺がヌルの時には右辺の値が左辺に代入される。元の変数の値そのものが変化する。`:=' の `:' は省略可能 When VAR2= : ${VAR2:=$VAR} (変数などの展開/評価のみを行う `:' コマンドを用いる) $VAR2 is: onetwothree When VAR2=four : ${VAR2:=$VAR} $VAR2 is: four `:-' のテスト。`:=' と似た評価だが元の変数の値は変化しない。つまり左辺を採用するか右辺を採用するかの評価。`:' は省略可能 When VAR2= VAR3=${VAR2:-$VAR} (`:-' では元の値は変化しないので `:' コマンドは用を成さない) $VAR3 is: onetwothree When VAR2=four VAR3=${VAR2:-$VAR} $VAR3 is: four `:+' のテスト。`:-' とは逆に、左辺がヌルでない場合に右辺が選ばれる。`:' は省略可能 When VAR2= VAR3=${VAR2:+$VAR} $VAR3 is: When VAR2=four VAR3=${VAR2:+$VAR} $VAR3 is: onetwothree `:?' のテスト。左辺がヌルの時には、FALSE を返してプロセスが中止される When VAR2= (VAR3=${VAR2:?$VAR} ; echo " \$VAR3 is:" $VAR3) (テスト上まずいのでコマンド全体を `( )' で囲み子プロセスを生成) ./bashvar.sh: line 48: VAR2: onetwothree 子プロセスがエラー終了 $? is: 1 `$?' は直前の処理のリターンコードを格納する変数。結果は 1 つまり FALSE When VAR2=four (VAR3=${VAR2:?$VAR} ; echo " \$VAR3 is:" $VAR3) $VAR3 is: four $? is: 0 VAR=one; VAR2=; VAR3=three `${!変数ワイルドカード}' のテスト ${!VAR*} is: VAR VAR2 VAR3 VAR* にマッチする変数名のリストが得られる。宣言さえしてあればその値がヌルでも挙がる ## replace functions ## 単純な検索置換 VAR=onetwothree ${VAR/two/four} is: onefourthree 値の中の文字列 two を four に置換 $VAR is: onetwothree (元の変数自体は変わらない) ${VAR/two} is: onethree 置換文字列を指定しないと一致文字列が削除される ${VAR//t/xx} is: onexxwoxxhree 最初の `/' を二重にするとPerlのgフラグのように一致箇所全てを置換 ${VAR^t} is: oneTwothree 最初に現れる `t' を大文字に変換 ${VAR^^t} is: oneTwoThree 全ての `t' を大文字に変換 ${VAR^^} is: ONETWOTHREE 全てを大文字に変換。${VAR^^?} と同義 VARUP=ONETWOTHREE ${VARUP,T} is: ONEtWOTHREE 最初に現れる `T' を小文字に変換 ${VARUP,,T} is: ONEtWOtHREE 全ての `T' を小文字に変換 ${VARUP,,} is: onetwothree 全てを小文字に変換。${VARUP,,?} と同義 ${VAR#one*t} is: wothree 値の前方からマッチする最短の文字列を取り除く ${VAR##one*t} is: hree 値の前方からマッチする最長の文字列を取り除く ${VAR%t*ee} is: onetwo 値の後方からマッチする最短の文字列を取り除く ${VAR%%t*ee} is: one 値の後方からマッチする最長の文字列を取り除く ${VAR: 0: 3} is: one 一番左の文字から3文字を取り出す ${VAR: 2: 3} is: etw 3番目の文字から3文字分を取り出す ${VAR: 2} is: etwothree 第2パラメータを省略すると`末尾まで'の意となる ${VAR: -4: 3} is: hre 末尾から数えて4番目の文字から、3文字分を取り出す ${VAR: -4} is: hree 第2パラメータを省略すると`末尾まで'の意 ${VAR: -1} is: e 最後の1文字を取り出すにはこう
##### test of array ##### ARRAY=(one two) 配列の宣言はスペース区切りの値を( )で囲む ${#ARRAY[@]} is: 2 配列の要素数をカウントする ${!ARRAY[@]} is: 0 1 インデックスのリスト # Let us push one element at the end 配列の末尾に要素を追加(Perlやphpで言う`push') ELEMENTS=${#ARRAY[@]} 要素数を得ておく ARRAY[$ELEMENTS]=three 配列の`要素数'番目に新たな値を代入 ARRAY[${#ARRAY[@]}]=three と一気に書くことも可能 ${#ARRAY[@]} is: 3 増えた $ARRAY is: one [序数]を省くと0番目の要素が参照できる ${ARRAY[@]} is: one two three [@] と [*] による参照方法による違いのテスト echo ${ARRAY[@]} by "for" loop: [@] ではリスト one two three ${ARRAY[*]} is: one two three echo ${ARRAY[*]} by "for" loop: [*] でもリスト one two three "${ARRAY[@]}" is: one two three echo "${ARRAY[@]}" by "for" loop: ダブルクォートした [@] でもリスト one two three "${ARRAY[*]}" is: one two three echo "${ARRAY[*]}" by "for" loop: ダブルクォートした[*]では文字列。特殊変数 $IFS の最初の文字(通常はスペース)区切り one two three ## slice of array ## BashにもPerlの`スライス'に当たる概念がある ARRAY=(one two three four five six) ${ARRAY[@]: 0: 1} is: one ARRAY配列の最初から要素を1つ取り出す ${ARRAY[@]: 2: 3} is: three four five ARRAY配列の3番目から要素を3つ取り出す ${ARRAY[@]: 2} is: three four five six 第2パラメータを省略すると`最後まで'の意 ${ARRAY[@]: -4: 3} is: two three four 後ろから4番目の要素から3つを取り出す ${ARRAY[@]: -4} is: two three four five six 第2パラメータを省略すると最後まで ${ARRAY[@]: -1} is: five six 残念ながら -1 を指定しても`最後の要素'とはならない ${ARRAY[((${#ARRAY[@]}-1))]} is: six 要素数の分からない配列から最後の要素を取り出す方法 ## how to copy an array ## 配列をコピーしようとするとちょっと癖がある ARRAY=(one two three four five six) ACOPY=(${ARRAY[@]}) コピーする時に再び()で囲む必要がある echo "${ACOPY[@]}" by "for" loop: one two three four five six unset ACOPY ()を再使用しないとこうなってしまうという例 # without re-specifying parentheses it fails, even if you use 'declare' builtin. declare -a ACOPY=${ARRAY[@]} one two three four five six echo ${ACOPY[0]} one two three four five six 0番目の要素に丸ごと全部入ってしまっている unset ACOPY # another successfull way to copy an array 配列コピーのもうひとつのうまい方法 unset ARRAY[3] "3"番目の要素が抜けているスパース配列の場合も忠実な複製が可能 for i in "${!ARRAY[@]}"; do 元の配列からインデックス番号を取り出してひとつひとつアサインしてゆく ACOPY[$i]=${ARRAY[$i]} done # print values with indices 0 one 1 two 2 three 4 five 5 six ## replace functions for array ## 配列要素の置換。要素全てを一度に処理できる ARRAY=(one.bin.back two.bin.back three.bin.back) ${ARRAY[@]/bin/exe} is: one.exe.back two.exe.back three.exe.back bin を exe に置換 ${ARRAY[@]} is: one.bin.back two.bin.back three.bin.back 元の配列自体は変化しない ${ARRAY[@]#o*.} is: bin.back two.bin.back three.bin.back 各要素に対し、前からマッチする最短の文字列を削除 ${ARRAY[@]##o*.} is: back two.bin.back three.bin.back 各要素に対し、前からマッチする最長の文字列を削除 ${ARRAY[@]%.b*k} is: one.bin two.bin three.bin 同様に後ろから最短マッチで削除 ${ARRAY[@]%%.b*k} is: one two three 同様に後ろから最長マッチで削除
Bash では配列機能さえ怪しいが、連想配列などなおさら存在しない。しかし内蔵ファンクションである eval を使えば連想配列「もどき」が操れる。
##### Test of pseudo-hash with eval function ##### VALUES_ALPHA=one 例えば VALUES_ で始まる複数の変数を定義する VALUES_BETA=two VALUES_DELTA=three $VALUES_ALPHA is: one (当然) for i in ALPHA BETA DELTA ; do 変数名の尻の部分をループで回して eval で要素を取り出す eval echo \$VALUES_$i eval の引数としてのコマンド中では本来の変数がシェルに評価されないよう $ をエスケープ eval 'echo $VALUES_'$i 展開されなければいいわけなので、シングルクォートしても同じ done なお、場合によってはカッコやクォート記号も 「対の法則」 が崩れないようエスケープしないと Bash に文句を言われる one one two two three three # 変数のテストの中で述べた変数名のマッチング構文 ${!X*}。VALUES_ で始まる変数の「名前」のリストが得られる... ${!VALUES_*} is: VALUES_ALPHA VALUES_BETA VALUES_DELTA for i in ${!VALUES_*} ; do ...ので、それを利用してこういうループも書ける eval echo \$$i eval 'echo $'$i done one one two two three three
### positional parameters ### command: testfunc one two three ユーザ関数`testfunc'に3つの引数を与えてコール(testfuncの定義はソースを見よ) ---here inside function--- (1) $0 is: ./bashfunc.sh $0 はコールされたコマンドのパスを格納 (2) $1 is: one (3) $2 is: two (4) $3 is: three (5) $# is: 3 $# は引数の数を格納 (6) $@ is: one two three $@ と $* はともに引数全てを格納 (7) $* is: one two three (8) ${1+"$@"} is: one two three 引数全てを受け取ってそのままexecでプログラムに渡す時にD.J.Bersteinが好んで使う 書き方。`${1:+"$@"}' の省略形(前述の「変数 (作成,参照,代入,置換)」参照) (9) ${@: 1: 2} is: one two スライス。1番目から2つの引数を得る (10) echo $@ by "for" loop: $@ と $* の参照方法による違い。$@ではリスト one two three (11) echo $* by "for" loop: $* でもリスト one two three (12) echo "$@" by "for" loop: ダブルクォートした $@ でもリスト one two three (13) echo "$*" by "for" loop: ダブルクォートした $* では文字列。特殊変数 $IFS の最初の文字(通常はスペース)区切り one two three (14) echo ${1+"$@"} by "for" loop: one two three return 88 testfuncは`88'を戻り値として返すように書いてある ---going out of function--- $? is: 88 $? で戻り値を参照してみる doing echo $? is: 0 echo コマンドを行ったため、もう $? は書き換わってしまっている
### Tilde Expansion test ### チルダ系記号の展開テスト ~+ is: /home/hoge/cabinet/bashtest ~+ と $PWD は全く同じもの。カレントディレクトリ $PWD is: /home/hoge/cabinet/bashtest doing pushd .. pushd コマンドでひとつ上の階層へ移動してみる ~/cabinet ~/cabinet/bashtest ~+ is: /home/hoge/cabinet カレントディレクトリ $PWD is: /home/hoge/cabinet 全く同じ ~- is: /home/hoge/cabinet/bashtest ディレクトリスタックに記憶されたひとつ前のカレントディレクトリ $OLDPWD is: /home/hoge/cabinet/bashtest ~- と $OLDPWD は全く同じもの doing popd ~/cabinet/bashtest popd コマンドでさっきのディレクトリに戻れる ~+ is: /home/hoge/cabinet/bashtest カレントディレクトリ $PWD is: /home/hoge/cabinet/bashtest 全く同じ ~- is: /home/hoge/cabinet ディレクトリスタックの情報も変化した $OLDPWD is: /home/hoge/cabinet ~- と $OLDPWD は全く同じもの
## bash arithmetics ### 二重丸カッコ(( ))を使うと計算/比較が行える。expr などの外部プログラム不要 $((3 + 2)) is: 5 足し算 $((3 - 2)) is: 1 引き算 $((3 * 2)) is: 6 掛け算 $((3 / 2)) is: 1 割り算 $((3 % 2)) is: 1 割った余り(剰余) $((3 ** 2)) is: 9 3の2乗(累乗) ### bash comparisons ### 比較 $((3 == 2)) is: 0 $((3 != 2)) is: 1 $((3 > 2)) is: 1 $((3 < 2)) is: 0 $((3 >= 2)) is: 1 $((3 <= 2)) is: 0 ### bash substitutions ### 変数の値を計算によって変化させる方法 VAR=4 変数VARに4を代入しておく : $((VAR+=3)) 変数の展開/評価のみを行う `:' コマンドを使う。`VAR=$((VAR + 3))' と同じだが、よりスマート $VAR is: 7 変数自体が変化した : $((VAR++)) ひとつ増加させたければこういう書き方もある $VAR is: 8 : $((VAR--)) ひとつ減算 $VAR is: 7 : $((VAR-=3)) やっていることは 7 - 3 $VAR is: 4 : $((VAR*=3)) $VAR is: 12 : $((VAR/=3)) $VAR is: 4 : $((VAR%=3)) $VAR is: 1
ヒアドキュメントの一種 <<< を使うと、変数を cat や echo してパイプ渡しするより高速。これは <<< 直後の式を評価してから <<< の左辺の STDIN に渡す働きをする。
#!/bin/bash VAR="hello" awk <<< $VAR '{ if (/^h/){ print; } }'
上記は下記より速い;
echo $VAR |awk '{if (/^h/){ print; } }'
Bash では通常、グロブ(ワイルドカード) `*' はファイル名の頭のドット `.' にはヒットしない。だが、shopt によって dotglob を有効にすると、ドットファイルにもヒットさせることができるようになる。 dotglob 有効時の `*' は `.' (カレントディレクトリ) と `..' (ペアレントディレクトリ) にはヒットしないので、例えば、或るディレクトリ下の全てのファイルを cp や mv する時に便利だ。
#!/bin/bash shopt -s dotglob cp ./* anywhere_else/
用が済んだら shopt -u dotglob で以後無効にできる。
上記とは別の shopt オプション extglob を有効にすると、Bash でも少しはマシな正規表現が使えるようになる。
※ 本当の拡張正規表現も使える。次項参照。
#!/bin/bash shopt -s extglob
extglobで拡張される正規表現
extglob正規表現 | 説明 | Perlでいうと.. |
?(exp|exp|...) | 0回または 1回のexpかexpか... | (exp|exp|...)? |
*(exp|exp|...) | 0回以上のexpかexpか... | (exp|exp|...)* |
+(exp|exp|...) | 1回以上のexpかexpか... | (exp|exp|...)+ |
@(exp|exp|...) | きっかり 1回のexpかexpか... | (exp|exp|...){1} |
!(exp|exp|...) | expでもexpでも...でもないもの | (?<!(exp|exp|...)).* |
exp で 1文字を対象にしたいのであれば ?([a-f]) のようにクラスを併用することも可能。必ずしも `|' を使わなければならないわけではない。また、正規表現は ?(bash@(ref)) のように入れ子にすることもできる。
二重の `[[' を使い比較演算子に `=~' を使うと、右辺は拡張正規表現とみなされる。前項とは異なりこちらは本物の正規表現だ。面白いのは、マッチ部分が BASH_REMATCH という配列にアサインされるという点。
VAR=$(LANG=C date +'%c') # VAR='Wed Dec 22 22:51:41 2010' DAYOFWEEK=Wed PATTERN='^'$DAYOFWEEK' ([[:alpha:]]{3}) [[:digit:]]{2} ([0-9:]+)' [[ $VAR =~ $PATTERN ]] echo $? echo ${BASH_REMATCH[0]} # Wed Dec 22 22:51:41 echo ${BASH_REMATCH[1]} # Dec echo ${BASH_REMATCH[2]} # 22:51:41
戻り値 (上例での $?) は、マッチすると 0、マッチしなければ 1、正規表現の文法エラーがあると 2 となる。そして、マッチすれば、マッチした全体が BASH_REMATCH[0] に、最初の丸括弧 `()' 内に一致した部分が BASH_REMATCH[1] に、2番目の丸括弧内に一致した部分が BASH_REMATCH[2] に...という具合に代入される。つまり、`if [[...]]; then' という風に使えば、一致するかどうかの評価と必要部分の切り出しがいっぺんにできるわけだ。
書き方には少々癖があって、`[[' オペレータと左辺の間にはスペースを入れないとエラーとなる。また、Bash のバージョン 3.1, 3,2, 4.x で実装の混乱があるらしく、パターン (特にスペースを含む場合) は、例のように一旦シェル変数に入れてから使った方がいいようだ (Bash FAQ の E14 を参照)。変数に入れずにパターンを直接投入する場合には、正規表現はダブルクォーテーションかシングルクォーテーションで囲まなければならず、例の $DAYOFWEEK のように正規表現内で変数を使いたい時には、シェル変数として展開させるためにそこだけクォートの外に出してやる必要がある。それでも駄目な時は (一旦変数に入れる場合も含めて)、パターン内の全てのスペースに `\' を前置してエスケープするか、スペースを文字クラス `[[:blank:]]' で置き換えてみるといいだろう。更に、Bash 4.x には、`=~ ' の右辺に対するクォートの扱いを 3.1 互換に戻す compat31 オプションがあるそうなので、古いシェルスクリプトを 4.x 上で使わなければならない場合は正規表現を使用する箇所より上で `shopt -s compat31' を唱えてやるといいかもしれない。
Perl 特に CGI のファイル書き込みでよく使われる flock() だが、flock のメカニズムは元来、OS に (Cライブラリの一関数として) 備わっているもの。 Perl の flock() もそれを利用しているに過ぎないわけであり、インターフェイスさえ書けば言語にかかわらず使用できる。ところが、RedHat系では最近まで、シェルから直接使える一個のプログラムとしては装備されいなかったため、使うにはちょっと敷居が高かった。
だが遂に、Fedora Core 5 からは util-linux パッケージに含まれるようになった (RHELでは 5から)。とはいえ、使用しているディストリビューションにないからといってあきらめることはない。GNU フリーソフトウェア財団のサイト からソースをダウンロードしてコンパイルすればいい。ただし、RedHat で使うには、プリプロセッサマクロ部のヘッダファイルが足りなかったりしてコンパイルできず、修正が必要だった。その時 (RedHat EL4 で使った時) に修正した点を、 Makefile の (うちの環境用の) チューニングを含めてパッチとして取っておいたので、参考までに置いておく;
flock のアーカイブを展開後、ソースディレクトリへ cd してから `patch -p1 < path_to_patch ' でパッチを適用した後、Makefile を適宜編集してから make していただきたい。
※ flock のソースには RPMパッケージ作成のための SPECファイルも付属しているが、2代 3代前のディストリビューションでない限り、今後 util-linux パッケージに盛り込まれる可能性もあるので、敢えて手動コンパイルすることにした。そうすれば実行ファイルは遠慮がちに /usr/local/bin に入るので、今後もバッティングすることはないだろう。ただし、付属の man も /usr/local/man/ 下にインストールされるので、読むには `man -M /usr/local/man flock' と唱えなければならない。
flock のありがたいところは、単にロックファイルの有り無しで先客の有無を判断するのとは異なり、デッドロックの可能性がほとんどなく、しかも、前にも述べたように言語の垣根を越えて排他制御の判断ができることだ。例えば、 Perlスクリプトの中で或るファイルに掛けた flock() を、全く別の Bashスクリプトから確かめることもできる。ただし、 Perl なら open(FH,..) して flock(FH,'LOCK_xx') して処理を行い close(FH) という具合に操作が明白だが、シェルスクリプトだと最初戸惑った。そこで、ユーザ定義ファンクションとして組んだ具体例を挙げておく。これは実験のために組んだシェルスクリプトからの抜粋だ。
filelock() { # Flock using a lockfile. Argument: lockfile path. [ -e $1 ] || touch $1 exec 9<$1 flock -e -n 9 return $? } fileunlock() { # Release flock. Argument: lockfile path. exec 9<&- rm -f $1 }
上で出てくる 9 (※) はファイルディスクリプタ番号。 exec と リダイレクトで目的のロックファイルを入力ディスクリプタに結びつけている。これは Perl で open() するのと同じ効果となる。逆に 9<&- はディスクリプタを閉じることを意味し、その時点で flock は解かれるので、`flock -u $1' する必要はない。理論的にはロックファイルを削除する必要もない (そこが flock の良いところ) のだが、きれい好きなので...。
※ ディスクリプタにはなるべく、本来の業務に影響を与えない「とうでもいい」番号を使いたい。 flockプログラムの man ではディスクリプタ 200 を使った例が示されているが、Bash のリファレンスマニュアルによると 「9 を超えるディスクリプタはシェルが内部的に使うことがあるため、使用には注意を要する」 とある。最近の Bash では最大 255 まで使用可能なようで、実際に 200 でも動いたが、石橋を叩いておいて悪いことはない。また、Linux ドキュメンテーションプロジェクト (TLDP) にある "Advanced Bash-Scripting Guide" には、「ディスクリプタ 5 だけは問題を起こすことがあるので使用を控えよ」 との記述があるので、5 も避けた方がいいだろう。なお、Perl の場合、確実に flock() するために `open(FH,"+<",$file)' のように読み書き両方でオープンするのが常套手段だが、 Bashでは敢えて読み書き (つまり `exec 9<>$file') で開く必要はなさそう。
read id name state rest < <(echo 1 centos5 running unnecessary value1 value2) echo "id=$id" echo "name=$name" echo "state=$state" echo "rest=$rest"
Xen に付属している xendomains 起動スクリプトで多用されているテクニック。この例は、或るプログラム (上例では単純に `echo "1 centos5 running"') の出力を id, name, state, rest という 4つの変数に登録している。出力結果は、
id=1 name=centos5 state=running rest=unnecessary value1 value2
となる。Bash のビルトインファンクション read は、 stdin から入力を読み取り、各行をシェル変数 IFS で区切ってばらばらにして変数にアサインする。引数として与えられた変数名の数よりも入力の要素のほうが多い場合には、残りすべての要素は最後の変数にアサインされる。デフォルトの IFS はタブ文字 (スペースも含む) だが、IFS を変更すれば他の文字を区切りと認識させることもできる;
OLDIFS=$IFS IFS=":" read id name state rest < <(echo 1:centos5: running: unnecessary: value1:value2) echo "id=$id" echo "name=$name" echo "state=$state" echo "rest=$rest" IFS=$OLDIFS
(rest の値は不要なゴミだとみなせば)、得られる結果は上の例と全く同じになる。
ただし、この `< <(command...)' という書き方は Bash の拡張機能らしく、POSIX 互換モードでのシェルスクリプト (冒頭のインタプリタ指定が `#!/bin/bash' でなく `#!/bin/sh' になっている時) では構文エラーとなる。
プログラムの標準出力をシェルの標準入力で直接受け取り、まるで AWK のように直接処理できる。上記と同じく xendomains 起動スクリプトで多用されているテクニックだ。この技を使うと、AWK を使わなくても、複数行に渡る入力を 行に分解 -> 要素に分解 して様々な処理を行うことができる。ひとつ前の項 値のリストから変数を簡単に登録 (read + 名前付きパイプ) も参照していただきたい。
all_zombies_by_virsh() { local all=0 local zomb=0 while read LN; do read id name state < <(echo "$LN") if [ "$id" = "0" -o "$state" = "shut off" ]; then continue; fi : $((all++)) if test "$state" = "no state"; then : $((zomb++)) fi done < <(env LANG=C virsh list --all 2>/dev/null | grep -E '^ +' | grep -Ev '^ *Id +Name') [ $all -eq 0 ] && return 1 [ $all -eq $zomb ] && return 0 return 1 }
この例では、`env LANG=C virsh list .. | grep -Ev ...' というコマンドの出力を `while read' でまず LN という変数に 1行丸ごと取り込んで、さらに前項で解説したコマンドサブスティテューションテクニックを使って要素に分解して id, name, state という変数にそれぞれ代入し、それらを判断処理に使っている。`while read LN; do' という行とその下の `read id name ...' は、一息に
while read id name state; do
と書くこともできるが、echo は前後のスペースをうまいこと丸めてくれるので、例のままでもあながち無駄とはいえない。
なお、`< <(command...)' という書き方は POSIX 互換ではなく、シェルスクリプト冒頭のインタプリタ指定が `#!/bin/bash' でなく `#!/bin/sh' になっていると構文エラーとなる。どうしても `#!/bin/sh' 指定のスクリプトの中で同様のことをやらなければならない場合は、
env LANG=C virsh list --all 2>/dev/null |grep -E '^ +' |grep -Ev '^ *Id +Name' | while read LN; do .... done
とパイプで渡すのもアリ。ほぼ同様の動きが得られる。ただし、パイプ+ループにはまた別の落とし穴がある。パイプから受けた while ループの中身はスクリプト本体とは別のサブシェルの中で行われることなので、while ループの中で変数に値をセットしても、元のサブルーティンやグローバルスコープの変数に影響を与えることはできず、ループを出ると値や変数は消滅してしまうのだ。よって、
XVAR=0 env LANG=C virsh list --all 2>/dev/null |grep -E '^ +' |grep -Ev '^ *Id +Name' | while read LN; do XVAR=4 .... done echo $XVAR
最後の `echo $XVAR' は 4 を出力しない。出力されるのは、ループの前に設定した 0 だ。一方、コマンドサブスティテューションで while に渡した場合は、ちゃんと 4が出力される。
これに関しては Bash について特に語ることはない。ただ、かねがねネットワークの Netmask の /25 のような CIDR 方式の表記と 255.255.255.128 のようなデシマル4オクテット表記との変換の計算が面倒くさいなぁと思っていたため、シェルスクリプトにしてみただけだ。
`maskconv 25' のように呼ぶと CIDR の /25 をデシマルオクテット表現に変換したものをプリントする。逆に `maskconv 255.255.255.128' のように呼ぶと CIDR で出力する。外部コマンド bc, cut, wc, grep, printf を利用している。ただ、これを書いた少し後に気づいたのだが、RedHat系 Linux には ipcalc というユーティリティが付属していて、これも含めたいろいろなネットワーク計算ができるのだった。/etc/sysconfig/network-scripts/network-function スクリプトの中でも使用されている。ipcalc は initscripts パッケージに含まれる。
getopts はbashのビルトインファンクションで、シェルスクリプトへのオプションを簡単にバイナリプログラムのように処理できる。使い方をしょっちゅう忘れるのでメモ。`-g test ' のように引数を採るオプションは `:' を付ける。引数を採らないオプション (下の例での -d) は `:' を付けない。
while getopts "g:dc:" opt; do case $opt in g) GROUP=$OPTARG ;; d) DEBUG=1 ;; c) CFGFILE=$OPTARG ;; esac done shift $(($OPTIND -1))
シェルスクリプトから出る標準出力と標準エラー出力を全部 nkf を通して文字コード変換する。コマンド毎にいちいち `|nkf ...' などとやるのは野暮ったい。まず、下記のようなインクルードファイルを作っておく。
#!/bin/sh NPIPE=/tmp/log2sjis$$ trap "sleep 2; rm -f $NPIPE" 0 1 2 15 /bin/mknod $NPIPE p /usr/bin/nkf -uWs <$NPIPE & exec 1>&- exec 2>&- exec 1>$NPIPE exec 2>&1
やっているのは、mknod でテンポラリな FIFO デバイスファイル (`$$' は主シェルスクリプトの PID に置き換わる) を作成し、シェルの stderr を stdout にまとめて FIFO に注ぎ込み、FIFO の出力側を nkf につなぐという仕業。元々の stderr と stdout は閉じている。上記の例は、シェル環境が UTF-8 で、それを Shift_JIS へ変換することを前提にしている。ただし、nkf は、RHEL/Cent OS 6 では排除されてしまい、標準パッケージは提供されなくなってしまった。が、Fedora ARM からでもビルド済みRPMを入手するかソースRPMをダウンロードしてビルドすれば、インストールは容易い。インストールが無理な人用に、EL6系でも標準パッケージのある iconv を使った例も紹介しておこう。
#!/bin/sh NPIPE=/tmp/log2sjis$$ trap "sleep 2; rm -f $NPIPE" 0 1 2 15 /bin/mknod $NPIPE p /usr/bin/iconv -f UTF8 -t SHIFT_JIS <$NPIPE & exec 1>&- exec 2>&- exec 1>$NPIPE exec 2>&1
使い方は非常にシンプルで、主スクリプトの冒頭付近で上記ファイルをインクルードしてやるだけだ。使い回しもきく。仮に、上記のインクルードスクリプトを log2sjis と名付けたとしよう。
#!/bin/sh DIR=$(dirname $0) [ -f ${DIR}/log2sjis ] && . ${DIR}/log2sjis # Do whatever you want ...
主シェルスクリプトが終了した時や Ctl+c で中断した場合にテンポラリ FIFO ファイルが残留してどんどん貯まっていくと困る。そこで、先のインクルードファイルで、シェルのビルトインファンクションである trap を仕掛けている。trap の使い方は、
trap COMMAND SIGNAL[ SIGNLAL[ SIGNAL]...]
シグナル SIGNAL が発生した時に COMMAND を実行せよという意味だ。シグナルの種類は、`kill -l' または `trap -l' で調べることができる。下記は、RHEL5 で出力したものだ。
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX
trap のトリガーとして一般的なのは 0(EXIT時), 1(HUP), 2(Ctl+cなどで発生), 3(Ctl+\などで発生しcoreファイルを作成), 15(TERM)。9(KILL) は即死であり身辺整理をする暇などないので trap を仕掛けても意味がない。FIFOファイルを rm する前に `sleep 2;' を挿入しているのは、FIFOが出力をしきるのを待ってやるため。こうしないと、たいていの場合、出力が中途で終わってしまう。
rm -f /path/to/some/long/dirname/*
などとやった時に、エラーになってしまうことがある。
UNIX系OS にはひとつのコマンドの長さ (より正確に言うと、コマンドそのもの+オプション+引数+引数の区切りのヌル の合計) の制限が設けられている。シェルによる展開を受けた後の引数が長大になると、これに引っかかることになる。制限値の正体はOSの ARG_MAX というパラメータで、`getconf ARG_MAX' とコマンドすると値が分かる。RHEL4 および RHEL5 で調べたところ、32bit/64bit にかかわらず 131072 (128kB) だった。`LANG=C rm -f file*' のように環境変数を渡した場合にその宣言部分が制限に含まれるかどうかはよく分からない。
ARG_MAX制限によるエラーを回避する方策を幾つか挙げる。もちろん for ループで 1ファイルずつ処理する手はあるが、引数 (処理ファイル数) が多いと処理に時間が掛かるので除外。先に /path/to/some/long/dirname へ cd しておくという手は併用する価値があるだろう。
例えばファイルの頭文字で分割処理する。
rm -f /path/to/some/long/dirname/[a-f]* rm -f /path/to/some/long/dirname/[g-l]* ...
ポジショナルパラメータの shift ができるようファンクションにしておいて、例えば 9個ずつ束にして処理。`shift 9 || shift $#' は最後の半端を処理するため。shift X は残りのポジショナルパラメータの数が X より少ないとエラーになるからだ。
do_delete () { while [ $# -gt 0 ]; do rm -f ${@: 1: 9} shift 9 || shift $# done } do_delete ${FILELIST[@]}
xargs は、渡された引数をコマンドに一度に渡すだけでなく、全長が規定値を超える場合にはコマンドを複数回に分けて発行してくれる機能を備えている。RHEL4 及び 5 の `man xargs' で見てみるとデフォルト値は 131072 byte(つまり OS の ARG_MAX と同値)。xargs へのオプション `-s byte ' で変更 (縮小) することもできる。
echo ${FILELIST[@]} |xargs rm -f
「echoの時点で制限に引っかかるんじゃないの?」っと思った方もおられるだろう。パスなしの echo はシェルの内部コマンドであり、内部コマンドは ARG_MAX の制限を受けない。ARG_MAX は、execve() システムコールつまり新たなコマンドプロセスを起こす時に適用される規制らしい。
cp を xargs と組み合わせる場合は、コピー先を先に指定するため -t オプションを活用。
echo /srcdir/*.txt |xargs cp -pf -t /destdir
なお、find の場合は、最近のバージョンでは -exec アクションが xargs と同等の機能を備えているので、
find /srcdir/ -name abc* -print0 |xargs -0 rm -f
などとやらなくても、下記のように ';' を '+' にすることで引数ををまとめて処理しなお且つ ARG_MAX制限も回避できる;
find /srcdir/ -name *.txt -exec rm -f '{}' '+'
参考サイト:
ssh を使って、リモートマシン側で標準入力を cat するという手がある。処理をシェルスクリプトで自動化したい場合は、接続時にパスワードを入力しなくてすむように、リモートマシンにあらかじめ ssh の非対称鍵 ( パブリックキー) を設置しておくといい。ローカル側のシェルスクリプトは下記のような要領だ。
#!/bin/sh echo NOW=$(date +%Y%m%d%H%M%S) |ssh remote.hoge.cxm cat - \>/var/tmp/test.txt
最後のリダイレクト記号をエスケープしないと /var/tmp/test.txt はローカルマシン上にできてしまう。内容を複数行にしたければ、下記のようにヒアドキュメントを使うか、
cat <<EOM |ssh remote.hoge.cxm cat - \>/var/tmp/test.txt NOW=$(date +%Y%m%d%H%M%S) MYHOSTNAME=$(hostname -f) EOM
あるいは echo にエスケープシーケンスを解釈させる -e オプションを付けて、
echo -e "NOW=$(date +%Y%m%d%H%M%S)\nMYHOSTNAME=$(hostname -f)" | \ ssh remote.hoge.cxm cat - \>/var/tmp/test.txt