Bash 算術式の応用的な使用方法について列挙していきます!
※この記事は AdC 2016 Shell Script 4日目
Bash $((算術式)) のすべて - Qiita の衛星記事です。
関連記事一覧:
- Bash $((算術式)) のすべて - Qiita
- Bash $((算術式)) のすべて - A 基本編 - Qiita
- Bash $((算術式)) のすべて - B 罠・バグ回避編 - Qiita
- Bash $((算術式)) のすべて - C 応用編 - Qiita (この記事)
記事の構成:
各節は比較的独立した内容です。できるだけ簡単なものから順に紹介していきます。まとめに関しては親記事 の §3 を御覧ください。
C1. 自作シェル関数を算術式に対応させる
初めに紹介するのはお手軽な小技です。皆様は自分で書いたシェル関数で、整数の引数を取るようなものをお持ちではありませんか? 例えば、以下のような関数です。
function hello {
local year=$1; shift
local month=$1; shift
local date=$1; shift
printf '%04d-%02d-%02d (%04d年%02d月%02d日)\n' \
"$year" "$month" "$date" \
"$year" "$month" "$date"
}
この関数は引数に整数値を渡さないと期待通りに動きません。計算をしたければ、以下のように明示的に算術式展開を書かなければなりません。
hello $((2016+delta)) 12 4
しかし、この関数を算術式に対応させてシンプルに
hello 2016+delta 12 4
などと呼び出せる様にするのはとっても簡単です! 変数の宣言にオプション -i
をつければ良いのです。
function hello {
local -i year=$1; shift
local -i month=$1; shift
local -i date=$1; shift
printf '%04d-%02d-%02d (%04d年%02d月%02d日)\n' \
"$year" "$month" "$date" \
"$year" "$month" "$date"
}
あっという間に算術式に対応した賢いシェル関数になりました!
C2. let
× ブレース展開技
みなさま算術式を使った処理で例えば以下の様な記述をしていませんか?
s=0
for ((i=0;i<100;i++)); do
((s+=i*(i+1)))
done
echo $s # 結果: 333300
ループ回数が固定であれば、ブレース展開を使って
s=0
for i in {0..99}; do
((s+=i*(i+1)))
done
echo $s # 結果: 333300
とした方が文字数が少ないですし実行速度的にも速いです。でも! 更にブレース展開と let
を組み合わせれば上記の処理をたった 1 行でかけてしまいます。しかも更に速くなります!
let s=0 i={0..99},'s+=i*(i+1)' # ← 1行で書ける
echo $s # 結果: 333300
C3. 算術式だけで処理を記述するということ
算術式を使えば変数に値を代入することができます。そして、カンマ演算子を使えば複数の処理を繋げることができます。条件演算子や論理演算子の短絡評価を利用すれば条件分岐もできます。つまり、算術式だけでも色々な処理ができるということになります。
C3.1 例
例えば C 言語で書かれた以下のような関数を考えてみます。この関数は文字コードを16進数の文字と思って対応する整数に変換します。細かいことを考えると面倒なので、アルファベットの文字コードは連続していると考えます (数字の文字コードが連続していることはC規格上保証されていますので気にしなくて大丈夫です)。
// 注意: アルファベットの a-f, A-F が連続している処理系のみで有効。
int get_value_of_xdigit(char xdigit) {
if ('0' <= xdigit && xdigit <= '9')
return xdigit - '0';
else if ('a' <= xdigit && xdigit <= 'f')
return xdigit - 'a' + 10;
else if ('A' <= xdigit && xdigit <= 'F')
return xdigit - 'A' + 10;
else
return -1;
}
この関数はBashでは算術式だけを使って以下の様に記述できます。
function get_value_of_xdigit ((
(0x30<=$1&&$1<=0x39)?(
return=$1-0x30
):((0x61<=$1&&$1<0x66)?(
return=$1-0x61+10
):((0x41<=$1&&$1<0x46)?(
return=$1-0x41+10
):(
return=-1
))),1
))
面倒なので ASCII コードの時の文字コードを直接使いました。シェル関数では整数の戻り値を返す方法がないので (終了ステータスは返せますが返せる値の範囲が小さすぎます)、代わりに return というシェル変数に戻り値を代入するという "呼び出し規約" にしました。
C3.2 複数の式文を繋げる
もう少しC言語の記述との対応について考えてみましょう。先ず、複数の文は算術式ではカンマ演算子を使って繋げることができます。
i = 1;
j = 2;
((
i=1,
j=2
))
更に、C言語の複文 { 文 文 … }
は、全体を普通の括弧で囲むだけで問題ありません。
{
i = 1;
j = 2;
}
((
(
i=1,
j=2
)
))
C3.3 if
文
Bash 算術式自体にはいわゆるif
文は備わっていません (勿論、算術式ではなくてシェルの構文として if
キーワードがありますが)。しかし、論理積演算子や条件演算子を用いれば if
文を実現できます。
if (i == 1) {
a = 1234;
}
((
(i==1)&&(
a=1234
)
))
if ~ else ~
は条件演算子を使えばできます。
if (i <= 10) {
a = 0;
b = 1234;
} else {
a = i++;
}
((
(i<=10)?(
a=0,
b=1234
):(
a=i++
)
))
更に unless
のようなこともできます!
if (!(i == 1)) {
y = 2016;
}
((
(i==1)||(
y=2016
)
))
C3.4 switch
文
if
文ができるのですから当然 switch
文もできます! というか if
文に変換してから算術式にするだけですが。
switch (i % 4) {
case 0:
x = 2016 + i;
break;
case 1:
y = 2016 - i;
break;
case 2:
x = 2016 - i;
y = 2016 + i;
break;
default:
x = y = 2016;
break;
}
((
case=i%4,
(case==0)?(
x=2016+i
):((case==1)?(
y=2016-i
):((case==2)?(
x=2016-i,
y=2016+i
):(
x=y=2016
)))
))
また別の方法として処理をテーブルにする方法もあります。
_jmptable1=(
'x=2016+i,1'
'y=2016-i,1'
'x=2016-i,y=2016+i,1'
)
((
_jmptable1[i%4]||(
x=y=2016
)
))
さて C言語では break;
を省略すれば fall-through させることができます。これを実現するのは面倒ですが何とか無理やり…
switch (i % 4) {
case 0:
x = 2016 + i;
/*FALL-THROUGH*/
case 1:
y = 2016 - i;
}
switch='_fallthrough=0'
break='_fallthrough=0'
FALLTHROUGH='_fallthrough=1'
((
switch,case=i%4,(
(case==0)&&(
x=2016+i,
FALLTHROUGH
),
(_fallthrough||case==1)&&(
y=2016-i,
break
)
)
))
これはちょっと汚いので使いづらいですね…。
C3.5 ループ (for
・while
・do
) およびジャンプ (goto
・throw
) など
ループは再帰に変換すれば可能です。ジャンプはそのままだと難しいですが、処理を配列に入れて、配列要素を順番に評価していく方式にすれば、ジャンプは次に実行する配列要素の変更として記述できます。かなり面倒な変換なので実用にはならないでしょう。
親記事 §5 "チューリング完全" で改めて取り扱うことにします。
C4. 変数名の入った変数 ≒ 参照(ポインタ)
C4.1 指定した変数名の変数に代入を行う
C言語でポインタを使うというと例えば以下のような形になります。
void process(int *result) {
*result = 20161204;
}
int main(void) {
int hello;
process(&hello);
return 0;
}
或いは C++ で:
void process(int& result) {
result = 20161204;
}
int main() {
int hello;
process(hello);
return 0;
}
これは普通 Bash では eval
だとか printf -v
だとかを使って実現します。
function process {
local result=$1
eval $result=20161204
}
process hello
echo $hello # 結果: 20161204
しかし! 算術式の世界では (つまり整数しか扱わないなら) 難しく考える必要はありません! こうです:
function process (($1=20161204))
算術式コマンドの実行は、1. 変数展開・コマンド置換・算術式展開を処理してから 2. 実際の算術式評価を行うという二段構成になっているので、わざわざ eval
を使う必要はないのです。
C4.2 指定した変数名の変数から値を取り出す
普通 Bash で変数名の入った変数を使って、その変数名の変数から値を取り出すには以下のように ${!varname}
を使います。
function world {
local varname=$1
((result=(${!varname})*2))
}
a=1234
world a
echo $result # 結果: a=2468
しかし算術式ならば実は深く考える必要はありません。単に以下の様にすればよいです。
function world {
local varname=$1
((result=varname*2))
}
というのも、算術式評価は元から再帰的に行われるからです。
C5. 変数名の結合 ≒ 構造体・クラス
Bash の算術式ではスカラーの整数しか扱えないと思っていらっしゃいませんか。まあ、そうです。スカラーの整数しか扱えません。しかし、工夫次第で複数の変数をひとまとまりとして取り扱うことも可能です。
実はこの手法は技術的には算術式と関係ありませんが、算術式を使うとメンバ変数のアクセスをそれっぽい見た目で記述することができます。基本的には前節§C4の方法の応用です。算術式で変数名を簡単に結合できることを利用します。
例えば以下のような C 言語のコードを考えます。
struct Rect {
int x, y, w, h;
};
int process(Rect* rect) {
return rect->w * rect->h;
}
int main(void) {
Rect rect;
process(&rect);
return 0;
}
これは算術式+色々を使えば、Bash では以下の様に書かれます。
Rect=(x y w h)
function process ((
return=${1}_w*${1}_h
))
function main {
local "${Rect[@]/#/rect_}" # 宣言 = rect_x ~ rect_h の変数を宣言
process rect # 参照渡し = 接頭辞 rect を指定
}
鍵は ${1}
と _w
を結合して独立した変数名 ${1}_w
を作ることにあります! 以下、一つ一つ詳しく考えていくことにしましょう。
C5.1 構造体の定義
メンバ変数名の一覧を持っていれば十分です。ここでは型名の配列に格納することにします。
Rect=(x y w h)
C5.2 変数の宣言
構造体のようなものを実現するといっても、Bash では精々複数の変数を組みにして扱うことしかできません。しかし、その複数の変数を意識せずに定義する方法があれば便利です。その為に、先に定義したメンバ変数名リスト Rect
を使います。
例えば、構造体 Rect
の変数として rect
を定義するといえば、実際には変数 rect_x
~ rect_h
を定義するということと考えます。これは以下のように書けます。
declare "${Rect[@]/#/rect_}"
C5.3 メンバ変数の読み書き
rect.x = 1;
rect.y = 2;
rect.w = 3;
rect.h = 4;
これは簡単です。以下のようにすれば良いです。
((
rect_x=1,
rect_y=2,
rect_w=3,
rect_h=4
))
更に、ptr=rect
の様に "参照" が変数 ptr
に格納されている場合には、
((
${ptr}_x=1,
${ptr}_y=2,
${ptr}_w=3,
${ptr}_h=4
))
の様にすれば良いです。C言語で言えば
(*ptr).x = 1;
(*ptr).y = 2;
(*ptr).w = 3;
(*ptr).h = 4;
と等価です (アロー演算子 ->
があるのでもっと簡潔に書けますが、Bash 算術式ではアロー演算子に対応するものを実現するのは難しそうです)。
あとは、rect
という文字列を関数に渡せば、関数内から外の構造体にもアクセスできるようになります。
C5.4 メンバ関数
関数に参照を渡すことができるとなればメンバ関数だって定義できます!
struct Rect {
int x, y, w, h;
int get_area() const;
void move(int x, int y);
};
int Rect::get_area() const {
return this->w * this->h;
}
void Rect::move(int dx, int dy) {
this->x += dx;
this->y += dy;
}
Rect=(x y w h)
function Rect::get_area {
local this=$1; shift
((return=${this}_w*${this}_h))
}
function Rect::move {
local this=$1; shift
local dx=$1 dy=$2
((
${this}_x+=dx,
${this}_y+=dy
))
}
呼び出しもちゃんとできます!
Rect rect;
rect_x = 1; rect_y = 2; rect_w = 3; rect_h = 4;
int r = rect.get_area
printf("%d\n", r);
rect.move(2, 1);
printf("%d %d\n", rect.x, rect.y);
declare "${Rect[@]/#/rect_}"
rect_x=1 rect_y=2 rect_w=3 rect_h=4
Rect::get_area rect
echo $return # 結果: 12
Rect::move rect 2 1
echo $rect_x $rect_y # 結果: 3 3
C5.5 コピー
ちょっと実装が面倒なのがコピー操作です。愚直には一つずつメンバをコピーする関数を構造体型毎に定義すれば良いです。しかし、C言語の場合には自分でわざわざ関数を定義しなくても構造体のコピーはできました。Bash でも、折角メンバ変数名のリストを持っているのですから、コピーを行う汎用の関数を定義できそうです。
function operator= {
local _type=$1; shift
local _dst=$1 _src=$2 _member
eval local _members=\(\"${$_type[@]}\"\)
for _member in "${_members[@]}"; do
((${_dst}_$_member=${_src}_$_member))
done
}
少し汚い実装ですがまあ致し方ないでしょう。実際にコピーする時はこうです:
rect1 = rect2;
operator= Rect rect1 rect2
C5.6 new
"ヒープ" を作るのも簡単です
_heapIndex=0
function new {
local ptrName=$1
local typeName=$2
local ptr=_heap$((_heapIndex++))
eval $ptrName=$ptr
eval "declare -g \"\${$typeName[@]/#/${ptr}_}\""
}
new prect Rect
C5.7 派生・継承
更に派生・メンバの継承もできてしまいます!
struct Board: Rect {
int color;
};
Board=("${Rect[@]}" color)
変数名が被ると問題になりますが、まあ、そうならない様に注意すれば何とかなります。
C5.8 仮想関数
更に! 仮想テーブルの ID を持つメンバ _vptr
を定義してやれば、仮想関数・動的多態性だって実現できてしまいます!
_number_of_types=0
typeinfo=()
function virtcall {
local this=$1 fname=$2; shift 2
local _type=${typeinfo[${this}_vtbl]}
local _entry=${_type}_vf_${fname}
"${!_entry}" "$this" "$@"
}
#----------------------------------------
# Rect 定義
Rect=(_vptr x y w h)
# typeid & 仮想関数テーブル
Rect_typeid=_number_of_types++
typeinfo[Rect_typeid]=Rect
Rect_vf_hoge=Rect::hoge
Rect_vf_fuga=Rect::fuga
# メンバ関数
function Rect::.ctor {
local this=$1
((${this}_vtbl=$Rect_typeid))
}
function Rect::hoge {
echo this is Rect
}
function Rect::fuga {
local this=$1
echo "Rect {${this}_x, ${this}_y, ${this}_w, ${this}_h}"
}
#----------------------------------------
# 使用
declare "${Rect[@]/#/rect_}"
virtcall rect hoge # 仮想関数呼び出し
C6. まとめ
まとめに関しては親記事 の §3 を御覧ください。