-1

I have a file using the following format:

M104 S200
M107
M104 S250
M110
M104 S275

I'm trying to find a way to match let's say the second occurrence of M104 and rewrite the entire line to something else ALSO containing M104.

I've tried

sed 's/^M104/s/.*/M104 S300/' file - but it replaces all instances of M104
sed '0,/M104/s//M104 S200/' file - leaves the trailing text on the line
  • a few other variations that are giving me unexpected results. Any help? ;)
4
  • 2
    What is the expected result?
    – Kusalananda
    Jun 19 at 21:47
  • 1
    I suggest to use awk for this job.
    – Cyrus
    Jun 19 at 21:49
  • Expected result should be a search for 1st, 2nd, or 3rd instance of "M104", then I can replace the entire line to something like "M104 S190" or my own value ignoring the current S numbers. I'm familiar with AWK/SED but even after 2 hours of searching google haven't found a solution yet ;)
    – J V
    Jun 19 at 21:54
  • 2
    Please edit question and add your desired output (no description, no images, no links) for that sample input to your question (no comment).
    – Cyrus
    Jun 19 at 21:56

3 Answers 3

2

If you have Perl, I'd do something like this:

$ perl -pe 'if (/M104/) { $i++; if($i == 2) { $_ = "M104 S999\n" } } ' test.txt
M104 S200
M107
M104 S999
M110
M104 S275

I.e. if /M104/ matches, increment $i, then check if $i is equal to 2, and if so, replace the current line (including the trailing newline, \n)

You could use s/^M104.*/.../ in the last part if you want to use a sed-like substitution there.

Perl has the sed-like -i[extension] option for in-place changes with or without a backup file.

Or passing the key values through env vars (makes scripting easier):

n=2 repl="S999" perl -pe 'if (/M104/) { $i++; if($i == $ENV{n}) { $_ = "M104 " . $ENV{repl} . "\n" } } ' test.txt

(or if you like chaining more than nesting, something like perl -pe '/M104/ and $i++ and $i == 2 and $_ = "M104 S999\n" ' test.txt.)


or in awk:

$ awk '/M104/ { i++; if (i == 2) $0 = "M104 S999"; } 1' test.txt

But there's no standard in-place option for awk.

3
  • This gives the expected result, however I'm totally new to perl, does it have a -i (interactive) command to write the result to file like SED?
    – J V
    Jun 19 at 22:00
  • @JV, yep, perl -i.bak -pe .... (I think -i is for "in-place" here)
    – ilkkachu
    Jun 19 at 22:03
  • wow that worked great! I may have to study up on perl a little more as this seems very effective. Thank you
    – J V
    Jun 19 at 22:05
0

Your (failed) attempts imply that your sed version allows the 0 for the start of a range (not all seds do). If so, try

sed -rn '0,/^M104/{p;b;}; 0,/(^M104).*/ s//\1 S300/; p' file
M104 S200
M107
M104 S300
M110
M104 S275

The idea is to apply the address range twice: the first range will find and print lines unaltered up to the first match. The second range will then substitute the next match. Again, not all sed versions accept the second range's start address when it's already consumed by the first one.

0

If you are using GNU sed, you can use the -z option to process the whole file in one buffer and use the number flags to the s command to substitute the nth occurence:

sed -zE 's/(^|\n)M104[^\n]*/\1M104 S300/2'

The 2 tells sed to replace the second match.

Note that I use extended regular expressions (option -E), so I can match for either a line break or the beginning of the file and reinsert the match (\1) to keep the line break.

The portable version can't use the -z option and is not allowed to use [^\n] for "everything but newline", so we use the hold bufer to collect lines and expect lines to contain nothing but printable characters:

sed -E 'H;1h;$!d;x;s/(^|\n)M104[[:print:]]*/\1M104 S300/2'

Also note that the buffer may be too big for very large files.

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.