Validating an IP Address in a Bash Script

June 26th, 2008 by Mitch Frazier in

Your rating: None Average: 4.2 (6 votes)

I've recently written about using bash arrays and bash regular expressions, so here's a more useful example of using them to test IP addresses for validity.

To belabor the obvious: IP addresses are 32 bit values written as four numbers (the individual bytes of the IP address) separated by dots (periods). Each of the four numbers has a valid range of 0 to 255.

The following bash script contains a bash function which returns true if it is passed a valid IP address and false otherwise. In bash speak true means it exits with a zero status, anything else is false. The status of a command/function is stored in the bash variable "$?".

#!/bin/bash

# Test an IP address for validity:
# Usage:
#      valid_ip IP_ADDRESS
#      if [[ $? -eq 0 ]]; then echo good; else echo bad; fi
#   OR
#      if valid_ip IP_ADDRESS; then echo good; else echo bad; fi
#
function valid_ip()
{
    local  ip=$1
    local  stat=1

    if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
        OIFS=$IFS
        IFS='.'
        ip=($ip)
        IFS=$OIFS
        [[ ${ip[0]} -le 255 && ${ip[1]} -le 255 \
            && ${ip[2]} -le 255 && ${ip[3]} -le 255 ]]
        stat=$?
    fi
    return $stat
}

# If run directly, execute some tests.
if [[ "$(basename $0 .sh)" == 'valid_ip' ]]; then
    ips='
        4.2.2.2
        a.b.c.d
        192.168.1.1
        0.0.0.0
        255.255.255.255
        255.255.255.256
        192.168.0.1
        192.168.0
        1234.123.123.123
        '
    for ip in $ips
    do
        if valid_ip $ip; then stat='good'; else stat='bad'; fi
        printf "%-20s: %s\n" "$ip" "$stat"
    done
fi

If you save this script as "valid_ip.sh" and then run it directly it will run some tests and prints the results:

  # sh valid_ip.sh
  4.2.2.2             : good
  a.b.c.d             : bad
  192.168.1.1         : good
  0.0.0.0             : good
  255.255.255.255     : good
  255.255.255.256     : bad
  192.168.0.1         : good
  192.168.0           : bad
  1234.123.123.123    : bad

In the function valid_ip, the if statement uses a regular expression to make sure the subject IP address consists of four dot separated numbers:

  if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
If that test passes then the code inside the if statement separates the subject IP address into four parts at the dots and places the parts in an array:
  OIFS=$IFS
  IFS='.'
  ip=($ip)
  IFS=$OIFS
It does this by momentarily changing bash's Internal Field Separator variable so that rather than parsing words as whitespace separated items, bash parses them as dot separated. Putting the value of the subject IP address inside parenthesis and assigning it to itself thereby turns it into an array where each dot separated number is assigned to an array slot. Now the individual pieces are tested to make sure they're all less than or equal to 255 and the status of the test is saved so that it can be returned to the caller:
  [[ ${ip[0]} -le 255 && ${ip[1]} -le 255 \
          && ${ip[2]} -le 255 && ${ip[3]} -le 255 ]]
  stat=$?
Note that there's no need to test that the numbers are greater than or equal to zero because the regular expression test has already eliminated any thing that doesn't consist of only dots and digits.

__________________________

Mitch Frazier is an Associate Editor for Linux Journal and the Web Editor for linuxjournal.com.


Special Magazine Offer -- Free Gift with Subscription
Receive a free digital copy of Linux Journal's System Administration Special Edition as well as instant online access to current and past issues. CLICK HERE for offer

Linux Journal: delivering readers the advice and inspiration they need to get the most out of their Linux systems since 1994.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.
Anonymous's picture

More easy

On April 20th, 2009 Anonymous (not verified) says:

#! /bin/sh

valid_ip()
{
# Check if IP format is num.num.num.num / num between 0..255
if [ "$(sipcalc $1 | grep ERR)" != "" ]; then
echo "incorrect"
return 0
fi
echo "correct"
}

Regards

Bill's picture

3 octet ip addresses are passing as valid

On December 4th, 2008 Bill (not verified) says:

On some of my machines (Centos 5.2, Debian Etch), it looks like 3 octet ip addresses are passing as valid:

(bill@shady) ~ > bash --version|head -1; head -1 /etc/issue.net; ./valid_ip.sh
GNU bash, version 3.1.17(1)-release (i486-pc-linux-gnu)
Debian GNU/Linux 4.0
4.2.2.2 : good
a.b.c.d : bad
192.168.1.1 : good
0.0.0.0 : good
255.255.255.255 : good
255.255.255.256 : bad
192.168.0.1 : good
192.168.0 : good
1234.123.123.123 : bad

(bill@spdb222) ~ > bash --version|head -1; head -1 /etc/issue.net; ./valid_ip.sh
GNU bash, version 3.2.25(1)-release (x86_64-redhat-linux-gnu)
CentOS release 5.2 (Final)
4.2.2.2 : good
a.b.c.d : bad
192.168.1.1 : good
0.0.0.0 : good
255.255.255.255 : good
255.255.255.256 : bad
192.168.0.1 : good
192.168.0 : good
1234.123.123.123 : bad

However, if I test on MacOSX, it works correctly:

(bill@quell) ~ > bash --version|head -1; uname -a; ./valid_ip.sh
GNU bash, version 3.2.17(1)-release (i386-apple-darwin9.0)
Darwin quell.local 9.5.0 Darwin Kernel Version 9.5.0: Wed Sep 3 11:29:43 PDT 2008; root:xnu-1228.7.58~1/RELEASE_I386 i386 i386
4.2.2.2 : good
a.b.c.d : bad
192.168.1.1 : good
0.0.0.0 : good
255.255.255.255 : good
255.255.255.256 : bad
192.168.0.1 : good
192.168.0 : bad
1234.123.123.123 : bad

Any ideas why this is?

Mitch Frazier's picture

A Late "Fix"

On December 16th, 2008 Mitch Frazier says:

In version 3.2 of Bash there was a change that eliminated the need to quote regular expressions, it appears to be related to that.

If you put quotes around the regular expression test it should work:

if [[ $ip =~ '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$' ]]; then
OR
if [[ $ip =~ "^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$" ]]; then

Now as to why it fails in the manner it fails, I'm not sure. I also don't understand why the CentOS version fails since it's version 3.2.25.

While researching this I also ran across this statment here:

Escaped "curly brackets" -- \{ \} -- indicate the number of occurrences of a preceding RE to match.

It is necessary to escape the curly brackets since they have only their literal character meaning otherwise. This usage is technically not part of the basic RE set.

"[0-9]\{5\}" matches exactly five digits (characters in the range of 0 to 9).

Which appears to indicate that you need to escape curly brackets for them to work, but that does not appear to be the case. I think the problem may be that even though the above link appears as part of the Advanced Bash-Scripting Guide, it looks like it may have been largely copied and pasted from another source.

__________________________

Mitch Frazier is an Associate Editor for Linux Journal and the Web Editor for linuxjournal.com.

Anonymous's picture

Not the bash I know

On July 9th, 2008 Anonymous (not verified) says:

I tried this with bash, since I wasn't aware that bash supported regular expressions, and it fell over when it encountered '=~', since it didn't know wnything about regular expressions.

What version of bash was this written with?

Mitch Frazier's picture

Version 3

On July 10th, 2008 Mitch Frazier says:

Regular expressions have been part of bash since version 3 arrived on July 27, 2004. Specifically, I have version 3.1.17.

__________________________

Mitch Frazier is an Associate Editor for Linux Journal and the Web Editor for linuxjournal.com.

John W. Krahn's picture

Re: Validating an IP Address

On July 4th, 2008 John W. Krahn (not verified) says:

Actually an IP address doesn't have to be in the dotted quad format to work correctly, it depends on where you use it:


$ perl -MSocket -le'print inet_ntoa inet_aton q/www.linuxjournal.com/'
66.240.243.113
$ perl -MSocket -le'print unpack q/N/, inet_aton q/66.240.243.113/'
1123087217
$ ping 1123087217
PING 1123087217 (66.240.243.113) 56(84) bytes of data.
64 bytes from 66.240.243.113: icmp_seq=1 ttl=52 time=72.6 ms
64 bytes from 66.240.243.113: icmp_seq=2 ttl=52 time=66.7 ms
64 bytes from 66.240.243.113: icmp_seq=3 ttl=52 time=66.7 ms

--- 1123087217 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1999ms
rtt min/avg/max/mdev = 66.702/68.711/72.667/2.813 ms
$ traceroute 1123087217
traceroute to 1123087217 (66.240.243.113), 30 hops max, 40 byte packets
1 * * *
2 ... etc.
$ perl -MSocket -le'print unpack q/N/, inet_aton q/0.240.243.113/'
15790961
$ ping 66.15790961
PING 66.15790961 (66.240.243.113) 56(84) bytes of data.
64 bytes from 66.240.243.113: icmp_seq=1 ttl=52 time=104 ms
64 bytes from 66.240.243.113: icmp_seq=2 ttl=52 time=99.8 ms

--- 66.15790961 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 99.822/102.097/104.372/2.275 ms
$ perl -MSocket -le'print unpack q/N/, inet_aton q/0.0.243.113/'
62321
$ ping 66.240.62321
PING 66.240.62321 (66.240.243.113) 56(84) bytes of data.
64 bytes from 66.240.243.113: icmp_seq=1 ttl=52 time=65.0 ms
64 bytes from 66.240.243.113: icmp_seq=2 ttl=52 time=93.5 ms
64 bytes from 66.240.243.113: icmp_seq=3 ttl=52 time=66.0 ms

--- 66.240.62321 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2001ms
rtt min/avg/max/mdev = 65.090/74.920/93.578/13.202 ms

Anonymous's picture

Dash Support

On June 29th, 2008 Anonymous (not verified) says:

Good but does not run using dash!!

I don't think dash supports the regex in compound expressions and doesn't support the function keyword and [[]] parenthesis.

Mitch Frazier's picture

Probably not

On July 1st, 2008 Mitch Frazier says:

Only tested it with bash, nothing else.

__________________________

Mitch Frazier is an Associate Editor for Linux Journal and the Web Editor for linuxjournal.com.

Timmy Jose's picture

What about IPv6 addresses?

On June 26th, 2008 Timmy Jose (not verified) says:

This does not handle IPv6 addresses which is a much more trickier gambit. And a combination of IPv4/ IPv6 addresses would indeed make things interesting. Add the mundane alphanumeric DNS names and you have a real stew of messy, unpredicatable code! ;-)

Mitch Frazier's picture

As They Say

On July 1st, 2008 Mitch Frazier says:

That's left as an exercise for the reader.

__________________________

Mitch Frazier is an Associate Editor for Linux Journal and the Web Editor for linuxjournal.com.

Post new comment

Please note that comments may not appear immediately, so there is no need to repost your comment.
The content of this field is kept private and will not be shown publicly.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <pre> <ul> <ol> <li> <dl> <dt> <dd> <i> <b>
  • Lines and paragraphs break automatically.

More information about formatting options

Newsletter

Each week Linux Journal editors will tell you what's hot in the world of Linux. You will receive late breaking news, technical tips and tricks, and links to in-depth stories featured on www.linuxjournal.com.
Sign up for our Email Newsletter

Tech Tip Videos

From the Magazine

July 2009, #183

News Flash: Linux Kernel 3.0 to include an on-the-go Expresso machine interface! Ok, maybe not, but Linux is definitely going mobile, from phones to e-readers. Find out more inside about Android, the Kindle 2, the Western Digital MyBook II, The Bug, and Indamixx (a portable recording studio). And if you've gone mobile and you been wanting more Emacs in your life then check out Conkeror.


To compliment the mobile we've got the stationary: parsing command line options with getopt, checking your Ruby code with metric_fu, and building a secure Squid proxy. How is this stationary you ask? What can we say? It's not. We just wanted to see if anybody actually read this part of the page :) .


All this and more, and all you have to do is get your hot sweaty hands on the latest copy of Linux Journal.





Read this issue