expr $x + 1
一番良くある例がこのexpr
を使った方法。
expr
は与えた引数を式として評価してくれるツールですが、
数式の足し算とかもできます。
終了ステータスは、
- 0: 式が正しく評価され、評価値が0かnull以外の場合
- 1: 式が正しく評価され、評価値が0かnullのとき
- 2: 式が不当なとき
- 3: (GNU版のみ)その他エラーが起こったとき
expr
にもGNU版とBSD版があり、GNU版のみに3が返る場合がありますが、
これはメモリ不足など外部要因なエラーが起きた際に返るものでまず出ないです。
いずれにしろ、0だけでなく、0か1の場合が成功、というのがexpr
の特殊なところです。
$ expr 1 + 1
2
$ echo $?
0
$
$ expr 1 - 1
0
$ echo $?
1
$ expr a + 1
expr: non-numeric argument
$ echo $?
2
$
というわけで、これを使って
1 2 3 4 5 6 7 8 9 10 11 |
|
みたいな感じで、調べたい変数を適当に足し算で試してやって、 終了ステータスが2より小さいかどうか、で数字かどうか、を調べることができます。
追記: 2023/10/26
if
文のところが$ret
ではなく$?
になってたので訂正。
追記ここまで
追記: 2023/10/26
コメントで指摘していただいた通り、
GNUのexpr
はmatch
, substr
, index
, length
といった特定の文字列が第一引数に来るとサブコマンド的な扱いで
それらに応じた計算を行います
1。
1 2 3 4 |
|
と言った感じでコマンドが正常終了します2。
したがって特定の文字列ではcheck_num_expr.shはXXX is a number
と返してしまいます。
また、--
はその後の引数を-
で始まってもオプションではなく単なる文字列として扱うための印なので、
これもexprの最初に渡すと+ 1
はただ1を返すので上のスクリプトだと-- is a number
になってしまいます。
BSD版3 では文字ようrコマンドは基本的にはないようですが、 一部実装されているものもあるみたいです。
ちなみに手元のmacOSでは/usr/bin/expr
はGNUのものが入っていました。
というわけで、この辺の問題も含めて整数であってもこの方法は避けた方が良いです。
追記ここまで
expr
は整数の足し算しかできないため、この方法で調べられるのは整数のみになります。
1.1
とかを渡すと数字じゃない、と言われてしまいます。
expr “$x” : “
expr
は計算式だけではなくていろいろな表現の式を評価することができます。
expr
を使った数字判断でもうひとつの方法が:
を使った比較式です。
expr expr1 : expr2
の形でexpr2
は正規表現を使いexpr1
の先頭からいくつマッチしているか、という評価値になります。
つまり
expr abcde : ab
はOK(2)、
expr abcde : bc
はNO(0)、と言った感じ。
これを使って
expr "$x" : "[0-9]*$" >&/dev/null
とすれば、x
が0以上の整数かどうか、とういことが判断できます。
[0-9]
: 0~9の数字のいずれかの文字*
: 直前の文字列を0回以上の繰り返し$
: 文字列終了
1 2 3 4 5 6 7 8 9 |
|
こちらだと式の評価値自体はマッチした文字の数、 一つもマッチして無ければ0になります。
上の形ならinput
に何を入れても式としては成立するので、
先に書いた終了ステータスのリストと比べると
マッチしている場合には0、そうでない場合には1が終了ステータスになります。
(式の評価値とは0と1の場合で逆になるのでちょっと注意。)
こちらの場合であれば成功していれば必ず終了ステータスが0になるので 上の様に足し算を使ったときよりもシンプルに書けるメリットがあります。
ただし、この場合は負の整数は文字列としてみなされてしまいます。
さらにこれを使えば少数も含めて判断できます。
1 2 3 4 5 6 7 8 9 |
|
数字だけの場合は前のほうで判断して、少数の場合は後半で判断。
.
はそのままだと全ての文字にマッチするのでエスケープして、
前後の数字はそれぞれ0回以上出てくれば良い、という形。
このままだと.
だけのものを数字と判断してしまうので最後にそれを覗いています。
これで1
、1.
、1.1
、.1
と言ったもの全てが数字として判断されます。
後半を
expr "$input" : "[0-9]*\.*[0-9]*$" >&/dev/null
の様にすれば.
が含まれない整数の場合にもマッチするので数字全体っぽいことが出来ますが、
これだとピリオドだけの場合やピリオドが複数個の場合も数字とみなされてしまいます。
それを取り除くのはまた面倒なので上の式で十分かな、という感じ。
GNU版のexpr
であればもっと簡単に
1 2 3 4 5 6 7 8 9 |
|
と、\+
(一回以上の繰り返し)や\?
(0回、または1回)と言った記述が使えるので
簡単にかけます。
また、最初に-
があってもなくても良いようにも簡単に出来るので負の整数も対応可能です。
.1
の様に整数の位がない状態も受けようと思うともう一段階複雑にする必要がありますがとりあえずは
こんなもんで良いかと。
ただし、この記述方法はMacなどのBSD版だと使えません。
((x+1)) : だめな方法
一時期血迷って間違って使ってた方法で、この方法は実際には上手く行かないので使えませんが メモとして。
シェルの機能に算術式という二重括弧で囲う式があり、 これを使うと数値計算が簡単に書けて実行することができます。
$ x=$(expr $a + $b)
$ x=$((a+b))
の2つはa
とb
が数字ならx
にそれらの和が同じように代入されます。
ここでa
が文字だとするとエラー終了になるときがあります。
$ a=1
$ b=1
$ echo $((a+b))
2
$ echo $?
0
$ a=a
$ b=1
$ echo $((a+b))
bash: a: expression recursion level exceeded (error token is "a")
$ echo $?
1
まあ、エラーメッセージを見ればわかるのですが、ここでのエラーの意味は
a
が文字列だからエラーなわけではなくて
a
がさらにa
を指していて無限ループに入ってしまってだめ、ということです。
算術式の中では$
をつけないで変数を展開しますが、展開後に文字列だとさらに
もう一度それを変数とみなして展開しようとするかなりアグレッシブなことをします。
実際、
$ a=5
$ b=a
$ c=b
$ echo $((c))
5
と言った感じに他段階で数字が探されてるのがわかります。
展開後が空ならそこで終わりですが、+5
としても同じ様に結果が5
になるだけなので、
$ a=aaa
$ b=1
$ echo $((a+b))
1
$ echo $?
0
と言った具合に文字列を与えてもエラーは起きません。
(ただし、aaa
がまたa
に戻ったりaaa
自身だったりするような変なループに入るような変数でない場合。)
さらにこの算術式について言えば、評価値が0の場合にも終了ステータスは1を返します。
$ a=-1
$ echo $((a+1))
0
$ echo $?
1
なので((a+1))
みたいな評価であれば-1以外の整数と言った特殊なものになります。
((a-5))
とかすれば5以外の整数とか特殊なことも出来ないことはないですがそんなものは普通に評価すればよいかと。
ちょっとa=a
的な簡単なテストをしてこれでいけるじゃん、ということでしばらく入れて
テストしてたら全然駄目だった、という失敗例です。
[[ “$x” =~ ^[0-9]+$ ]]
expr
のマッチ条件に似たもので、Bashなどのbuiltinコマンドの[[
を使う方法があります。
1 2 3 4 5 6 7 8 9 |
|
[[ expr1 =~ expr2 ]]
でexpr2の表現がexpr1に含まれるかどうか、を判断します。
expr
とちょっと正規表現が違う形でこちらのほうが+
で1回以上の繰り返し、などの表現も使えたりします。
^
: 文字列開始[0-9]
: 0~9の数字いずれかの文字+
: 直前の文字を1回以上繰り返し$
: 文字列終了
これで0以上の整数を判断できます。
さらにはGNU版exprのように小数や負の数にも対応可能です。
1 2 3 4 5 6 7 8 9 |
|
[[ ]]
はBashやZshなどの組み込みコマンドなので
GNUな環境でもBSDな環境でも同じ様に使えます。
grep
追記: 2018/09/07
下の速度の比較でepxr
だけが悪いいみたいになってもいけないので、
他の方法としてgrep
を使った方法も挙げておきます。
1 2 3 4 5 6 7 8 9 |
|
こんな感じでできます。
小数や負の数も入れたい場合も
1 2 3 4 5 6 7 8 9 |
|
でいけます。
grep
もGNU版、BSD版がありますが、この方法はどちらのgrep
でも同様にできます。
追記ここまで
速度比較
もともと例にならってexpr
を使ってましたが、
ちょっと回数をこなすときにexpr
を呼ぶとかなり遅くなるので
他のを考えて(())
とか使ったり[[]]
を使ったりしました。
実際は測ってみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
これをGNUな環境のCentOS7が入ってるマシンで測ってみると、
$ ./num_check_test.sh
loop_time
real 0m0.002s
user 0m0.001s
sys 0m0.001s
expr add
real 0m0.808s
user 0m0.435s
sys 0m0.370s
expr match
real 0m0.881s
user 0m0.432s
sys 0m0.446s
grep
real 0m1.505s
user 0m0.724s
sys 0m0.777s
[[]]
real 0m0.027s
user 0m0.027s
sys 0m0.000s
こんな感じで[[]]
の方が数十倍位速いです。
上ので簡単なスクリプトで1000回ループなので
ちょっと色々なことをするスクリプトならexpr
を使うか使わないかで秒かかるか一瞬で終わるか、
という感じになってきます。
さらに同じスクリプトを手元のMacで走らせてみると
$ ./num_check_test.sh
loop_time
real 0m0.004s
user 0m0.003s
sys 0m0.000s
expr add
real 0m2.276s
user 0m0.878s
sys 0m1.189s
expr match
real 0m2.328s
user 0m0.903s
sys 0m1.224s
grep
real 0m2.608s
user 0m1.206s
sys 0m1.700s
[[]]
real 0m0.013s
user 0m0.012s
sys 0m0.001s
と、[[]]
が100分の1位のスピードになっています。
この辺どれだけ差が出るかはマシンスペックにかなり依存して、
CentOS7でもgrep
とexpr
が同じ位の環境もあったりしたので
この2つの差はあまりないと考えてもらって良いと思いますが、
[[]]
は桁違いに速いことはわかります。
ちなみに手元のWindowsのUbuntu On Windowsでは
expr
/grep
が10秒位、[[]]
が0.03秒とかで更にすごい差がついてました。
ちなみに上のスクリプトだと全て数字でOKになる場合ですが、a=aaa
とかして
文字列にしても時間は有意には変わりませんでした。
小数チェックにしても時間的には見える影響はないです。
このテストをする前はサブプロセスを立ち上げるとそれがものすごくコストが高い、と思っていたのですが、 単に外部コマンドを呼ぶことだけでかなり大きなコストになってるようです。
ただ、サブプロセスの影響も無視できないほどあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
これをMacで測ってみると
$ ./num_check_subprocess_test.sh
loop_time
real 0m0.004s
user 0m0.003s
sys 0m0.000s
expr add
real 0m2.342s
user 0m0.896s
sys 0m1.232s
expr add, subprocess
real 0m2.668s
user 0m0.968s
sys 0m1.297s
[[]]
real 0m0.015s
user 0m0.014s
sys 0m0.001s
[[]] subprocess
real 0m0.869s
user 0m0.349s
sys 0m0.483s
こんな感じでexpr
の方は相対的に小さいのであまり変わりませんが、
[[]]
の方は元が小さい分、かかってる時間はほぼサブプロセス化によるもので影響が大きく出ています。
ちなみにCentOS7だと
$ ./num_check_subprocess_test.sh
loop_time
real 0m0.002s
user 0m0.002s
sys 0m0.000s
expr add
real 0m0.786s
user 0m0.414s
sys 0m0.370s
expr add, subprocess
real 0m0.851s
user 0m0.436s
sys 0m0.413s
[[]]
real 0m0.027s
user 0m0.024s
sys 0m0.002s
[[]] subprocess
real 0m0.405s
user 0m0.279s
sys 0m0.125s
Ubuntu on Windowsだと
$ ./num_check_subprocess_test.sh
loop_time
real 0m0.004s
user 0m0.000s
sys 0m0.000s
expr add
real 0m14.396s
user 0m0.750s
sys 0m6.688s
expr add, subprocess
real 0m26.435s
user 0m0.813s
sys 0m8.000s
[[]]
real 0m0.081s
user 0m0.031s
sys 0m0.031s
[[]] subprocess
real 0m8.808s
user 0m0.500s
sys 0m2.391s
とかなりサブプロセスの負荷も大きくなってます。 これはUbuntuというよりWindowsのUbuntuだからみたいです。
実際同じUbuntu 18.04.1をGCEで作ってみて試した所 CentOS7(これもGCE上で試したものです)と同じ位の結果になっています。
さらにForkの悪さに定評のあるCygwinで見てみると
$ ./num_check_subprocess_test.sh
loop_time
real 0m0.006s
user 0m0.000s
sys 0m0.000s
expr add
real 0m35.275s
user 0m4.664s
sys 0m13.084s
expr add, subprocess
real 0m43.586s
user 0m5.111s
sys 0m15.446s
[[]]
real 0m0.089s
user 0m0.079s
sys 0m0.000s
[[]] subprocess
real 0m18.913s
user 0m2.587s
sys 0m6.644s
な感じでサブプロセス化するだけで20秒位かかっています。
ただ、expr
を見るとそこまで差がないので、
単にサブプロセスになるのとコマンドを呼ぶことによる負荷はなんらか相互作用で多少は薄まるようです。
(この辺のOSの動作は全く理解してなくて状況証拠だけです…)
これ見てるとUbuntu on WindowsはUbuntuがWindows上で動くようになったとはいえ、 割とCygwinの悪いところ(Forkの悪さ)が解消されたとは言えない様な… ま、もっと良いWindowsPCだとまた違うのかもしれませんが。
まとめ
シェルスクリプトで変数が数字かどうかチェックする方法は一般的にはexpr
が用いられますが、
[[]]
を使ったチェックを使うと見た目もすっきり書けますし
実行時間も圧倒的に速くなります。
BashやZshが用意されていない環境やどうしてもUbuntuとかの/bin/sh
(dash)を使わなくてはいけない、
といった場合には駄目ですが、
殆どの場合[[]]
は使える環境だと思うので
シェルスクリプトでの数字の判断は
[[ "$a" =~ ^[0-9]+$ ]]
の形を使うのが一番良いと思います。
これを使えば小数や負の数に関してもチェックすることが可能です。
(expr
でも負の整数は簡単、ただ小数は結構面倒です。)
また、実行時間を考えるとやはりなるべくサブプロセスを使わないで 直接評価できるような形でチェックを行うべき、ということです。
length
は第二引数の文字の長さを返す。1
が一文字なので1。) ↩