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:
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). bash
script) que executa robustamente substituições de linha única . bash
como o shell (as reformulações compatíveis com POSIX são possíveis): 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'
^
é colocado em sua própria expressão [...]
conjunto de caracteres para tratá-lo como um literal.
^
é o único char. você não pode representar como [^]
, porque tem um significado especial nessa localização (negaçã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 :
^
como \^
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
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"
'\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.
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"
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"
\
-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. 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///
. 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. $(...)
) 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.
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.