Showing posts with label expr. Show all posts
Showing posts with label expr. Show all posts

Tuesday, February 1, 2011

Is this string a number or an integer? Solutions with expr, ksh and Perl

Taking my experiments with expr posted previously a little further I thought what about writing shell functions which determine whether a given string is a number or an integer?

I had already solutions in ksh for that but wanted to see them with expr (which would be a true Bourne shell solution) and also I did them in Perl.

A number is meant to be a sequence of digits ie. 1234.
An integer is either a number or a number prefixed with a minus sign ie. 1234 or -1234.
#!/bin/sh

# Test if arg $1 is a number
########################################

isNum1() {
    ksh -c "[[ \"$1\" = +([0-9]) ]]"
    return $?
}
isNum2() {
    [ `expr "$1" : '[0-9][0-9]*$'` = "0" ] && return 1
    return 0
}
isNum3() {
    perl -e 'exit 1 unless($ARGV[0]=~/^\d+$/)' -- "$1"
}

# Test if arg $1 is an integer
########################################
isInt1() {
    ksh -c "[[ \"$1\" = *(-)+([0-9]) ]]"
}
isInt2() {
    [ `expr "$1" : '[0-9][0-9]*$'` = "0" -a `expr "$1" : '-[0-9][0-9]*$'` = "0" ] && return 1
    return 0
}
isInt3() {
    perl -e 'exit 1 unless($ARGV[0]=~/^-?\d+$/)' -- "$1"

    # Here's an alternative, better to read in Perl maybe but two commands and a pipe:
    #   echo "$1" | perl -n -e 'exit 1 unless(/^-?\d+$/)'
}

# Test suite
for i in 204 -13 +88 1-2 4+5 46.09 -7.2 a abc 2x -2x t56 -t5 "2 4"
do
  isNum1 "$i" && echo Num1 $i
  isNum2 "$i" && echo Num2 $i
  isNum3 "$i" && echo Num3 $i
  isInt1 "$i" && echo Int1 $i
  isInt2 "$i" && echo Int2 $i
  isInt3 "$i" && echo Int3 $i
done

Executing the script results in
Num1 204
Num2 204
Num3 204
Int1 204
Int2 204
Int3 204
Int1 -13
Int2 -13
Int3 -13

i.e. only the first entry is a number and the first two entries are correctly identified as integers.
One drawback to expr is that it supports only basic regular expressions i.e. some useful special characters like + or '?' cannot be used and thus ksh and Perl provide more concise solutions to the above problem.

Using 'expr' in scripts

As many scriptors know (and probably hate) Bourne shell doesn't have inbuilt arithmetic capabilities so one has to resort to expr for calculations, the most famous maybe being the loop increase:
i=0
while [ $i -lt 10 ] ; do
  ...
  i=`expr $i + 1`
done
expr has more operators though than just the basic arithmetics and I don't see them used very frequently, I think I haven't used them at all so - stumbling upon it accidentally - I thought I'd play with it a little to get a better understanding and here's the result.

The match operator (string comparison with regular expressions)


The operator to compare a string to a regular expression is the colon (:).
expr will return the number of bytes matched (the curious might look into the xpg4 version of expr which returns the number of characters matched).

Also important: the regular expression always starts to compare at the beginning of the string so as if one would have used ^.

A few examples.
f="/a/c"

# does $f match ^a ? No.
expr $f : a
0

# does $f match ^/a ? Yes.
expr $f : /a
2

# does $f contain an 'a' ? Yes.
expr $f : '.*a'
4

# does $f contain a 'b' ? No.   (the regexp must be enclosed in simple quotes here)
expr $f : '.*b'
0

# does $f end with a 'c' ? Yes.
expr $f : '.*c$'
4

# does $f end with a 'b' ? No.
expr $f : '.*b$'
0
All of these examples can be used in a decision process to check whether the result is zero (no match) or not.
x=`expr ... : ...`
if [ $x -eq 0 ] ; then
  : # no match
else
  : # match
fi

To make the code a little safer one has to consider that the string to be matched might contain white space. The examples above will fail so one needs to use double quotes.
f="a b c"
expr $f : a
expr: syntax error
# since this translates to    expr a b c : a    which does not compute

# Double quotes around the string do help
expr "$f" : a
1

And even more useful is to use the extraction reg exp \1: instead of returning the number of bytes in a match one gets a string (if successful) or an empty string.
f="/ab/cd/efg"

# Extract the filename 
expr "$f" : '.*/\(.*\)'
efg

# Extract the dirname
expr "$f" : '\(.*\)/.*'
/ab/cd

f="abcdefg"

expr "$f" : '.*/\(.*\)'
        <---- # Note: this is an empty string here !!!

expr "$f" : '\(.*\)/.*'
        <---- # Note: this is an empty string here !!!

# Why empty strings? because we were trying to match a slash which is not present in $f
# Why empty strings and not 0? because we requested a string between (...)

# Now what if we wanted to solve the following: 
# if $f contains a slash then the filename is everything after the slash
# if $f does not contain a slash it should be considered a filename
# Rather than using  if ... else ... fi we can use expr, read on.

The 'or' and 'and' operator


The or operator is | and the and operator is &, both have to be escaped always.
They can be used to compare two expressions.

or: the first expression is evaluated. If it is NULL (i.e. empty or non-existing) or 0 the second expression will be evaluated too.
a=111
b=0
c=
e=555

# In the 4  comparisons below the second expression is always valid
expr "$a" \| "$e"
111

expr "$b" \| "$e"
555

expr "$c" \| "$e"
555

expr "$d" \| "$e"
555

# Here we compare a 0 value with an empty value
expr "$b" \| "$c"
0

# Here we compare an empty value to a non-existing one
#   The result of expr is also 0
expr "$c" \| "$d"
0 
and: both expressions are evaluated. If any of them is NULL or 0 then 0 is returned. Otherwise the first expression.
a=111
b=0
c=
e=555

# In the 4  comparisons below the second expression is always valid
expr "$a" \& "$e"
111

expr "$b" \& "$e"
0

expr "$c" \& "$e"
0

expr "$d" \& "$e"
0

Combining 'match' and 'and/or'


One can use these in combination to solve the emptry string issue above and a default value can be assigned, it is important to remember that expr evaluates left to right.
f="abcdefg"

expr "$f" : '.*/\(.*\)' \| "$f"
abcdefg

# These are two operations, one 'match' and one 'or':   expr   ... : ... \| ...
# If the match operation fails   "$f" : '.*/\(.*\)'    
# then evaluate the right hand side of 'or' and make this the result of 'expr'.
#   So: no slash found in $f then return the original string.