Вопрос или проблема
Используя версию 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
Итак:
- Самое важное: почему они разные?
- Второстепенное любопытство: Есть ли способ использовать 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-подобных системах и избежать распространенных ошибок при поиске и анализе текстовой информации.