2

How can I use sed to substitute a space with an underscore only when it occurs in a text file after the string title= and between " "

For example (lines in a text file):

title="This is the title of my book" img=scr " </header><!-- .entry-header -->
title="Today is a beautiful day" img=scr " </header><!-- .entrrrrkkky-header -->

Desired modified text file after sed:

title="This_is_the_title_of_my_book" img=scr " </header><!-- .entry-header -->
title="Today_is_a_beautiful_day" img=scr " </header><!-- .entrrrrkkky-header -->

Basically, the space would only be substituted for a _ when it occurs between the " " after the string title=

The name of the text file is arbitrary - say file.txt

1 Answer 1

3

You would have to do this in a loop:

s/\(^.*title="[^" ]*\) \([^"]*".*$\)/\1_\2/

or (faster)

s/\(title="[^" ]*\) \([^"]*"\)/\1_\2/

or (probably again faster, because why add another match just to replace it by itself?!)

s/\(title="[^" ]*\) /\1_/

and use sed's test-and-branch feature, retrying the substitute until no more changes are made. The point of the patterns in this command is to split the line at the first (remaining) space and replace that space with an underscore.

Here is a script:

#!/bin/sh
sed     -e ':loop' \
        -e 's/\(title="[^" ]*\) \([^"]*"\)/\1_\2/' \
        -e 't loop' <foo.in >foo.out
diff -u foo.in foo.out

The initial answer used a wider pattern, but @g-man commented that it was not necessary. It was slower, as illustrated by timing sed on a 10Mb file (tested with GNU sed on Debian 7):

$ ./foo1
27.03user 0.01system 0:27.18elapsed 99%CPU (0avgtext+0avgdata 1104maxresident)k
0inputs+0outputs (0major+333minor)pagefaults 0swaps
9.54user 0.00system 0:09.60elapsed 99%CPU (0avgtext+0avgdata 972maxresident)k
0inputs+0outputs (0major+301minor)pagefaults 0swaps

With OSX, the difference is not as much:

$ ./foo1

real    0m11.943s
user    0m11.897s
sys     0m0.024s

real    0m5.858s
user    0m5.839s
sys     0m0.014s

Interestingly enough, the wider pattern does not work with Solaris's sed (but the shorter one does). It does not match either end of the line within the grouping \( and \), while both BSD and GNU do this. Likewise, it works with HPUX 11.31 and AIX 7.1

POSIX sed uses BREs, and the grouping feature is covered by 9.3.6 BREs Matching Multiple Characters:

A subexpression can be defined within a BRE by enclosing it between the character pairs "\(" and "\)". Such a subexpression shall match whatever it would have matched without the "(" and ")", except that anchoring within subexpressions is optional behavior; see BRE Expression Anchoring. Subexpressions can be arbitrarily nested.

9.3.8 BRE Expression Anchoring explains the term:

A BRE can be limited to matching strings that begin or end a line; this is called "anchoring".

so, in the context of the standard as it is implemented, this is a known limitation of Solaris sed which the standard allows as "optional" behavior.

Further reading:

2
  • I believe that it works at least as well, and maybe better, if you leave out the ^.* and the .*$. Apr 10, 2016 at 5:49
  • Fastest, btw, is my personal sed extension with only flag to the s command and \h to place the hold buffer in the substition: sed 'h;s/title="[^"]*/&/o;y/ /_/;x;s//\h/' is easy to read and seven times faster.
    – Philippos
    Sep 23, 2019 at 8:19

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .