44

Original file

claudio
antonio
claudio
michele

I want to change only the first occurrence of "claudio" with "claudia" so that I would get the following:

claudia
antonio
claudio
michele

I have tried the following:

sed -e '1,/claudio/s/claudio/claudia/' nomi

The above command performs global substitution (it replaces all occurrences of 'claudio') . Why?

elbarna
  • 12,695
  • 1
    Look here http://www.linuxtopia.org/online_books/linux_tool_guides/the_sed_faq/sedfaq4_004.html and also info sed: (0,/REGEXP/: A line number of 0 can be used in an address specification like 0,/REGEXP/ so that sed will try to match REGEXP in the first input line too. In other words, 0,/REGEXP/ is similar to 1,/REGEXP/, except that if ADDR2 matches the very first line of input the 0,/REGEXP/ form will consider it to end the range, whereas the 1,/REGEXP/ form will match the beginning of its range and hence make the range span up to the second occurrence of the regular expression) – jimmij Mar 04 '15 at 23:59
  • http://stackoverflow.com/questions/148451/how-to-use-sed-to-replace-only-the-first-occurrence-in-a-file – muru Mar 05 '15 at 00:01
  • awk '/claudio/ && !ok { sub(/claudio/,"claudia"); ok=1 } 1' nomi should do – Adam Katz Mar 06 '15 at 21:33
  • http://stackoverflow.com/questions/148451/how-to-use-sed-to-replace-only-the-first-occurrence-in-a-file – Ciro Santilli OurBigBook.com Apr 26 '16 at 13:36

10 Answers10

38

If you are using GNU sed, try:

sed -e '0,/claudio/ s/claudio/claudia/' nomi

sed does not start checking for the regex that ends a range until after the line that starts that range.

From man sed (POSIX manpage, emphasis mine):

An editing command with two addresses shall select the inclusive range from the first pattern space that matches the first address through the next pattern space that matches the second.

The 0 address is not standard though, that's a GNU sed extension not supported by any other sed implementation.

Using awk

Ranges in awk work more as you were expecting:

$ awk 'NR==1,/claudio/{sub(/claudio/, "claudia")} 1' nomi
claudia
antonio
claudio
michele

Explanation:

  • NR==1,/claudio/

    This is a range that starts with line 1 and ends with the first occurrence of claudio.

  • sub(/claudio/, "claudia")

    While we are in the range, this substitute command is executed.

  • 1

    This awk's cryptic shorthand for print the line.

John1024
  • 74,655
  • 2
    That assumes GNU sed though. – Stéphane Chazelas Mar 04 '15 at 23:54
  • @StéphaneChazelas It also works if POSIXLY_CORRECT is set but I guess that doesn't mean as much as I would like. Answer updated (I lack for BSD test machines). – John1024 Mar 05 '15 at 00:00
  • The awk can, IMO, be simpler with a boolean status variable: awk '!r && /claudio/ {sub(/claudio/,"claudia"); r=1} 1' – glenn jackman Mar 05 '15 at 00:13
  • 1
    @glennjackman or awk !x{x=sub(/claudio/,"claudia")}1 –  Mar 05 '15 at 09:43
  • I also could not successfully use a different delimiter in the first part: 0,/claudio/ – Pat Myron Aug 19 '19 at 05:02
  • which kind of sed wouldn't be able to do this @StéphaneChazelas ? – Levi Uzodike Mar 12 '20 at 22:39
  • 2
    @LeviUzodike, all but GNU sed, the 0 address is an extension of GNU sed. AFAIK, no other implementation supports it. In my tests, none of the ones based on the original sed implementation, nor busybox's nor ast-open's nor FreeBSD's nor NetBSD's support it. – Stéphane Chazelas Mar 13 '20 at 06:20
  • Oh you're talking about the 0 address. I thought you were talking about range addresses that mix line numbers with regex. After reading the answer again, I'm not sure how I thought that lol my bad. Also, cool to know what other sed's there are. I always only see GNU or FreeBSD.Thanks – Levi Uzodike Mar 13 '20 at 15:38
9

A new version of GNU sed supports the -z option.

Normally, sed reads a line by reading a string of characters up to the end-of-line character (new line or carriage return).
The GNU version of sed added a feature in version 4.2.2 to use the "NULL" character instead. This can be useful if you have files that use the NULL as a record separator. Some GNU utilities can generate output that uses a NULL instead a new line, such as "find . -print0" or "grep -lZ".

You can use this option when you want sed to work over different lines.

echo 'claudio
antonio
claudio
michele' | sed -z 's/claudio/claudia/'

returns

claudia
antonio
claudio
michele
Walter A
  • 736
7

Sumary

GNU syntax:

sed '/claudio/{s//claudia/;:p;n;bp}' file

Or even (to use only one time the word to be replaced:

sed '/\(claudi\)o/{s//\1a/;:p;n;bp}' file

Or, in POSIX syntax:

sed -e '/claudio/{s//claudia/;:p' -e 'n;bp' -e '}' file

works on any sed, process only as many lines as needed to find the first claudio, works even if claudio is in the first line and is shorter as it use only one regex string.

Detail

To change only one line you need to select only one line.

Using a 1,/claudio/ (from your question) selects:

  • from the first line (unconditionally)
  • to the next line that contains the string claudio.
$ cat file
claudio 1
antonio 2
claudio 3
michele 4

$ sed -n '1,/claudio/{p}' file claudio 1 antonio 2 claudio 3

To select any line that contains claudio, use:

$ sed -n `/claudio/{p}` file
claudio 1
claudio 3

And to select only the first claudio in the file, use:

sed -n '/claudio/{p;q}' file
claudio 1

Then, you can make a substitution on that line only:

sed '/claudio/{s/claudio/claudia/;q}' file
claudia 1

Which will change only the first occurrence of the regex match on the line, even if there may be more than one, on the first line that match the regex.

Of course, the /claudio/ regex could be simplified to:

$ sed '/claudio/{s//claudia/;q}' file
claudia 1

And, then, the only thing missing is to print all other lines un-modified:

sed '/claudio/{s//claudia/;:p;n;bp}' file
  • Thank you. Went through quite a few variations, yours finally worked. Could you expand on the "print all other lines" syntax, since you're dropping the "q" there as well? – Zael Aug 27 '21 at 16:27
6

Here are 2 more programmatic efforts with sed: they both read the whole file into a single string, then the search will only replace the first one.

sed -n ':a;N;$bb;ba;:b;s/\(claudi\)o/\1a/;p' file
sed -n '1h;1!H;${g;s/\(claudi\)o/\1a/;p;}' file

With commentary:

sed -n '                # don't implicitly print input
  :a                    # label "a"
  N                     # append next line to pattern space
  $bb                   # at the last line, goto "b"
  ba                    # goto "a"
  :b                    # label "b"
  s/\(claudi\)o/\1a/    # replace
  p                     # and print
' file
sed -n '                # don't implicitly print input
  1h                    # put line 1 in the hold space
  1!H                   # for subsequent lines, append to hold space
  ${                    # on the last line
    g                     # put the hold space in pattern space
    s/\(claudi\)o/\1a/    # replace
    p                     # print
  }
' file
glenn jackman
  • 85,964
1

You can use awk with a flag to know if the replacement was already done. If not, proceed:

$ awk '!f && /claudio/ {$0="claudia"; f=1}1' file
claudia
antonio
claudio
michele
fedorqui
  • 7,861
  • 7
  • 36
  • 74
1

It's actually really easy if you just setup a little delay - there's no need to go reaching for unreliable extensions:

sed '$H;x;1,/claudio/s/claudio/claudia/;1d' <<\IN
claudio
antonio
claudio
michele
IN

That just defers the first line to the second and the second to the third and etc.

It prints:

claudia
antonio
claudio
michele
mikeserv
  • 58,310
1

And one more option

sed --in-place=*.bak -e "1 h;1! H;\$! d;$ {g;s/claudio/claudia/;}" -- nomi

The advantage is it uses double quotation, so you can use variables inside, ie.

export chngFrom=claudio
export chngTo=claudia
sed --in-place=*.bak -e "1 h;1! H;\$! d;$ {g;s/${chngFrom}/${chngTo}/;}" -- nomi
utom
  • 111
  • 1
    Yeah, you're right. The general idea is the same. But, please, give a try to substitute single, into double quotes directly, and see if it works. The devil lies in the details. In this example these are spaces and one escape. I believe that this continuation of the earlier answers may save someone's time. And that's the reason why I decided to publish the post. – utom Apr 09 '16 at 20:37
1

This can also be done without the hold space and without concating all lines into the pattern space:

sed -n '/claudio/{s/o/a/;bx};p;b;:x;p;n;bx' nomi

Explanation: We try to find "claudio" and if we do it we jump into the small print-load-loop between :x and bx. Otherwise we print and restart the script with the next line.

sed -n '      # do not print lines by default
  /claudio/ { # on lines that match "claudio" do ...
    s/o/a/    # replace "o" with "a"
    bx        # goto label x
  }           # end of do block
  p           # print the pattern space
  b           # go to the end of the script, continue with next line
  :x          # the label x for goto commands
  p           # print the pattern space
  n           # load the next line in the pattern space (clearing old contents)
  bx          # goto the label x
  ' nomi
Lucas
  • 2,845
1
sed -n '/claudia/{p;Q}'

sed -n '           # don't print input
    /claudia/      # regex search
    {              # when match is found do
    p;             # print line
    Q              # quit sed, don't print last buffered line
    {              # end do block
jgshawkey
  • 1,639
0

First pipe some output to awk

Then you can use awk variables to substitute

cat someFile | awk '!x{x=sub("wordToReplace","wordThatIsReplaced")}1'

Notice the number near the end.

It is required but can be any number, I chose 1 to signify the first instance.