É possível escaping metacaracteres regex de forma confiável com sed

Eu estou querendo saber se é possível escrever um comando sed 100% confiável para escaping de qualquer metacaracteres regex em uma seqüência de input para que ele possa ser usado em um comando sed subseqüente. Como isso:

 #!/bin/bash # Trying to replace one regex by another in an input file with sed search="/abc\n\t[az]\+\([^ ]\)\{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 

Eu sei que existem ferramentas melhores para trabalhar com strings fixas em vez de padrões, por exemplo, awk , perl ou python . Eu gostaria apenas de provar se é possível ou não com sed . Eu diria que vamos nos concentrar nos regexes POSIX básicos para nos divertir ainda mais! 🙂

Eu tentei muitas coisas, mas a qualquer momento eu poderia encontrar uma input que quebrou minha tentativa. Eu pensei que mantê-lo abstrato como script to escape não levaria ninguém para a direção errada.

Aliás, a discussão surgiu aqui . Eu pensei que este poderia ser um bom lugar para coletar soluções e, provavelmente, quebrá-las e / ou elaborá-las.

Nota:

  • Se você está procurando uma funcionalidade pré-empacotada com base nas técnicas discutidas nesta resposta:
    • bash funções bash que permitem um escape robusto, mesmo em substituições de várias linhas, podem ser encontradas na parte inferior deste post (mais uma solução perl que usa o suporte embutido do perl para tal escape).
    • @ Resposta do EdMorton contém uma ferramenta ( bash script) que executa robustamente substituições de linha única .
  • Todos os trechos assumem o bash como o shell (as reformulações compatíveis com POSIX são possíveis):

Soluções de linha única


Escapando uma string literal para uso como regex no sed :

Para dar crédito onde o crédito é devido: eu encontrei o regex usado abaixo nesta resposta .

Supondo que a cadeia de pesquisa seja uma cadeia de linha única :

 search='abc\n\t[az]\+\([^ ]\)\{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' 
  • Cada caractere, exceto ^ é colocado em sua própria expressão [...] conjunto de caracteres para tratá-lo como um literal.
    • Note que ^ é o único char. você não pode representar como [^] , porque tem um significado especial nessa localização (negação).
  • Então, ^ chars. são escapados como \^ .

A abordagem é robusta, mas não eficiente.

A robustez vem de não tentar antecipar todos os caracteres especiais de regex - que variam de acordo com os dialetos regex -, mas concentrar - se em apenas dois resources compartilhados por todos os dialetos regex :

  • a capacidade de especificar caracteres literais dentro de um conjunto de caracteres.
  • a capacidade de escaping de um literal ^ como \^

Escapando uma string literal para uso como a string substituta no comando s/// sed :

A string de substituição em um comando sed s/// não é uma regex, mas reconhece espaços reservados que se referem a toda a string correspondida pelo regex ( & ) ou a resultados específicos do grupo de captura por index ( \1 , \2 ,. ..), então estes devem ser escapados, junto com o (usual) delimitador regex, / .

Supondo que a string de substituição seja uma string de linha única :

 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 


Soluções multi-linha


Escapando um literal de cadeia MULTI-LINE para uso como regex no sed :

Nota : Isso só faz sentido se várias linhas de input (possivelmente ALL) tiverem sido lidas antes de tentar corresponder.
Como ferramentas como sed e awk operam em uma única linha de cada vez por padrão, etapas extras são necessárias para fazê-las ler mais de uma linha por vez.

 # Define sample multi-line literal. search='/abc\n\t[az]\+\([^ ]\)\{2,3\}\3 /def\n\t[AZ]\+\([^ ]\)\{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" 
  • As novas linhas em seqüências de input de várias linhas devem ser convertidas em '\n' strings , que é como as novas linhas são codificadas em uma expressão regular.
  • $!a\'$'\n''\\n' acrescenta string '\n' a cada linha de saída, mas a última (a última nova linha é ignorada, porque foi adicionada por <<< )
  • tr -d '\n então remove todas as novas linhas reais da string ( sed adiciona uma sempre que ele imprime seu espaço de padrão), efetivamente substituindo todas as novas linhas na input por '\n' strings.
  • -e ':a' -e '$!{N;ba' -e '}' é a forma compatível com POSIX de um idioma sed que lê todas as linhas de input em um loop, deixando comandos subseqüentes para operar em todas as linhas de input em uma vez.

    • Se você estiver usando o GNU sed (somente), você pode usar a opção -z para simplificar a leitura de todas as linhas de input de uma só vez:
      sed -z "s/$searchEscaped/foo/" <<<"$search"

Escapando um literal de cadeia MULTI-LINE para uso como a string substituta no comando s/// sed :

 # 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" 
  • Novas linhas na cadeia de input devem ser mantidas como novas linhas reais, mas \ -escapadas.
  • -e ':a' -e '$!{N;ba' -e '}' é a forma POSIX-compliant de um idioma sed que lê todas as linhas de input um loop.
  • 's/[&/\]/\\&/g escapa todos os & , \ e / instances, como na solução de linha única.
  • s/\n/\\&/g' then \ -prefina todas as novas linhas reais.
  • IFS= read -d '' -r é usado para ler a saída do comando sed como está (para evitar a remoção automática de novas linhas à direita que uma substituição de comando ( $(...) ) executaria).
  • ${REPLY%$'\n'} então remove uma única nova linha à direita, a qual <<< implicitamente anexou à input.


funções bash baseadas no acima (para sed ):

  • quoteRe() cita (escapa) para uso em um regex
  • quoteSubst() cita para uso na string de substituição de uma chamada s/// .
  • ambos manipulam input multi-linha corretamente
    • Observe que, como o sed lê uma única linha por vez, por padrão, o uso de quoteRe() com sequências de várias linhas só faz sentido em comandos sed que explicitamente leem várias (ou todas) linhas de uma só vez.
    • Além disso, usar substituições de comando ( $(...) ) para chamar as funções não funcionará para sequências que tenham novas linhas à direita ; Nesse caso, use algo como IFS= read -d '' -r escapedValue <(quoteSubst "$value")
 # SYNOPSIS # quoteRe  quoteRe() { sed -e 's/[^^]/[&]/g; s/\^/\\^/g; $!a\'$'\n''\\n' <<<"$1" | tr -d '\n'; } 
 # SYNOPSIS # quoteSubst  quoteSubst() { IFS= read -d '' -r < <(sed -e ':a' -e '$!{N;ba' -e '}' -e 's/[&/\]/\\&/g; s/\n/\\&/g' <<<"$1") printf %s "${REPLY%$'\n'}" } 

Exemplo:

 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" 

Observe o uso de -e ':a' -e '$!{N;ba' -e '}' para ler todas as inputs de uma vez, para que a substituição de várias linhas funcione.



solução perl :

O Perl tem suporte embutido para escaping de cadeias arbitrárias para uso literal em um regex: a function quotemeta() ou seu equivalente \Q...\E citando .
A abordagem é a mesma para cadeias de uma ou várias linhas; por exemplo:

 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" 
  • Observe o uso de -0777 para ler todas as inputs de uma vez, para que a substituição de várias linhas funcione.

  • A opção -s permite colocar -= as definições da variável estilo Perl após -- após o script, antes de quaisquer operandos de nome de arquivo.

Baseando-se na resposta de @mklement0 neste tópico, a seguinte ferramenta replaceá qualquer string de linha única (em oposição a regexp) por qualquer outra string de linha única usando sed e 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" 

Para ilustrar a necessidade dessa ferramenta, considere tentar replace a.*/b{2,}\nc por d&e\1f chamando sed diretamente:

 $ 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 

ou use a ferramenta acima:

 $ sedstr 'a.*/b{2,}\nc' 'd&e\1f' file d&e\1f axx/bb\nc 

A razão pela qual isso é útil é que pode ser facilmente aumentado o uso de delimitadores de palavras para replace palavras, se necessário, por exemplo, na syntax do GNU sed :

 sed "s/\<$escOld\>/$escNew/g" "$file" 

enquanto as ferramentas que realmente operam em strings (por exemplo, o index() awk index() ) não podem usar delimitadores de palavras.