A Bash Script to Generate a List of IPs for a Subnet

Posted on Sun 26 April 2015 in Tech Blog

As a part of what I do, I often need to document network infrastructure where little or no records have been kept. One of the biggest challenges in doing this is finding all the network devices. I got to thinking recently how handy it would be to have a Linux utility that would generate a list of all available IP addresses in a subnet. The list could then be used to automate tasks like pinging each address in the network to see if a device responds.

A check of the internet revealed several attempts at bash scripts that could do this; however, the ones I tested all seemed to have problems handling certain inputs. What I needed was a tool that could accurately generate IP addresses for a network with a mask of 16 to 30 bits; the scripts I happened to test often had problems with mask values of less than 24 bits; or they generated inaccurate data when the IP address given to them was not a subnet address. I decided to see if I could do better. And incidentally, I also learned a lot about newer features in bash, such as integer arrays, and math mode. Enjoy!

#!/bin/bash

# Copyright 2015 by Duane Penzien 
# All rights reserved.
#
# Redistribution and use of this script, with or without modification,
# Redistribution and use of this script, with or without modification,
# is permitted provided that the following conditions are met:
#
# 1. Redistributions of this script must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#
#  THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY EXPRESS OR IMPLIED
#  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
#  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO
#  EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
#  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
#  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
#  OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
#  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
#  OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
#  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# -----------------------------------------------------------------------------

#####################
# Globals
#####################
SCRIPT=`basename $0`
noSB=                       #flag: if set, do not output subnet or bcast ips. (-a)
details=                    #flag: if set, display subnet information. (-d)
noList=                     #flag: if set, do not output address list (-n)
#inaddr=                    #host ip part of cidr array   
#subIP=                     #subnet ip array
#brIP=                      #broadcast ip array
#subMask=                   #subnet mask array
#invMask=                   #inverted subnet mask array
declare -a -i inaddr subIP brIP subMask invMask
declare -i ipcount

#####################
# Functions
#####################
function helpMsg() {
    echo -e "\n  $SCRIPT, a IPV4 subnet tool." >&2
    echo "  Use: $SCRIPT [-a -d -h] cidr [cidr]..." >&2
    echo "  -a = no subnet or broadcast ip in output" >&2
    echo "  -d = describe the subnet" >&2
    echo "  -n = do not output addresses" >&2 
    echo -e "  -h = this help message\n" >&2
    exit 2
}

# Check for a properly formatted cidr string.  Requires cidr passes as parameter.
#Return false if invalid, true if valid.    
function checkCidr() {
    if [[  ! $1 =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]{1,2}$  ]]; then 
        echo "ERROR: Invalid cidr string $1" >&2
        return `false`
    fi
    local arr=($(echo $cidr | tr '/' "\n"))
    local arr2=($(echo ${arr[0]} | tr '.' "\n"))
    local x
    local y=1
    for (( x = 0; x &2
            return `false`
        fi
        (( y++ ))
    }
    if [ ${arr[1]} -lt 8 ] || [ ${arr[1]} -gt 32 ]; then
        echo "ERROR: cidr subnet value out of rance, must be between 8 and 32." >&2
        return `false`
    fi
    return `true`
}

# Split an IP in dotted decimal notation into an array. Requires parameter
# with address to split, updates global inaddr[] array.
function splitIP {
    (( inaddr[0] = inaddr[1] = inaddr[2] = inaddr[3] = 0 ))
    inaddr=($(echo $1 | tr '.' "\n"))
}

# Generate a subnet mask from the cidr bit count. Requires bit count passed
# to function. Updates global sub[] array.
function genMaskFromBitcount() {
    local c=0
    local m=$1

    (( sub[0] = sub[1] = sub[2] = sub[3] = 0 ))
    while [ $m -gt 0 ]; do
    if [ $m -ge 8 ]; then
        let sub[$c]=255
        let m=m-8
    else
        let m=8-m
        let m=256-2**$m
        let sub[$c]=$m
        let m=0
    fi
    let c=c+1
    done
}

# Calculate the address of the subnet (base addrs ess) by doing a bitwise
# AND of the supplied ip (in inaddr[]) and the subnet mask in sub[].
# Updates global array subIP[].
function calcSubnetAddress() {
    local x
    for (( x=0 ; x = 0 ; x-- ));do
    ipcount+=$(( invMask[$x] * 256**$y ))
    let y=y+1
    done
    ipcount=ipcount+1
}

# Echo the current value of subIP[] to stdout.
function echoIP() {
    echo "${subIP[0]}.${subIP[1]}.${subIP[2]}.${subIP[3]}"
}

# Increment the IP address in subIP[] by one. Used when generating address list.
function incrementIP() {
    local x=3
    while [ $x -ge 0 ];do
    (( subIP[$x]++ ))
    if [ ${subIP[$x]} -lt 256 ]; then
        x=-1
    else
        (( subIP[$x]=0 ))
        let x=x-1
    fi
    done

}


######################
# Main Program
######################
while getopts ":adnh" opt; do
    case $opt in
        a)
            noSB='t'
            ;;
    d)
        details='t'
        ;;
    n)
        noList='t'
        ;;
        h)
            helpMsg;
            ;;
        *)
            echo -e "\n  ****** Invalid Option: -$OPTARG ******" >&2
            helpMsg;
            ;;
    esac
done
shift $((OPTIND-1))

for cidr in $@; do
    if ( ! checkCidr $cidr ); then
    echo "Not processing string $cidr" >&2
    continue
    fi
    x=
    # split the cidr into ip and bit count in x[].
    x=($(echo $cidr | tr '/' "\n"))
    splitIP ${x[0]}
    genMaskFromBitcount ${x[1]}
    calcSubnetAddress
    invertMask
    calcBroadcast
    countIP
    if [ -n "$details" ] || [ -n "$noList" ]; then
    echo "Processing CIDR: $cidr"
    echo "Subnet Mask:     ${sub[0]}.${sub[1]}.${sub[2]}.${sub[3]}"
    echo "Subnet IP:       ${subIP[0]}.${subIP[1]}.${subIP[2]}.${subIP[3]}"
    echo "Broadcast IP:    ${brIP[0]}.${brIP[1]}.${brIP[2]}.${brIP[3]}"
    echo "Number of IPs:   $ipcount"
    fi

    if [ -z "$noList" ]; then
    #Output the addresses. Strip subnet and broadcast if -a option.
    nosub=-1
    nobroad=-1
    if [ -n "$noSB" ]; then
        nosub=0
        nobroad=$(( ipcount - 1 ))
    fi
    for (( x = 0; x < $ipcount ; x++ )) {
        if [ $x -ne $nosub ] && [ $x -ne $nobroad ]; then
        echoIP
        fi
        incrementIP
    }
    fi

done

Till Next Time,

Duane