9

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

IP=109.96.77.15
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=109.96.77.15
:~/bin$ IFS=. read -a ArrIP<<<"$IP"
:~/bin$ echo ${ArrIP[1]}
    96
:~/bin$ printf "%s\n" "${ArrIP[@]}"
    109
    96
    77
    15

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 Jan 29 '18 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 Jan 29 '18 at 4:20
  • @muru Could you please provide the Zsh solution? – sci9 Jan 29 '18 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 Jan 29 '18 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 Jan 29 '18 at 13:17
16

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. – Digital Trauma Jan 29 '18 at 21:42
7

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:

first=${IP%%.*}
last3=${IP#*.}
second=${last3%%.*}
last2=${last3#*.}
third=${last2%.*}
fourth=${last2#*.}
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:

slice="$IP"
count=1
while [ "$count" -le 4 ]
do
    declare sec"$count"="${slice%%.*}"
    slice="${slice#*.}"
    count=$((count+1))
done

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.
5

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 Jan 29 '18 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=109.96.77.15 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" – Stéphane Chazelas Jan 29 '18 at 15:39
  • 4
    It probably works for you because you had set IFS to . in one of your previous tests. Run IP=1.2.3.4 bash -xc 'IFS=. set $IP; echo "$2"' and you'll see it doesn't work. And see IP=1.2.3.4 bash -o posix -xc 'IFS=. set $IP; echo "\$1=$1 \$2=$2 IFS=$IFS"' to illustrate @chepner's point. – Stéphane Chazelas Jan 29 '18 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 Jan 29 '18 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 Jan 29 '18 at 21:40
4

With zsh, you can nest parameter substitutions:

$ ip=12.34.56.78
$ echo ${${ip%.*}##*.}
56
$ echo ${${ip#*.}%%.*}
34

This is not possible in bash.

4

Sure, let's play the elephant game.

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

or

$ ipsplit() { local IFS=.; echo $*; }
$ set -- `ipsplit 10.1.2.3`
$ echo $1
10
  • 2
    What's the "elephant game"? – Wildcard Jan 29 '18 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 Jan 30 '18 at 1:16
4

Not the easiest, but you could do something like:

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

That should work in ksh93 (where that ${var//pattern/replacement} operator comes from), bash, mksh and zsh, though of course in zsh, there are much simpler approaches.

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.

In ksh93, you can also do:

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

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]}'
96

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 127.0.0.1 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:

$((n=32,(${IP//./"<<(n-=8))+("})))

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

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

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).

3

With IP=12.34.56.78.

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

And

#!/bin/bash
IP=$1

regex="(${IP//\./)\.(})"
[[ $IP =~ $regex ]]
echo "${BASH_REMATCH[@]:1}"

Description:

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:

regex=(12)\.(34)\.(56)\.(78)

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:

#!/bin/bash
IP=$1

regex="(${IP//:/):(})"
[[ $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
3

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.

#!/bin/sh
set -o noglob
get_ip_sections(){
    slice="$1"
    count=0
    while [ -n "${slice}" ] && [ "$count" -ne 4 ]
    do
        num="${slice%%.*}"
        printf '%s ' "${num}"
        slice="${slice#*${num}.}"
        count=$((count+1))
    done
}

ip="109.96.77.15"
named_pipe="/tmp/ip_stuff.fifo"
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:109.96.77.15
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.

  • I have modified your answer so it doesn’t use a pipe and added it to my existing answer. – G-Man Feb 2 '18 at 4:22
0
$ ip_=192.168.2.3

$ echo $ip_ 

192.168.2.3

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

192
  • “...using parameter expansion...” – Jeff Schaller Jan 30 '18 at 1:39
  • And where are the other three? – G-Man Jan 30 '18 at 3:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.