Sed — Замените первые k экземпляры слова в файле

Можно попробовать следующие Дистрибутивы Linux

Существуют намного больше, и у большинства из них есть поддержка IPv6. Необходимо будет проверить их индивидуально.

Также возражайте смотреть эта Страница Википедии.

24
16.09.2014, 12:13
7 ответов

В первом разделе описано использование sed для изменения первых k-повторов на линии. Вторая секция расширяет этот подход, изменяя только первые k-повторжения в файле, независимо от того, на какой строке они появляются.

Линейно-ориентированное решение

Со стандартным sed существует команда для замены k-го появления слова на строке. Если k равно 3, например:

sed 's/old/new/3'

Или можно заменить все вхождения на:

sed 's/old/new/g'

Ни то, ни другое.

GNU sed предлагает расширение, которое изменит k-е вхождение и все последующее. Если k равно 3, например:

sed 's/old/new/g3'

Они могут быть объединены для того, чтобы делать то, что вы хотите. Измените первые 3 вхождения:

$ echo old old old old old | sed -E 's/\<old\>/\n/g4; s/\<old\>/new/g; s/\n/old/g'
new new new old old

где \n здесь полезно, потому что мы можем быть уверены, что это никогда не произойдет на строке.

Объяснение:

Мы используем три команды замены sed:

  • s/\/\n/g4

    Это расширение GNU для замены четвертого и всех последующих вхождений old на \n.

    Расширенная функция регекса \< используется для совмещения начала слова и \> для совмещения конца слова. Это обеспечивает соответствие только полным словам. Расширенный регекс требует, чтобы опция -E использовалась для sed.

  • s/\/new/g

    Остались только первые три вхождения -E старого, и это заменяет их все на -New.

  • s/\n/old/g

    На первом этапе четвертое и все остальные вхождения старого были заменены на \n. Это возвращает их в исходное состояние.

Решение не-GNU

Если GNU sed недоступно и вы хотите изменить первые 3 вхождения старого на новое, то используйте три команды s:

$ echo old old old old old | sed -E -e 's/\<old\>/new/' -e 's/\<old\>/new/' -e 's/\<old\>/new/'
new new new old old

Это хорошо работает, когда k небольшое число, но плохо масштабируется до большого k.

Поскольку некоторые не-GNU седаны не поддерживают объединения команд с точкой с запятой, каждая команда здесь вводится со своей собственной -e опцией. Также может потребоваться проверить, что ваш sed поддерживает символы границ слов, \< и \>.

Файло-ориентированное решение

Мы можем сказать sed прочитать весь файл и затем выполнить замены. Например, чтобы заменить первые три вхождения old с помощью команды sed:

sed -E -e 'H;1h;$!d;x' -e 's/\<old\>/new/' -e 's/\<old\>/new/' -e 's/\<old\>/new/'

Команды sed H;1h;$!d;x прочитать весь файл в формате BSD.

Поскольку вышеприведенное не использует никаких расширений GNU, оно должно работать на BSD (OSX) sed. Обратите внимание, подумайте, что такой подход требует sed, который может работать с длинными строками. GNU sed должно быть в порядке. Те, кто пользуется не GNU-версией sed, должны проверить его способность работать с длинными линиями.

С помощью GNU sed мы можем далее использовать g трюк, описанный выше, но с \n, заменив его на \x00, чтобы заменить первые три случая:

sed -E -e 'H;1h;$!d;x; s/\<old\>/\x00/g4; s/\<old\>/new/g; s/\x00/old/g'

Этот подход масштабируется так же хорошо, как k становится большим. Однако это предполагает, что \x00 не находится в исходной строке. Так как невозможно поместить символ \x00 в бэш-строку, это обычно безопасное предположение.

32
27.01.2020, 19:40

С помощью Awk

Команды awk могут быть использованы для замены первых N вхождений слова на замену.
Команды будут заменяться только в том случае, если слово полностью совпадает.

В примерах ниже я заменяю первые 27 появления old на new

Используя sub

awk '{for(i=1;i<=NF;i++){if(x<27&&$i=="old"){x++;sub("old","new",$i)}}}1' file

Эта команда проходит через каждое поле до тех пор, пока оно не совпадет с old, она проверяет, что счетчик находится ниже 27, приращения и заменяет первое совпадение на строке. Затем переходит к следующему полю/строке и повторяет.

Замена поля вручную

awk '{for(i=1;i<=NF;i++)if(x<27&&$i=="old"&&$i="new")x++}1' file

Аналогично команде до этого, но так как в нем уже есть маркер, на котором поле находится до ($i), он просто меняет значение поля с старого на новое.

Проверка перед

awk '/old/&&x<27{for(i=1;i<=NF;i++)if(x<27&&$i=="old"&&$i="new")x++}1' file

Проверка того, что строка содержит старое и счетчик ниже 27 SHOULD обеспечивает небольшой прирост скорости, так как он не будет обрабатывать строки, если они ложные.

RESULTS

E.g.

old bold old old old
old old nold old old
old old old gold old
old gold gold old old
old old old man old old
old old old old dog old
old old old old say old
old old old old blah old

to

new bold new new new
new new nold new new
new new new gold new
new gold gold new new
new new new man new new
new new new new dog new
new new old old say old
old old old old blah old
9
27.01.2020, 19:40

С помощью GNU awk вы можете установить разделитель записей RS на заменяемое слово , разделенное границами слов. Тогда это случай установки разделителя записей на выходе на слово замены для первых k записей с сохранением исходного разделителя записей для остатка

awk -vRS='\\ylinux\\y' -vreplacement=unix -vlimit=50 \
'{printf "%s%s", $0, NR <= limit? replacement: RT}' file

ИЛИ

awk -vRS='\\ylinux\\y' -vreplacement=unix -vlimit=50 \
'{printf "%s%s", $0, limit--? replacement: RT}' file
2
27.01.2020, 19:40

Простое, но не очень быстрое решение - это перехват команд, описанных в https://stackoverflow.com/questions/148451/how-to-use-sed-to-replace-only-the-first-occurrence-in-a-file

for i in $(seq 50) ; do sed -i -e "0,/oldword/s//newword/"  file.txt  ; done

Эта конкретная команда sed, вероятно, работает только для GNU sed и если новое слово не является частью старого слова . Для не-GNU sed смотрите здесь как заменить только первый шаблон в файле.

3
27.01.2020, 19:40

Краткая альтернатива на Perl:

perl -pe 'BEGIN{$n=3} 1 while s/old/new/ && ++$i < $n' your_file

Измените значение `$ n $ по своему вкусу.

Как это работает:

  • Для каждой строки он пытается заменить новый на старый ( s / old / new / ) и всякий раз, когда может, он увеличивает переменную $ i ( ++ $ i ).
  • Он продолжает работать в строке ( 1, пока ... ) до тех пор, пока он сделал менее $ n замен в общей сложности и может произвести хотя бы одну замену на эта линия.
4
27.01.2020, 19:40

Скажем, вы хотите заменить только первые три экземпляра строки...

seq 11 100 311 | 
sed -e 's/1/\
&/g'              \ #s/match string/\nmatch string/globally 
-e :t             \ #define label t
-e '/\n/{ x'      \ #newlines must match - exchange hold and pattern spaces
-e '/.\{3\}/!{'   \ #if not 3 characters in hold space do
-e     's/$/./'   \ #add a new char to hold space
-e      x         \ #exchange hold/pattern spaces again
-e     's/\n1/2/' \ #replace first occurring '\n1' string w/ '2' string
-e     'b t'      \ #branch back to label t
-e '};x'          \ #end match function; exchange hold/pattern spaces
-e '};s/\n//g'      #end match function; remove all newline characters

Примечание: вышеописанное, скорее всего, не будет работать со встроенными комментариями
... или, в моем примере, с "1"...

OUTPUT:

22
211
211
311

Там я использую две примечательные техники. Во-первых, каждое появление 1 на строке заменяется на \n1. Таким образом, так как я делаю рекурсивные замены далее, я могу быть уверен, что не буду заменять вхождение дважды , если в строке моей замены будет содержаться моя строка замены. Например, если я заменю he на hey, это все равно сработает.

Я делаю это так:

s/1/\
&/g

Во-вторых, я считаю замены, добавляя символ в hстарую строку для каждого вхождения. Как только я достигаю трёх больше не происходит. Если вы примените это к своим данным и измените \{3\} на желаемые вами общие замены, а /\n1/ адреса на все, что вы хотите заменить, вы должны заменять только столько, сколько захотите.

Я сделал все -e только для удобочитаемости. POSIXly Можно было бы написать так:

nl='
'; sed "s/1/\\$nl&/g;:t${nl}/\n/{x;/.\{3\}/!{${nl}s/$/./;x;s/\n1/2/;bt$nl};x$nl};s/\n//g"

И w/ GNU sed:

sed 's/1/\n&/g;:t;/\n/{x;/.\{3\}/!{s/$/./;x;s/\n1/2/;bt};x};s/\n//g'

Помните также, что sed ориентирован на чтение строк - он читает не во всем файле, а затем пытается зациклиться на нем, как это часто бывает в других редакторах. sed прост и эффективен. При этом часто удобно делать что-то вроде:

Вот небольшая функция оболочки, которая объединяет ее в просто выполняемую команду:

firstn() { sed "s/$2/\
&/g;:t 
    /\n/{x
        /.\{$(($1))"',\}/!{
            s/$/./; x; s/\n'"$2/$3"'/
            b t
        };x
};s/\n//g'; }

Так что с помощью этого я могу сделать:

seq 11 100 311 | firstn 7 1 5

... и получить....

55
555
255
311

... или...

seq 10 1 25 | firstn 6 '\(.\)\([1-5]\)' '\15\2'

... чтобы...

10
151
152
153
154
155
16
17
18
19
20
251
22
23
24
25

... или, в соответствии с вашим примером (на меньший порядок):

yes linux | head -n 10 | firstn 5 linux 'linux is an os kernel'
linux is an os kernel
linux is an os kernel
linux is an os kernel
linux is an os kernel
linux is an os kernel
linux
linux
linux
linux
linux
7
27.01.2020, 19:40

Используйте цикл оболочки и ex !

{ for i in {1..50}; do printf %s\\n '0/old/s//new/'; done; echo x;} | ex file.txt

Да, это немного глупо.

;)

Примечание. Это может привести к сбою, если в файле менее 50 экземпляров старого . (Я не тестировал его.) Если это так, он оставит файл без изменений.


А еще лучше использовать Vim.

vim file.txt
qqgg/old<CR>:s/old/new/<CR>q49@q
:x

Объяснение:

q                                # Start recording macro
 q                               # Into register q
  gg                             # Go to start of file
    /old<CR>                     # Go to first instance of 'old'
            :s/old/new/<CR>      # Change it to 'new'
                           q     # Stop recording
                            49@q # Replay macro 49 times

:x  # Save and exit
4
27.01.2020, 19:40

Теги

Похожие вопросы