スタートアップCTOによるITエンジニアのためのブログ

Bashの算術展開(Arithmetic Expansion)を使いこなそう

Bashで数値を扱う際、いまだにexprを使っていませんか? もしそうなら、シェルスクリプトをもっと速く、そしてスマートに書く方法があります。

本記事では、Bashスクリプトをワンランク上の品質に引き上げる「算術展開(Arithmetic Expansion)」を徹底解説します。

目次候補

  • 概要
  • メリット
  • 基本的な使い方
  • 利用可能な演算子と基数
  • 実用例
  • 注意点
  • 算術評価 (( ... )) との使い分け
  • 最終評価値とクォートの必要性
  • 変数のドル記号($)の有無による違い
  • 脆弱性の危険性と防御策
  • まとめ

概要

Bashの算術展開(算術式展開)は、$(( ... )) という構文を使って整数演算を行い、その結果を展開する機能です。シェル内で直接計算を行うため、外部コマンドを呼び出す必要がなく、非常に軽量かつ強力です。

メリット

一昔前のスクリプトでは expr コマンドがよく使われていましたが、算術展開には以下の圧倒的なメリットがあります。

  • パフォーマンス: expr は外部プロセスを起動(fork)しますが、算術展開はBashの組み込み機能なので高速です。
  • 可読性: * をエスケープ(\*)する必要がなく、数式を自然に書けます。
  • 柔軟性: 多くのC言語ライクな演算子が使えます。
# Bash
# exprの場合
echo "$(expr 2 \* 3)"

# 算術展開の場合
echo "$(( 2 * 3 ))"

基本的な使い方

基本は $(( 式 )) です。演算子の前後にスペースがあってもなくても動作は同じです。

# Bash
echo $(( 1 + 2 ))  # 3

echo $((1+2)) # 3

負の整数も扱えます。

# Bash
echo $(( -5 + 2 )) # -3

変数も使えます。括弧内では変数名の前の $ を省略できます。

# Bash
a="2"
b="3"

# 一般的な省略した書き方
echo $(( a + b )) # 5

# 省略しない場合
echo $(( $a + $b )) # 5

注意)後述しますが、$を省略した場合と省略しない場合では変数の評価方法が異なり、結果が変わることがあります。

$[数式] という書き方もありますが、これは古い形式で非推奨です。現代的なスクリプトでは必ず $(( ... )) を使いましょう。

# Bash
# 非推奨
echo $[ 1 + 2 ]
# 推奨
echo $(( 1 + 2 ))

利用可能な演算子と基数

C言語でおなじみの演算子がほぼ網羅されています。

カテゴリ演算子
四則演算+, -, *, /, % (余り), ** (べき乗)$((5 ** 2)) → 25
比較==, !=, <, >, <=, >=結果は 1 (真) または 0 (偽)
論理演算 &&, ||, !結果は 1 (真) または 0 (偽)
インクリメント++, --((i++)) のように使用
ビット演算<<, >>, &, |, ^$(( 1 & 1 )) -> 1

基数(進数)の指定

デフォルトは10進数ですが、以下の記法で他進数も扱えます。

  • 16進数: 0x から始める(例: 0xff
  • 8進数: 0 から始める(例: 010 は 8)
  • 任意の基数: [基数]#[数値](例: 2#1010 は 10進数の 10)

どの基数を使っても、評価の結果は10進数です。

# Bash
# 2進数のビット演算
echo $(( 2#101 | 2#10)) # 7

実用例

実用的な例をいくつか紹介します。

1. 進捗状況(パーセンテージ)の可視化

ループ処理やバックアップ処理などで、「今、全体の何%終わったか」を表示する際に便利です。

# Bash
total_files=200
current=50
percent=$(( current * 100 / total_files ))
echo "進捗率: ${percent}%"

小数点以下は切り捨てられるため、あらかじめ 100 を掛けてから割るのがコツです。

2. 単位変換(バイト → メガバイトなど)

ログファイルやディスク使用量など、大きな数値を読みやすい単位に変換する際に使われます。

# Bash
size_bytes=10485760
echo "$(( size_bytes / 1024 / 1024 )) MB"

3. 処理時間の計測

「エポック秒(1970年からの経過秒数)」を使って、処理時間を計算します。

# Bash
start_time=$(date +%s)

# ... 何らかの重い処理 ...
sleep 3

end_time=$(date +%s)
elapsed=$(( end_time - start_time ))

echo "処理時間: ${elapsed} 秒"

注意点

1.整数演算のみ

Bashの算術展開は浮動小数点数を扱えません。

# Bash
# 0.5ではなく0になる
echo $(( 1 / 2)) 

小数が必要な場合は bcawk を使いましょう。

# Bash
echo "1 / 2" | bc -l # .50000000000000000000

awk 'BEGIN { print 1 / 2 }' # 0.5

2. 「08」や「09」の罠(8進数エラー)

もっともハマりやすいのが、数値を 0 で埋めている場合です。

# Bash
# エラーになる例(08は8進数として不正)
month="08"
echo $(( month + 1 ))
# bash: 08: 基底の値が大きすぎます (エラーのあるトークンは "08")

これを防ぐには、10#$month のように基数を明示するか、10#${month#0} として先頭の0を削る工夫が必要です。

算術評価 (( ... )) との使い分け

$ を付けない (( ... )) は算術評価と呼ばれ、値の展開ではなく、評価(状態の変化や比較)を目的に使います。

構文用途特徴
$(( ... ))値の取得(展開)echo や変数代入の右辺で使う。
(( ... ))評価・条件分岐if 文の条件式やループのカウンタ更新で使う。
# Bash
score=80

# 推奨:算術評価 (( ))
if (( score >= 70 )); then
    echo "合格です"
fi

(( )) 内の結果が 0以外なら終了ステータス 0(成功)、結果が 0なら終了ステータス 1(失敗) を返します。

最終評価値とクォートの必要性

算術展開は、中身がどれほど複雑な式(コンマ演算子や三項演算子)であっても、最終的な評価が終わると1つの連続した数字の並びに置き換わります。

Bashの算術展開が生成するのは以下の文字のみです:

  • 数字 (0-9)
  • マイナス記号 (-) ※負の整数の場合

算術展開の結果に「スペース」や「改行」が含まれることは、Bashの仕様上ありません。そのため、クォートを忘れたとしても、その結果が複数の引数に分裂(単語分割)する心配はありません。

ただし、理論上は $(( )) をクォートしなくても単語分割は起きませんが、実務的には "$(( ))" とダブルクォートで囲むのが一般的です。

理由は、計算結果そのものの安全性というより、「変数の展開はクォートする」というBashプログラミングの一貫性(ベストプラクティス)に従うためです。

# Bash
# どちらも同じ結果だが、下の方が丁寧
result=$((a + b))
result="$((a + b))"

変数のドル記号($)の有無による違い

算術展開の中で $ を付けて変数名を書く場合と省略した場合では、評価方法が異なります。

# Bash
a="1+2"
b="4"
echo "$(( a * b ))" # 12
echo "$(( $a * $b ))" # 9

1. $を省略する場合

算術展開 の中で、$ を付けずに変数名を書いた場合、Bashはその変数の値を「算術式」として再帰的に評価します。

  1. a の中身が 1+2であることを知り、その場で計算(評価)して3 とする。
  2. bを評価し4とする。
  3. 3 * 4を評価し12とする。

また、もし変数が未設定や空文字の場合は0に評価されます。

# Bash
unset a
b=""
echo "$(( a * b ))" # 0 ($((0 * 0))に評価)

2. $を付ける場合

算術展開が始まる前にシェルがパラメータ展開を行い、再帰評価は行われません。

  1. $aがパラメータ展開で”1+2″という文字列に置き換わる。
  2. $bがパラメータ展開で”4″という文字列に置き換わる。
  3. 1+2*4が算術評価され、9になる。

なお、変数が未設定や空の場合、空文字で置換されるため、構文エラーになる可能性があります。

# Bash
unset a
b=""
echo "$(( $a * $b ))" # $(( * ))にパラメータ置換される
# -bash: *  : 構文エラー: オペランドが予期されます (エラーのあるトークンは "*  ")

どちらの表記を使うべきか

結論からお伝えすると、基本的には$ なしの表記が推奨されます。

  • 空変数の安全性: $((空変数))0 になりますが、$(($空変数)) は計算式が $(( )) となり、構文エラー(operand expected)になることがあります。
  • 再帰: 前述の通り、変数の中に式や変数名が入っていても自動で解決してくれます。
  • 可読性: $ が並ばないので、数式として読みやすくなります。

算術展開内の変数表記の違いの比較表

var$var
パラメータ置換されないされる
再帰評価されるされない
未設定・空0空文字

脆弱性の危険性と防御策

「再帰評価」は便利ですが、外部からの入力をそのまま算術展開に入れると、任意のコマンドを実行される(コードインジェクション) 危険があります。

攻撃の仕組み

Bashの算術展開内では配列のインデックスとしてコマンド置換を埋め込むことが可能です。

# Bash
# 悪意のある入力例
user_input='a[$(id >&2)]'
echo $((user_input + 1))
  1. user_input の中身 a[$(id >&2)] を見に行く。
  2. 配列 a の添字部分にある $(id >&2) を実行してしまう。
  3. id コマンドが実行され、ユーザー情報が漏洩(あるいは破壊的なコマンドrm -rf /などが実行)される。

防御策

外部入力を算術展開に渡す前に、正規表現で正規表現で数値のみであることを必ず確認してください。

# Bash
if [[ "$user_input" =~ ^-?[0-9]+$ ]]; then
    echo $((user_input + 1))
else
    echo "不正な入力です"
fi

まとめ

Bashの算術展開 $(( ... )) は、高速で読みやすく、かつ強力な計算ツールです。

  • expr は卒業して $(( ... )) を使う。
  • 整数専用! 小数は bcawk へ。
  • 0809 などの「0埋め数値」による8進数エラーに注意。
  • 外部入力を使う際は必ずバリデーションを行う。

これらを押さえるだけで、シェルスクリプトをより堅牢で効率的なものにできます。