Как удалить строку, если она содержит символ ровно один раз

10

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

Например:

DTHGTY
FGTHDC
HYTRHD
HTCCYD
JUTDYC

Здесь символ, который я хочу удалить C, таков: команда должна удалять строки FGTHDCи JUTDYCпотому, что они есть Cровно один раз.

Как я могу сделать это, используя sedили awk?

Namz
источник

Ответы:

20

В awkвы можете установить разделитель поля ни к чему. Если вы установите его C, то у вас будет столько полей +1, сколько вхождений C.

Так что, если вы говорите, awk -F'C' '{print NF}' <<< "C1C2C3"вы получаете 4: CCCсостоит из 3 Cс, и, следовательно, 4 поля.

Вы хотите удалить строки, в которых Cвстречается ровно один раз. Принимая это во внимание, в вашем случае вы захотите удалить те строки, в которых есть ровно два Cполя. Так что просто пропустите их:

$ awk -F'C' 'NF!=2' file
DTHGTY
HYTRHD
HTCCYD
fedorqui
источник
4
Проницательное использование awkразделителя полей!
Валентин Б.
интересный, как в случае по умолчанию (FS = ""), он игнорирует начальные пробелы ($ 1 = первый не пробел в строке), а также повторы (вы можете иметь 5 пробелов для разделения поля 1 и поля 2) ... пробел наверное лечится специально? (чтобы увидеть это, можно сделать awk 'BEGIN { print "FS={" FS"}","OFS={" OFS "}";} {printf "%d fields : ",NF; for (i=1;i<=NF;i++) {printf "{" $i "} ";}; print "" }'и накормить несколько строк, некоторые из которых имеют несколько spces, а другие начинаются с пробела (ов))
Оливье Дюлак
2
@OlivierDulac, да, пространство обрабатывается специально, как указано в POSIX .
Wildcard
8

Сед подход:

sed -i '/^[^C]*C[^C]*$/d' input

-i опция позволяет модифицировать файл на месте

/^[^C]*C[^C]*$/- сопоставляет строки, содержащие Cтолько один раз

d - удалить совпавшие строки

RomanPerekhrest
источник
8

Это можно сделать с помощью sed:

Код:

sed '/C.*C/p;/C/d' file1

Полученные результаты:

DTHGTY
HYTRHD
HTCCYD

Как?

  1. Матч и напечатать любую линию , по крайней мере в двух экземплярах Cчерез/C.*C/p
  2. Удалите любую строку с помощью Cvia /C/d, включая строки, уже напечатанные на шаге 1
  3. По умолчанию выведите остальные строки
Стивен Раух
источник
2
Умный альтернативный подход; Мне это нравится.
Wildcard
6

Это удаляет строки только с одним вхождением C.

grep -v '^[^C]*C[^C]*$' file

Регулярное выражение [^C]соответствует одному символу, который не является символом C (или новой строкой), а оператор повторения (он же звезда Клини) *задает ноль или более повторений предыдущего выражения.

Вывод по умолчанию grep(и большинства других текстовых инструментов) - стандартный вывод; перенаправить на новый файл и, возможно, переместить его поверх исходного файла, если вы этого хотите. То же самое регулярное выражение можно использовать sed -iдля редактирования на месте:

sed -i '/^[^C]*C[^C]*$/d' file

(На некоторых платформах, особенно * BSD, включая macOS, -iопция требует аргумент, например -i ''.)

tripleee
источник
1
sed -i '/^[^C]*C[^C]*$/d' file- Похоже, это было опубликовано ранее, как вы думаете, плагиат?
РоманПерехрест
1
Действительно, есть некоторое дублирование. Я начал с grepответа, но он, очевидно, легко распространяется на sed -iвариант. Не видел ваш ответ, потому что я искал предыдущие grepответы.
tripleee
1
Это безопаснее , просто явно избежать -iс sedи вместо того, чтобы перенаправить в новый файл и заменить оригинал с тем , если sedутилита вышла без ошибок.
Кусалананда
2
Илиgrep -vx '[^C]*C[^C]*'
Стефан
@Kusalananda Но тогда вы могли бы также использовать, grepпотому что это яснее и надежнее (в частности, sedимеет менее информативный код выхода).
tripleee
4

Инструмент POSIX для редактирования файла по сценарию (вместо печати измененного содержимого в стандартный формат) ex.

printf '%s\n' 'g/^[^C]*C[^C]*$/d' x | ex file.txt

Конечно, вы можете использовать,sed -i если ваша версия Sed поддерживает это, просто имейте в виду, что это не переносимо, если вы пишете сценарий, предназначенный для запуска в разных типах систем.


Дэвид Фёрстер спросил в комментариях:

Есть ли причина, почему вы используете, printfа не echoили что-то подобное ex -c COMMAND?

Ответ: да.

Для printfпротив echoэто вопрос переносимости; Посмотрите, почему printf лучше, чем echo? И также легче перемежать переводы строк между командами, использующими printf.

Для printf ... | exпротив ex -c ..., это вопрос обработки ошибок. Для этой конкретной команды это не имеет значения, но в целом это имеет значение; например, попробуйте положить

ex -c '%s/this pattern is not in the file/replacement text/g | x' filename

в сценарии. Сравните это со следующим:

printf '%s\n' '%s/no matching lines/replacement/g' x | ex file

Первый будет зависать и ждать ввода; второй завершится, когда команда получит EOF ex, поэтому сценарий продолжится. Существуют альтернативные обходные пути, например s///e, но они не указаны в POSIX. Я предпочитаю использовать переносную форму, которая показана выше.

Для gкоманды в конце должен быть символ новой строки, и я предпочитаю использовать printfдля переноса команд, а не вставлять новую строку в одинарные кавычки.

Wildcard
источник
1
Есть ли причина, почему вы используете, printfа не echoили что-то подобное ex -c COMMAND?
Дэвид Фёрстер
@DavidFoerster, да. Я начал отвечать вам в комментариях, но он стал длинным, поэтому я добавил его к ответу.
Wildcard
Спасибо и +1! Я знал о printfпротив echo(хотя я обычно предпочитаю, echoкогда аргумент жестко запрограммирован), но я до сих пор не использовал exшироко.
Дэвид Фёрстер
2

Вот несколько вариантов использования Perl.

Поскольку вы сопоставляете только один символ, вы можете использовать tr/C//(перевод, без замен), чтобы вернуть количество совпадений C:

perl -lne 'print if tr/C// != 1' file

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

perl -lne 'print if (@m = /C/g) != 1' file

Это назначает совпадения регулярного выражения /C/gсписку @mи печатает строки, когда длина этого списка не равна 1.

-iПереключатель может быть добавлена возможность редактирования «на месте».

Том Фенек
источник
2
sed -e '
  s/C/&/2;t   # when 2nd C matches skip processing and print
  /C/d        # either one C or no C, so delete on C
'

sed -e '
   /C/!b     # no C, skip processing and print
   /C.*C/!d  # not(at least 2 C) => 1 C => delete
'

perl -lne 's/C/C/g == 1 or print'

источник
Обратите внимание, что предполагается sed, t #...что GNU , как правило , будет переходить к метке, вызываемой #...в большинстве других sedреализаций.
Стефан
Даже !bGNU sed, так как ветке не нравится ничего, кроме метки или новой строки после него.
Да, b, t, :, }r file, w file...) не может иметь команду после них на одной и той же линии. Вы также можете использовать отдельные -eпараметры.
Стефан
Ваша опция perl не дает правильного вывода. Я полагаю, вы забыли добавить gмодификатор.
Том Фенек,
@ TomFenech Вы правы. Я исправляю это. Спасибо.
1

Для тех, кто хочет awkконкретно, я бы предложил

awk '/C[^C]*C/{next}//{print}'

пропустите строку, если она соответствует шаблону, выведите ее в противном случае. Вам на самом деле не нужно {print}, вы можете использовать //и печать по умолчанию, но я думаю, что это более четко прописано.

Моей первой мыслью было использовать egrep -vтот же шаблон, но на самом деле это не отвечает на поставленный вопрос.

nigel222
источник
1
Какой смысл сопоставлять что-либо после {next}? Просто скажите, awk '/pattern/ {next} 1'и все строки, не соответствующие шаблону, будут напечатаны. Или лучше awk '!/pattern/'распечатать их напрямую.
Федорки
@Fedorqui хорошая идея !/pattern/(что почему-то ускользнуло от меня), но я бы предпочел увидеть объяснения, а //{print}не загадку 1. Предполагайте наименьшую компетентность и беглость от следующего человека, чтобы поддерживать ваш код, в соответствии с тем, чтобы не сделать его серьезно менее эффективным или действенным.
nigel222