Как заменить только N-е вхождение шаблона в файл?

10

Как заменить третье вхождение строки в файле с помощью sedкоманды.

Пример:

Изменение только третье вхождение isв usв файле.

Мой входной файл содержит:

hai this is linux.
hai this is unix.
hai this is mac.
hai this is unchanged.

Я ожидаю, что результат будет:

hai this is linux.
hai thus is unix.
hai this is mac.
hai this is unchanged.
Sureshkumar
источник
3
Вход и выход одинаковы.
Хауке Лагинг
4
sedэто не правильный инструмент для работы.
Чороба
@don_crissti Я исправил это. ОП не использовала инструменты форматирования (кстати, Сурешкумар, см. Здесь помощь по редактированию ваших вопросов), и последующие редакторы неправильно поняли, что нужно.
Terdon

Ответы:

11

Это намного проще сделать perl.

Чтобы изменить 3- е вхождение:

perl -pe 's{is}{++$n == 3 ? "us" : $&}ge'

Чтобы изменить каждый 3- й случай:

perl -pe 's{is}{++$n % 3 ? $& : "us"}ge'
Стефан Шазелас
источник
3

Когда замена строки происходит только один раз в строке, вы можете комбинировать различные утилиты.
Когда вход находится в файле «input» и вы заменяете «is» на «us», вы можете использовать

LINENR=$(cat input | grep -n " is " | head -3 | tail -1 | cut -d: -f1)
cat input | sed ${LINENR}' s/ is / us /'
Вальтер А
источник
В примере в вопросе есть более одного isна строку.
Terdon
Я думал, что вы ищете "есть" с пробелами. Я мог бы отредактировать свой ответ с помощью команды tr, как @jimmij, но мое решение стало бы намного хуже его.
Уолтер
Я не спрашиваю :). Я думал , что то же самое, поэтому я upvoted своего ответа, но если вы посмотрите на оригинальную версию вопроса (нажмите на кнопку «Edited X минут назад» ссылка) вы увидите , что OP ожидается это в этом чтобы быть изменен таким образом . Кстати, кошка там не нужна .
Тердон
2

Сценарий ниже (с использованием синтаксиса GNU sed ) можно использовать для редактирования на месте, а не для вывода, поскольку он останавливает печать строк после желаемой замены:

sed -i '/is/{: 1 ; /\(.*is\)\{3\}/!{N;b1} ; s/is/us/3 ; q}' text.file

Если вам нравится решение Чороба, вы можете изменить выше, чтобы

sed '/is/{:1 ; /\(.*is\)\{3\}/!{N;b1} ; s/is/us/3 ; :2 ; n ; $!b2}' text.file

который выводит все строки

Или вы должны поместить все строки в пространство образца (в памяти, поэтому будьте осторожны с ограничением размера) и выполните замену

sed ': 1 ; N ; $!b1 ; s/is/us/3 ' text.file
Костас
источник
2

Вы можете использовать sedдля этого, если ранее новые строки были заменены на любые другие символы, например:

tr '\n' '\000' | sed 's/is/us/3' | tr '\000' '\n'

И то же самое с чистым (GNU) sed:

sed ':a;N;$!ba;s/\n/\x0/g;s/is/us/3;s/\x0/\n/g'

( sedзамена новой строки бессовестно похищенной с https://stackoverflow.com/a/1252191/4488514 )

jimmij
источник
Если вы собираетесь использовать sedспецифический для GNU синтаксис, вы также можете использовать его sed -z 's/is/us/3'.
Стефан Шазелас
@ StéphaneChazelas, -zдолжно быть, какая-то новая функция, моя GNU sed version 4.2.1ничего не знает об этой опции.
Джимми
1
Добавлено в 4.2.2 (2012). В вашем втором решении вам не нужно переходить на \x0шаг.
Стефан Шазелас
Извините за редактирование. Я не видел оригинальную версию вопроса, и кто-то неправильно понял ее и отредактировал неправильную строку. Я вернулся к предыдущей версии.
Terdon
1
p='[:punct:]' s='[:space:]'
sed -Ee'1!{/\n/!b' -e\}            \
     -e's/(\n*)(.*)/ \2 \1/'       \
     -e"s/is[$p]?[$s]/\n&/g"       \
     -e"s/([^$s])\n/\1/g;1G"       \
-e:c -e"s/\ni(.* )\n{3}/u\1/"      \
     -e"/\n$/!s/\n//g;/\ni/G"      \
     -e's//i/;//tc'                \
     -e's/^ (.*) /\1/;P;$d;N;D'

Этот бит sedпросто переносит количество isпоявлений с одной строки на другую. Он должен надежно обрабатывать столько ises на строку, сколько вы набрасываете на него, и ему не нужно буферизовать старые строки, пока он это делает - он просто сохраняет один символ новой строки для каждого is, с чем встречается, который не является частью другого слова.

В результате он будет изменять только третье вхождение в файле - и он будет иметь число в строке. Так что если файл выглядит так:

1. is is isis
2. is does

... это будет печатать ...

1. is is isis
2. us does

Сначала он обрабатывает края, вставляя пробел в начало и конец каждой строки. Это немного облегчает определение границ слов.

Затем он ищет действительные значения is, вставляя \newline до того, как все вхождения isэтого предшествуют нулю или одному знаку препинания, за которым следует пробел. Он делает еще один проход и удаляет все электронные \nстроки, которым непосредственно предшествует непробельный символ. Эти оставленные маркеры будут совпадать is.иis , но не thisили ?is.

Затем он собирает каждый маркер в конец строки - для каждого \niсовпадения в строке он добавляет линию \newline к концу строки и заменяет ее либо на, iлибо на u. Если в \nконце строки собраны 3 строки, тогда используется символ u, иначе - i. Первый раз, когда используется au, также является последним - замена запускает бесконечный цикл, который сводится к get line, print line, get line, print line,и так далее.

В конце каждого цикла цикла try он очищает вставленные пробелы, печатает только до первой встречающейся новой строки в пространстве шаблона и возвращается снова.

Я добавлю lкоманду ook в начало цикла, например:

l; s/\ni(.* )\n{9}/u\1/...

... и посмотрим, что он делает, как работает с этим входом:

hai this is linux.
hai this is unix.


hai this is mac.
hai this is unchanged is.

... так вот что он делает:

 hai this \nis linux. \n$        #behind the scenes
hai this is linux.               #actually printed
 hai this \nis unix. \n\n$       #it builds the marker string
hai this is unix.
  \n\n\n$                        #only for lines matching the

  \n\n\n$                        #pattern - and not otherwise.

 hai this \nis mac. \n\n\n$      #here's the match - 3 ises so far in file.
hai this us mac.                 #printed
hai this is unchanged is.        #no look here - this line is never evaled

Это имеет больше смысла, может быть, с большим количеством ises в строке:

nthword()(  p='[:punct:]' s='[:space:]'         
    sed -e '1!{/\n/!b' -e\}             \
        -e 's/\(\n*\)\(.*\)/ \2 \1/'    \
        -e "s/$1[$p]\{0,1\}[$s]/\n&/g"  \
        -e "s/\([^$s]\)\n/\1/g;1G;:c"   \
        -e "${dbg+l;}s/\n$1\(.* \)\n\{$3\}/$2\1/" \
        -e '/\n$/!s/\n//g;/\n'"$1/G"    \
        -e "s//$1/;//tc" -e 's/^ \(.*\) /\1/'     \
        -e 'P;$d;N;D'
)        

Это практически то же самое, но написано с помощью POSIX BRE и элементарной обработки аргументов.

 printf 'is is. is? this is%.0s\n' {1..4}  | nthword is us 12

... получает ...

is is. is? this is
is is. is? this is
is is. is? this us
is is. is? this is

... и если я включу ${dbg}:

printf 'is is. is? this is%.0s\n' {1..4}  | 
dbg=1 nthword is us 12

... мы можем смотреть это повторяться ...

 \nis \nis. \nis? this \nis \n$
 is \nis. \nis? this \nis \n\n$
 is is. \nis? this \nis \n\n\n$
 is is. is? this \nis \n\n\n\n$
is is. is? this is
 \nis \nis. \nis? this \nis \n\n\n\n\n$
 is \nis. \nis? this \nis \n\n\n\n\n\n$
 is is. \nis? this \nis \n\n\n\n\n\n\n$
 is is. is? this \nis \n\n\n\n\n\n\n\n$
is is. is? this is
 \nis \nis. \nis? this \nis \n\n\n\n\n\n\n\n\n$
 is \nis. \nis? this \nis \n\n\n\n\n\n\n\n\n\n$
 is is. \nis? this \nis \n\n\n\n\n\n\n\n\n\n\n$
 is is. is? this \nis \n\n\n\n\n\n\n\n\n\n\n\n$
is is. is? this us
is is. is? this is
mikeserv
источник
Вы поняли, что ваш пример говорит "isis"?
flarn2006
@ flarn2006 - я уверен, что это так.
mikeserv
0

Вот логическое решение, которое использует sedи trдолжно быть записано в сценарии, чтобы оно работало. Приведенный ниже код заменяет каждое третье вхождение слова, указанного в sedкоманде. Заменить i=3наi=n , чтобы сделать эту работу для любого n.

Код:

# replace new lines with '^' character to get everything onto a single line
tr '\n' '^' < input.txt > output.txt

# count number of occurrences of the word to be replaced
num=`grep -o "apple" "output.txt" | wc -l`

# in successive iterations, replace the i + (n-1)th occurrence
n=3
i=3
while [ $i -le $num ]
do
    sed -i '' "s/apple/lemon/${i}" 'output.txt'
    i=$(( i + (n-1) ))
done

# replace the '^' back to new line character
tr '^' '\n' < output.txt > tmp && mv tmp output.txt


Почему это работает:

Предположим, текстовый файл a b b b b a c a d a b b b a b e b z b s b a b.

  • Когда n = 2: мы хотим заменить каждое второе вхождение b.

    • a b b b b a c a d a b b b a b e b z b s b a b
      . . ^ . ^ . . . . . . ^ . . ^ . . . ^ . ^ . ^
    • Сначала мы заменяем 2-е вхождение, затем 3-е вхождение, затем 4-е, 5-е и т. Д. Посчитайте в последовательности, показанной выше, чтобы убедиться в этом.
  • Когда n = 3: мы хотим заменить каждое третье вхождение b.

    • a b b b b a c a d a b b b a b e b z b s b a b
      . . . ^ . . . . . . . ^ . . . . ^ . . . . . ^
    • Сначала мы заменяем 3-е вхождение, затем 5-е, затем 7-е, 9-е, 11-е и т. Д.
  • Когда n = 4: мы хотим заменить каждое третье вхождение b .

    • Сначала мы заменяем 4-е вхождение, затем 7-е, затем 10-е, 13-е и так далее.
agdhruv
источник