Chris F
Chris F

Reputation: 16803

How to use sed to replace a string within a string?

I read the following article "Using grep and sed to find and replace a string" but how can I extend it to chain multiple greps. For example I have the following directory/file structure

dir1/metadata.txt
dir2/metadata.txt

dir1/metadata.txt has

filename1 '= 1.0.0'
filename2 '= 1.0.0'

dir2/metadata.txt has

filename1     '= 1.0.0'
long_filename '= 1.0.0'

In other words, both dir1/metadata.txt and dir2/metadata.txt contain "filename '1.0.0'" but the spaces between the "filename" and the "'1.0.0'" in each file is different.

Now I want to replace filename1's associated version to '2.0.0' in ALL metadata.txt files so the resulting files look like...

dir1/metadata.txt has

filename1 '= 2.0.0'
filename2 '= 1.0.0'

dir2/metadata.txt has

filename1     '= 2.0.0'
long_filename '= 1.0.0'

I'm trying

find . -name metadata.txt | xargs grep filename1 | sed -i "s/1\.0\.0/2.0.0/g" <some option here>

but I know the "some option here" part. Any clues?

Upvotes: 1

Views: 1073

Answers (2)

John1024
John1024

Reputation: 113934

find . -name metadata.txt -exec grep -l --null filename1 {} + | xargs -0 sed -i "/^filename1 /{s/'= 1\.0\.0'/'= 2.0.0'/;}"

sed -i will update the timestamp of every file it processes regardless of whether it changes the contents of the file. This is because, in operation, sed -i creates a new file for each file processed and then overwrites the old file with the new file. To limit this, the above code uses grep to select only the files that might need modification and sends only those file names, via a pipeline, to sed -i for the update.

If the timestamp/overwriting issue is not important, consider mklement0's answer which eliminates the need for a pipeline, simplifying the command.

How it works

  • find . -name metadata.txt -exec grep -l --null filename1 {} +

    This produces the list of files name metadata.txt that also contain filename.

    The --null tells grep to separate file names with the NUL character.

  • xargs -0 sed -i "/^filename1 /{s/'= 1\.0\.0'/'= 2.0.0'/;}"

    This applies sed -i to change in-place the files whose names were returned by the above find command.

    In more detail:

    • /^filename1 /

      This selects lines that start with filename1 followed by a space. This assures that we match neither sfilename1 nor filename12.

    • s/'= 1\.0\.0'/'= 2.0.0'/

      This changes the version number for the selected lines. (This assumes only one space after the equal sign. If this assumption is not correct, we can easily change it.)

    The -0 option to xargs tells it to expect its input to be a NUL-separated list of file names. This makes the pipeline safe even if the file names include spaces, newlines, or other difficult characters.

Upvotes: 3

mklement0
mklement0

Reputation: 439692

Try the following:

Linux:

find . -name metadata.txt \
  -exec sed -i "s/^\(filename1[[:space:]]\{1,\}'= \)1\.0\.0/\12.0.0/" {} +

OSX / BSD:

find . -name metadata.txt \
  -exec sed -i '' "s/^\(filename1[[:space:]]\{1,\}'= \)1\.0\.0/\12.0.0/" {} +

Note: The only reason why platform-specific commands are required is that GNU sed and BSD sed interpret the nonstandard -i option, which specifies the suffix to use for an optional backup of the original file, differently: GNU sed considers the option-argument for -i optional, whereas BSD sed considers it mandatory, requiring an explicit argument to specify the empty string (indicating the desire not to create a backup file)

  • exec ... + is a find feature that invokes the specified command with as many matching paths as can fit on a single command line, potentially resulting in multiple invocations, but typically resulting in only 1, which makes the invocation efficient.

  • "s/\(filename1[[:space:]]\{1,\}'= \)1\.0\.0/\12.0.0/" is a POSIX-compliant sed script that matches literal filename1 at the beginning of a line, followed by a variable amount of whitespace ([[:space:]]\{1,\}), followed by literal '= 1.0.0, and replaces the 1.0.0. with 2.0.0.

  • Note that if there are metadata.txt files that do not have lines beginning with filename1, they are still rewritten, because sed's -i option blindly "updates" the input files given (read: creates a new file that then replaces the original). If that is undesired, consider John1024's answer.

POSIX-compliance notes:

  • The -exec ... + variant of find's -exec primary has been part of POSIX since 2001 (POSIX.1-2001 / IEEE Std 1003.1-2001 / SUS v3 - see http://pubs.opengroup.org/onlinepubs/009695399/; thanks, @JonathanLeffler)
  • By contrast, sed's -i option for in-place updating is not POSIX-compliant - so you may have to work around that.

Upvotes: 4

Related Questions