オフィシャルドキュメント: BASH Reference Manual (FSF)

Bash 変数の基礎と小技集

サンプルスクリプトを通して Bash の変数,配列,展開,代入などの基本的な使い方を解剖。上記リファレンスマニュアルと併せて読むと理解しやすいだろう。項目の冒頭にサンプルスクリプトファイルへのリンクが張ってあるものでは、その下に表してあるのは補足を加えたスクリプトの出力となっている。

Table of Contents

変数 (作成,参照,代入,置換)

bashvar.sh

##### 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文字を取り出すにはこう

配列 (作成,参照,スライス,置換)

basharray.sh

##### 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                      同様に後ろから最長マッチで削除

evalを使った擬似ハッシュ(連想配列)

Bash では配列機能さえ怪しいが、連想配列などなおさら存在しない。しかし内蔵ファンクションである eval を使えば連想配列「もどき」が操れる。

bashhash.sh

##### 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

ポジショナルパラメータ, ファンクション, 戻り値

bashfunc.sh

### 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 コマンドを行ったため、もう $? は書き換わってしまっている

チルダの展開

bashtild.sh

### 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 は全く同じもの

数値演算,比較,代入

basharith.sh

## 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より速い <<<

ヒアドキュメントの一種 <<< を使うと、変数を catecho してパイプ渡しするより高速。これは <<< 直後の式を評価してから <<< の左辺の STDIN に渡す働きをする。

#!/bin/bash
VAR="hello"
awk <<< $VAR '{ if (/^h/){ print; } }'

上記は下記より速い;

echo $VAR |awk '{if (/^h/){ print; } }'

ドットで始まるファイルを特別扱いしない (dotglob)

Bash では通常、グロブ(ワイルドカード) `*' はファイル名の頭のドット `.' にはヒットしない。だが、shopt によって dotglob を有効にすると、ドットファイルにもヒットさせることができるようになる。 dotglob 有効時の `*' は `.' (カレントディレクトリ) と `..' (ペアレントディレクトリ) にはヒットしないので、例えば、或るディレクトリ下の全てのファイルを cpmv する時に便利だ。

#!/bin/bash
shopt -s dotglob
cp ./* anywhere_else/

用が済んだら shopt -u dotglob で以後無効にできる。

Bashで正規表現(1) (extglob)

上記とは別の 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で正規表現(2) ([[ =~ ]]オペレータ)

二重の `[[' を使い比較演算子に `=~' を使うと、右辺は拡張正規表現とみなされる。前項とは異なりこちらは本物の正規表現だ。面白いのは、マッチ部分が 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' を唱えてやるといいかもしれない。

シェルスクリプトでflock

Perl 特に CGI のファイル書き込みでよく使われる flock() だが、flock のメカニズムは元来、OS に (Cライブラリの一関数として) 備わっているもの。 Perlflock() もそれを利用しているに過ぎないわけであり、インターフェイスさえ書けば言語にかかわらず使用できる。ところが、RedHat系では最近まで、シェルから直接使える一個のプログラムとしては装備されいなかったため、使うにはちょっと敷居が高かった。

だが遂に、Fedora Core 5 からは util-linux パッケージに含まれるようになった (RHELでは 5から)。とはいえ、使用しているディストリビューションにないからといってあきらめることはない。GNU フリーソフトウェア財団のサイト からソースをダウンロードしてコンパイルすればいい。ただし、RedHat で使うには、プリプロセッサマクロ部のヘッダファイルが足りなかったりしてコンパイルできず、修正が必要だった。その時 (RedHat EL4 で使った時) に修正した点を、 Makefile の (うちの環境用の) チューニングを含めてパッチとして取っておいたので、参考までに置いておく;

flock-1.0.1-rhel4.patch

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 と リダイレクトで目的のロックファイルを入力ディスクリプタに結びつけている。これは Perlopen() するのと同じ効果となる。逆に 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 + 名前付きパイプ)

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' になっている時) では構文エラーとなる。

この `< <(command)' の意味が今ひとつ謎だったのだが、wiki.bash-hackers.orgのこの記事でやっとスッとした。これは名前付きパイプの中でも Command Substitution(コマンド代入)と呼ばれるもので、'<(command)' は、command の実行結果を格納した /dev/fd/xx (xx は例えば63) というファイルディスクリプタファイルに化ける。その手前の '<' は通常のファイルからのリダイレクトで、そのファイルがたまたま fdファイルだというだけ。中間的には '</dev/fd/63' のように解釈されているわけだ。ただし、通常ならばリダイレクト記号の右にはスペースを入れても入れなくてもいいが、ふたつ続くと Bash はこれをヒアドキュメントの開始マーク '<<' だと解釈してしまうので、この場合はスペースが必要なのである。

 

プログラムの複数行出力を直接処理 (while + read + 名前付きパイプ)

プログラムの標準出力をシェルの標準入力で直接受け取り、まるで 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が出力される。

ネットマスクの CIDR とオクテット・デシマル表記の変換

これに関しては 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 スクリプトの中でも使用されている。ipcalcinitscripts パッケージに含まれる。

getopts の使い方

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))

スクリプトの標準出力を根こそぎ文字コード変換するストリーミングフィルタ (execリダイレクト、trapの使い方)

シェルスクリプトから出る標準出力と標準エラー出力を全部 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 に置き換わる) を作成し、シェルの stderrstdout にまとめて FIFO に注ぎ込み、FIFO の出力側を nkf につなぐという仕業。元々の stderrstdout は閉じている。上記の例は、シェル環境が 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が出力をしきるのを待ってやるため。こうしないと、たいていの場合、出力が中途で終わってしまう。

コマンド引数の長さ制限を回避する (ARG_MAXとxargs)

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/dirnamecd しておくという手は併用する価値があるだろう。

小分けにする(1)

例えばファイルの頭文字で分割処理する。

rm -f /path/to/some/long/dirname/[a-f]*
rm -f /path/to/some/long/dirname/[g-l]*
...
小分けにする(2)

ポジショナルパラメータの 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 を活用

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() システムコールつまり新たなコマンドプロセスを起こす時に適用される規制らしい。

cpxargs と組み合わせる場合は、コピー先を先に指定するため -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