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

Строка замены в seds/// команда не является регулярным выражением, но она распознает заполнители, которые ссылаются либо на всю строку, совпадающую с регулярным выражением (&) или конкретные результаты захвата группы по индексу (\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"

Экранирование строкового литерала 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" <<<''
Другие вопросы по тегам