Возможно ли избежать метасимволов регулярных выражений с помощью 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 не приведет никого в неправильное направление.

Btw, обсуждение появилось здесь. Я думал, что это может быть хорошим местом для сбора решений и, возможно, для их разлома и/или разработки.

Ответ 1

Заметка:

  • Если вы ищете готовые функции на основе методов, обсуждаемых в этом ответе:
    • функции bash которые обеспечивают надежное экранирование даже в многострочных подстановках, можно найти в нижней части этой публикации (плюс решение perl которое использует встроенную поддержку perl для такого экранирования).
    • Ответ @EdMorton содержит инструмент (скрипт bash), который надежно выполняет подстановки single-.
  • Все фрагменты предполагают bash как оболочку (возможны переформулировки, совместимые с POSIX):

single- линейные решения


Вывод строкового литерала для использования в качестве регулярного выражения в sed:

Чтобы получить кредит, когда кредит должен быть: я нашел регулярное выражение, используемое ниже в этом ответе.

Предполагая, что строка поиска является строкой строки single-:

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'
  • Каждый символ, кроме ^, помещается в свой собственный набор символов [...] чтобы рассматривать его как литерал.
    • Заметим, что ^ - один символ. вы не можете представлять как [^], потому что в этом месте (отрицание) оно имеет особое значение.
  • Тогда, ^ chars. экранируются как \^.

Этот подход является надежным, но не эффективным.

Устойчивость исходит из того, что вы не пытаетесь предвидеть все специальные символы регулярных выражений, которые будут варьироваться в зависимости от диалектов регулярных выражений, но сосредоточиться только на двух функциях, общих для всех диалектов regex:

  • возможность указывать буквенные символы внутри набора символов.
  • возможность избежать буквальное ^ а \^

Экранирование строкового литерала для использования в качестве замены строки в команде sed s///:

Строка замены в команде sed s/// не является регулярным выражением, но она распознает заполнители, которые ссылаются либо на всю строку, сопоставляемую с выражением regex (&), либо с конкретными результатами группы захвата по индексу (\1, \2 ,...), поэтому они должны быть экранированы вместе с (обычным) разделителем регулярных выражений, /.

Предполагая, что строка замены является строкой строки single-:

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


Выключение строкового литерала 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"

Исключение строкового литерала MULTI-LINE для использования в качестве замены в команде sed 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 scaped.
  • -e ':a' -e '$!{N;ba' -e '}' - это POSIX-совместимая форма sed идиомы, которая считывает все входные строки цикла.
  • 's/[&/\]/\\&/g экранирует все &, \ и / экземпляры, как в линейном решении single-.
  • s/\n/\\&/g' затем \ -prefixes все фактические строки новой строки.
  • IFS= read -d '' -r используется для чтения вывода команды sed как есть (чтобы избежать автоматического удаления конечных строк, которые могла бы выполнять замена команд ($(...))).
  • ${REPLY%$'\n'} затем удаляет одну конечную новую строку, которую <<< неявно добавляет к вводу.


bash на основе вышеизложенного (для sed):

  • quoteRe() кавычки (escapes) для использования в регулярном выражении
  • 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 котировка.
Такой подход одинаковый как для single-, так и для многострочных строк; например:

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> -s мозоль определение Perl переменного следующий -- после того, как сценарий перед любым именем операндов.

Ответ 2

Основываясь на @mklement0 answer в этом потоке, следующий инструмент заменит любую строку с одной строкой (в отличие от regexp) с любой другой однострочной строкой, используя 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 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 index()), не могут использовать разделители слов.