Возможно ли избежать метасимволов регулярных выражений с помощью sed?
Мне интересно, можно ли написать на 100% надежно sed
команда для экранирования любых метасимволов регулярных выражений во входной строке, чтобы их можно было использовать в последующей команде sed. Как это:
#!/bin/bash
# Trying to replace one regex by another in an input file with sed
search="/abc\n\t[a-z]\+\([^ ]\)\{2,3\}\3"
replace="/xyz\n\t[0-9]\+\([^ ]\)\{2,3\}\3"
# Sanitize input
search=$(sed 'script to escape' <<< "$search")
replace=$(sed 'script to escape' <<< "$replace")
# Use it in a sed command
sed "s/$search/$replace/" input
Я знаю, что есть лучшие инструменты для работы с фиксированными строками вместо шаблонов, например awk
, perl
или же python
, Я просто хотел бы доказать, возможно ли это с sed
, Я бы сказал, давайте сосредоточимся на базовых регулярных выражениях POSIX, чтобы получить еще больше удовольствия!:)
Я перепробовал много вещей, но в любое время я мог найти вход, который сломал мою попытку. Я думал держать это абстрактно как script to escape
не приведет никого в неправильном направлении.
Кстати, обсуждение возникло здесь. Я думал, что это может быть хорошим местом для сбора решений и, возможно, взломать и / или разработать их.
4 ответа
Замечания:
- Если вы ищете готовую функциональность, основанную на методах, обсужденных в этом ответе:
bash
Функции, обеспечивающие надежное экранирование даже в многострочных подстановках, можно найти внизу этого поста (плюсperl
решение, которое используетperl
встроенная поддержка для такого выхода).- @ EdMorton ответ содержит инструмент (
bash
скрипт), который надежно выполняет однострочные замены.
- Все фрагменты предполагают
bash
в качестве оболочки (возможны переформулировки в соответствии с POSIX):
SINGLE-line Решения
Экранирование строкового литерала для использования в качестве регулярного выражения в sed
:
Чтобы дать кредит там, где он нужен: я нашел регулярное выражение, использованное ниже в этом ответе.
Предполагая, что строка поиска является однострочной:
search='abc\n\t[a-z]\+\([^ ]\)\{2,3\}\3' # sample input containing metachars.
searchEscaped=$(sed 's/[^^]/[&]/g; s/\^/\\^/g' <<<"$search") # escape it.
sed -n "s/$searchEscaped/foo/p" <<<"$search" # if ok, echoes 'foo'
- Каждый персонаж кроме
^
помещается в свой собственный набор символов[...]
выражение, чтобы относиться к нему как к буквальному.- Обратите внимание, что
^
это один символ Вы не можете представлять как[^]
потому что это имеет особое значение в этом месте (отрицание).
- Обратите внимание, что
- Затем,
^
символы. сбежали как\^
,
Подход надежный, но не эффективный.
Надежность возникает из-за того, что вы не пытаетесь предугадать все специальные символы регулярных выражений, которые будут различаться в разных диалектах регулярных выражений, а сосредоточены только на двух функциях, общих для всех диалектов регулярных выражений:
- возможность указывать буквенные символы внутри набора символов.
- способность избежать буквального
^
как\^
Экранирование строкового литерала для использования в качестве строки замены в sed
"s s///
команда:
Строка замены в sed
s///
команда не является регулярным выражением, но она распознает заполнители, которые ссылаются либо на всю строку, совпадающую с регулярным выражением (&
) или конкретные результаты захвата группы по индексу (\1
, \2
,...), поэтому их необходимо экранировать вместе с (обычным) разделителем регулярных выражений, /
,
Предполагая, что замещающая строка является однострочной:
replace='Laurel & Hardy; PS\2' # sample input containing metachars.
replaceEscaped=$(sed 's/[&/\]/\\&/g' <<<"$replace") # escape it
sed -n "s/\(.*\) \(.*\)/$replaceEscaped/p" <<<"foo bar" # if ok, outputs $replace as is
MULTI-Line Solutions
Экранирование строкового литерала MULTI-LINE для использования в качестве регулярного выражения в sed
:
Примечание. Это имеет смысл, только если перед попыткой сопоставления было прочитано несколько строк ввода (возможно, ВСЕ).
Так как такие инструменты, как sed
а также awk
по умолчанию работают с одной строкой за один раз, необходимы дополнительные шаги, чтобы заставить их читать более одной строки за один раз.
# Define sample multi-line literal.
search='/abc\n\t[a-z]\+\([^ ]\)\{2,3\}\3
/def\n\t[A-Z]\+\([^ ]\)\{3,4\}\4'
# Escape it.
searchEscaped=$(sed -e 's/[^^]/[&]/g; s/\^/\\^/g; $!a\'$'\n''\\n' <<<"$search" | tr -d '\n') #'
# Use in a Sed command that reads ALL input lines up front.
# If ok, echoes 'foo'
sed -n -e ':a' -e '$!{N;ba' -e '}' -e "s/$searchEscaped/foo/p" <<<"$search"
- Новые строки в многострочных строках ввода должны быть переведены в
'\n'
строки, то есть, как новые строки кодируются в регулярном выражении. $!a\'$'\n''\\n'
добавляет строку'\n'
до каждой выходной строки, кроме последней (последняя новая строка игнорируется, потому что она была добавлена<<<
)tr -d '\n
затем удаляет все действительные символы новой строки из строки (sed
добавляет один всякий раз, когда он печатает свое пространство образца), эффективно заменяя все символы новой строки во вводе'\n'
строки.
-e ':a' -e '$!{N;ba' -e '}'
это POSIX-совместимая формаsed
идиома, которая читает все входные строки в цикле, поэтому оставляя последующие команды работать со всеми входными строками одновременно.- Если вы используете GNU
sed
(только), вы можете использовать его-z
возможность упростить чтение всех строк ввода одновременно:sed -z "s/$searchEscaped/foo/" <<<"$search"
- Если вы используете GNU
Экранирование строкового литерала MULTI-LINE для использования в качестве строки замены в sed
"s s///
команда:
# Define sample multi-line literal.
replace='Laurel & Hardy; PS\2
Masters\1 & Johnson\2'
# Escape it for use as a Sed replacement string.
IFS= read -d '' -r < <(sed -e ':a' -e '$!{N;ba' -e '}' -e 's/[&/\]/\\&/g; s/\n/\\&/g' <<<"$replace")
replaceEscaped=${REPLY%$'\n'}
# If ok, outputs $replace as is.
sed -n "s/\(.*\) \(.*\)/$replaceEscaped/p" <<<"foo bar"
- Новые строки во входной строке должны быть сохранены как фактические новые строки, но
\
убежал. -e ':a' -e '$!{N;ba' -e '}'
это POSIX-совместимая формаsed
идиома, которая читает все входные строки цикла.'s/[&/\]/\\&/g
ускользает от всех&
,\
а также/
экземпляры, как в однострочном решении.s/\n/\\&/g'
затем\
-приставляет все актуальные переводы строк.IFS= read -d '' -r
используется для чтенияsed
вывод команды как есть (чтобы избежать автоматического удаления завершающих строк, которые подставляет команда ($(...)
) будет выполнять).${REPLY%$'\n'}
затем удаляет один завершающий перевод строки, который<<<
неявно добавлен к входу.
bash
функции на основе вышеизложенного (для sed
):
quoteRe()
кавычки (экранированные символы) для использования в регулярных выраженияхquoteSubst()
кавычки для использования в строке замещенияs///
вызов.- оба правильно обрабатывают многострочный ввод
- Обратите внимание, что, потому что
sed
по умолчанию читает одну строку за раз, использованиеquoteRe()
с многострочными строками имеет смысл только вsed
команды, которые явно читают несколько (или все) строк одновременно. - Также, используя подстановки команд (
$(...)
) вызов функций не будет работать для строк, которые имеют завершающие символы новой строки; в этом случае используйте что-то вродеIFS= read -d '' -r escapedValue <(quoteSubst "$value")
- Обратите внимание, что, потому что
# SYNOPSIS
# quoteRe <text>
quoteRe() { sed -e 's/[^^]/[&]/g; s/\^/\\^/g; $!a\'$'\n''\\n' <<<"$1" | tr -d '\n'; }
# SYNOPSIS
# quoteSubst <text>
quoteSubst() {
IFS= read -d '' -r < <(sed -e ':a' -e '$!{N;ba' -e '}' -e 's/[&/\]/\\&/g; s/\n/\\&/g' <<<"$1")
printf %s "${REPLY%$'\n'}"
}
Пример:
from=$'Cost\(*):\n$3.' # sample input containing metachars.
to='You & I'$'\n''eating A\1 sauce.' # sample replacement string with metachars.
# Should print the unmodified value of $to
sed -e ':a' -e '$!{N;ba' -e '}' -e "s/$(quoteRe "$from")/$(quoteSubst "$to")/" <<<"$from"
Обратите внимание на использование -e ':a' -e '$!{N;ba' -e '}'
читать все входные данные одновременно, так что многострочная подстановка работает.
perl
решение:
Perl имеет встроенную поддержку экранирования произвольных строк для буквального использования в регулярном выражении: quotemeta()
функция или ее эквивалент \Q...\E
цитирование
Подход одинаков для однострочных и многострочных строк; например:
from=$'Cost\(*):\n$3.' # sample input containing metachars.
to='You owe me $1/$& for'$'\n''eating A\1 sauce.' # sample replacement string w/ metachars.
# Should print the unmodified value of $to.
# Note that the replacement value needs NO escaping.
perl -s -0777 -pe 's/\Q$from\E/$to/' -- -from="$from" -to="$to" <<<"$from"
Обратите внимание на использование
-0777
читать все входные данные одновременно, так что многострочная подстановка работает.-s
опция позволяет разместить-<var>=<val>
определения переменных Perl в стиле--
после скрипта, перед любым операндом имени файла.
Основываясь на ответе @mklement0 в этой теме, следующий инструмент заменит любую однострочную строку (в отличие от регулярного выражения) любой другой однострочной строкой, используя sed
а также bash
:
$ cat sedstr
#!/bin/bash
old="$1"
new="$2"
file="${3:--}"
escOld=$(sed 's/[^^]/[&]/g; s/\^/\\^/g' <<< "$old")
escNew=$(sed 's/[&/\]/\\&/g' <<< "$new")
sed "s/$escOld/$escNew/g" "$file"
Чтобы проиллюстрировать необходимость использования этого инструмента, попробуйте заменить a.*/b{2,}\nc
с d&e\1f
позвонив sed
непосредственно:
$ cat file
a.*/b{2,}\nc
axx/bb\nc
$ sed 's/a.*/b{2,}\nc/d&e\1f/' file
sed: -e expression #1, char 16: unknown option to `s'
$ sed 's/a.*\/b{2,}\nc/d&e\1f/' file
sed: -e expression #1, char 23: invalid reference \1 on `s' command's RHS
$ sed 's/a.*\/b{2,}\nc/d&e\\1f/' file
a.*/b{2,}\nc
axx/bb\nc
# .... and so on, peeling the onion ad nauseum until:
$ sed 's/a\.\*\/b{2,}\\nc/d\&e\\1f/' file
d&e\1f
axx/bb\nc
или используйте вышеуказанный инструмент:
$ sedstr 'a.*/b{2,}\nc' 'd&e\1f' file
d&e\1f
axx/bb\nc
Причина, по которой это полезно, заключается в том, что его можно легко дополнить, используя при необходимости разделители слов для замены слов, например, в GNU. sed
синтаксис:
sed "s/\<$escOld\>/$escNew/g" "$file"
в то время как инструменты, которые на самом деле работают со строками (например, awk
"s index()
) нельзя использовать разделители слов.
Следует отметить, что регулярное выражение, используемое в некоторых ответах выше ( /questions/17122770/vozmozhno-li-izbezhat-metasimvolov-regulyarnyih-vyirazhenij-s-pomoschyu-sed/17122785#17122785 и /questions/17122770/vozmozhno-li-izbezhat-metasimvolov-regulyarnyih-vyirazhenij-s-pomoschyu-sed/17122782#17122782):
's/[^^\\]/[&]/g; s/\^/\\^/g; s/\\/\\\\/g'
кажется неправильным:
- Делать сначала
s/\^/\\^/g
с последующимs/\\/\\\\/g
это ошибка, как и любой^
сбежал первым, чтобы\^
тогда он снова сбежит.
Кажется, лучший способ:
's/[^\^]/[&]/g; s/[\^]/\\&/g;'
.
-
[^^\\]
с sed (BRE / ERE) должно быть просто[^\^]
(или же[^^\]
).\
не имеет особого значения внутри выражения в квадратных скобках и не требует заключения в кавычки.
Расширение параметра Bash можно использовать для экранирования строки для использования в качестве строки замены Sed:
# Define a sample multi-line literal. Includes a trailing newline to test corner case
replace='a&b;c\1
d/e
'
# Escape it for use as a Sed replacement string.
: "${replace//\\/\\\\}"
: "${_//&/\\\&}"
: "${_//\//\\\/}"
: "${_//$'\n'/\\$'\n'}"
replaceEscaped=$_
# Output should match "$replace"
sed -n "s/.*/$replaceEscaped/p" <<<''
В bash 5.2+ это можно упростить:
# Define a sample multi-line literal. Includes a trailing newline to test corner case
replace='a&b;c\1
d/e
'
# Escape it for use as a Sed replacement string.
shopt -s extglob
shopt -s patsub_replacement # An & in the replacement will expand to what matched. bash 5.2+
: "${replace//@(&|\\|\/|$'\n')/\\&}"
replaceEscaped=$_
# Output should match "$replace"
sed -n "s/.*/$replaceEscaped/p" <<<''
Инкапсулируйте его в функцию bash:
##
# escape_replacement -v var replacement
#
# Escape special characters in _replacement_ so that it can be
# used as the replacement part in a sed substitute command.
# Store the result in _var_.
escape_replacement() {
if ! [[ $# = 3 && $1 = '-v' ]]; then
echo "escape_replacement: invalid usage" >&2
echo "escape_replacement: usage: escape_replacement -v var replacement" >&2
return 1
fi
local -n var=$2 # nameref (requires Bash 4.3+)
# We use the : command (true builtin) as a dummy command as we
# trigger a sequence of parameter expansions
# We exploit that the $_ variable (last argument to the previous command
# after expansion) contains the result of the previous parameter expansion
: "${3//\\/\\\\}" # Backslash-escape any existing backslashes
: "${_//&/\\\&}" # Backslash-escape &
: "${_//\//\\\/}" # Backslash-escape the delimiter (we assume /)
: "${_//$'\n'/\\$'\n'}" # Backslash-escape newline
var=$_ # Assign to the nameref
# To support Bash older than 4.3, the following can be used instead of nameref
#eval "$2=\$_" # Use eval instead of nameref https://mywiki.wooledge.org/BashFAQ/006
}
# Test the function
# =================
# Define a sample multi-line literal. Include a trailing newline to test corner case
replace='a&b;c\1
d/e
'
escape_replacement -v replaceEscaped "$replace"
# Output should match "$replace"
sed -n "s/.*/$replaceEscaped/p" <<<''