Почему последовательности управляющих символов ASCII для ‘ обрабатываются по-разному в grep/sed/awk?

Вопрос или проблема

Используя версию GNU всех 3 инструментов, если я хочу найти ' во вводе с помощью awk с использованием скрипта, разделенного на ', я могу использовать шестнадцатеричные или восьмеричные ESC-последовательности ASCII:

$ echo "'" | awk '/\x27/'
'

$ echo "'" | awk '/\047/'
'

Теперь давайте попробуем то же самое с sed (с -E или без него):

$ echo "'" | sed -n '/\x27/p'
'

$ echo "'" | sed -n '/\047/p'
$

и grep (также с -E или без него):

$ echo "'" | grep '\x27'
grep: warning: stray \ before x

$ echo "'" | grep '\047'
grep: warning: stray \ before 0

Итак:

  1. Самое важное: почему они разные?
  2. Второстепенное любопытство: Есть ли способ использовать ESC-последовательность в grep, чтобы совпасть с ', не прибегая к неподъемной опции GNU grep -P и не расширяя ESC-последовательность до того, как grep увидит её, используя конструкции оболочки, такие как grep $'\047'?

Особенно раздражает, что восьмеричная \047 является рекомендованной ESC-последовательностью в awk (см. http://awk.freeshell.org/PrintASingleQuote, или https://web.archive.org/web/20230530010453/http://awk.freeshell.org/PrintASingleQuote, если это недоступно) но такая же ESC-последовательность, похоже, вообще не работает в sed, только шестнадцатеричная \x27.

В целях этого вопроса меня не интересуют альтернативы, позволяющие использовать литералы ' или другие инструменты, я просто пытаюсь выяснить, почему эти 3 конкретных инструмента для сопоставления regexp обрабатывают ESC-последовательности ASCII по-разному. Тем не менее, мне было бы интересно узнать, как BSD или другие варианты этих 3 инструментов ведут себя с теми же скриптами, показанными выше.

Правка:

Вот поведение FreeBSD 13.1:

% echo "'" | awk '/\x27/'
'
% echo "'" | awk '/\047/'
'
% echo "'" | sed -n '/\x27/p'
'
% echo "'" | sed -n '/\047/p'
% echo "'" | grep '\x27'
grep: trailing backslash (\)
% echo "'" | grep '\047'
% 

\1, \2, \3, \4… используются для обратных ссылок в базовых регулярных выражениях (ed, grep, sed…).

Расширенные регулярные выражения были впервые введены с egrep в конце 70-х с новым алгоритмом регулярных выражений, который не имел (и не мог иметь с тем алгоритмом) поддержку обратных ссылок.

awk использовал ERE с самого начала, имел строки с литералами, похожими на C, внутри которых можно было использовать ESC-последовательности \47 (как в C), и ничто не мешало добавлять эти ESC-последовательности также в строки /ERE/, так как ERE не могли иметь обратные ссылки.

За пределами awk, ни POSIX BRE, ни ERE не поддерживают эти ESC-последовательности. Только \n указано для sed (так как это исторически поддерживалось оригинальным sed).

\47 в качестве ESC-последовательности для байта 0x27 определенно не может быть добавлен в BRE. Поскольку многие реализации ERE добавили поддержку обратных ссылок с конца 70-х, добавление его в ERE также больше не является вариантом. Это досадно, что большинство awk не поддерживают обратные ссылки, и в тех, которые поддерживают, таких как busybox’s, вам нужно делать awk '$0 ~ "^(.*)\\1$"' для эквивалента grep -x '\(.*\)\1' (не awk '/^(.*)\1$/', так как это \1 является ^A, а awk '/^(.*)\\1$/' используется для сопоставления с чем-то, что заканчивается на \1).

Обратите внимание, что синтаксис в каждом инструменте, кроме echo, для этих восьмеричных последовательностей (возможно, изначально из C) – это \ за которым следуют 1 до 3 восьмеричных чисел, нет требований к начальной 0, и вы не можете иметь ведущую 0 для номеров байтов выше 63 (077). (\0377 в чем угодно, кроме echo, это \037 (^_) за которым следует 7), так что, хотя \047 не конфликтует с обратными ссылками, так как \0 не является действительной обратной ссылкой (по крайней мере, в POSIX BRE, есть некоторые, где \0 означает все совпадение), \377 будет конфликтовать.

Использование \xHH из perl (в его строках и литералах regexp) не имеет этой проблемы конфликта с обратными ссылками, но пока не поддерживаются всеми механизмами regexp. Некоторые поддерживают \x{HH}, который может быть расширен до \x{20AC} в режиме Unicode, но будьте осторожны, что, например, в ksh93, $'\x20AC' не является байтом 0x20 (пробел), за которым следует AC, а символом U+20AC, закодированным в UTF-8.

Операторы кавычек оболочки $'\47' и $'\x27' из ksh93 теперь в POSIX sh с 2024 года (хотя пока не в dash), но, как и "\47" (или $'\x27'¹), они расширяются до байта 0x27, так что только до ' на системах, использующих ASCII в качестве своей базовой кодировки, поэтому я воздерживаюсь от его использования, так как не вижу смысла вводить зависимость от конкретной кодировки символов.

Оператор $'\u0027' (или $'\u27' или $'\U00000027') из zsh, который действительно расширяется до символа ', почти был добавлен в POSIX 2024, но поскольку между оболочками возникли некоторые разногласия и различия в том, какую кодировку символов они должны расширять (UTF-8 безусловно в стиле ksh93, кодировка символов локали в момент считывания кода в стиле bash, кодировка символов локали в момент выполнения в стиле zsh) и что делать, если в кодировке символов нет соответствующего символа, его включение было отложено до следующей основной версии.

В любом случае, для ' в частности, нет необходимости указывать код точки, так как $'\'' работает.

Мой собственный предпочтительный способ вкладывания ' внутри аргумента кода с одинарными кавычками sed, awk, perl, sh, или любого аргумента с одинарными кавычками к любой команде – это вставить его как \' снаружи одинарных кавычек:

sh -c 'echo "It'\''s not pretty, but it works"'
awk 'BEGIN{print "It'\''s not pretty, but it works"}'

С awk вы можете сделать:

awk -v q="'" 'BEGIN{print "It" q "s not really prettier"}'

В rc или zsh -o rcquotes

awk 'BEGIN{print "It''s slightly prettier"}'

В fish вы можете сделать:

awk 'BEGIN{print "It\'s slightly prettier"}'

Но это означает, что '...' там не являются полностью сильными кавычками, поэтому, как и для $'...' в ksh93, вам нужно обратить внимание на \, что неудобно.

Еще один вариант – это сделать:

awk_code=$(cat << 'EOF'
BEGIN {print "It's much more legible albeit a bit longer"}
EOF
)
awk "$awk_code"

¹ однако будьте осторожны, что необходимо убедиться, что то, что следует за этим, не является более шестнадцатеричными цифрами, так как поведение ksh93 там $'\x20ac' расширяется до , как упоминалось выше, допускается, но не требует POSIX, поэтому если вам нужен байт 0x27, за которым следует AC, вам нужно $'\x27'$'AC' например (или используйте восьмеричную форму, которая не имеет этой проблемы; ksh93 поддерживает $'\x{27}AC', но это ещё не указано POSIX и, как уже упоминалось, редко встречается.

Ответ или решение

Объяснение различий в обработке ASCII escape последовательностей в grep, sed и awk

Когда речь заходит о работе с ASCII escape последовательностями, особенно в контексте символа одинарной кавычки ('), три инструмента — grep, sed и awk — демонстрируют различное поведение. Разберем, почему так происходит, а также ответим на некоторые сопутствующие вопросы.

1. Различия в обработке ASCII escape последовательностей

Стандартизация и контекст использования:

  • awk: Использует ERE (расширенные регулярные выражения) с поддержкой C-подобных строковых литералов. Это позволяет применять как шестнадцатеричные (\xHH), так и восьмеричные (\0HH) escape последовательности. Когда вы используете awk, \x27 и \047 успешно распознаются как символ '. Стандарт POSIX позволяет такую функциональность.

  • sed: Предоставляет более ограниченные возможности. Несмотря на то что некоторые версии sed позволяют использовать \xHH для шестнадцатеричных кодов, большинство реализаций по умолчанию ассоциируют это с восьмеричными значениями, и только \xHH допускается в некоторых современный реализациях. Результаты тестов показывают, что восьмеричное представление (\047) не распознается в некоторых версиях, таких как GNU sed, что является серьезным ограничением.

  • grep: Механизм работы с grep также определяется POSIX стандартами, которые не поддерживают ASCII escape последовательности для символов, кроме \n. Использование -P для Perl-совместимых регулярных выражений дает возможность использовать \xHH, но это не переносимо. При прямой попытке использовать \x27 или \047 в grep, мы получим ошибку о "страй" символах.

2. Как использовать escape последовательности в grep без расширения до исполнения

К сожалению, в стандартной версии grep закреплены ограничения на использование escape последовательностей. Один из способов обойти данную проблему — использовать различные подходы к экранированию или выбору подхода через оболочку, чтобы избежать необходимости в ASCII escape последовательностях. Например:

echo "'" | grep -F "'"

или

echo "'" | grep "'"

Используя -F (фиксированный поиск), вы можете проверить наличие символа ' без необходимости использовать escape последовательности.

Заключение

Разница в обработке ASCII escape последовательностей в awk, sed и grep обусловлена историческими факторами, спецификациями POSIX и различиями в механизмах регулярных выражений, которые используют эти инструменты. Чтобы добиться одинакового поведения, рекомендуется использовать подходы, соответствующие каждому конкретному инструменту, и избегать полагания на escape последовательности там, где это не поддерживается.

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

Оцените материал
Добавить комментарий

Капча загружается...