Bash – checking an argument to a bash script is a string of all digits

bashscriptingshellwildcards

The Bash FAQ says

If you're validating a simple "string of digits", you can do it with a glob:

# Bash
if [[ $foo = *[!0-9]* ]]; then
    echo "'$foo' has a non-digit somewhere in it"
else
    echo "'$foo' is strictly numeric"
fi

I thought, "Goody, that looks nice and simple".
I then pasted exactly that into a script, except I added "exit 1" after the first echo and changed $foo to $1, so that it looked like

if [[ $1 = *[!0-9]* ]]; then
    echo "'$1' has a non-digit somewhere in it"
    exit 1
else
    echo "'$1' is strictly numeric"
fi

I then tried to run this and got

$ sh foo.sh bar
bar
foo.sh: 6: [[: not found
'bar' is strictly numeric

I'm Bash illiterate, I'm ashamed to say, so I've no idea what could be wrong here. I had the impression, supported by the online Bash manual that the operator for matching with regexes is =~, but changing that doesn't make any difference. And that [[ operator that seems to be problematic here looks standard, though I don't know what the difference is between [[ ]] and [ ], which both correspond to testing the expression, as far as I know. I'm using Debian squeeze with bash

$ bash --version
GNU bash, version 4.1.5(1)-release (i486-pc-linux-gnu)

Debian says version 4.1-3.

Best Answer

Why are you calling sh, if that's a bash script? It's clear that on your system, sh is not bash, but some other shell in the Bourne/POSIX family. In fact, it's dash, a smaller shell designed for low memory consumption and speed that pretty much only supports POSIX constructs and built-in utilities.

[[ … ]] is a ksh extension to the Bourne syntax that was picked up by bash and zsh but not by POSIX. In a portable script, you need to use [ … ] for tests. The standard construct doesn't have any support for pattern matching; the standard idiom is to use a case construct:

case $1 in                        # branch to the first pattern that $1 matches
  *[!0-9]*)                       # pattern = anything containing a non-digit
    echo not a number             # do this if the first pattern triggered
    ;;                            # end of this case branch
  *)                              # pattern = anything (else)
    echo successor of $(($1-1))   # do this if the second pattern triggered
    ;;                            # end of this case branch
esac                              # end of the case construct

Here's a function that tests if its argument is all-digits:

is_all_digits () {
  case $1 in *[!0-9]*) false;; esac
}

Digression: I initially made a typo in the snippet above: I'd written $(($0-1)). This caused odd-looking error messages:

$ ash foo.sh 42
foo.sh: 4: arithmetic expression: expecting EOF: "foo.sh-1"
$ ash ./foo.sh 42
./foo.sh: 4: arithmetic expression: expecting primary: "./foo.sh-1"
$ ksh ./foo.sh 42
./foo.sh: line 3: foo.sh: invalid variable name
$ pdksh ./foo.sh 42
./foo.sh[4]: ./foo.sh-1: unexpected `.'
$ bash foo.sh 42         
foo.sh: line 3: foo.sh-1: syntax error: invalid arithmetic operator (error token is ".sh-1")
$ bash ./foo.sh 42
./foo.sh: line 3: ./foo.sh-1: syntax error: operand expected (error token is "./foo.sh-1")
$ zsh foo.sh 42
foo.sh:3: bad floating point constant

$0 is the name of the script, so the arithmetic expression to be evaluated was foo.sh-1 or ./foo.sh-1. You can watch the diversity of error mesages amongst shells. I was a little surprised to see that ash's messages and bash's message without ./ were the clearest: none of the other shells mention that the problem is in an arithmetic expression. Ash and pdksh do get docked points for reporting the error one line too far.

Related Question