We can use the syntax ${var##pattern} and ${var%%pattern} to extract the last and first section of an IPv4 address:

echo IP: $IP
echo 'Extract the first section using ${var%%pattern}: ' ${IP%%.*}
echo 'Extract the last section using ${var##pattern}: ' ${IP##*.}

How we can extract the second or third section of an IPv4 address using parameter expansion?

Here is my solution: I use an array and change the IFS variable.

:~/bin$ IP=
:~/bin$ IFS=. read -a ArrIP<<<"$IP"
:~/bin$ echo ${ArrIP[1]}
:~/bin$ printf "%s\n" "${ArrIP[@]}"

Also I have written some solutions using the awk, sed, and cut commands.

Now, my question is: Is there a simpler solution based on parameter expansion which does not use array and IFS changing?

  • 1
    You should only set IFS for the read there: IFS=. read -a ArrIP<<<"$IP"
    – muru
    Commented Jan 29, 2018 at 4:08
  • 1
    Not in bash without using multiple variables at least. A single parameter expansion cannot get the second or third components. Zsh can nest parameters expansions, so it might be possible there.
    – muru
    Commented Jan 29, 2018 at 4:20
  • @muru Could you please provide the Zsh solution?
    – sci9
    Commented Jan 29, 2018 at 4:38
  • 4
    What guarantee do you have that you will always be dealing with IP v4 addresses, and will never have an IP v6 address?
    – Mawg
    Commented Jan 29, 2018 at 10:46
  • 4
    Is there some reason IFS=. read a b c d <<< "$IP" isn't acceptable (if you're using Bash, that is)? Why does it have to be done with parameter expansion?
    – ilkkachu
    Commented Jan 29, 2018 at 13:17

10 Answers 10


Assuming the default value of IFS you extract each octet into it's own variable with:

read A B C D <<<"${IP//./ }"

Or into an array with:

A=(${IP//./ })
  • 2
    +1. It seems to me that this is the simplest, most straightforward method that abides by the OPs restrictions. Commented Jan 29, 2018 at 21:42

I do realize that you specifically asked for a solution that DID NOT temporarily redefine IFS, but I have a sweet and simple solution that you didn't cover, so here goes:

IFS=. ; set -- $IP

That short command will put the elements of your IP address in the shell's positional parameters $1, $2, $3, $4. However, you'll probably want to first save the original IFS and restore it afterwards.

Who knows? Maybe you'll reconsider and accept this answer for its brevity and efficiency.

(This was previously incorrectly given as IFS=. set -- $IP)

  • 5
    I don't think that works: if you change IFS on the same command line, the new value doesn't take effect when variables on the same command line are expanded. Same as with x=1; x=2 echo $x
    – ilkkachu
    Commented Jan 29, 2018 at 13:15
  • 6
    @chepner, but again, set doesn't make use of $IFS at all, $IFS is only used for the word splitting of $IP, but here it's assigned too late, so that has no effect. This answer is basically wrong. IP= bash -c 'IFS=. set -- $IP; echo "$2"' outputs nothing whether in POSIX mode or not. You'd need IFS=. command eval 'set -- $IP', or IFS=. read a b c d << "$IP" Commented Jan 29, 2018 at 15:39
  • 4
    It probably works for you because you had set IFS to . in one of your previous tests. Run IP= bash -xc 'IFS=. set $IP; echo "$2"' and you'll see it doesn't work. And see IP= bash -o posix -xc 'IFS=. set $IP; echo "\$1=$1 \$2=$2 IFS=$IFS"' to illustrate @chepner's point. Commented Jan 29, 2018 at 19:21
  • 2
    haste did make waste, the answer is apparently wrong, and should be corrected to work, since it's still roughly how I'd do it, only with more code.
    – Lizardx
    Commented Jan 29, 2018 at 21:31
  • 3
    @user1404316, can you post the exact set of commands you used to test it? (Plus the version of your shell, perhaps.) There are four other users who've told you here in the comments that it doesn't work as written in the answer. With examples.
    – ilkkachu
    Commented Jan 29, 2018 at 21:40

Your problem statement may be a bit more liberal than you intended.  At the risk of exploiting a loophole, here’s the solution muru alluded to:

echo "$IP -> $first, $second, $third, $fourth"

This is somewhat clunky.  It defines two throw-away variables, and it is not readily adapted to handle more sections (e.g., for a MAC or IPv6 address).  Sergiy Kolodyazhnyy’s answer inspired me to generalize the above to this:

while [ "$count" -le 4 ]
    declare sec"$count"="${slice%%.*}"

This sets sec1, sec2, sec3 and sec4, which can be verified with

printf 'Section 1: %s\n' "$sec1"
printf 'Section 2: %s\n' "$sec2"
printf 'Section 3: %s\n' "$sec3"
printf 'Section 4: %s\n' "$sec4"
  • The while loop should be easy to understand — it iterates four times.
  • Sergiy chose slice as the name for a variable that takes the place of last3 and last2 in my first solution (above).
  • declare sec"$count"="value" is a way to assign to sec1, sec2, sec3 and sec4 when count is 1, 2, 3 and 4.  It’s a little like eval, but safer.
  • The value, "${slice%%.*}", is analogous to the values my original answer assigns to first, second and third.

Not the easiest, but you could do something like:

$ IP=
$ echo "$((${-+"(${IP//./"+256*("}))))"}&255))"
$ echo "$((${-+"(${IP//./"+256*("}))))"}>>8&255))"
$ echo "$((${-+"(${IP//./"+256*("}))))"}>>16&255))"
$ echo "$((${-+"(${IP//./"+256*("}))))"}>>24&255))"

That should work in ksh93 (where that ${var//pattern/replacement} operator comes from), bash 4.3+, busybox sh, yash, mksh and zsh, though of course in zsh, there are much simpler approaches. In older versions of bash, you'd need to remove the inner quotes. It works with those inner quotes removed in most other shells as well, but not ksh93.

That assumes $IP contains a valid quad-decimal representation of an IPv4 address (though that would also work for quad-hexadecimal representations like 0x6d.0x60.0x4d.0xf (and even octal in some shells) but would output the values in decimal). If the content of $IP comes from an untrusted source, that would amount to a command injection vulnerability.

Basically, as we're replacing every . in $IP with +256*(, we end up evaluating:

 $(( (109+256*(96+256*(77+256*(15))))>> x &255 ))

So we're constructing a 32 bit integer out of those 4 bytes like an IPv4 address ultimately is (though with the bytes reversed)¹ and then using the >>, & bitwise operators to extract the relevant bytes.

We use the ${param+value} standard operator (here on $- which is guaranteed to be always set) instead of just value because otherwise the arithmetic parser would complain about mismatched parenthesis. The shell here can find the closing )) for the opening $((, and then perform the expansions inside that will result in the arithmetic expression to evaluate.

With $(((${IP//./"+256*("}))))&255)) instead, the shell would treat the second and third )s there as the closing )) for $(( and report a syntax error.

In ksh93, you can also do:

$ echo "${IP/@(*).@(*).@(*).@(*)/\2}"

bash, mksh, zsh have copied ksh93's ${var/pattern/replacement} operator but not that capture-group handling part. zsh supports it with a different syntax:

$ setopt extendedglob # for (#b)
$ echo ${IP/(#b)(*).(*).(*).(*)/$match[2]}'

bash does support some form of capture group handling in its regexp matching operator, but not in ${var/pattern/replacement}.

POSIXly, you'd use:

(IFS=.; set -o noglob; set -- $IP; printf '%s\n' "$2")

The noglob to avoid bad surprises for values of $IP like 10.*.*.*, the subshell to limit the scope of those changes to options and $IFS.

¹ An IPv4 address is just a 32 bit integer and for instance is just one of many (though the most common) textual representations. That same typical IPv4 address of the loopback interface can also be represented as 0x7f000001 or 127.1 (maybe a more appropriate one here to say it's the 1 address on the 127.0/8 class A network), or 0177.0.1, or the other combinations of 1 to 4 numbers expressed as octal, decimal or hexadecimal. You can pass all those to ping for instance and you'll see they will all ping localhost.

If you don't mind the side effect of setting an arbitrary temporary variable (here $n), in bash or ksh93 or zsh -o octalzeroes or lksh -o posix, you can simply convert all those representations back to a 32 bit integer with:


And then extract all the components with >>/& combinations like above.

$ IP=0x7f000001
$ echo "$((n=32,(${IP//./"<<(n-=8))+("})))"
$ IP=127.1
$ echo "$((n=32,(${IP//./"<<(n-=8))+("})))"
$ echo "$((n=32,((${IP//./"<<(n-=8))+("}))>>24&255))"
$ perl -MSocket -le 'print unpack("L>", inet_aton(""))'

mksh uses signed 32 bit integers for its arithmetic expressions, you can use $((# n=32,...)) there to force the use of unsigned 32 bit numbers (and the posix option for it to recognise octal constants).

  • I understand the big concept, but I've never seen ${-+ before. I can't find any documentation on it either. It works, but I'm just curious to confirm, is it just to turn the string into a mathematical expression? Where can I find the formal definition? Also, the extra quotations inside the parameter expansion replacement section don't work in GNU bash, version 4.1.2(2)-release CentOS 6.6. I had to do this instead echo "$((${-+"(${IP//./+256*(}))))"}>>16&255))" Commented Mar 3, 2020 at 23:13
  • 1
    @LeviUzodike, see edit. Commented Mar 4, 2020 at 8:23
  • To construct a 32 bit integer out of those 4 bytes like an IPv4 address ultimately is (with the bytes NOT reversed): echo $((${-+"(256**3*${IP//./+(256**3*})/256)/256)/256)"})) (GNU bash, version 4.2.46(2)-release CentOS 7.9.2009; I'm guessing later versions require more quotations). And then with the original example, &255=>15, >>8&255=>77, >>16&255=>96, >>24&255=>109. Not as elegant since $(()) won't do 1/256 as a coefficient, but it works. Thanks, I learned a lot from this post! Commented Jul 27, 2021 at 16:32

With zsh, you can nest parameter substitutions:

$ ip=
$ echo ${${ip%.*}##*.}
$ echo ${${ip#*.}%%.*}

This is not possible in bash.

  • 1
    In zsh, you may prefer ${${(s(.))ip}[3]} Commented Jan 29, 2018 at 14:36

Sure, let's play the elephant game.

$ ipsplit() { local IFS=.; ip=(.$*); }
$ ipsplit
$ echo ${ip[1]}


$ ipsplit() { local IFS=.; echo $*; }
$ set -- `ipsplit`
$ echo $1
  • 2
    What's the "elephant game"?
    – Wildcard
    Commented Jan 29, 2018 at 20:57
  • 1
    @Wildcard kind of an elliptical pun/joke, it's a reference both to the blind-people-describing an elephant story, there's so may parts to look at everybody's going to have their own take, and to the ancient custom of a king who wanted to give gifts with ruinously expensive upkeep, softened over the centuries to gifts of merely dubious value that tend to be regifted for entertainment value.. Both seemed to apply here :-)
    – jthill
    Commented Jan 30, 2018 at 1:16

With IP=

IFS=. read a b c d <<<"$IP"



[[ $IP =~ $regex ]]
echo "${BASH_REMATCH[@]:1}"


Use of parameter expansion ${IP// } to convert each dot in the ip to an opening parenthesis a dot and a closing parenthesis. Adding an initial parenthesis and a closing parenthesis, we get:


which will create four capture parenthesis for the regex match in the test syntax:

[[ $IP =~ $regex ]]

That allows the printing of the array BASH_REMATCH without the first component (the whole regex match):

echo "${BASH_REMATCH[@]:1}"

The amount of parenthesis is automatically adjusted to the matched string. So, this will match either a MAC or an EUI-64 of an IPv6 address despite them being of different length:


[[ $IP =~ $regex ]]
echo "${BASH_REMATCH[@]:1}"

Using it:

$ ./script 00:0C:29:0C:47:D5
00 0C 29 0C 47 D5

$ ./script 00:0C:29:FF:FE:0C:47:D5
00 0C 29 FF FE 0C 47 D5

Here's a small solution done with POSIX /bin/sh ( in my case that's dash ), a function that repeatedly uses parameter expansion (so no IFS here), and named pipes, and includes noglob option for reasons mentioned in Stephane's answer.

set -o noglob
    while [ -n "${slice}" ] && [ "$count" -ne 4 ]
        printf '%s ' "${num}"

mkfifo "${named_pipe}"
get_ip_sections "$ip" > "${named_pipe}" &
read sec1 sec2 sec3 sec4 < "${named_pipe}"
printf 'Actual ip:%s\n' "${ip}"
printf 'Section 1:%s\n' "${sec1}"
printf 'Section 3:%s\n' "${sec3}"
rm  "${named_pipe}"

This works as so:

$ ./get_ip_sections.sh 
Actual ip:
Section 1:109
Section 3:77

And with ip changed to 109.*.*.*

$ ./get_ip_sections.sh 
Actual ip:109.*.*.*
Section 1:109
Section 3:*

The loop keeping counter of 4 iterations accounts for 4 sections of a valid IPv4 address, while acrobatics with named pipes account for need to further use sections of ip address within script as opposed to having variables stuck in a subshell of a loop.


Why don't use simple solution with awk?

$ IP="" $ echo $IP | awk -F '.' '{ print $1" "$2" "$3" "$4;}'

Result $ 192 168 1 1

$ ip_=

$ echo $ip_

$ echo $ip_ |cut -d "." -f 1


You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .