Replace string in the first file it is found

Viewed 165

I have a bunch of files named like this:

chapter1.tex
chapter2.tex
chapter3.tex
...
chapter 10.tex
chapter 11.tex
etc.

I am trying to use sed to find and replace the first instance of AAAAAA with ZZZZZZ within all of the files.

sed -i "0,/AAAAAA/s//ZZZZZZ/" chapter*.tex

I tried this above command, but there are two problems:

  1. It finds and replaces the first instance of AAAAAA within each file. I want only the first instance among all files.
  2. I suspect, like many Bash tools, it doesn't properly sort my files in order. E.g. if I type ls then chapter10.tex is listed before chapter1.tex. It is critical it searches the files in order of the chapters.

How to use Bash tools to find and replace first instance, from among a large list of files, so only the first instance in the first found file is replaced, while also respecting the file order (chapter1.tex is first, chapter10.tex is tenth)?

4 Answers

With the complete GNU toolchest you don't need a loop.

printf '%s\0' chapter*.tex    \
| sort -zV                    \
| xargs -0 grep -FlZ 'AAAAAA' \
| head -zn1                   \
| xargs -0r sed -i 's/AAAAAA/ZZZZZZ/'

Here is a bash loop based solution that will work with filenames such as chapter 10.tex i.e. filenames with spaces etc:

while IFS= read -r -d '' file; do
   if grep -q 'AAAAAA' "$file"; then
      echo "changing $file"
      sed -i '0,/AAAAAA/s//ZZZZZZ/' "$file"
      break
   fi
done < <(printf '%s\0' chapter*.tex | sort -z -V)

This is assuming both sed and sort are from gnu utils.


If you have gnu awk 4+ version that supports in-place editing i.e. -i inplace then you can replace grep + sed with single awk:

while IFS= read -r -d '' file; do
   awk -i inplace '!n {n=sub(/AAAAAA/, "ZZZZZZ")} 1;
   END {exit !n}' "$file" && break
done < <(printf '%s\0' chapter*.tex | sort -z -V)

This might work for you (GNU sed and grep):

grep -ns 'AAAAAA' chapter{1..9999}.txt | head -1 |
sed -nE 's#([^:]*):([^:]*):.*#sed -i "\2s/AAAAAA/ZZZZZZ/" \1#e'

Use grep and bash's braces expansion to identify the one possible matching file and line number and build a sed script to update that file at that line number.

N.B. Brace expansion generates the filenames in the correct order and the -s command line option for grep suppresses the non-existent files messages.


Alternative using GNU parallel:

grep -sno 'AAAAAA' chapter{1..9999}.txt | head -1 |
parallel --colsep : sed '{2}s/{3}/ZZZZZZ/' {1}

#update

I stand on the backs of giants, lol

Kudos to @potong for a great sorting solution with brace expension! That means this whole thing can be reduced to a single-process one-liner:

sed -i '0,/^AAA/{ /^AAA/{ s/AAA/ZZZ/; h; } }; ${ x; /./{x;q;}; x; }' chapter\ {[0-9],[0-9][0-9]}.tex 

#edit

As pointed out, the original solution below would process and change the first occurrence in every file, and does not correct the file order. @anubhava already provided an excellent, elegant sorting solution on which I will not try to improve.

while IFS= read -r -d '' file; do lst+=( "$file" ); done < <(printf '%s\0' chapter*.tex | sort -z -V)

This creates a list of the filenames in proper order which can be passed to a single call of sed to process them en masse.

To apply that to ordering to a sed-based solution and only hit the first occurrence in any file -

sed -i '0,/^AAA/{ /^AAA/{ s/AAA/ZZZ/; h; } }; ${ x; /./{x;q;}; x; }' "${lst[@]}"

This will look through each file and change the first occurrence it finds in that file, holding the line where it first finds it. On the last line of each file it exchanges the current line for the hold buffer and checks to see if after the swap there is anything in the pattern buffer. If there is not, it swaps it back and continues. If there is, it swaps it back and quits, skipping all subsequent files.

While somewhat complicated, this does not spawn processes for each file.


Original


Use a double condition -

sed -i '0,/AAAAAA/{ /AAAAAA/s/AAAAAA/ZZZZZZ/ }' chapter*.tex

To see the same general logic in action:

$: cat a.tex b.tex
111
AAA
BBB
AAA
222

111
AAA
BBB
AAA
222

$: sed -i '0,/^AAA/{ /^AAA/s/AAA/ZZZ/; }' *.tex
$: cat a.tex b.tex
111
ZZZ
BBB
AAA
222

111
ZZZ
BBB
AAA
222

'0,/^AAA/ is right, as it ranges from the start of the file to the first occurrance of the target string.

{ opens a block, in which we can use a second search to make sure it only affects the targeted string.

Inside the block, /^AAA/s/AAA/ZZZ/; substitutes the AAA string and ignores all the records before it. } closes the block. All records after will be untouched.

Related