rcmdnk's blog
Last update

「数字」が読めると本当に儲かるんですか?

シェルスクリプトの変数に型はありませんが、 それが数字かどうか判断したい時があります。

そんなときにチェックする方法について。

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
$

というわけで、これを使って

check_num_expr.sh
1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env bash

input=$1

expr "$input" + 1 >&/dev/null
ret=$?
if [ $ret -lt 2 ];then
  echo "$input is a number"
else
  echo "$input is not a number"
fi

みたいな感じで、調べたい変数を適当に足し算で試してやって、 終了ステータスが2より小さいかどうか、で数字かどうか、を調べることができます。

追記: 2023/10/26

if文のところが$retではなく$?になってたので訂正。

追記ここまで

追記: 2023/10/26

コメントで指摘していただいた通り、 GNUのexprmatch, substr, index, lengthといった特定の文字列が第一引数に来るとサブコマンド的な扱いで それらに応じた計算を行います 1

1
2
3
4
$ exprt length + 1
1
$ echo $?
0

と言った感じでコマンドが正常終了します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回以上の繰り返し
  • $: 文字列終了
check_num_match.sh
1
2
3
4
5
6
7
8
9
#!/usr/bin/env bash

input=$1

if expr "$input" : "[0-9]*$" >&/dev/null;then
  echo "$input is a number"
else
  echo "$input is not a number"
fi

こちらだと式の評価値自体はマッチした文字の数、 一つもマッチして無ければ0になります。

上の形ならinputに何を入れても式としては成立するので、 先に書いた終了ステータスのリストと比べると マッチしている場合には0、そうでない場合には1が終了ステータスになります。 (式の評価値とは0と1の場合で逆になるのでちょっと注意。)

こちらの場合であれば成功していれば必ず終了ステータスが0になるので 上の様に足し算を使ったときよりもシンプルに書けるメリットがあります。

ただし、この場合は負の整数は文字列としてみなされてしまいます。

さらにこれを使えば少数も含めて判断できます。

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

input=$1

if expr "$input" : "[0-9]*$" >&/dev/null || ( expr "$input" : "[0-9]*\.[0-9]*$" >&/dev/null && [ "$input" != "." ] );then
  echo "$input is a number"
else
  echo "$input is not a number"
fi

数字だけの場合は前のほうで判断して、少数の場合は後半で判断。

.はそのままだと全ての文字にマッチするのでエスケープして、 前後の数字はそれぞれ0回以上出てくれば良い、という形。 このままだと.だけのものを数字と判断してしまうので最後にそれを覗いています。

これで11.1.1.1と言ったもの全てが数字として判断されます。

後半を

expr "$input" : "[0-9]*\.*[0-9]*$" >&/dev/null

の様にすれば.が含まれない整数の場合にもマッチするので数字全体っぽいことが出来ますが、 これだとピリオドだけの場合やピリオドが複数個の場合も数字とみなされてしまいます。 それを取り除くのはまた面倒なので上の式で十分かな、という感じ。

GNU版のexprであればもっと簡単に

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

input=$1

if expr "$input" : "-\?[0-9]\+\.\?[0-9]*$" >&/dev/null;then
  echo "$input is a number"
else
  echo "$input is not a number"
fi

と、\+(一回以上の繰り返し)や\?(0回、または1回)と言った記述が使えるので 簡単にかけます。

また、最初に-があってもなくても良いようにも簡単に出来るので負の整数も対応可能です。 .1の様に整数の位がない状態も受けようと思うともう一段階複雑にする必要がありますがとりあえずは こんなもんで良いかと。

ただし、この記述方法はMacなどのBSD版だと使えません。

((x+1)) : だめな方法

一時期血迷って間違って使ってた方法で、この方法は実際には上手く行かないので使えませんが メモとして。

シェルの機能に算術式という二重括弧で囲う式があり、 これを使うと数値計算が簡単に書けて実行することができます。

$ x=$(expr $a + $b)
$ x=$((a+b))

の2つはabが数字なら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コマンドの[[を使う方法があります。

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

input=$1

if [[ "$input" =~ ^[0-9]+$ ]];then
  echo "$input is a number"
else
  echo "$input is not a number"
fi

[[ expr1 =~ expr2 ]]でexpr2の表現がexpr1に含まれるかどうか、を判断します。 exprとちょっと正規表現が違う形でこちらのほうが+で1回以上の繰り返し、などの表現も使えたりします。

  • ^: 文字列開始
  • [0-9]: 0~9の数字いずれかの文字
  • +: 直前の文字を1回以上繰り返し
  • $: 文字列終了

これで0以上の整数を判断できます。

さらにはGNU版exprのように小数や負の数にも対応可能です。

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

input=$1

if [[ "$input" =~ ^-?[0-9]+\.?[0-9]*$ ]];then
  echo "$input is a number"
else
  echo "$input is not a number"
fi

[[ ]]はBashやZshなどの組み込みコマンドなので GNUな環境でもBSDな環境でも同じ様に使えます。

grep

追記: 2018/09/07

下の速度の比較でepxrだけが悪いいみたいになってもいけないので、 他の方法としてgrepを使った方法も挙げておきます。

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

input=$1

if echo "$input" | grep -q "^[0-9]\+$";then
  echo "$input is a number"
else
  echo "$input is not a number"
fi

こんな感じでできます。

小数や負の数も入れたい場合も

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

input=$1

if echo "$input" | grep -q "^-\?[0-9]\+\.\?[0-9]*$";then
  echo "$input is a number"
else
  echo "$input is not a number"
fi

でいけます。

grepもGNU版、BSD版がありますが、この方法はどちらのgrepでも同様にできます。

追記ここまで

速度比較

もともと例にならってexprを使ってましたが、 ちょっと回数をこなすときにexprを呼ぶとかなり遅くなるので 他のを考えて(())とか使ったり[[]]を使ったりしました。

実際は測ってみます。

num_check_test.sh
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
#!/usr/bin/env bash

loop=$(seq 0 1000)
a=1

echo "loop_time"
time for n in ${loop};do
  :
done
printf "\n\n"

echo "expr add"
time for n in ${loop};do
  expr $a + 1 >&/dev/null
done
printf "\n\n"

echo "expr match"
time for n in ${loop};do
  expr "$a" : "[0-9]*$" >&/dev/null
done
printf "\n\n"

echo "grep"
time for n in ${loop};do
  echo "$input" | grep -q "^[0-9]\+$"
done
printf "\n\n"

echo "[[]]"
time for n in ${loop};do
  [[ "$a" =~ ^[0-9]+$ ]]
done

これを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でもgrepexprが同じ位の環境もあったりしたので この2つの差はあまりないと考えてもらって良いと思いますが、 [[]]は桁違いに速いことはわかります。

ちなみに手元のWindowsのUbuntu On Windowsでは expr/grepが10秒位、[[]]が0.03秒とかで更にすごい差がついてました。

ちなみに上のスクリプトだと全て数字でOKになる場合ですが、a=aaaとかして 文字列にしても時間は有意には変わりませんでした。

小数チェックにしても時間的には見える影響はないです。

このテストをする前はサブプロセスを立ち上げるとそれがものすごくコストが高い、と思っていたのですが、 単に外部コマンドを呼ぶことだけでかなり大きなコストになってるようです。

ただ、サブプロセスの影響も無視できないほどあります。

num_check_subprocess_test.sh
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
#!/usr/bin/env bash
loop=$(seq 0 1000)
a=1

echo "loop_time"
time for n in ${loop};do
  :
done
printf "\n\n"

echo "expr add"
time for n in ${loop};do
  expr $a + 1 >&/dev/null
done
printf "\n\n"

echo "expr add, subprocess"
time for n in ${loop};do
  x=$(expr $a + 1)
done
printf "\n\n"

echo "[[]]"
time for n in ${loop};do
  [[ "$a" =~ ^[0-9]+$ ]]
done
printf "\n\n"

echo "[[]] subprocess"
time for n in ${loop};do
  x=$([[ "$a" =~ ^[0-9]+$ ]])
done

これを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でも負の整数は簡単、ただ小数は結構面倒です。)

また、実行時間を考えるとやはりなるべくサブプロセスを使わないで 直接評価できるような形でチェックを行うべき、ということです。

Sponsored Links
  1. String expressions (GNU Coreutils 9.4)

  2. lengthは第二引数の文字の長さを返す。1が一文字なので1。)

  3. expr(1)

Sponsored Links

« シェルスクリプト関数でポインタ渡しもどきを実装する CentOS 7などのSystemdに対応したデーモンプログラムを作る »

}