シェルスクリプトでの数字判定について

そもそもシェルスクリプトには、他の言語のように API が存在しないため、ツールが提供する機能以外に何か必要な機能があれば、自作するしかありません。

シェルスクリプトで正しい数字かどうか判定する方法としては、expr を使う方法がよく知られています。

しかし、expr は、整数計算は出来ますが、実数計算(小数)が出来ないため、実数は文字列として認識されてしまう弱点があります。

代案として、expr と実数計算をサポートしている bc を併用し、もっと精度の高い数字判定を行う 方法についてご紹介します。

整数と実数について

当記事内で 整数実数 という言葉を使っています。

数学に詳しい方、プログラミングに詳しい方が見たときに誤解されやすい部分もあるかなと思いますので、当記事内で使用する用語の定義は、以下とします。

例えば、C 言語で扱われる数値には、整数実数 の 2種類あるので、プログラミングの観点から。

整数 : 自然数に、0 と負の数を加えた数のこと

-1、0、1、2、3

実数 : 整数に、自然数を加えた数のこと

-1、0、1、2、3、-1.0、1.0、3.141592

数字判定 その1. expr を利用する (整数判定)

まずは、数字計算に使われる expr を利用して、計算結果が数字かどうか判定する方法です。

このやり方が一番シンプルで、世の中では、このやり方がよく知られています。

expr は、数字を計算するためのコマンドなので、ユーザが判定したい数字に + 1 を足し算し、返ってきた戻り値でその結果が正常か異常か判定します。

そのため、もし、ユーザが入力した値が文字列だったり、数字の中に文字が入っていたりすると文法エラーになるので、判定結果は、異常になります。

以下は、expr の戻り値 です。

man expr

Exit status is 0 if EXPRESSION is neither null nor 0, 1 if EXPRESSION is null or 0, 2 if EXPRESSION is syntactically invalid, and 3 if an error occurred.

ケース戻り値
計算結果が 0 以外の場合0
計算結果が 0 の場合1
文法エラー2
その他、エラー3

戻り値を見れば分かるように、0 と 1 が正常 なので、ユーザが判定したい値に + 1 をし、戻り値が 2 より小さかったら正常と判定し、それ以外については、異常とします。

文法エラーについて

ここでいう 文法エラー とは、数字と文字列の足し算したり、引き算するようなことです。

12345abcdefg6789 + 1

ただし、以下のように、ダブルクォーテーションで囲んだ数字の計算は、文法エラーではありません。

"123456789" + 1

is_numric.sh
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/bash

PARAM_1=0

#### 引数の長さが 0 でなければ、
#### PARAM_1 にそれをセット
if [ ! -z $1 ]; then
    PARAM_1=$1
fi

#### PARAM_1 に + 1 をし、その戻り値を RET に保存
expr $PARAM_1 + 1 > /dev/null 2>&1
RET=$?

#### 戻り値をもとに数字が正しいか判定
echo "戻り値 : $RET"

#### 戻り値を使って正常か異常か判定
if [ $RET -lt 2 ]; then
    echo "$PARAM_1 : 数字です"
else
    echo "$PARAM_1 : 数字ではありません"
fi
実行結果
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ ./is_numric.sh 123456789
戻り値 : 0
123456789 : 数字です

$ ./is_numric.sh "123456789"
戻り値 : 0
123456789 : 数字です

$ ./is_numric.sh -1
戻り値 : 1
-1 : 数字です

$ ./is_numric.sh "-1"
戻り値 : 1
-1 : 数字です

$ ./is_numric.sh 12345abcdefg6789
戻り値 : 2
12345abcdefg6789 : 数字ではありません

$ ./is_numric.sh ABCDEFG
戻り値 : 2
ABCDEFG : 数字ではありません

$ ./is_numric.sh 1.1
戻り値 : 2
1.1 : 数字ではありません

expr は、実数計算ができない

expr は、一つ弱点がありまして、そもそも実数計算が出来ない点です。

上記の結果を見れば分かるように、expr は、実数計算ができないため、1.1 という実数を文字列として判定してしまいます。

なので、実数を含めて、厳密に数字かどうか判定するのに expr を使うのは、お勧めしません

数字判定 その2. expr と bc を併用する (実数判定)

今まで説明した expr は、どっちかと言うと簡単な整数計算によく使われます。

ファイルやパイプラインなどで引き渡された値を処理したり、実数計算のようなもっと複雑な演算がしたい場合には、expr ではなく、bc を使う必要があります。

expr と bc 簡単なテスト
1
2
3
4
5
6
7
8
$ echo "1 + 1" | expr
expr: missing operand

$ expr 0.9 + 0.1
expr: non-numeric argument

$ echo "0.9 + 0.1" | bc
1.0

bc が入っていなければ、先に入れておく

ちなみに、expr は、実行バイナリファイルとして /usr/bin ディレクトリの中に入っていますが、bc を使うためには、別途 rpm パッケージをインストールしないと使えないため、まずは、bc が入っているか確認し、入ってなければ、yum でインストールします。

bc パッケージ導入
01
02
03
04
05
06
07
08
09
10
11
12
13
■ bc が入っているか確認
# rpm -qa | grep "^bc"

■ 上記コマンド実行結果、何も出てこなかったらインストール
# yum -y install bc

■ bc がインストールされたか確認
# rpm -qa | grep "^bc"
bc-1.06.95-1.el6.x86_64

# bc --version
bc 1.06.95
Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006 Free Software Foundation, Inc.

bc は、賢すぎる

bc は、数学ライブラリ参照scale 指定数字比較処理 など、色んな機能をサポートしています。

bc の使い方については、一概に言えないため、詳細については、$ man bc をご確認ください。

言いたかったのは、bc は、16進数を 10進数に置き換えて計算してくれるところです。

16 進数10 進数
A10
B11
C12
D13
E14
F15

では、16 進数で計算してみます。

16 進数である A ~ F までは、自動的に 10進数に変換されることが分かります。 そして、G は、16 進数ではないため、普通の文字列として認識され、文法エラーになってしまいます。

bc 16進数計算
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
$ echo "A + 100" | bc
110

$ echo "B + 100" | bc
111

$ echo "C + 100" | bc
112

$ echo "D + 100" | bc
113

$ echo "E + 100" | bc
114

$ echo "F + 100" | bc
115

$ echo "G + 100" | bc
(standard_in) 1: illegal character: G
(standard_in) 1: syntax error

また、echo 文内に # が入っている場合、# は、コメントとして認識されます。

echo 計算式内 # の扱い
01
02
03
04
05
06
07
08
09
10
$ echo "#1 + 100" | bc

$ echo "1 #+ 100" | bc
1

$ echo "2 #+ 100" | bc
2

$ echo "3 #+ 100" | bc
3

また、. は、0 として、.1 は、0.1 として認識されます。

. は、0 扱い
01
02
03
04
05
06
07
08
09
10
11
12
13
14
$ echo ". + 1" | bc
1

$ echo ". + 2" | bc
2

$ echo ". + 3" | bc
3

$ echo "-.1 + 1" | bc
.9

$ echo "+.1 + 1" | bc
(standard_in) 1: syntax error

expr と bc を併用して数字判定

上記の例外事項を踏まえ、整数計算以外は、全て異常と判断してしまう expr のシンプルさをうまく利用して、bc と併用して数字判定を行います。

  • expr で数字かどうか判定
  • 数字じゃない場合、. が含まれているかチェックし、含まれていれば(実数)、bc でもう一回数字判定
  • 数字として認めるのは、-nn.n0 ~ 9 に限定する
  • 実数として認めるのは、0.1-0.1 のように . 前に数字が存在する場合のみ
  • bc の場合、演算結果が異常だったら syntax error を吐くので、それを拾って異常と判定する
is_numric.sh
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/bin/bash

PARAM_1=0

#### 引数の長さが 0 でなければ、
#### PARAM_1 にそれをセット
if [ ! -z $1 ]; then
    PARAM_1=$1
fi

#### 1次判定
#### expr で数字判定
#### 数字であれば終了
expr $PARAM_1 + 1 > /dev/null 2>&1

if [ $? -lt 2 ]; then
    echo "$PARAM_1 : 数字です"
    exit
fi

#### 2次判定
#### PARAM_1 に「.」が含まれている場合(実数) のみ、「bc」でもう一回数字判定
#### ただし、「.」、「-.」から始まるのは、実数として認めない
echo "$PARAM_1" | grep "\." | grep -v "^\." | grep -v "\-\." > /dev/null 2>&1

if [ $? -eq 0 ]; then

    BC_RESULT=`echo "$PARAM_1 + 1" | bc 2>&1`

    if [[ "$BC_RESULT" =~ "error" ]]; then
        echo "$PARAM_1 : 数字ではありません"
    else
        echo "$PARAM_1 : 数字です"
    fi

else

    echo "$PARAM_1 : 数字ではありません"

fi
実行結果
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
$ ./is_numric.sh 123456789
123456789 : 数字です

$ ./is_numric.sh "123456789"
123456789 : 数字です

$ ./is_numric.sh -1
-1 : 数字です

$ ./is_numric.sh "-1"
-1 : 数字です

$ ./is_numric.sh 12345abcdefg6789
12345abcdefg6789 : 数字ではありません

$ ./is_numric.sh ABCDEFG
ABCDEFG : 数字ではありません

$ ./is_numric.sh .
. : 数字ではありません

$ ./is_numric.sh .1
.1 : 数字ではありません

$ ./is_numric.sh 1.1
1.1 : 数字です

$ ./is_numric.sh -.1
-.1 : 数字ではありません

$ ./is_numric.sh +.1
+.1 : 数字ではありません

$ ./is_numric.sh A
A : 数字ではありません

$ ./is_numric.sh B
B : 数字ではありません

$ ./is_numric.sh C
C : 数字ではありません

$ ./is_numric.sh D
D : 数字ではありません

$ ./is_numric.sh E
E : 数字ではありません

$ ./is_numric.sh F
F : 数字ではありません

$ ./is_numric.sh -
- : 数字ではありません

終わりに

これで、expr よりだいぶ精度の高い数字判定が出来るようになりました。

なので、数字かどうかを判定する際には、expr 単独で使うよりは、状況に応じて、exprbc を使い分ける必要があるかなと思います。

もちろん、整数だけ判定したい場合には、expr だけで良いです。

expr も面倒だったらシンプルに grep で判定するやり方もあります。

is_numric.sh
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash

P=$1

echo "$P" | grep [^0-9] > /dev/null 2>&1

#### 数字以外の文字が含まれていれば、数字ではない
if [ $? -eq 0 ]; then
    echo "$P : 数字ではありません"

#### 数字だけで構成されているけど、
#### 0 から始まる数字を認めたくない場合はさらに先頭の文字チェックが必要
else
    CHR=`echo "$P" | cut -c1`

    if [ ! -z "$CHR" -a $CHR -ne 0 ]; then
        echo "$P : 数字です"
    else
        echo "$P : 数字ではありません"
    fi
fi
実行結果
1
2
3
4
5
6
7
8
$ ./is_numric.sh 01234
01234 : 数字ではありません

$ ./is_numric.sh 1234z
1234z : 数字ではありません

$ ./is_numric.sh 1234
1234 : 数字です

以上、シェルスクリプト数字判定 でした。