16
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Bash $((算術式)) のすべて - C 応用編

Last updated at Posted at 2016-12-04

Bash 算術式の応用的な使用方法について列挙していきます!

※この記事は AdC 2016 Shell Script 4日目 Bash $((算術式)) のすべて - Qiita の衛星記事です。

関連記事一覧:

記事の構成:

各節は比較的独立した内容です。できるだけ簡単なものから順に紹介していきます。まとめに関しては親記事 の §3 を御覧ください。

:pencil: 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"
}

あっという間に算術式に対応した賢いシェル関数になりました!

:pencil: 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

:pencil: C3. 算術式だけで処理を記述するということ

算術式を使えば変数に値を代入することができます。そして、カンマ演算子を使えば複数の処理を繋げることができます。条件演算子や論理演算子の短絡評価を利用すれば条件分岐もできます。つまり、算術式だけでも色々な処理ができるということになります。

C3.1 例

例えば C 言語で書かれた以下のような関数を考えてみます。この関数は文字コードを16進数の文字と思って対応する整数に変換します。細かいことを考えると面倒なので、アルファベットの文字コードは連続していると考えます (数字の文字コードが連続していることはC規格上保証されていますので気にしなくて大丈夫です)。

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では算術式だけを使って以下の様に記述できます。

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言語の記述との対応について考えてみましょう。先ず、複数の文は算術式ではカンマ演算子を使って繋げることができます。

C言語
i = 1;
j = 2;
Bash 算術式
((
  i=1,
  j=2
))

更に、C言語の複文 { 文 文 … } は、全体を普通の括弧で囲むだけで問題ありません。

C言語
{
  i = 1;
  j = 2;
}
Bash 算術式
((
  (
    i=1,
    j=2
  )
))

C3.3 if

Bash 算術式自体にはいわゆるif文は備わっていません (勿論、算術式ではなくてシェルの構文として if キーワードがありますが)。しかし、論理積演算子や条件演算子を用いれば if 文を実現できます。

C言語
if (i == 1) {
  a = 1234;
}
Bash 算術式
((
  (i==1)&&(
    a=1234
  )
))

if ~ else ~ は条件演算子を使えばできます。

C言語
if (i <= 10) {
  a = 0;
  b = 1234;
} else {
  a = i++;
}
Bash 算術式
((
  (i<=10)?(
    a=0,
    b=1234
  ):(
    a=i++
  )
))

更に unless のようなこともできます!

C言語
if (!(i == 1)) {
  y = 2016;
}
Bash 算術式
((
  (i==1)||(
    y=2016
  )
))

C3.4 switch

if 文ができるのですから当然 switch 文もできます! というか if 文に変換してから算術式にするだけですが。

C言語
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;
}
Bash 算術式
((
  case=i%4,
  (case==0)?(
    x=2016+i
  ):((case==1)?(
    y=2016-i
  ):((case==2)?(
    x=2016-i,
    y=2016+i
  ):(
    x=y=2016
  )))
))

また別の方法として処理をテーブルにする方法もあります。

Bash 算術式
_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 させることができます。これを実現するのは面倒ですが何とか無理やり…

C言語
switch (i % 4) {
case 0:
  x = 2016 + i;
  /*FALL-THROUGH*/
case 1:
  y = 2016 - i;
}
Bash 算術式
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 ループ (forwhiledo) およびジャンプ (gotothrow) など

ループは再帰に変換すれば可能です。ジャンプはそのままだと難しいですが、処理を配列に入れて、配列要素を順番に評価していく方式にすれば、ジャンプは次に実行する配列要素の変更として記述できます。かなり面倒な変換なので実用にはならないでしょう。

親記事 §5 "チューリング完全" で改めて取り扱うことにします。

:pencil: 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 だとかを使って実現します。

Bash
function process {
  local result=$1
  eval $result=20161204
}

process hello
echo $hello # 結果: 20161204

しかし! 算術式の世界では (つまり整数しか扱わないなら) 難しく考える必要はありません! こうです:

Bash 算術式
function process (($1=20161204))

算術式コマンドの実行は、1. 変数展開・コマンド置換・算術式展開を処理してから 2. 実際の算術式評価を行うという二段構成になっているので、わざわざ eval を使う必要はないのです。

C4.2 指定した変数名の変数から値を取り出す

普通 Bash で変数名の入った変数を使って、その変数名の変数から値を取り出すには以下のように ${!varname} を使います。

Bash
function world {
  local varname=$1
  ((result=(${!varname})*2))
}

a=1234
world a
echo $result # 結果: a=2468

しかし算術式ならば実は深く考える必要はありません。単に以下の様にすればよいです。

Bash
function world {
  local varname=$1
  ((result=varname*2))
}

というのも、算術式評価は元から再帰的に行われるからです。

:pencil: C5. 変数名の結合 ≒ 構造体・クラス

Bash の算術式ではスカラーの整数しか扱えないと思っていらっしゃいませんか。まあ、そうです。スカラーの整数しか扱えません。しかし、工夫次第で複数の変数をひとまとまりとして取り扱うことも可能です。

実はこの手法は技術的には算術式と関係ありませんが、算術式を使うとメンバ変数のアクセスをそれっぽい見た目で記述することができます。基本的には前節§C4の方法の応用です。算術式で変数名を簡単に結合できることを利用します。

例えば以下のような C 言語のコードを考えます。

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 では以下の様に書かれます。

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_xrect_h を定義するということと考えます。これは以下のように書けます。

構造体定義
declare "${Rect[@]/#/rect_}"

C5.3 メンバ変数の読み書き

C言語
rect.x = 1;
rect.y = 2;
rect.w = 3;
rect.h = 4;

これは簡単です。以下のようにすれば良いです。

Bash
((
  rect_x=1,
  rect_y=2,
  rect_w=3,
  rect_h=4
))

更に、ptr=rect の様に "参照" が変数 ptr に格納されている場合には、

Bash
((
  ${ptr}_x=1,
  ${ptr}_y=2,
  ${ptr}_w=3,
  ${ptr}_h=4
))

の様にすれば良いです。C言語で言えば

C言語
(*ptr).x = 1;
(*ptr).y = 2;
(*ptr).w = 3;
(*ptr).h = 4;

と等価です (アロー演算子 -> があるのでもっと簡潔に書けますが、Bash 算術式ではアロー演算子に対応するものを実現するのは難しそうです)。

あとは、rect という文字列を関数に渡せば、関数内から外の構造体にもアクセスできるようになります。

C5.4 メンバ関数

関数に参照を渡すことができるとなればメンバ関数だって定義できます!

C++
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;
}
Bash
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
  ))
}

呼び出しもちゃんとできます!

C++
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);
Bash
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 でも、折角メンバ変数名のリストを持っているのですから、コピーを行う汎用の関数を定義できそうです。

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
}

少し汚い実装ですがまあ致し方ないでしょう。実際にコピーする時はこうです:

C言語
rect1 = rect2;
Bash
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 派生・継承

更に派生・メンバの継承もできてしまいます!

C++
struct Board: Rect {
  int color;
};
Bash
Board=("${Rect[@]}" color)

変数名が被ると問題になりますが、まあ、そうならない様に注意すれば何とかなります。

C5.8 仮想関数

更に! 仮想テーブルの ID を持つメンバ _vptr を定義してやれば、仮想関数・動的多態性だって実現できてしまいます!

Bash 仮想関数
_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 を御覧ください。

16
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?