Я хочу заменить только первые k
экземпляры слова.
Как я могу это сделать?
Например. Скажем, файл foo.txt
содержит 100 вхождений слова «linux».
Мне нужно заменить только первые 50 случаев.
Я хочу заменить только первые k
экземпляры слова.
Как я могу это сделать?
Например. Скажем, файл foo.txt
содержит 100 вхождений слова «linux».
Мне нужно заменить только первые 50 случаев.
Ответы:
Первый раздел ниже описывает использование 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/\<old\>/\n/g4
Это расширение GNU для замены четвертого и всех последующих вхождений old
с \n
.
Расширенная функция регулярного выражения \<
используется, чтобы соответствовать началу слова и \>
совпадать с концом слова. Это гарантирует, что сопоставляются только полные слова. Расширенное регулярное выражение требует -E
опции sed
.
s/\<old\>/new/g
Только первые три вхождения old
остаются, и это заменяет их всех new
.
s/\n/old/g
Четвертый и все остальные вхождения old
были заменены \n
на первом шаге. Это возвращает их обратно в исходное состояние.
Если GNU sed недоступен и вы хотите изменить первые 3 вхождения old
на new
, тогда используйте три 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 seds не поддерживают объединение команд с точкой с запятой, каждая команда здесь представлена со своей -e
опцией. Также может быть необходимо убедиться, что вы sed
поддерживаете символы границы слова, \<
и \>
.
Мы можем сказать sed прочитать весь файл и затем выполнить замены. Например, чтобы заменить первые три случая old
использования sed в стиле BSD:
sed -E -e 'H;1h;$!d;x' -e 's/\<old\>/new/' -e 's/\<old\>/new/' -e 's/\<old\>/new/'
Команды sed H;1h;$!d;x
читают весь файл в.
Поскольку вышеупомянутое не использует никакого расширения GNU, оно должно работать на sed BSD (OSX). Обратите внимание, думал, что этот подход требует, 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
в строку bash, это обычно безопасное предположение.
tr '\n' '|' < input_file | sed …
. Но, конечно, это преобразует весь ввод в одну строку, и некоторые не-GNU seds не могут обрабатывать произвольно длинные строки. (2) Вы говорите: «… выше, строка в кавычках '|'
должна быть заменена любым символом или строкой символов…» Но вы не можете использовать tr
для замены символа строкой (длиной> 1). (3) В последнем примере вы говорите -e 's/\<old\>/new/' -e 's/\<old\>/w/' | tr '\000' '\n'\>/new
. Кажется, это опечатка для -e 's/\<old\>/new/' -e 's/\<old\>/new/' -e 's/\<old\>/new/' | tr '\000' '\n'
.
Команды awk могут использоваться для замены первых N вхождений слова на замену.
Команды будут заменены, только если слово полностью соответствует.
В приведенных ниже примерах, я вместо первых 27
вхождений old
сnew
Используя суб
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)
, она просто меняет значение поля сold
наnew
.
Выполнение проверки перед
awk '/old/&&x<27{for(i=1;i<=NF;i++)if(x<27&&$i=="old"&&$i="new")x++}1' file
Проверка того, что строка содержит старые данные и счетчик ниже 27,
SHOULD
обеспечивает небольшое повышение скорости, поскольку они не будут обрабатывать строки, если они ложные.
ПОЛУЧЕННЫЕ РЕЗУЛЬТАТЫ
Например
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
в
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
Скажем, вы хотите заменить только первые три экземпляра строки ...
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" ...
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"
И ж / 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
Краткая альтернатива в Perl:
perl -pe 'BEGIN{$n=3} 1 while s/old/new/ && ++$i < $n' your_file
Измените значение `$ n $ по своему вкусу.
Как это работает:
new
на old
( s/old/new/
) и всякий раз , когда это возможно, это увеличивает переменную $i
( ++$i
).1 while ...
) до тех пор, пока он произвел меньше, чем $n
подстановок, и может сделать хотя бы одну замену в этой строке.Используйте петлю оболочки и ex
!
{ for i in {1..50}; do printf %s\\n '0/old/s//new/'; done; echo x;} | ex file.txt
Да, это немного глупо.
;)
Примечание. Может произойти сбой, если old
в файле содержится менее 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
Простое, но не очень быстрое решение - это циклическое переключение команд, описанных в /programming/148451/how-to-use-sed-to-replace-only-the-first-occurrence-in-a -файл
for i in $(seq 50) ; do sed -i -e "0,/oldword/s//newword/" file.txt ; done
Эта конкретная команда sed, вероятно, работает только для GNU sed, и если newword не является частью oldword . Для не-GNU sed смотрите здесь, как заменить только первый шаблон в файле.
С 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