É 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 dobash
que permitem escape robusto mesmo em substituições de várias linhas podem ser encontradas na parte inferior deste post (mais uma soluçãoperl
que usa o suporteperl
doperl
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).
-
Observe que
-
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.
-
Observe que você não pode simplesmente escapar de todos os caracteres colocando um
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 idiomased
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"
-
Se você estiver usando o
GNU
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 idiomased
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 comandosed
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 chamadas///
. -
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 dequoteRe()
com sequências de várias linhas só faz sentido em comandossed
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 comoIFS= read -d '' -r escapedValue <(quoteSubst "$value")
-
Observe que, como o
# 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.