Using grep with test and without using [[ ]]

As an exercise, I'm trying to re-write this code without the compound square brackets, using grep and test. Need to know what to do about the "equal-tilde".

#!/bin/bash
# test-integer2: evaluate the value of an integer.
INT=-5
if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
if [ "$INT" -eq 0 ]; then
echo "INT is zero."
else
if [ "$INT" -lt 0 ]; then
echo "INT is negative."
else
echo "INT is positive."
fi

Welcome to the forum.

The "equal-tilde" is explained in man bash 's "Compound Commands" paragraph:

You can use your regex (almost) as given in a grep command:

grep "^[-0-9]\+$" <<<  "$INT"

The + needs to be escaped as grep uses basic regexes by default. For the rest of your request, just replace the [ with test and drop the ] . Please be aware that your script is missing two fi statements. And, reasonable indentation improves readability and understandability of your code.

if grep "^[-0-9]\+$" <<<  "$INT"
  then  if test "$INT" -eq 0 
          then  echo "INT is zero."
          elif  test "$INT" -lt 0 
            then        echo "INT is negative."
            else        echo "INT is positive."
        fi
  else  echo "No int"
fi
3 Likes

Instead of converting the ERE to a BRE (that allows some unintended strings like -5-2 , --- , and even just - to be considered to be a valid number), we could just tell grep to process its input treating the regex as an ERE and to exit with a zero exit code if and only if a matching line was found:

if grep -Eq '^-?[0-9]+$' <<< "$INT"; then

P.S. But, of course, Xubuntu56 will also have to fix his/her code to have an fi to match each if .

2 Likes

Thanks so much, guys! This place is great! Looking forward to learning bash.

1 Like

grep is an external command, that's a (small) overhead.
To only use shell builtins, you can try to convert the RE to a glob (or multiple globs), and use it in a case-esac.
The glob match is less powerful, and often will look ugly - a reason to hide it in a function.

is_int(){
# error if $1
#   contains a character that is not a dash or digit
#   is empty
#   is dash only
#   has a dash not at the first position
  case $1 in
  *[!-0-9]*|""|-|*?-*) return 1;;
  esac
  return 0
}

INT=-5
if is_int "$INT"; then
  if [ "$INT" -eq 0 ]; then
    echo "INT is zero."
  elif [ "$INT" -lt 0 ]; then
    echo "INT is negative."
  else
    echo "INT is positive."
  fi
fi
2 Likes

This suggestion relies on bash parameter expansion and test, and gets rid of the need for grep altogether. On the down side you need an intermediary variable:

zzz=${INT#-}                  # removes the negative sign, if it exists
if test -z "${zzz//[0-9]/}"   # removes all digits; resulting string should be zero length
then 
   test ${INT} -lt 0 && printf "%s is negative\n" ${INT}
   test ${INT} -eq 0 && printf "%s is zero\n" ${INT}
   test ${INT} -gt 0 && printf "%s is positive\n" ${INT}
fi

Naturally the ${param//string/replacement} construct isn't POSIX-compliant.

Another alternative to using grep is expr:

expr "$INT" : '-\?[0-9][0-9]*$'

but while that works with GNU expr, the Solaris versions (there are FOUR versions on my Sol 10 system!) do not accept the \? construct.

Andrew

2 Likes

Hi MadeInGermany...
(Or anyone...)

Relative to this thread as you have used 'case....esac':
What is the reasoning behind ;; , why 2 semicolons instead of 1, after each command inside a 'case' statement?
I have always wondered as I can't find any mention of it.
A question to help our newbie friend to understand too.

Last login: Wed Jan 23 12:04:15 on ttys000
AMIGA:amiga~> help case
case: case WORD in [PATTERN [| PATTERN]...) COMMANDS ;;]... esac
    Selectively execute COMMANDS based upon WORD matching PATTERN.  The
    `|' is used to separate multiple patterns.
AMIGA:amiga~> _

And from the manual, (man bash):

       case word in [ [(] pattern [ | pattern ] ... ) list ;; ] ... esac
              A case command first expands word, and tries to match it against
              each pattern in turn, using the same matching rules as for path-
              name expansion (see Pathname  Expansion  below).   The  word  is
              expanded  using  tilde  expansion, parameter and variable expan-
              sion, arithmetic  substitution,  command  substitution,  process
              substitution  and  quote  removal.   Each  pattern  examined  is
              expanded using tilde expansion, parameter  and  variable  expan-
              sion, arithmetic substitution, command substitution, and process
              substitution.  If the shell option nocasematch is  enabled,  the
              match  is  performed  without  regard  to the case of alphabetic
              characters.  When a match is found, the  corresponding  list  is
              executed.   After  the  first  match,  no subsequent matches are
              attempted.  The exit status is zero if no pattern matches.  Oth-
              erwise,  it  is  the exit status of the last command executed in
              list.
1 Like

Hi wisecracker,

please regard ;; as a fix syntax element to separate "cases". You can't use single semicolons as the "list" (in your man bash excerpt) can consist of several commands separated by ; .

2 Likes

Thanks RudiC...

That makes sense, I can see it in my minds eye now.
Sorry to tie up this thread with that question but it is useful for our new friend to know.

<Thumbs Up>

1 Like

You could get away without test s, grep s, square brackets, if s, just using case ... esac :

case "$INT" in
   *[^0-9-]*)   echo no int;;
   -*[^0]*)     echo negative;;
   *[^-0]*)     echo positive;;
   *)           echo null;;
esac

The logics may need to be refined (e.g. to eliminate minus sign in the middle of the string), but should be doable.

1 Like

@RudiC
The standard shell glob only mentions the ! as a negation of the following character set. The ^ is an extension in bash (and maybe some other shells).

case "$INT" in
   *[!0-9-]*|?*-*)   echo no int;;
   -[0-9]*)     echo negative;;
   [0-9]*)     echo positive;;
   *)           echo null;;
esac

@apmcd47
expr uses BRE that defines \{m,n\}
The GNU-extensions \? and \+ are generally \{0,1\} and \{1,\}
Unfortunately expr needs a work-around for a leading dash in its first argument

expr X"$INT" : X'-\{0,1\}[0-9]\{1,\}$'

And expr is an external command as well.

@wisecracker
You even posted the man page that mentions the list ;;
A list is a list of commands, separated with newline or ; (as usual).

2 Likes

@MadeInGermany: Thanks for pointing that out. I double checked before posting; man bash

, and I (frequently but not always) test proposals on two systems (Linux and FreeBSD), but I'm afraid doing so on all possible systems / configurations is beyond me.

Thanks as well for extending the logics as I alluded to. Be aware that "-0" is identified as "negative" with your approach..

1 Like

Thanks to everyone!

#!/bin/bash

INT=9

if grep "^[-0-9]\+$" <<< "$INT"; then
    if test "$INT" -eq 0; then
        echo "INT is zero."
    else
        if test "$INT" -lt 0; then
            echo "INT is negative."
        else
            echo "INT is positive."
        fi
        if test $((INT % 2)) -eq 0; then
            echo "INT is even."
        else
            echo "INT is odd."
        fi
    fi
else
    echo "INT is not an integer." >&2
    exit 1
fi




-0 feels slightly less than 0 :rolleyes:

I have found now:
[!a-z] works in all Bourne-derived shells, only an interactive zsh does history substitution (and needs [^a-z] or "setopt no_bang_hist").
the [^a-z] works in ksh93 and dash-0.5.8 and zsh in addition to the [!a-z].

A curious example:
dash-0.5.8 has got the ^ in addition, but its man page still says

@Xubuntu56
I do not understand why you want the builtin [[ =~ ]] replaced with grep but introduce the <<< operator that is again specific to bash.

1 Like

How about introducing a "feeling" operator (but only for almost-integers)?

On the only 1's complement machines I've ever worked on (CDC 3600 and CDC 6500) you could have both -0 and +0. And if you used the machine language instructions to compare integer values, they tested equal (48-bit integers on the 3600 and 60-bit integers on the 6500). If you use the machine language instructions to compare their bitwise values, they tested unequal (since the sign bit was different).

2 Likes

@ Don...

I am experimenting with this in the shell ATM and does indeed have -0 :
Minifloat - Wikipedia
Similarly floating point formats in general have this __anomaly__...
(Just for the record.)

@ MadeInGermany...
list ;; , yes, but it does not explain WHY, RudiC's reply just made it make sense even if there is only one command in the list.

1 Like

Hi, for fun:
( bash, ksh, zsh ):

if [ -n "${XX#-}" -a "${XX#-}" = "${XX##*[^0-9]}" ]
then 
  [ "${XX#-}" -eq 0 ] && echo "$XX: null" || { [ "${XX#-}" = "${XX}" ] && echo "$XX: positif" || echo "$XX: negatif" ;}
else
  echo "$XX: ko"
fi

dash:

if [ -n "${XX#-}" -a "${XX#-}" = "${XX##*[!0-9]}" ]
then 
     [ "${XX#-}" -eq 0 ] && echo "$XX: null" || { [ "${XX#-}" =  "${XX}" ] && echo "$XX: positif" || echo "$XX: negatif" ;}
else
  echo "$XX: ko"
fi