シェルスクリプトの計算は、整数なら $(( ))、小数が必要なら awk か bc を使うのが基本です。 $(( )) はシェル自身が計算するので速く、sh(POSIXシェル)でも bash でも同じように書けます。小数を扱えるのは外部コマンドの bc と awk で、桁数の指定や四捨五入のしかたに違いがあります。

この記事では、bash 5.2.21 と dash 0.5.12(Ubuntu 24.04 の /bin/sh)で実際に動かした結果をもとに、7つの方法を比較表にまとめました。08 や 09 でエラーになる理由、整数の割り算が切り捨てになること、set -e を付けたスクリプトが (( i++ )) で止まる挙動を、実際のエラーメッセージつきで再現しています。四捨五入・切り上げ・パーセント計算・小数比較は、bash と dash の両方で同じ結果になることを確かめたコピペ集として載せました。

シェルスクリプトの計算方法の早見表

方法 書き方の例 小数 POSIX sh で使えるか 外部プロセスの起動 2000回の計算にかかった時間
$(( )) x=$(( a * 3 + 1 )) 整数のみ ○ なし 8 ms
(( )) (( i += 1 )) 整数のみ −(bash・ksh・zsh) なし 8 ms($(( )) と同じ仕組み)
let let "x = a * 3" 整数のみ −(dash では not found) なし 8 ms
expr x=$(expr $a \* 3) 整数のみ ○(外部コマンド) あり 2811 ms
bc echo "scale=2; 7 / 3" | bc ○(桁数は scale で指定) ○(外部コマンド) あり 3813 ms
awk awk 'BEGIN { print 7 / 3 }' ○(倍精度の浮動小数点) ○(外部コマンド) あり 4171 ms
printf printf '%.2f' 3.14159 整形と丸めのみ(計算はしない) ○ なし(組み込み) −

時間は、今回の環境(2コア)で「i × 3 + 1」を2000回くり返したときの実測値です。シェル組み込みの $(( )) は、外部コマンドを毎回起動する方法より数百倍速いという結果でした。数値は環境によって変わります。

どれを選ぶかは、次の表で判断できます。

やりたいこと 選ぶ方法
カウンタ、ループ、整数の四則演算 $(( ))
bash 専用スクリプトでのインクリメントや条件判定 (( ))
小数の計算結果を、桁数をそろえて表示 awk の printf
桁数の多い計算、桁数を厳密に指定したい計算 bc
ファイルの列を合計・平均する awk
数値の表示形式だけ整える(ゼロ埋め、小数点以下の桁) printf
sh しか使えない環境で整数を計算する $(( ))(expr は互換目的のときだけ)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「シェルスクリプトの計算方法まとめ $(( ))・expr・bc・awk比較表つき」

検証環境

項目 バージョン
OS Ubuntu 24.04.4 LTS
bash 5.2.21
dash(/bin/sh) 0.5.12
expr / printf(外部コマンド版) GNU coreutils 9.4
bc 1.07.1
awk GNU Awk 5.2.1(一部は mawk でも確認)

$(( )) の基本:整数の四則演算

$(( 式 )) は算術展開と呼ばれ、式を計算した結果の文字列に置き換わります。かっこの中では変数に $ を付けなくても参照でき、空白も自由に入れられます。

a=7; b=2
echo "add: $((a + b))  sub: $((a - b))  mul: $((a * b))  div: $((a / b))  mod: $((a % b))  pow: $((a ** b))"
echo "neg div: $((-7 / 2))  neg mod: $((-7 % 2))"
echo "ternary: $(( a > b ? a : b ))  bit: $(( 1 << 10 ))  hex: $(( 0xff ))  base2: $(( 2#1010 ))"

bash での実行結果:

add: 9  sub: 5  mul: 14  div: 3  mod: 1  pow: 49
neg div: -3  neg mod: -1
ternary: 7  bit: 1024  hex: 255  base2: 10
演算子 意味 bash dash
+ - * / % 加減乗除と余り ○ ○
** べき乗 ○ −(エラー)
++ -- インクリメント・デクリメント ○ −(エラー)
+= -= *= など 代入つき演算 ○ ○
< > == != <= >= 比較(真なら 1、偽なら 0) ○ ○
? : 三項演算子 ○ ○
<< >> & | ^ ビット演算 ○ ○
0x で始まる数 16進数 ○ ○
2#1010 のような「基数#数」 任意の基数 ○ −(エラー)

dash で ** を使ったときのメッセージは次のとおりです。

$ dash -c 'i=5; echo $((i ** 2))'
dash: 1: arithmetic expression: expecting primary: "i ** 2"

/bin/sh で動かすスクリプトでは、べき乗(**)、++、--、基数の指定を使わないのが安全です。Ubuntu や Debian の /bin/sh は dash なので、1行目が #!/bin/sh のスクリプトはこの制約を受けます。

知っておくと役立つ挙動

x=; echo "empty: $(( x + 1 ))"
echo "max: $(( 9223372036854775807 + 1 ))"
empty: 1
max: -9223372036854775808
挙動 内容
空や未設定の変数は 0 として扱われる 入力の検証をしていないと、気づかないまま 0 で計算される
64ビット整数の範囲を超えると折り返す 上限は 9223372036854775807。エラーにはならない
文字列の変数は、その名前の変数として再評価される a=abc のとき $(( a + 1 )) は 1(abc が未設定なので 0 扱い)
カンマは演算子として解釈される s="1,200" のとき $(( s + 1 )) は 201

最後の例は、桁区切りのカンマが入った値をそのまま渡したときの実行結果です。${s//,/} でカンマを取り除くと、期待どおり 1201 になりました。

$ bash -c 's="1,200"; echo $(( s + 1 ))'
201
$ bash -c 's="1,200"; echo $(( ${s//,/} + 1 ))'
1201

変数のインクリメント:書き方と前置・後置の違い

書き方 bash dash(sh) set -e 下で i=0 のとき
i=$(( i + 1 )) ○ ○ 続行する
: $(( i += 1 )) ○ ○ 続行する
(( i += 1 )) ○ − 続行する(結果が 1)
(( ++i )) ○ − 続行する(結果が 1)
(( i++ )) ○ − 止まる(式の値が 0)
let i++ ○ − 止まる(式の値が 0)
i=$(expr $i + 1) ○ ○ 続行する(遅い)

どのシェルでも同じように動くのは i=$(( i + 1 )) です。bash 専用のスクリプトなら (( i += 1 )) や (( ++i )) が短く書けます。

前置(++i)と後置(i++)の違い

i=5; a=$(( i++ )); echo "a=$a i=$i"
i=5; b=$(( ++i )); echo "b=$b i=$i"
arr=(x y z); k=0; echo "${arr[k++]} ${arr[k++]} k=$k"
a=5 i=6
b=6 i=6
x y k=2
形 式の値 変数
i++(後置) 増やす前の値 1 増える
++i(前置) 増やした後の値 1 増える

変数に入る値はどちらも同じで、違うのは「式そのものの値」です。この違いが、次の set -e の話につながります。

dash でのインクリメント

$ dash -c 'i=0; i=$((i + 1)); echo $i; : $((i += 1)); echo $i; echo $((i++))'
1
2
dash: 1: arithmetic expression: expecting primary: "i++"
$ dash -c 'i=0; ((i++)); echo "status=$? i=$i"'
dash: 1: i++: not found
status=127 i=0
$ dash -c 'let i=1+2; echo $i'
dash: 1: let: not found

dash では (( )) が算術コマンドとして解釈されず、i++ という名前のコマンドを探しに行っています。let も組み込まれていません。

つまずきやすい3つの挙動を再現する

1. 08 や 09 でエラーになる(先頭の 0 は8進数)

$(( )) の中では、0 で始まる数字は8進数として解釈されます。8進数に 8 と 9 は存在しないため、08 と 09 はエラーになります。日付の「月」「日」「時」「分」をそのまま計算に使ったときによく出会います。

$ bash -c 'm=08; echo $(( m + 1 ))'
bash: line 1: 08: value too great for base (error token is "08")
$ dash -c 'm=08; echo $(( m + 1 ))'
dash: 1: Illegal number: 08

エラーにならない 010 のような値は、気づかないまま8進数として計算されます。

$ bash -c 'm=010; echo $(( m + 1 )); echo $(( 10#$m + 1 ))'
9
11

010 は8進数で 8 なので、1 を足して 9 になっています。bash では 10#$変数 と書いて10進数であることを明示すると解決します。

$ bash -c 'm=09; echo $(( 10#$m + 1 ))'
10
$ bash -c 'h=$(date +%H); m=$(date +%M); echo "$h:$m -> $(( 10#$h * 60 + 10#$m ))"'
13:39 -> 819

10# は bash の機能で、dash では使えません。sh では ${m#0} で先頭の 0 を取り除きます。

$ dash -c 'm=08; echo $(( 10#$m + 1 ))'
dash: 1: arithmetic expression: expecting EOF: " 10#08 + 1 "
$ dash -c 'm=08; echo $(( ${m#0} + 1 ))'
9
方法 08 + 1 の結果 メモ
$(( m + 1 ))(bash) エラー value too great for base
$(( m + 1 ))(dash) エラー Illegal number: 08
$(( 10#$m + 1 ))(bash) 9 bash で確実な書き方
$(( ${m#0} + 1 ))(sh) 9 先頭の 0 を1つ取り除く。2桁の値向け
expr 08 + 1 9 10進数として扱う
echo "08 + 1" | bc 9 10進数として扱う
awk -v m="$m" 'BEGIN { print m + 1 }' 9 10進数として扱う
printf '%d' 08(bash) エラー invalid number(8進数として解釈)

負の数に 10# を付けるときは、符号を外に出します。

$ bash -c 'n=-08; echo $(( 10#$n ))'
bash: line 1: 10#: invalid integer constant (error token is "10#")
$ bash -c 'n=-08; echo $(( -10#${n#-} ))'
-8

date コマンドで数値を取り出すときは、date +%-m や date +%-d のように「-」を入れると、最初から 0 埋めなしの値を得られます(GNU date の書式)。

2. 整数の割り算は切り捨てになる

$(( )) は整数しか扱わないため、割り算の小数部分は捨てられます。

$ bash -c 'echo $(( 7 / 2 )) $(( -7 / 2 )) $(( 1 / 3 * 3 )) $(( 1 * 3 / 3 )) $(( 50 * 100 / 300 ))'
3 -3 0 1 16
式 結果 説明
7 / 2 3 3.5 の小数部分を捨てる
-7 / 2 -3 0 に近い方向へ丸める(-4 にはならない)
1 / 3 * 3 0 先に 1 / 3 が 0 になる
1 * 3 / 3 1 掛け算を先にすると正しく計算できる
50 * 100 / 300 16 パーセント計算は「掛けてから割る」

小数を渡したときは、次のメッセージになります。

$ bash -c 'echo $(( 1.5 + 1 ))'
bash: line 1: 1.5 + 1 : syntax error: invalid arithmetic operator (error token is ".5 + 1 ")
$ dash -c 'echo $(( 1.5 + 1 ))'
dash: 1: arithmetic expression: expecting EOF: " 1.5 + 1 "
$ expr 1.5 + 1
expr: non-integer argument

0 で割ったときのメッセージも、方法ごとに確認しました。

$ bash -c 'echo $(( 1 / 0 ))'
bash: line 1: 1 / 0 : division by 0 (error token is "0 ")
$ dash -c 'echo $(( 1 / 0 ))'
dash: 1: arithmetic expression: division by zero: " 1 / 0 "
$ expr 1 / 0
expr: division by zero
$ echo "1/0" | bc
Runtime error (func=(main), adr=3): Divide by zero
$ awk "BEGIN{print 1/0}"
awk: cmd. line:1: error: division by zero attempted

割る数が変数のときは、計算の前に 0 でないことを確かめておくと安心です。

3. set -e 下で (( i++ )) がスクリプトを止める

set -e は「コマンドが 0 以外の終了ステータスを返したらスクリプトを終了する」設定です。そして (( 式 )) は、式の値が 0 のとき終了ステータス 1 を返します。

#!/bin/bash
set -e
i=0
echo "before: i=$i"
(( i++ ))
echo "after: i=$i"
$ bash sete.sh; echo "exit=$?"
before: i=0
exit=1

エラーメッセージは何も出ず、「after」の行に届く前に終了しています。後置の i++ は「増やす前の値」= 0 が式の値になるためです。終了ステータスだけを確かめると、仕組みがよくわかります。

$ bash -c 'i=0; (( i++ )); echo "status=$? i=$i"; (( i++ )); echo "status=$? i=$i"; (( 0 )); echo "status=$?"; (( 5 - 5 )); echo "status=$?"; (( 3 > 2 )); echo "status=$?"; let "x = 0"; echo "let status=$?"'
status=1 i=1
status=0 i=2
status=1
status=1
status=0
let status=1

i は 1 に増えているのに、1回目だけ status が 1 です。カウンタを 0 から始めたときの最初の1回だけ止まるので、原因に気づきにくい挙動です。trap で ERR を拾うと、止まった行番号がわかります。

#!/bin/bash
set -e
trap 'echo "ERR: line $LINENO: status=$?"' ERR
count=0
for f in a b c; do
  (( count++ ))
  echo "processed $f"
done
$ bash trapd.sh; echo "exit=$?"
ERR: line 6: status=1
exit=1

set -e のもとで安心して使える書き方を、1つのスクリプトで確かめました。

#!/bin/bash
set -e
i=0
(( ++i ));        echo "1) (( ++i ))        i=$i"
(( i += 1 ));     echo "2) (( i += 1 ))     i=$i"
i=$(( i + 1 ));   echo "3) i=\$(( i + 1 ))   i=$i"
: $(( i++ ));     echo "4) : \$(( i++ ))     i=$i"
i=0
(( i++ )) || true; echo "5) (( i++ )) || true i=$i"
i=0
let i++ || true;  echo "6) let i++ || true  i=$i"
i=0
let i++
echo "ここには届かない"
$ bash sete2.sh; echo "exit=$?"
1) (( ++i ))        i=1
2) (( i += 1 ))     i=2
3) i=$(( i + 1 ))   i=3
4) : $(( i++ ))     i=4
5) (( i++ )) || true i=1
6) let i++ || true  i=1
exit=1

1〜6 は続行し、最後の let i++ で終了しました。

書き方 set -e 下の動き おすすめ度
i=$(( i + 1 )) 常に続行(代入の終了ステータスは 0) いちばん確実。sh でも動く
(( i += 1 )) / (( ++i )) 結果が 0 にならない限り続行 0 から増やすカウンタなら問題なし
: $(( i++ )) 常に続行(: は常に 0 を返す) sh でも使える形(dash では i += 1 に)
(( i++ )) || true 常に続行 既存のコードを最小の変更で直すとき
(( i++ )) / let i++ i が 0 のとき終了 set -e と組み合わせるときは上の形に置き換える

(( ++i )) も、i が -1 のときは結果が 0 になり、終了ステータスが 1 になります(確認済み)。増減の方向や初期値に左右されない i=$(( i + 1 )) を基本にすると迷いません。

expr にも同じ性質があります。計算結果が 0 のとき、終了ステータスが 1 になります。

$ expr 5 - 5; echo "expr status=$?"
0
expr status=1
$ bash -c 'set -e; n=$(expr 5 - 5); echo "到達 n=$n"'; echo "exit=$?"
exit=1

(( )) と let:bash で計算と判定をまとめて書く

(( 式 )) は、計算だけを行って結果を出力しない書き方です。終了ステータスが「式の値が 0 以外なら成功」になるため、if や while の条件にそのまま使えます。

(( 10 > 9 )) && echo arith
s=0; for i in {1..100}; do (( s += i )); done; echo $s
arith
5050

数値の比較では、[[ ]] の中の > が文字列の比較になる点に注意します。

$ bash -c '[[ 10 -gt 9 ]] && echo num; [[ 10 > 9 ]] || echo "str: 10 > 9 is false"; (( 10 > 9 )) && echo arith'
num
str: 10 > 9 is false
arith
書き方 比較の種類 10 と 9 の比較
[ "$a" -gt "$b" ] 数値(sh でも可) 真
[[ $a -gt $b ]] 数値 真
[[ $a > $b ]] 文字列 偽("1" は "9" より前)
(( a > b )) 数値 真

let は (( )) とほぼ同じ働きをする組み込みコマンドです。式に空白や * を含めるときは引用符で囲みます。

i=5; let "i = i * 2"; echo "let: $i"; let i++; echo "let i++: $i"
let: 10
let i++: 11

bash には、変数を整数として宣言する declare -i もあります。代入のたびに右辺が計算されます。

$ bash -c "declare -i z; z=3+4; echo \$z; z+=5; echo \$z"
7
12

$[ ] という書き方も bash では動きます(echo $[a+b] が 9 になることを確認)。以前から使われてきた書き方で、新しく書くスクリプトでは $(( )) にそろえるのがおすすめです。

expr:互換性のために残る外部コマンド

expr は、$(( )) が広く使えるようになる前から使われてきた計算コマンドです。今も多くのスクリプトで見かけるため、読み方を知っておくと役立ちます。

n=$(expr 7 + 2); echo "expr: $n"
echo "expr mul: $(expr 7 \* 2)"
expr: 9
expr mul: 14
注意点 例 結果
演算子の前後に空白が必要 expr 7+2 7+2(文字列としてそのまま出力)
* はエスケープが必要 expr 7 * 2 expr: syntax error(* がファイル名に展開される)
小数は扱えない expr 1.5 + 1 expr: non-integer argument
結果が 0 だと終了ステータスが 1 expr 5 - 5 set -e 下で止まる
毎回プロセスを起動する 2000回で 2811 ms ループの中では $(( )) に置き換える

* をエスケープしなかったときの実際のメッセージです(カレントディレクトリにファイルがある状態で実行)。

$ expr 7 * 2
expr: syntax error: unexpected argument 't1.sh'

bc:桁数を指定して小数を計算する

bc は任意精度の計算ができるコマンドです。小数点以下の桁数は scale で指定します。

echo "7 / 2" | bc
echo "scale=3; 7 / 2" | bc
echo "7 / 2" | bc -l
3
3.500
3.50000000000000000000
入力 出力 説明
7 / 2 3 scale の既定値は 0。割り算は整数になる
scale=3; 7 / 2 3.500 小数第3位まで
7 / 2(bc -l) 3.50000000000000000000 -l は数学ライブラリを読み込み、scale を 20 にする
scale=3; 7 * 2 14 掛け算の結果には scale の桁が付かない
scale=3; 1.5 * 2.25 3.375 掛け算は元の数の桁数が基準
scale=2; 1 / 3 .33 1未満は先頭の 0 が省略される
scale=2; 10 % 3 .01 scale を設定すると余りの意味が変わる
10 % 3 1 余りを求めるときは scale を指定しない
2 ^ 100 1267650600228229401496703205376 大きな整数もそのまま計算できる
sqrt(2)(bc -l) 1.41421356237309504880
1.5 > 1.2 1 比較は真なら 1、偽なら 0
obase=2; 10 1010 出力の基数を変える
ibase=16; FF 255 入力の基数を変える

scale は「割り算の結果を何桁で切り捨てるか」の指定で、四捨五入ではありません。また、結果が 1 未満のとき .33 のように先頭の 0 が付かないため、表示を整えたいときは printf に渡します。

$ r=$(echo "scale=2; 2 / 4" | bc); printf "%.2f\n" "$r"
0.50

bash ではヒアストリングで短く書けます。

$ bc <<< "scale=4; 22 / 7"
3.1428

bc は最小構成の環境には入っていないことがあります(Ubuntu では apt install bc)。入っていない環境も想定するなら、awk を使う形にしておくと持ち運びやすくなります。

awk:小数の計算と書式指定を1回で済ませる

awk は倍精度の浮動小数点数で計算します。printf で桁数を指定すれば、計算と四捨五入を1つのコマンドで行えます。

awk 'BEGIN { print 7 / 2, int(7 / 2), 7 % 2, 2 ^ 10, sqrt(16) }'
awk 'BEGIN { printf "%.2f\n", 7 / 3 }'
3.5 3 1 1024 4
2.33

シェル変数は -v で渡します。式の中に $a のように直接埋め込むより、引用符の扱いが単純になります。

a=7; b=3
awk -v a="$a" -v b="$b" 'BEGIN { printf "%.2f\n", a / b }'
特徴 内容
小数 そのまま計算できる(7 / 2 は 3.5)
整数への切り捨て int()
桁数の指定と四捨五入 printf "%.2f"
精度 倍精度浮動小数点。0.1 + 0.2 を17桁で表示すると 0.30000000000000004
大きな整数 2 の 53 乗を超えると 1 単位の精度がなくなる(2 ^ 53 + 1 が 9007199254740992 と表示された)
ファイル処理 列の合計や平均を1行で書ける

浮動小数点の誤差は、表示桁数を指定すれば通常は表に出ません。

$ awk "BEGIN { print 0.1 + 0.2; printf \"%.17f\n\", 0.1 + 0.2 }"
0.3
0.30000000000000004
$ echo "0.1 + 0.2" | bc
.3

桁を厳密に扱いたい計算は bc、表示用の概算や集計は awk、という分け方がわかりやすいです。

ループの中で毎回 awk や bc を起動すると時間がかかります。くり返しごと awk や bc の中に入れると、起動は1回で済みます。冒頭の表と同じ2000回の計算で比べました。

方法 時間
シェルのループの中で毎回 awk を起動 4171 ms
awk の中で for ループ(起動は1回) 3 ms
シェルのループの中で毎回 bc を起動 3813 ms
bc の中で for ループ(起動は1回) 3 ms

printf:計算結果の表示を整える

printf は計算そのものは行いませんが、小数の丸めやゼロ埋めに使えます。

printf '%.2f\n' 3.14159
printf '%05d\n' 42
3.14
00042

丸めについて、今回の検証でわかった注意点が2つあります。

1つめ:ちょうど半分の値は、偶数側に丸められます。

$ printf "%.0f %.0f %.0f %.0f\n" 0.5 1.5 2.5 3.5
0 2 2 4
$ awk "BEGIN { printf \"%.0f %.0f %.0f %.0f\n\", 0.5, 1.5, 2.5, 3.5 }"
0 2 2 4

0.5 が 0、2.5 が 2 になっています。学校で習う四捨五入(0.5 は常に切り上げ)にしたいときは、0.5 を足して切り捨てる形にします。

$ awk "BEGIN { printf \"%d %d\n\", 2.5 + 0.5, int(3.5 + 0.5) }"
3 4

2つめ:2進数で正確に表せない値は、実装によって結果が変わります。

$ bash -c "printf '%.1f\n' 2.35"
2.3
$ dash -c "printf '%.1f\n' 2.35"
2.4
$ awk 'BEGIN{printf "%.1f\n", 2.35}'
2.4
$ printf "%.2f\n" 1.005
1.00

同じ 2.35 でも、bash の printf は 2.3、dash の printf と awk は 2.4 でした。1.005 を小数第2位に丸めた結果は 1.00 です。境界の値まで結果をそろえたいときは、bc で「丸めたい桁の半分を足してから切り捨てる」方法が確実です。

$ echo "scale=2; (1.005 + 0.005) / 1" | bc
1.01

コピペ集:四捨五入・切り上げ・パーセント・小数比較

次のスクリプトは POSIX sh の範囲で書いてあり、bash と dash の両方で実行して、出力が完全に一致することを確認しました(cmp コマンドで比較)。

# POSIX sh で書いたコピペ集(bash でも dash でも動く)
a=7; b=3

# 1. 割り算を小数第2位まで
echo "1  awk  : $(awk -v a="$a" -v b="$b" 'BEGIN { printf "%.2f", a / b }')"
echo "1  bc   : $(echo "scale=2; $a / $b" | bc)   (切り捨て)"

# 2. 整数だけで四捨五入(割り算の結果を整数に)
echo "2  round: $(( (a + b / 2) / b ))   (7/3=2.33 -> 2)"
echo "2  round: $(( (8 + b / 2) / b ))   (8/3=2.67 -> 3)"

# 3. 切り上げ
echo "3  ceil : $(( (a + b - 1) / b ))   (7/3 -> 3)"
echo "3  ceil : $(( (6 + b - 1) / b ))   (6/3 -> 2)"

# 4. パーセント(整数、掛けてから割る)
done_n=37; total=120
echo "4  pct  : $(( done_n * 100 / total ))%   (切り捨て)"
echo "4  pct  : $(( (done_n * 100 + total / 2) / total ))%   (四捨五入)"
echo "4  pct  : $(awk -v d="$done_n" -v t="$total" 'BEGIN { printf "%.1f", d * 100 / t }')%   (小数第1位)"

# 5. 整数だけで小数第1位まで出す
x=$(( done_n * 1000 / total ))
echo "5  fixed: $(( x / 10 )).$(( x % 10 ))%"

# 6. 小数の比較
v=1.25; limit=1.3
if awk -v v="$v" -v l="$limit" 'BEGIN { exit !(v < l) }'; then echo "6  awk  : $v < $limit"; fi
if [ "$(echo "$v < $limit" | bc)" -eq 1 ]; then echo "6  bc   : $v < $limit"; fi

# 7. 小数の四捨五入(小数第1位に)
echo "7  awk  : $(awk 'BEGIN { printf "%.1f", 2.35 + 0.0000001 }')"
echo "7  bc   : $(echo "scale=1; (2.35 + 0.05) / 1" | bc)"

# 8. 平均・合計(ファイルの数値列)
printf '10\n20\n35\n' > nums.txt
echo "8  sum  : $(awk '{ s += $1 } END { print s }' nums.txt)  avg: $(awk '{ s += $1 } END { printf "%.2f", s / NR }' nums.txt)"

# 9. 小数の掛け算(1.1倍して整数に四捨五入)
echo "9  mul  : $(awk 'BEGIN { printf "%d", 1980 * 1.1 + 0.5 }')  bc: $(echo "1980 * 1.1" | bc)"

# 10. KB -> MB、GB の単位変換
kb=1536000
echo "10 unit : $(( kb / 1024 )) MB  $(awk -v k="$kb" 'BEGIN { printf "%.2f", k / 1024 / 1024 }') GB"

bash recipes.sh と dash recipes.sh の実行結果(同一):

1  awk  : 2.33
1  bc   : 2.33   (切り捨て)
2  round: 2   (7/3=2.33 -> 2)
2  round: 3   (8/3=2.67 -> 3)
3  ceil : 3   (7/3 -> 3)
3  ceil : 2   (6/3 -> 2)
4  pct  : 30%   (切り捨て)
4  pct  : 31%   (四捨五入)
4  pct  : 30.8%   (小数第1位)
5  fixed: 30.8%
6  awk  : 1.25 < 1.3
6  bc   : 1.25 < 1.3
7  awk  : 2.4
7  bc   : 2.4
8  sum  : 65  avg: 21.67
9  mul  : 2178  bc: 2178.0
10 unit : 1500 MB  1.46 GB

式の早見表

やりたいこと 整数だけで書く($(( ))) 小数を使う
四捨五入した割り算 $(( (a + b / 2) / b )) awk 'BEGIN { printf "%.0f", a / b }'(半分ちょうどは偶数側)
切り上げ $(( (a + b - 1) / b )) −
切り捨て $(( a / b )) awk 'BEGIN { print int(a / b) }'
パーセント $(( x * 100 / total )) awk 'BEGIN { printf "%.1f", x * 100 / total }'
小数第1位まで $(( x / 10 )).$(( x % 10 ))(10倍の値を持つ) printf "%.1f"
小数の比較 −(整数に直してから比較) awk 'BEGIN { exit !(v < l) }'

整数だけの四捨五入と切り上げの式は、a と b が正の数のときに成り立ちます。小数の比較で使っている exit !(条件) は、条件が真のとき終了ステータス 0(成功)を返すための書き方です。

入力値を計算に使う前のチェック

$(( )) は、変数の中身を式として評価します。数字以外が入ると、思いがけない結果やエラーになります。

$ bash -c 's="12abc"; echo $(( s + 1 ))'
bash: line 1: 12abc: value too great for base (error token is "12abc")
$ bash -c 'a="1 2"; echo $(( a + 1 ))'
bash: line 1: 1 2: syntax error in expression (error token is "2")
$ bash -c 's=" 12 "; echo $(( s + 1 ))'
13

外から受け取った値(引数、ファイルの内容、コマンドの出力)は、計算の前に整数かどうかを確かめると安心です。sh でも動く書き方は次のとおりです。

case $n in
  ''|*[!0-9]*) echo "整数を指定してください: $n" >&2; exit 1 ;;
esac

dash で n=12a を渡すと「整数を指定してください」の側に入り、n=12 では通過することを確認しました。

チェック項目 対応
空ではないか 空は 0 として計算されるため、先に確認する
数字だけか case や [[ $n =~ ^[0-9]+$ ]](bash)で確認
先頭に 0 が付いていないか bash は 10#$n、sh は ${n#0}
桁区切りのカンマが入っていないか ${s//,/}(bash)で取り除く
0 で割っていないか 割る数が 0 でないことを確認

よくある質問

Q. $(( )) と (( )) はどう違いますか? A. $(( )) は計算結果の文字列に置き換わる「展開」で、x=$(( a + 1 )) や echo の引数に使います。(( )) は計算を行う「コマンド」で、結果は出力されず、式の値が 0 以外なら成功の終了ステータスを返します。(( )) は bash・ksh・zsh の機能で、dash などの sh では使えません。

Q. 小数の計算は bc と awk のどちらがよいですか? A. 表示用に桁をそろえたい、ファイルの集計もしたい、という場合は awk が手軽です。桁数の多い計算や、切り捨て位置を厳密に決めたい計算は bc が向いています。bc は入っていない環境もあるため、配布するスクリプトでは awk のほうが持ち運びやすい場面が多いです。

Q. sh でインクリメントするにはどう書きますか? A. i=$(( i + 1 )) と書きます。i++ や (( ))、let は dash では使えませんでした。set -e を付けたスクリプトでも、この書き方なら i の値に関係なく続行します。

Q. 日付の計算で「value too great for base」と出ます。 A. 08 や 09 のように 0 で始まる値が8進数として解釈されています。bash では $(( 10#$m )) のように 10# を付けて10進数と明示するか、date の書式を %-m のようにして 0 埋めなしで取得します。

まとめ

  • 整数の計算は $(( ))。シェル組み込みで速く、sh でも bash でも使える
  • 小数は awk か bc。awk は printf で桁をそろえやすく、bc は scale で桁数を厳密に指定できる
  • 0 で始まる数は8進数。08 と 09 はエラーになるので、bash では 10#$変数、sh では ${変数#0} を使う
  • 整数の割り算は切り捨て。パーセントは「掛けてから割る」、四捨五入は (a + b / 2) / b、切り上げは (a + b - 1) / b
  • set -e のもとでは (( i++ )) が i=0 のとき止まる。インクリメントは i=$(( i + 1 )) を基本にする
  • sh(dash)では **、++、(( ))、let、10# が使えない。#!/bin/sh のスクリプトは POSIX の範囲で書く
  • expr や、ループ内で毎回起動する bc・awk は時間がかかる。くり返しはシェルの算術展開か、awk の中のループにまとめる

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「シェルスクリプトの計算方法まとめ $(( ))・expr・bc・awk比較表つき」

株式会社RJCでは、シェルスクリプトの計算方法のような基礎を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?