find recurring pattern with `sed`

Viewed 175

I am using GNU bash 4.3.48

I expected that

echo "23S62M1I19M2D" | sed 's/.*\([0-9]*M\).*/\1/g'

would output 62M19M... But it doesn't.

sed 's/\([0-9]*M\)//g' deletes ALL [0-9]*M and retrieves 23S1I2D. but the group \1 is not working as I thought it would.

sed 's/.*\([0-9]*M\).*/ \1 /g', retrieves M...

What am I doing wrong?

Thank you!

5 Answers

With your shown samples and with awk you could try following program.

echo "23S62M1I19M2D" | 
awk '
{
  val=""
  while(match($0,/[0-9]+M/)){
    val=val substr($0,RSTART,RLENGTH)
    $0=substr($0,RSTART+RLENGTH)
  }
  print val
}
'

Explanation: Simple explanation would be, using echo to print values and sending it as a standard input to awk program. In awk program using its match function to match regex mentioned in it(/[0-9]+M) running loop to find all matches in each line and printing the collected matched values at last of each line.

This might work for you (GNU sed):

sed -nE '/[0-9]*M/{s//\n&\n/g;s/(^|\n)[^\n]*\n?//gp}' file

Surround the match by newlines and then remove non-matching parts.

Alternative, using grep and tr:

grep -o '[0-9]*M' file | tr -d '\n' 

N.B. tr removes all newlines (including the last one) to restore the last newline, use:

grep -o '[0-9]*M' file | tr -d '\n' | paste  

The alternate solution will concatenate all results into a single line. To achieve the same result with the first solution use:

sed -nE '/[0-9]*M/{s//\n&\n/g;s/(^|\n)[^\n]*\n?//g;H};${x;s/\n//gp}' file

Your substitution is probably working, but not substituting what you think it is.

In the substitution s/\(foo...\)/\1/, the \1 matches whatever \(...\) matches and captures, so your substitution is replacing foo... by foo...!

% echo "1234ABC" | sed 's/\([A-Z]\)/-\1-/'g
1234-A--B--C-

So you'll need to match more, but capture only a portion of the match. For example:

echo "23S62M1I19M2D" | sed 's/[0-9]*[A-LN-Z]*\([0-9]*M\)/\1/g'
62M19M2D

In the case of sed 's/.*\([0-9]*M\).*/\1/g' (did that appear in an edit to the question, or did I just miss it?), the .* matches ‘greedily’ – it matches as much as it possibly can, thus including the digits before the M. In the example above, the [A-LN-Z] is required to be at the end of the uncaptured part, so the digits are forced to be matched by the [0-9] inside the capture.

Getting a clear idea of what ‘greedy’ means is a really important idea when writing or interpreting regexps.

If you know you will only encounter the suffixes S, M, I and D, an alternative approach would be explicitly deleting the combinations you don't want:

echo "23S62M1I19M2D" | sed 's/[0-9]\+[SID]//g'

This gives the expected:

62M19M

Update: This variant produces the same output, but rejects all non-numeric, non-M suffixes:

echo "23S62M1I19M2D" | sed 's/[0-9]\+[^0-9M]//g'    

The problem is that the .* is greedy. Since only M is obligatory, when the engine finds last M, it satisfies the regex, so all string is matched, M is captured and thus kept after replacing with \1 backreference.

That means, you can't easily do this with sed. You can do that with Perl much easier since it supports matching and skipping pattern:

#!/bin/bash
perl -pe 's/\d+M(*SKIP)(*F)|.//g' <<< "23S62M1I19M2D"

See the online demo. The pattern matches

  • \d+M(*SKIP)(*F) - one or more digits, M, and then the match is omitted and the next match is searched for from the failure position
  • |. - or matches any char other than a line break char.

Or simply match all occurrences and concatenate them:

perl -lane 'BEGIN{$a="";} while (/\d+M/g) {$a .= $&} END{print $a;}' <<< "23S62M1I19M2D"

All \d+M matches are appended to the $a variable which is printed at the end of processing the string.

Related