Change any number of delimiters in found pattern with sed

Viewed 330

I want to change every . to @.@ with sed, but only if the . is enclosed with numbers.
For example:

This sentence ends with a dot. 1.2.3
Dot. 1.2.3.4.5 Dot.

The goal:

This sentence ends with a dot. 1 @.@ 2 @.@ 3
Dot. 1 @.@ 2 @.@ 3 @.@ 4 @.@ 5 Dot.

The pattern could contain any number of integers.

I tried:

sed -E 's/([0-9]+)\.([0-9]+)/\1 @\.@ \2/g'

but it only works for the first two number in the pattern.

5 Answers

For the repeated pattern (number-dot-number-dot-number...) that substitution doesn't work because the number following the dot is "consumed" and so the engine moved along the string, so the next character it sees is a dot, not the needed num-dot-num pattern.

One solution is to use lookarounds,† which are "zero-width" assertions, so with which the engine doesn't consume the match and doesn't move along, but it merely "looks" from its "spot" between characters to assert that the pattern (ahead or behind) matches, so to say

s/ (?<=[0-9]) \. (?=[0-9]) / @.@ /gx;

For a testable example (in Perl, as tagged)

perl -wE'$_=q(Dot. 1.2.3.4.5 Dot.); say; s/(?<=[0-9])\.(?=[0-9])/ @.@ /g; say'

which prints

Dot. 1.2.3.4.5 Dot.
Dot. 1 @.@ 2 @.@ 3 @.@ 4 @.@ 5 Dot.

But the lookbehind won't work with a "number" that consists of more than one digit, since then we'd need [0-9]+ which has variable and unlimited length, whiat lookbehinds can't (yet) do.

If it is indeed possible to have multi-digit numbers in your case, then the number before the . need be captured -- this still works with the number before the dot -- and then put back

s/([0-9]+)\.(?=[0-9])/$1 @.@ /g;

This can be done anyway, of course, even if it's all always single digits; i used lookbehind originally only for the symmetry with the other side (needing a lookahead)


† In a tool that supports them, which in my understanding sed isn't. (Thanks to comments by potong and Ed Morton for informing of that) I still offer this solution since Perl is one of the tagged languages.

As for the 1st line, the regex matches 1.2 for the 1st trial. The next pattern match starts with the character . just after the previous match then it fails.
With sed please try:

sed -E '
:l
s/([[:digit:]])\.([[:digit:]])/\1 @.@ \2/
t l
' file

which iterates the pattern match from the start of the string.

As you are adding perl in the tag, here is an alternative with perl:

perl -pe 's/(?<=\d)\.(?=\d)/ @.@ /g' file

With your shown samples, please try following. Written and tested in GNU awk.

awk -v RS='([0-9]+.)+[0-9]+' '{gsub(/\./," @.@ ",RT);ORS=RT;print}' Input_file

Explanation: Simply making record separator as digits(one or more occurrences) with dot(.)'s one or more occurrences followed with 1 or more digits occurrences. Then substituting dot coming at last with @.@ as per OP's request, resetting ORS and printing that line then. To get more info on awk you could check man awk page too.

Using any sed in any shell on every Unix box:

$ sed 's/\([0-9]\)\.\([0-9]\)/\1 @.@ \2/g; s/\([0-9]\)\.\([0-9]\)/\1 @.@ \2/g' file
This sentence ends with a dot. 1 @.@ 2 @.@ 3
Dot. 1 @.@ 2 @.@ 3 @.@ 4 @.@ 5 Dot.

You need 2 passes of the input because given input of 1.2.3 only 1.2 will be matched by the first pass of [0-9]\.[0-9], the 2.3 won't be recognized in that pass because the 2 was already consumed by the first match so all that's left of the input in that first pass is .3, you need the 2nd pass (now against 1 @.@ 2.3) to match 2.3.

Here is a POSIX awk solution:

awk '{while (match($0, /[0-9]\.[0-9]/))
   $0 = substr($0, 1, RSTART) " @.@ " substr($0, RSTART+2)} 1' file

This sentence ends with a dot. 1 @.@ 2 @.@ 3
Dot. 1 @.@ 2 @.@ 3 @.@ 4 @.@ 5 Dot.
Related