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
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}"
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).
IFS
for theread
there:IFS=. read -a ArrIP<<<"$IP"
– muru Jan 29 '18 at 4:08IFS=. 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