To parse colon-delimited fields I can use read
with a custom IFS
:
$ echo 'foo.c:41:switch (color) {' | { IFS=: read file line text && echo "$file | $line | $text"; }
foo.c | 41 | switch (color) {
If the last field contains colons, no problem, the colons are retained.
$ echo 'foo.c:42:case RED: //alert' | { IFS=: read file line text && echo "$file | $line | $text"; }
foo.c | 42 | case RED: //alert
A trailing delimiter is also retained...
$ echo 'foo.c:42:case RED: //alert:' | { IFS=: read file line text && echo "$file | $line | $text"; }
foo.c | 42 | case RED: //alert:
...Unless it's the only extra delimiter. Then it's stripped. Wait, what?
$ echo 'foo.c:42:case RED:' | { IFS=: read file line text && echo "$file | $line | $text"; }
foo.c | 42 | case RED
Bash, ksh93, and dash all do this, so I'm guessing it is POSIX standard behavior.
I want to parse the strings above into three variables and I don't want to mangle any text in the third field. I had thought read
was the way to go but now I'm reconsidering.
Yes, that's standard behaviour (see the read
specification and Field Splitting). A few shells (ash
-based including dash
, pdksh
-based, zsh
, yash
at least) used not to do it, but except for zsh
(when not in POSIX mode), busybox
sh, most of them have been updated for POSIX compliance.
That's the same for:
$ var='a:b:c:' IFS=:
$ set -- $var; echo "$#"
3
(see how the POSIX specification for read
actually defers to the Field Splitting mechanism where a:b:c:
is split into 3 fields, and so with IFS=: read -r a b c
, there are as many fields as variables).
The rationale is that in ksh
(on which the POSIX spec is based) $IFS
(initially in the Bourne shell the internal field separator) became a field delimiter, I think so any list of elements (not containing the delimiter) could be represented.
When $IFS
is a separator, one can't represent a list of one empty element (""
is split into a list of 0 element, ":"
into a list of two empty elements¹). When it's a delimiter, you can express a list of zero element with ""
, or one empty element with ":"
, or two empty elements with "::"
.
It's a bit unfortunate as one of the most common usages of $IFS
is to split $PATH
. And a $PATH
like /bin:/usr/bin:
is meant to be split into "/bin"
, "/usr/bin"
, ""
, not just "/bin"
and "/usr/bin"
.
Now, with POSIX shells (but not all shells are compliant in that regard), for word splitting upon parameter expansion, that can be worked around with:
IFS=:; set -o noglob
for dir in $PATH""; do
something with "${dir:-.}"
done
That trailing ""
makes sure that if $PATH
ends in a trailing :
, an extra empty element is added. And also that an empty $PATH
is treated as one empty element as it should be.
That approach can't be used for read
though.
Short of switching to zsh
, there's no easy work around other than inserting an extra :
and remove it afterwards like:
echo a:b:c: | sed 's/:/::/2' | { IFS=: read -r x y z; z=${z#:}; echo "$z"; }
Or (less portable):
echo a:b:c: | paste -d: - /dev/null | { IFS=: read -r x y z; z=${z%:}; echo "$z"; }
I've also added the -r
which you generally want when using read
.
Most likely here you'd want to use a proper text processing utility like sed
/awk
/perl
instead of writing convoluted and probably inefficient code around read
which has not been designed for that.
¹ Though in the Bourne shell, that was still split into zero elements as there was no distinction between IFS-whitespace and IFS-non-whitespace characters there, something that was also added by ksh
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With