はじめに

1992 年に POSIX でシェルが標準化されて以来、シェルスクリプトの数値計算に expr コマンドは使いません。 expr コマンドを使っていたのは Bourne シェル(古い sh)時代の話で、現在の POSIX sh 時代では数値計算に expr コマンドを使いません。

数値計算に使うのは $(( ... ))

数値計算には $(( ... )) を使います。expr コマンドによる数値計算は書き方が面倒です。例えば $(( ... )) を使った場合、以下のように直感的で可読性が高い式が、

value=$(( (i + 1) * 5 ))

value=$(((i+1)*5)) # つなげすぎると読みづらくなるが、つなげて書いても良い

expr コマンドを使うと以下のように長く読みづらくなります。

value=`expr \( $i + 1 \) \* 5` # 外部コマンド呼び出しの `...` は非推奨の古い書き方

value=$(expr \( $i + 1 \) \* 5) # 今は外部コマンド呼び出しに $(...) を使う

変数は $ が必要ですし、( )* はシェルのメタ文字とみなされないようにエスケープが必要ですし、スペースを無くしてつなげて書いてはいけません。

$(( ... )) は bash 拡張機能ではなく全ての POSIX シェルで使える

「bash では $(( ... )) で数値計算ができます」みたいな、いかにも bash 専用の独自機能ですというような書き方を見かけますが、 $(( ... )) は POSIX で標準化された移植性が高い書き方です。bash だけではなく全ての POSIX 準拠のシェルで使うことができます。使えないのは古い UNIX で使われていた Bourne シェルです。Bourne シェルは POSIX に準拠していません。

ついでに言いますが、紛らわしいので sh = Bourne シェルとは言わないようにしてください。現在の sh は POSIX 準拠のシェルに置き換わっており Bourne シェルはもはや使われていません。

expr コマンドは約 1000 倍遅い

シェルに組み込まれた機能である $(( ... )) とは異なり expr コマンドは外部コマンドであるため、コマンド呼び出しに大きく時間がかかります。下記の例では 963.875 倍遅いことがわかります。当然ですがシェルや環境によってどれくらい遅いかは異なります。

$ time dash -c 'i=0; while [ $i -lt 100000 ]; do i=$(( i + 1 )); done; echo $i'
100000

real	0m0.152s
user	0m0.151s
sys	0m0.000s

$ time dash -c 'i=0; while [ $i -lt 100000 ]; do i=$(expr $i + 1); done; echo $i'
100000

real	2m26.509s
user	1m48.080s
sys	0m46.850s

$(( ... )) は bash ではなく ksh88 で発明された

多くの人が bash の拡張機能だと思いこんでいる機能の多くは、元々は ksh88 または ksh93 がオリジナルです。ksh (KornShell) は UNIX を開発した AT&T で開発されたシェルで、UNIX で Bourne シェルを置き換えるシェルとして使われていたシェルです。POSIX シェルの標準規格のベースとなったシェルで、POSIX シェルは ksh88 のサブセットです。

“In early proposals, a form \$[expression] was used. It was functionally equivalent to the "\$(())" of the current text, but objections were lodged that the 1988 KornShell had already implemented "\$(())" and there was no compelling reason to invent yet another syntax. Furthermore, the "\$[]" syntax had a minor incompatibility involving the patterns in case statements.”

注意: $((010)) は 8 進数かもしれないし 10 進数かもしれない

expr コマンドは頭に 0 をつけても 10 進数として解釈されますが、$((...)) の場合 8 進数として解釈されることもあれば 10 進数として解釈されることもあります。例えば ksh 93u+m や zsh ではデフォルトでは $((010)) は 10 進数として解釈され 10 になります。これは POSIX 準拠モードにすることで 8 進数として解釈され 8 として扱われます。

このようになった経緯の詳細は完全に調べていませんが、おそらく ksh88 の時点で頭に 0 が来ても 10 進数として解釈したのが元凶です。ksh88 は 16 進数の 0x 表記に対応しているのですが、8 進数には対応していませんでした。その後の互換シェルでは ksh88 と同じように 10 進数として解釈しましたが、bash のように 8 進数として解釈するシェルも登場しました。そして POSIX で標準化されたためか、10 進数として解釈していたシェルでも POSIX 準拠モードで 8 進数として解釈するようになりました。ksh は少しややしく 93u+ では 8 進数として解釈されていたのですが、最近リリースされた 93u+m ではデフォルトで(ksh88 と同じく)10 進数、新しく追加された POSIX 準拠モードを有効にすることで 8 進数として解釈されるように変わりました。

最近のシェルであればデフォルト、または POSIX 準拠モードにすることで 頭に 0 が来る数値は 8 進数として解釈するはずですが、古いシェルの中には、POSIX 準拠モードにしても 10 進数として解釈するものがあるので注意してください。なお以下のコードを利用すれば、頭 0 を削除することができます。

num="001230"
echo "${num#"${num%%[!0]*}"}" # => 1230

macOS のデフォルトのログインシェルは zsh に変わりましたが、ターミナルで echo $((010)) した結果は 10 ですが、シェルスクリプト (#!/bin/sh) にして動かすと 8 になります。このような話を知らなければきっと混乱することでしょう。どの環境でも動くシェルスクリプトを書くためにはこのような問題も知って対処しなければならないのが辛い所です……。

おまけ: 残る expr の役目は文字列の大小比較と正規表現マッチ

POSIX で標準化された範囲ではシェルに文字列の大小比較と正規表現マッチングがないため、expr コマンドの役目は残っています。しかしこれも bash、ksh、zsh であればシェルにその機能が含まれているため expr コマンドの役目は残っていません。

expr コマンドは正規表現マッチングができますが、基本正規表現 (BRE) を使うため使いづらいコマンドです。この問題を解決したい方は「シェルスクリプトの世界から基本正規表現(BRE)をなくそう!」を参照してください。

おまけ: 小数の計算には bc ではなく awk の方が良い

expr コマンドも $((...)) も POSIX で標準化された範囲では整数のみの対応で小数計算はできません。小数の計算には bc コマンドを使うという記事が多いのですが、awk を使った方が良いです。bc コマンドではだめということはないのですが、bc コマンドは POSIX で標準化されたコマンドでありながら、インストールされていない環境があるので注意が必要です。まあインストールするのであれば bc コマンドでも良いですし、なんなら dc コマンドや Perl 等でも問題ありません。

さいごに

いい加減 expr コマンドを使って数値計算する記事は無くなった方がいいと思います。新しく記事を書く人は expr コマンドによる数値計算は書かないか古いやり方だと明記するようにしましょう。わざわざ不便で遅い expr コマンドを使って整数の数値計算をする必要はありません。これ以上古い書き方の記事を増やさないでください。


原创声明:本文系作者授权爱码网发表,未经许可,不得转载;

原文地址:https://www.likecs.com/show-308628371.html

相关文章: