2026年8月28日

2026年8月28日

bashの算術演算の使い方|$(( ))・(( ))・let・exprの違いと落とし穴

bashで整数の計算をするなら、基本は $(( ))(算術展開)と (( ))(算術コマンド)の2つで足ります。let は (( )) とほぼ同じ働きの古い書き方、expr は外部コマンドで、新しく書くスクリプトで使う理由はほとんどありません。この記事では、それぞれの書き方と違い、set -e で ((i++)) がスクリプトを止めてしまう問題、先頭が0の数値がエラーになる問題、小数の計算方法、外部入力を計算に使うときの注意点までまとめます。

(2026年10月現在)主要な Linux ディストリビューションの bash は 5 系です。この記事の内容は bash 4 以降を前提にしていますが、基本的な算術演算は古い bash でも同じように動きます。

$(( )) で計算結果を取り出す

$(( 式 )) は、式を計算して結果の数値に置き換える「算術展開」です。結果を変数に入れたり、echo に渡したりするときに使います。

#!/usr/bin/env bash
a=7
b=3

echo $(( a + b ))    # 10
echo $(( a - b ))    # 4
echo $(( a * b ))    # 21
echo $(( a / b ))    # 2(小数点以下は切り捨て)
echo $(( a % b ))    # 1(余り)
echo $(( a ** 2 ))   # 49(べき乗)

total=$(( a * b + 10 ))
echo "$total"        # 31

$(( )) の中では、変数名の前の $ を省略できます。$(( a * b )) と $(( $a * $b )) はたいてい同じ結果になりますが、変数が未定義や空のときに差が出ます。$ を付けない書き方では未定義の変数は 0 として扱われますが、$a と書くと先に空文字に展開されるため、$(( * 3 )) のような式になり「operand expected」というエラーになります(足し算の場合は単項の + と解釈されてエラーにならず、気付きにくい誤りになります)。特別な理由がなければ $ を付けない書き方をおすすめします。

また、* は $(( )) の中ではファイル名の展開を受けないので、エスケープは不要です。演算子の前後の空白も自由で、読みやすいように入れて構いません。

(( )) で条件判定と代入をする

(( 式 )) は計算結果を出力せず、「式の値が0以外なら成功(終了ステータス0)、0なら失敗(終了ステータス1)」を返すコマンドです。if や while の条件、カウンタの増減に使います。

count=0
max=5

(( count++ ))          # count を1増やす
(( count += 2 ))       # count は3

if (( count < max )); then
  echo "まだ上限未満です"
fi

while (( count > 0 )); do
  (( count-- ))
done

(( )) の中では < > == != <= >= がそのまま数値の比較になり、&& || も使えます。[ "$a" -lt "$b" ] のような test コマンドの書き方より読みやすく、< をリダイレクトと誤解されることもありません。

使える主な演算子は次のとおりです。

  • 四則演算と余り・べき乗: + - * / % **
  • 複合代入: += -= *= /= %=
  • 増減: ++ --(前置・後置の両方)
  • 比較と論理: < > <= >= == != && || !
  • ビット演算: & | ^ ~ << >>
  • 三項演算子: 条件 ? 値1 : 値2
n=7
echo $(( n % 2 == 0 ? 0 : 1 ))   # 1(奇数なら1)
echo $(( 1 << 4 ))               # 16

let と expr は何が違うのか

let

let は bash の組み込みコマンドで、(( )) とほぼ同じ計算と終了ステータスの規則を持ちます。

let "x = 5 * 3"
let x++
echo "$x"   # 16

引数を空白で区切ると別々の式として扱われるため、空白を入れるなら引用符で囲む必要があります。(( )) なら引用符は不要なので、今から書くなら (( )) で十分です。既存のスクリプトで let を見かけたら、同じ意味だと理解しておけば読めます。

expr

expr はシェルの機能ではなく外部コマンドです。古い Bourne シェルには算術展開がなかったため、かつてはこれで計算していました。

expr 5 + 3       # 8
expr 5 \* 3      # 15(* はエスケープしないとファイル名に展開される)
x=$(expr 10 / 3) # 3

演算子と数値の間に空白が必須で、* のエスケープを忘れるとカレントディレクトリのファイル名に展開されて意図しない結果になります。さらに、計算結果が0のときに終了ステータスが1になるため、set -e 環境で突然スクリプトが止まる原因にもなります。ループの中で毎回外部コマンドを起動するので遅くもなります。POSIX sh でも $(( )) は使えるため、expr を選ぶ場面はほぼありません。

set -e と ((i++)) の落とし穴

set -e(エラーで停止)を使っているスクリプトで、次のコードは1回目で終了します。

#!/usr/bin/env bash
set -e
i=0
(( i++ ))          # ここでスクリプトが終了する
echo "ここに来ない"

後置の i++ は「増やす前の値」を式の値として返します。i が0のとき式の値は0になり、(( )) は失敗(終了ステータス1)を返すため、set -e によって終了します。let と expr も同じ規則です。

対策は次のいずれかです。

(( ++i ))          # 前置にする(増やした後の値1が返る)
i=$(( i + 1 ))     # 代入文にする(代入自体は成功扱い)
(( i++ )) || true  # 失敗を無視する

カウンタの増加は i=$(( i + 1 )) か (( ++i )) と書く、と決めておくと迷いません。(( count-- )) で0になる場合や、(( x = 0 )) のような代入も同じ理由で失敗扱いになる点に注意してください。

先頭が0の数値は8進数として扱われる

bash の算術演算では、0 で始まる数値は8進数、0x で始まる数値は16進数とみなされます。日付や時刻を扱うときによく問題になります。

month=$(date +%m)       # 8月なら "08"
echo $(( month + 1 ))
# bash: 08: value too great for base (error token is "08")

8進数に 8 と 9 は存在しないため、08 と 09 だけがエラーになります。7月までは動くので、テストをすり抜けやすい不具合です。10# を付けて10進数であることを明示すれば解決します。

month=$(date +%m)
echo $(( 10#$month + 1 ))   # 9

GNU coreutils の date なら date +%-m で先頭の0なしの値を取る方法もありますが、10# の方が入力元を問わず使えます。逆に、基数を指定して読むこともできます。

echo $(( 2#1010 ))    # 10(2進数)
echo $(( 16#ff ))     # 255(16進数)
echo $(( 0x1F ))      # 31

小数の計算は bc か awk を使う

bash の算術演算は整数だけを扱います。$(( 10 / 3 )) は3になり、$(( 1.5 + 1 )) は構文エラーです。小数が必要な場合は bc か awk を使います。

# bc: scale で小数点以下の桁数を指定
echo "scale=2; 10 / 3" | bc          # 3.33

# awk: printf で書式を指定
awk 'BEGIN { printf "%.2f\n", 10 / 3 }'   # 3.33

# 変数を渡すときは -v を使う
used=37 total=120
awk -v u="$used" -v t="$total" 'BEGIN { printf "%.1f%%\n", u / t * 100 }'   # 30.8%

bc は最小構成のサーバーやコンテナイメージには入っていないことがあります。awk はほとんどの環境にあるので、移植性を考えるなら awk が無難です。

整数の範囲で済ませる工夫もあります。たとえば割合をパーセントの整数で出すなら、先に100を掛けてから割ります。

echo $(( used * 100 / total ))   # 30

掛け算と割り算の順序を逆にすると 37 / 120 が先に0になってしまうので、整数演算では「掛けてから割る」が基本です。なお、整数の範囲は多くの環境で64ビットの符号付き整数で、範囲を超えてもエラーにならず値が回り込みます。大きな数を扱う場合は bc を使ってください。

0で割るとエラーになる

$(( 10 / 0 )) は「division by 0」というエラーになり、その行の処理は失敗します。外部から受け取った値で割る場合は、事前に0でないことを確認します。

if (( total == 0 )); then
  echo "total が0のため計算できません" >&2
  exit 1
fi
echo $(( used * 100 / total ))

外部入力をそのまま計算に使わない

bash の算術評価は、変数の値を「式」として再評価します。そのため、ユーザー入力やファイルの内容など信頼できない値をそのまま $(( )) や (( )) に入れると、配列の添字に書かれたコマンド置換が実行されるといった危険があることが知られています。

# 悪い例: 入力値をそのまま算術評価する
read -r n
echo $(( n * 2 ))

計算に使う前に、数字だけで構成されているかを正規表現で確認してください。

read -r n
if [[ ! $n =~ ^[0-9]+$ ]]; then
  echo "0以上の整数を入力してください" >&2
  exit 1
fi
echo $(( 10#$n * 2 ))

10# は前述の8進数対策も兼ねます。負の数も受け付けたい場合、n=-8 に 10#$n と書くと 10#-8 という式になり、bash 5.2 ではエラーになります。正規表現を ^-?[0-9]+$ に広げたうえで、符号を外して $(( -10#${n#-} )) のように計算するなど、手元の bash で動作を確かめてください。CGI や Web フックから呼ばれるスクリプト、WP-CLI と組み合わせて外部の値を受け取るスクリプトでは、特に注意が必要です。

実用例:処理時間とディスク使用率

最後に、運用スクリプトでよく使う形を2つ紹介します。

#!/usr/bin/env bash
set -euo pipefail

start=$(date +%s)

# ここで時間のかかる処理(例: バックアップ)
sleep 2

end=$(date +%s)
elapsed=$(( end - start ))
printf '処理時間: %d分%02d秒\n' $(( elapsed / 60 )) $(( elapsed % 60 ))

bash には起動からの経過秒数を保持する SECONDS という変数もあり、スクリプト全体の所要時間ならこれで足ります。

#!/usr/bin/env bash
set -euo pipefail

limit=80
# GNU coreutils の df を想定。% 記号と空白を取り除く
usage=$(df --output=pcent / | tail -n 1 | tr -dc '0-9')

if (( usage >= limit )); then
  echo "ディスク使用率が ${usage}% です(しきい値 ${limit}%)" >&2
  exit 1
fi

df --output は GNU 版のオプションなので、BSD 系の環境では df -P の出力を awk で切り出すなどの調整が必要です。

まとめ

  • 計算結果を取り出すなら $(( ))、条件判定やカウンタの更新なら (( )) を使う
  • let は (( )) の古い書き方、expr は外部コマンドで、新しいスクリプトでは不要
  • set -e 環境では、値が0になる (( i++ )) で終了する。i=$(( i + 1 )) か (( ++i )) と書く
  • 先頭が0の数値は8進数扱いになるため、日付などは $(( 10#$var )) で10進数を明示する
  • 小数は awk か bc で計算し、整数演算では掛けてから割る
  • 外部入力は正規表現で整数か確認してから算術評価に渡す

お気軽にご相談ください

お見積りへ お問い合わせへ
▲