rcmdnk's blog

カエルの世界はきっと8進数

最近のBashなどのシェルスクリプトでは 二重括弧を使うことで数字の足し算などが出来ますが、 おもったよりも賢い点が逆にバグを生んだ話。

二重括弧による計算

Bashなどでは、

1
2
3
4
$ a=1
$ x=$(( a + 2 ))
$ echo $x
3

みたいな感じで二重括弧の中で数式を書くことで計算することが出来ます。

実際には数字を代入したりすることも含めていろいろな事ができます。

Arithmetic Expansion (Bash Reference Manual)

0Xな数字を二重括弧で計算する

ファイル名などで、数字をインクリメントして名前をつけていきたい、 10個以上になることもあるので10の位をゼロ埋めした状態にしたい、ということがある場合。

01, 02, 03….

みたいな感じにするために

1
2
3
4
5
6
7
8
9
#!/usr/bin/env bash

n=01
f1=file-$n
n=$(printf "%02d" $((n + 1)))
f2=file-$n
n=$(printf "%02d" $((n + 1)))
f3=file-$n
...

のようにしていくと、

1
2
3
file-01
file-02
file-03

といった感じのファイル名が出来上がります。

$(())の中で01とかでも足し算で2とかにしてくれるので一見これで良いような。

nを分けて、ファイル名にする方だけでprintfして02のような形も出来ますし、 この場合はそれでもあまり変わらないのですが、 場合によっては直接suffixとして変更していったほうがわかりやすいこともあるかと思います。

問題はnが8を超えた時、

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env bash

n=01
f1=file-$n
n=$(printf "%02d" $((n + 1)))
f2=file-$n
n=$(printf "%02d" $((n + 1)))
f3=file-$n
n=$(printf "%02d" $((n + 1)))
f4=file-$n
n=$(printf "%02d" $((n + 1)))
f5=file-$n
n=$(printf "%02d" $((n + 1)))
f6=file-$n
n=$(printf "%02d" $((n + 1)))
f7=file-$n
n=$(printf "%02d" $((n + 1)))
f8=file-$n
n=$(printf "%02d" $((n + 1)))
f9=file-$n
...

とすると、最後の足し算のところで、

1
08: value too great for base (error token is "08")

といったエラーが起こります。

これはn08の時ですが、01 + 1というのは実は普通の1 + 1ではなくて、 前者を8進数の1として解釈して10進数の1と足して10進数での2になってそれを表示しています。

01~07は10進数でも1~7になるので小さい数字だけでチェックすると問題を見落とします。

8進数なので9という数は存在せずエラーになります。

二重括弧での数字

他の言語とかでも00が先頭なら8進数、0xなら16進数、 また、0bだと2進数とするものがあります。

Bashなどの(())の計算では、0から始まるものは8進数、0xなら16進数として扱われます。

1
2
3
4
$ echo $(( 011 ))
9
$ echo $(( 0x11 ))
17

加えて、64までであれば自由な数字で進数を作ることが出来ます。 ベースとなる数字を#の前に書けば、

1
2
3
4
5
6
$ echo $(( 2#11 ))
3
$ echo $(( 7#1 ))
8
$ echo $(( 64#1 ))
65

といった感じに64進数まで使えます。

Shell Arithmetic (Bash Reference Manual)

64までは、0~9の数字、a~z10~35A~Z36~61、残り@_6263を表します。

1
2
3
4
5
6
$ echo $(( 64#Z ))
61
$ echo $(( 64#@ ))
62
$ echo $(( 64#_ ))
63

これらで使われる文字が表す数字がベースの数字以上になるとエラーになります。

1
2
$ echo $(( 63#_ ))
bash: 63#_: value too great for base (error token is "63#_")

exprでは単に無視する

シェルスクリプトで簡単に計算しようと思うとexprというコマンドがありますが、 こちらでは088も同じように0がないものとして扱われます。

1
2
$ expr 08 + 1
9

単に左にある0を無視するだけなので、0xaのような16進数のようなものは使えません。

1
2
$ expr 0xa + 01
expr: non-integer argument

この辺ちゃんとそれぞれ分かっていれば良いのですが、 書き換えとかで同じようにしてしまうと間違えてしまうかも。

このexprですが現在では ShellCheck とかで

1
^--^ SC2003 (style): expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]].

といった忠告を出すようになっていて二重括弧による計算が推奨されています。

ということで

1
2
3
4
5
6
7
8
9
#!/usr/bin/env bash

n=1
f1=file-$(printf "%02d" $n)
n=$((n + 1)))
f2=file-$(printf "%02d" $n)
n=$((n + 1)))
f3=file-$(printf "%02d" $n)
...

のように素直に数字は数字でインクリメントしてファイル名などに埋める時に0埋めするべきだと。

Sponsored Links
Sponsored Links

« シェルスクリプトでは`-e`, `-n`の引数は使えない(ということはない) GitHub Actionsで`bash`を指定すると`pipefail`オプションが付く »

}