LINUX.ORG.RU

sed захлебывается от grep

 , ,


0

1

Всем доброго времени суток!

Имеется директория, содержащая несколько поддиректорий, некоторые из которых тоже содержат поддиректории. Все это хозяйство набито текстовыми файлами, общим числом где-то под тысячу. Требуется инструмент для контроля вхождений некоторого шаблона в эти файлы. Проблема в том, что отдельные вхождения могут быть закомментированы по правилам Си, т.е. возможны однострочные (//…), и многострочные (/*** … ***/) комменты, которые надо игнорировать.

Выходной формат этого инструмента должен включать путь до файла, в котором найдено вхождение шаблона, номер строки с ним и собственно строку. Подробно поясняю чтобы было понятнее, для чего весь огород.

Итак, инструмент:

grep -IHrn . $SRC | sed '/\/\*.*\*\// d; /\/\*/,/\*\// d; s://.*$::' | grep -w $SHAB

где $SRC - источник, $SHAB - искомый шаблон (текстовая строка без пробелов).

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

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

Кто-нибудь может объяснить что происходит и как с этим бороться без лишней заморочки с разделением на подкаталоги? Я предполагаю что sed не успевает обрабатывать вывод grep-а, захлебывается и в канале теряются буфера.

какой shell? В другом такое же поведение? Выглядит ак будто что-то грохает второй пайп после окончании первого grep

GPFault ★★★
()

коль sed захлебывается, а у него тож лимиты имеются, то использовать (вообще вместо bash) perl это монстр по регексп обработке текста или использовать python.
костыль2 использовать xargs для построчного скармливания мощного выхлопа grep в sed

pfg ★★★★★
()

У нас тут у всех очевидная проблема: не можем воспроизвести.

Может, поделишься данными, которые ломают пайплайн?

Я создал 10,000,000 уникальных строк (seq 10000000) и через этот пайплайн могу найти любую строку.

Но у меня не GNU coreutils, а uutils-coreutils. Возможно, есть какой-то баг в GNU.

kaldeon ★★
()
Последнее исправление: kaldeon (всего исправлений: 2)

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

mky ★★★★★
()

Кстати, да, sed не сможет корректно обработать данный вход:

/*
	foo
*/ /* */

bar

/*
	baz
*/

qux

/*
	quux
*/

На выходе будет просто «qux». Почему не знаю, я до сих пор не осилил модель работы sed.

В sam этой проблемы нет, данная команда корректно убирает все комментарии (на уровне простого текста, а не синтаксиса Си):

ssam 'x!/\*(([^*]|\n)|\*([^/]|\n))*\*/! d'

Модель leftmost-longest и переводы строк, к сожалению, усложняют выражение, но зато это единое целостное выражение и оно работает.

kaldeon ★★
()
Последнее исправление: kaldeon (всего исправлений: 4)

Кто-нибудь может объяснить что происходит

Ты взял очень сложный и при этом неправильный инструмент для парсинга синтаксиса - регулярные выражения

Попробуй удалить комменты через clang/gcc/что угодно только не sed

Gary ★★★★★
()

Смотри, регулярками нельзя корректно обрабатывать ЯП. Такие дела. Потому и извращаются с построением абстрактных синтаксических деревьев в любом более-менее нормальном парсере. Под нормальным понимается тот, которым можно пользоваться и не бояться что он какую-то дичь сделает. У тебя варианта только 2: первый - взять пкакой-нибудь ЯП (тот же Python) и написать полноценный парсер (задача очень сложная, если у тебя там реально сишный или крестовый код, по факту это треть простого компилятора надо реализовывать). Второй вариант - я его настоятельно советую - взять готовый компилятор и использовать его для чистки файлов, как - спрашивай у гугла.

anonymous
()

sed не может захлёбываться от grep, пайпы не так работают.

bash создаёт блокирующие пайпы. Если sed не успел выполниться, а пайп заполнен, то grep будет висеть на вызове write, пока в пайпе не появится место.

Проверь отдельно именно те файлы, которые твоя констуркция пропускает.

anarquista ★★★★★
()

Препроцессор си можно запускать отдельно, при этом он может оставлять отметки о правильных номерах строк даже если используются include или вырезаются многострочные комментарии, покури в эту сторону.

legolegs ★★★★★
()
Ответ на: комментарий от Gary

Ты взял очень сложный и при этом неправильный инструмент для парсинга синтаксиса - регулярные выражения

А какой правильный инструмент для парсинга синтаксиса?

Jullyfish
()
Ответ на: комментарий от kaldeon

Почему не знаю, я до сих пор не осилил модель работы sed.

Если я правильно понял, проблема в порядке действий:

$ grep . test01
/*
	foo
*/ /* */
bar
/*
	baz
*/
qux
/*
	quux
*/

Берём первую вложенную команду примера ТС:

$ grep . test01 | sed '/\/\*.*\*\// d;'
/*
	foo
bar
/*
	baz
*/
qux
/*
	quux
*/

И тут уже видно что не так. Если взять только вторую вложенную команду ТС, то всё ок:

$ grep . test01 | sed '/\/\*/,/\*\// d'
bar
qux

Я в синтаксис не углублялся, похоже d полностью удаляет строку, когда её удалять не надо, а просто убрать часть строки соответствующей паттерну.

Jullyfish
()

В одном из файлов не закрыт многострочный комментарий. Как следствие, последующие файлы становятся «закомментированными» и игнорируются.

KivApple ★★★★★
()

Вторую субкоманду тоже надо править, доработал тестовый пример @kaldeon (спасибо ему за тест):

$ cat test02
/*
	foo
*/ hello /* */
bar
/*
	baz
*/
qux
/*
	quux
*/
$ grep . test02 | sed 's/\/\*.*\*\///; /\/\*/,/\*\// d;'
bar
qux
Jullyfish
()

Вообще вот так выглядит правильно:

$ grep . test02 | sed -z 's/\/\*[^*/]*\]*\*\///g'
 hello 
bar

qux

Если вместо [^*/] сделать .* тогда удаляется всё от первого /* до последнего */.

Jullyfish
()
Последнее исправление: Jullyfish (всего исправлений: 1)

Попробуй убирать комменты так:

remove_c_comments.sh:

#!/bin/sh
cpp -E -undef -fpreprocessed -dDI | tail -n+2

(нужен гнутый препроцессор си cpp, не путать с g++)

legolegs ★★★★★
()
Ответ на: комментарий от Jullyfish

Комментарии вида '//' убирать отдельным sed? Так выглядит нормально, только ТС ничего не написал относительно вложенных комментариев, проверяется ли чем-то, что их нет.

Ну и, вроде, у старых sed с ″-z″ были проблемы с памятью на мегабайтных файлах.

mky ★★★★★
()
Ответ на: комментарий от Jullyfish

Вот тест-кейс, который ломает:

/*
	foo
/*/

bar

Есть специальная техника как это обрабатывается в leftmost-longest выражениях. Если нужен промежуток до первого символа, например от A до B, то всё просто: A[^B]*B. Если до двух символов, идущих подряд, например от A до BC, то нужно так: A([^B]|B[^C])*BC. Если до трёх символов, то A([^B]|B[^C]|BC[^D])*BCD. Это довольно неочевидная, но работающая математическая головоломка.

Соответственно,

sed -z 's!/\*\([^*]\|\*[^/]\)*\*/!!g'
kaldeon ★★
()
Последнее исправление: kaldeon (всего исправлений: 1)
Ответ на: комментарий от mky

Можно вот так:

sed 's!//\([^/]\|/[^/]\)*$!!'

Чтобы в первой строке не тронуть //hello:

foo = "http://hello"; // comment
bar = "http://still-not-optimal";

Хотя во второй строке //still-not-optimal всё равно будет удалён. Увы, без синтаксиса лучше уже никак.

kaldeon ★★
()
Последнее исправление: kaldeon (всего исправлений: 1)
Ответ на: комментарий от router

Да, но в конструкции от ТС grep, вроде как, отфильтрует бинарники. А вот sed получит на вход много строк, поэтому я за "-z" и переживаю :)

mky ★★★★★
()
Ответ на: комментарий от mky

Комментарии вида ‘//’ убирать отдельным sed?

Да, и, наверное, перед sed -z.

Ну и, вроде, у старых sed с ″-z″ были проблемы с памятью на мегабайтных файлах.

Это отдельно надо придумывать на чём проверять.

Jullyfish
()
Ответ на: комментарий от kaldeon

Вот тест-кейс, который ломает:

Хорошшш. :^)

leftmost-longest

Что это такое?..

Если до трёх символов

Круто, спасибо! Тут бы я уже не додумался до такого.

И то, что можно ! использовать – тоже на знал. С ними гораздо легче читается.

Jullyfish
()
Ответ на: комментарий от kaldeon

всё равно будет удалён.

Вот они, проделки Страуструпа, не мог какую-то уникальную последовательность для комментариев выбрать :) Создал, понимашь, синтаксис, из которого регэкспами комменты не убрать!

А, так, двойные кавычки и регулярку для многострочных комментов могут поломать, ведь не запрщенено:

printf("/////*****\n");

Хотя, ТС написал расплывчато: «могут быть закомментированы по правилам Си», но он не писал, что это корректный Си-код, и что там строковые литералы могут содержать символы комментариев.

mky ★★★★★
()
Ответ на: комментарий от Jullyfish

И то, что можно ! использовать – тоже на знал.

Вы про S-выражение? Дак ТС же двоеточие (:) использовал...

mky ★★★★★
()
Ответ на: комментарий от legolegs

Более законченный скрипт. Тегаю тс @tig1818

grep_no_comments.sh:

#!/bin/sh
USAGE="$0 <file> <pattern and other grep args...>"
if [ $# -lt 2 ]; then
    echo "$USAGE" >&2
    exit 1
fi
FILE=$1
shift
cpp -E -undef -fpreprocessed -dDI "$FILE" |
    tail -n+2 | # cut up the extra line from cpp
    grep -In "$@" |
    awk -vF="$FILE" '{print F ":" $0;}'

Использование:

SRC=.
PATTERN="t"
$ find "$SRC" -type f -not -name '*.sh' -exec ./grep_no_comments.sh {} "$PATTERN" \;
./test.txt:1:#include "testinc.txt"
./test.txt:3:printf("http://localhost/*");
./test05:17:$ grep . test05 | sed -z 's/\/\*[^*/]*\]*\*\///g'
./test07:1:foo = "http://hello";
./test07:2:bar = "http://still-not-optimal";
./test08:1:printf("/////*****\n");
./testinc.txt:1:this file is included

Всё пограничные случаи из топика оно обрабатывает правильно. Однако, хотя оно не трогает #директивы препроцессора, но всё равно пытается их понять, а понять шебанг #!/bin/sh оно не может и жалуется. Но всё равно работает.

legolegs ★★★★★
()
Ответ на: комментарий от Jullyfish

leftmost-longest – это POSIX, leftmost-first – это Perl.

Про разницу между leftmost-longest и leftmost-first: 1, 2, 3

Про то, что leftmost-first по своей сути запутаннее leftmost-longest: 4

kaldeon ★★
()
Ответ на: комментарий от Jullyfish

С ними гораздо легче читается.

Можно ещё добавить флаг -E. Тогда можно будет убрать излишние бэкслеши.

sed -Ez 's!/\*([^*]|\*[^/])*\*/!!g'
kaldeon ★★
()
Ответ на: комментарий от anonymous

Всё это правда. Но мантра «регулярные выражения не подходят для обработки языков программирования» может зайти очень далеко. Регулярными выражениями, по такой же логике, нельзя искать в естественном языке: поиск до точки может остановиться на разделителе числа, а не на конце предложения.

Теперь для всего писать парсеры? Суть регулярных выражений не в том, что они подходят под любую задачу, а в том, что они достаточно гибки для решения большинства задач без сложного туллинга, когда цена ошибки допустима.

kaldeon ★★
()
Последнее исправление: kaldeon (всего исправлений: 2)
Ответ на: комментарий от anonymous

Смотри, регулярками нельзя корректно обрабатывать ЯП.

Комментарии удалять можно, а больше ничего и не нужно. Только готовые регулярки удаляют информацию о номерах строк, поэтому нужно свой конечный автомат ваять.

ugoday ★★★★★
()

sed ‘//*.*// d; //*/,/*// d; s://.$::’

Попробуй заменить свой sed на perl -pe 's{/\*.*?\*/}{}g; s{//.*}{} if !s{/\*.*}{} .. s{.*\*/}{}'

annulen ★★★★★
()
Ответ на: комментарий от Jullyfish

А какой правильный инструмент для парсинга синтаксиса?

То, что может правильно отслеживать вложенность структур и контекст, я прямо в том же сообщении привёл пример - компиляторы уже это могут, если погуглить есть ещё 100500 готовых инструментов, в крайнем случае можно написать свой но это должен быть токенайзер + парсер грамматики а не регулярки.

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

Gary ★★★★★
()
Ответ на: комментарий от Gary

sed через пайп вообще вроде обрабатывает текст построчно

sed всегда обрабатывает текст построчно, но при этом у него всегда есть и pattern/hold space, и команды с диапазоном адресов и флаг -z.

mky ★★★★★
()
Ответ на: комментарий от Siborgium

Можно!

ИМХО, обработка подразумевает и обнаружение синтаксических ошибок с выдачей диагностических сообщений. А иначе такая обработка при некоррекном входном файле может закончится чувствительными ошибками...

mky ★★★★★
()

Всем спасибо!

Углубленный анализ выявил ряд файлов, содержащих абсолютно рандомный мусор. Откуда они взялись пока неясно, но это уже другая тема. Все ваши наработки и замечания будут обязательно учтены в дальнейшем.

Тему закрываем.

Удачи!

tig1818
() автор топика

Прошу прощения, поторопился закрыть тему. Всплыли некоторые моменты, могут заинтересовать.

  1. sed -z 's!/\*\([^*]\|\*[^/]\)*\*/!!g' не работает с
/*
 *
 * pattern
 *
 */
  1. sed 's://.*$::' не работает, если в строке после // есть символы кириллицы, в частности в cp1251. То есть строки типа

// pattern - русскоязычный текст в ansi не фильтруются. Походу, sed вообще не воспринимает коды больше 0x7F.

Есть соображения?

tig1818
() автор топика
Ответ на: комментарий от tig1818

Не запускать sed в utf-8 локали, чтобы он не рассматривал многобайтовые последовательности?

Гораздо веселее, если у вас там будет не cp1251, а utf-16/utf-32...

mky ★★★★★
()
Последнее исправление: mky (всего исправлений: 1)
Ответ на: комментарий от tig1818

Да ему без разницы, он использует библиотечные функции для работы со строками, и там принято, что "." — это любой правильный символ, некорректные UFT-8 последовательности не являются UTF-8 любым симовлом.

И всё не так просто, как бы вам хотелось, utf-8, доспустим, кодирует «NO-BREAK SPACE» двума байтами 0xC2 0xA0, если рассматривать utf-8 просто как поток байт, то этот неразрывный пробел станет символом перевода строки.

mky ★★★★★
()
Ответ на: комментарий от mky

Указал LC_ALL=C sed ..., все заработало. И первый пункт и второй.

Честно, даже не догадывался что работа sed-а (а, возможно, и не только его) зависит от текущей локали.

Спасибо!

tig1818
() автор топика
  • Markdown
Пустая строка (два раза Enter) начинает новый абзац. Знак '>' в начале абзаца выделяет абзац курсивом цитирования.
Внимание: прочитайте описание разметки Markdown.
Используйте Ctrl-Enter для размещения комментария