É possível escapar dos metacaracteres regex de maneira confiável com o sed




(2)

Com base na resposta de @ mklement0 neste tópico, a ferramenta a seguir substituirá qualquer sequência de linhas simples (em oposição a regexp) por qualquer outra sequência de linhas únicas 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, tente substituir 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 substituir palavras, se necessário, por exemplo, na sintaxe sed GNU:

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.

Eu estou querendo saber se é possível escrever um comando sed 100% confiável para escapar de qualquer metacaractere regex em uma string de entrada, para que possa ser usado em um comando sed subsequente. Como isso:

#!/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

Eu sei que existem ferramentas melhores para trabalhar com cadeias de caracteres fixas em vez de padrões, por exemplo, awk , perl ou python . Gostaria apenas de provar se é possível ou não com o sed . Eu diria que vamos nos concentrar em expressões regulares POSIX básicas para nos divertirmos ainda mais! :)

Eu tentei muitas coisas, mas a qualquer momento encontrei uma entrada que interrompeu minha tentativa. Eu pensei que mantê-lo abstrato como script to escape não levaria ninguém na direção errada.

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


Nota:

  • Se você procura funcionalidade pré-empacotada com base nas técnicas discutidas nesta resposta:
    • bash funções do bash que permitem 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 perl do perl para esse escape).
    • A resposta do @ EdMorton contém uma ferramenta (script bash ) que efetivamente executa substituições de linha única .
  • Todos os trechos assumem o bash como o shell (são possíveis reformulações compatíveis com POSIX):

Soluções de linha única

Escapando uma string literal para uso como uma expressão regular no sed :

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

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

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'
  • Todo caractere, exceto ^ é colocado em sua própria expressão [...] conjunto de caracteres para tratá-lo como um literal.
    • Observe que ^ é o único caractere. você não pode representar como [^] , porque ele possui um significado especial nesse local (negação).
  • Então, ^ chars. são escapados como \^ .
    • Observe que você não pode simplesmente escapar de todos os caracteres colocando um \ na frente dele, porque isso pode transformar um caracter literal em um metacarpo, por exemplo, \< e \b são limites de palavras em algumas ferramentas, \n é uma nova linha, \{ é o início de um intervalo RE como \{1,3\} etc.

A abordagem é robusta, mas não eficiente.

A robustez vem de não tentar antecipar todos os caracteres especiais de regex - que variam entre os dialetos de regex -, mas focar apenas em dois recursos compartilhados por todos os dialetos de regex :

  • a capacidade de especificar caracteres literais dentro de um conjunto de caracteres.
  • a capacidade de escapar de um literal ^ as \^

Escapando um literal de cadeia de caracteres para uso como a cadeia de substituição no comando s/// sed :

A cadeia de substituição em um comando sed s/// não é uma regex, mas reconhece espaços reservados que se referem à cadeia inteira correspondida pela regex ( & ) ou a resultados específicos do grupo de captura pelo índice ( \1 , \2 ,. ..), portanto, eles devem ser escapados, juntamente com o delimitador de regex (habitual), / .

Supondo que a cadeia de substituição seja uma cadeia 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-line

Escapando uma literal de seqüência de caracteres MULTI-LINE para uso como regex no sed :

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

# 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"
  • As novas linhas nas seqüências de entrada de várias linhas devem ser convertidas em '\n' , que é como as novas linhas são codificadas em uma regex.
  • $!a\'$'\n''\\n' anexa a string '\n' a cada linha de saída, exceto a última (a última nova linha é ignorada, porque foi adicionada por <<< )
  • tr -d '\n remove todas as novas linhas reais da string ( sed adiciona uma sempre que imprime seu espaço de padrão), substituindo efetivamente todas as novas linhas da entrada por strings '\n' .
  • -e ':a' -e '$!{N;ba' -e '}' é a forma compatível com POSIX de um idioma sed que lê todas as linhas de entrada em um loop, deixando, portanto, comandos subseqüentes para operar em todas as linhas de entrada em uma vez.

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

Escapando um literal de sequência MULTI-LINE para uso como a sequência de substituição 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" 
  • As novas linhas na sequência de entrada devem ser mantidas como novas linhas reais, mas \ -escapadas.
  • -e ':a' -e '$!{N;ba' -e '}' é a forma compatível com POSIX de um idioma sed que lê todas as linhas de entrada de um loop.
  • 's/[&/\]/\\&/g escapa a todas as instâncias & , \ e / , como na solução de linha única.
  • s/\n/\\&/g' então \ corrige 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 finais que uma substituição de comando ( $(...) ) executaria).
  • ${REPLY%$'\n'} remove uma única nova linha à direita, que o <<< anexou implicitamente à entrada.

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

  • quoteRe() aspas (escapes) para uso em uma regex
  • quoteSubst() aspas para uso na cadeia de substituição de uma chamada s/// .
  • ambos lidam corretamente com a entrada de várias linhas
    • Observe que, como o sed lê uma única linha no momento, por padrão, o uso de quoteRe() com sequências de várias linhas só faz sentido em comandos sed que lêem explicitamente várias (ou todas) linhas de uma só vez.
    • Além disso, o uso de substituições de comandos ( $(...) ) para chamar as funções não funcionará para cadeias que possuem novas linhas à direita ; nesse caso, use algo como 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'}"
}

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 entradas de uma só vez, para que a substituição de várias linhas funcione.

solução perl :

O Perl possui suporte quotemeta() para escapar seqüências arbitrárias para uso literal em um regex: a função quotemeta() ou sua citação \Q...\E equivalente .
A abordagem é a mesma para seqüências de caracteres de uma e 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 entradas de uma só vez, para que a substituição de várias linhas funcione.

  • A opção -s permite colocar as definições de variáveis ​​Perl no estilo -<var>=<val> seguir -- após o script, antes de qualquer operando de nome de arquivo.





sed