悩み
たとえば 0010
という文字列を 10
という文字列にしたい。または、 10 という数値として処理したい。
もちろん、0000
の場合は 0
が適切である。
簡単にいけるかと思ったが、数値文字列を数値として扱う場合、0で始まる文字列を 8進数として扱う仕様があり、悩まされた。なお、sed などの万能ツールは使わない。
$ STR="0010"
$ echo ${STR##*0} #=> 空文字列になってしまう
そもそも最初はパラメータ展開でいけると思ってたんだけど、普通に失敗。
$ expr “$STR” “+” “0” #=> 10になるけど負けた気がする
$ expr "$STR" #=> 10 になる。というか1引数でいけたのか expr
$ echo $(( $STR + 0 )) #=> 8 になってしまう
$ echo $(( $STR )) #=> 8 になってしまう。
$ printf "%d" $STR #=> 8 になってしまう
整数型の変数にしてみても、
$ declare -i B="0010"
$ echo $B #=> 8 になってしまう
一緒なわけだ。
これは、そもそも「0010」という文字列を数値に変換する際に 8進数として変換されてしまうからだ。変数の型の問題ではない。
expr がうまく機能するのは、 expr に引数が文字列として渡されているからだ。bash 上で文字列を数値に変換する処理が走ってしまうと失敗する。だから、似たような処理をしてても $(( ))
による展開だとうまくいかない。値がその場で型変換されて、bash の文字列-数値の変換ルールに従って8進数扱いされてしまう。
“[base]#n” の意味
bash の man を見るとARITHMETIC のセクションに、以下のような記述がある。
Otherwise, numbers take the form [base#]n, where the optional base is a decimal number between 2 and 64 representing the arithmetic base, and n is a number in that base.
で、最初ここの意味がわからなかった。というかいくつか試してみて「あれ、動かなくね?」と悩んだ。
算術処理をする際、10進数で強制的に扱いたいなら文字列の頭に “10#” とつけちゃえばいいって話なんだけど、bash で算術処理が走るケースでなければ意味はない。文字列として変数がプロセスに渡された後ではこのルールは有効ではない。
実のところ、何も考えずに出力したとき「10」が「8」として出力されてしまう、つまりそのように扱われてしまうケースにおいて意味があるということである。
だから、
$ echo $(( 10#${STR} )) #=> 10 になる
$ printf "%d" $(( 10#${STR} )) #=> 10 になる
$ declare -i C=10#$STR
$ echo $C #=> 10になる
完璧。
なんだけど、引数を文字列として扱ってる場所ではうまくいかない。
expr はこうなる。
$ expr 10#$STR "+" 0
expr: not a decimal number: '10#0010'
もちろん、普通に echo しても
$ echo 10#$STR
10#0010
こうなる。
printf も同様である。
$ printf "%d" 10#$STR
-bash: printf: 10#0010: 無効な数字です
10
あくまでも、bash そのものが値を文字列から数値に変換する場合の仕様であることを理解すればそれほどややこしい話ではない。ルーク、bash の気持ちになるんだ。