2013-12-06

算術式 - Bash Advent Calendar - Day 6

Array, Associative Array と予定どおり A から順にネタを消化中で、今日は Arithmetic Expression。辞書順的に後戻りですが、細かいことは気にしないでいきましょう。

扱える範囲

とりあえず小数は扱えません。小数を使うような計算が必要なときは素直に他のLLを使いましょう。

扱える整数の範囲は実装依存で、いわゆる long の範囲です。最近の 64bit 環境であれば -2^63 〜 2^63 - 1 なので、十分実用に堪えると思います。

算術式が使われる場所

ちゃんとチェックしてないので抜けがあるかもしれませんが

  1. 数値変数への代入 (local, readonly, declare, typeset コマンドを含む)
  2. let "算術式"
  3. ((算術式))
  4. for ((算術式; 算術式; 算術式)) do ... done
  5. $((算術式))  ($[算術式] もいまだに使えると思いますが、そのうち使えなくなるはず)
  6. 配列変数[算術式] や ${配列変数[算術式]} などの配列変数のインデックス。
  7. 部分文字列展開: ${変数:算術式} および ${変数:算術式:算術式}
  8. 数値比較 [[ 算術式 OP 算術式 ]] (OP は -gt, -ge, -lt, -le のいずれか)

ぐらいですかね。最後のは undocumented なので、期待したとおりに動かなくても当方は関知いたしません。

数値変数

説明の都合上数値変数の定義だけ。

declare -i で変数を数値変数として定義できます。
$ declare -i foo=1
$ declare -p foo
declare -i foo="1"
declare -p の結果の右辺が "1" とクォートされているので文字列っぽく見えますが、ここはそういうものだと(今日のところは)思っててください。declare の後にちゃんと -i と付いているので、これは確かに数値変数です。

ちなみに、配列 / 連想配列とは直行する概念なので
$ declare -ia foo
$ declare -iA bar
$ declare -p foo bar
declare -ai foo='()'
declare -Ai bar='()'
といった感じで数値配列、数値連想配列なんてのも作成できます。

さて、逆説的ですが、数値変数と普通の変数との違いは「右辺が算術式として評価される」だけです。では先に進みましょう。

算術式評価

だいたい、普通にありそうな演算子がそのまま使えます。以下 id は変数を指します。
  • 単項演算子
    • +, -
    • ++id, id++, --id, id--
    • ! 式 (論理否定: 0→1, 0 以外→0)
  • 二項演算子
    • 四則演算 +, -, *, /
    • 剰余 %
    • べき乗 **
    • 比較 >, <, >=, <=, ==, !=
    • 論理演算 ||, &smp;&smp;
  • 三項演算子
    • 式 ? 式 : 式
  • ビット演算(単項演算子、二項演算子両方)
    • ~式 (ビット反転, e.g. 0→-1, 1→-2, ...)
    • &, |, ^ (XOR)
    • ビットシフト <<, >>
その他の文法として
  • 複文
    • 式, 式
  • 代入
    • id=式, id*=式, id/=式, id+=式, id-=式, id%=式
    • id>>=式, id<<=式, id|=式, id&=式, id^=式
  • 結合
    • (式)
と、まぁ C 系のプログラマにとっては妥当なものかと思います。結合の優先順位がどうなってるかは man ページにないのでよくわかりませんが、多分、常識的なものだと思います。 が、とにかくあやしい場合には、() を使いましょう。

さて、昨日も少しだけ触れましたが、式内の上記以外の文字列は
  1. 空文字列は 0
  2. "0x" または "0X" で始まる文字列は 16 進数の数値
  3. "0" (ゼロ) で始まる文字列は 8 進数の数値
  4. アルファベットまたは "_" で始まる文字列(二文字目以降は数字も可)は変数名。変数の中身は算術式として評価されます。
  5. "基数#文字列" は "基数"進数の数値(後述)
  6. それ以外の数字列は 10 進数の数値
と評価されます。

具体例

では、上から順に具体例をば。まずは空文字列
$ declare -i foo=""
$ declare -p foo
declare -i foo="0"
次に 16 進数。
$ declare -i foo=0xabc
$ declare -p foo
declare -i foo="2748"
$ printf '%x\n' "${foo}"
abc
printfについては説明略ということで。まーとにかく普通。お次は 8 進数。
$ declare -i foo="0123"
$ declare -p foo
declare -i foo="83"
$ printf '%o\n' "${foo}"
123
こんな単純じゃつまらないので、ここで早くもこれは使いづらいという例を。前提として GNU date
$ date
Fri Dec  6 22:12:30 PST 2013
$ date --date "next Monday"
Mon Dec  9 00:00:00 PST 2013
を使います。
$ declare -i today monday
$ today=$(date +%d)
$ declare -p today
declare -i today="6"
$ monday=$(date +%d --date "next Monday")
bash: 09: 基底の値が大きすぎます (エラーのあるトークンは "09")
エラーメッセージで分かっちゃうと思いますが、
$ date +%d --date "next Monday"
09
と、date コマンドのこの結果は、先頭に 0 が付きます。それでもって、上で説明した通り 0 から始まる文字列は 8 進数とみなされるので、8 進数では使えない "9" が出てきてエラーになると。

では続きを。次は変数。
$ declare foo="1"
$ declare -i bar
$ bar="foo"
$ declare -p bar
declare -i bar="1"
上で説明したように、変数の中身が算術式評価されるので、bar が文字列変数でも問題ありません。それどころか
$ declare foo="1"
$ declare bar="foo"
$ declare -i buzz
$ buzz="bar"
$ declare -p buzz
declare -i buzz="1"
となります。何が起こってるかというと
  1. buzz="bar" の右辺は変数名なので、変数の中身 "foo" を算術式評価
    1. "foo" は変数名なので変数の中身 "1" を算術式評価
      1. 文字列 "1" は 1
    2. よって "foo" → 1
  2. よって "bar" → 1
となります。楽しくなって来ましたね。変数の中身もまた算術式評価されるということは
$ declare foo=1
$ declare bar="foo+2"
$ declare -i buzz="bar+3"
$ declare -p buzz
declare -i buzz="6"
$ declare -p bar
declare -- bar="foo+2"
と、"-i" を一箇所に付けるだけで普通のシェルスクリプトはひと味も二味も違うことができます。

大分長くなってきたので、かなり中途半端ですが続きは明日。とりあえず中途半端ですが、今日の結論は「やっぱり数値計算するならちゃんとしたLLを使おう」です。bash で数値計算しようとする考えが根本的に間違ってると思います。


0 件のコメント:

prometheusのrate()関数の罠

 久しぶりのAdventカレンダー挑戦、うまくいく気がしません。 閑話休題。実のところ、rate()関数というよりは、サーバー側のmetric初期化問題です。 さて、何らかのサーバーAがあったとして、それが更に他のサーバーBにRPCを送っているとします。サーバーBの方でホワイトボ...