Existe uma maneira de impedir que o sed interprete a sequência de substituição? [fechadas]

14

Se você deseja substituir uma palavra-chave por uma string usando sed, sed tenta muito interpretar sua string de substituição. Se a string de substituição tiver caracteres que o sed considere especiais, como um caractere '/', ela falhará, a menos que você queira que a string de substituição tenha caracteres que digam ao sed como agir.

Ex:

VAR="hi/"

sed "s/KEYWORD/$VAR/g" somefile

Existe alguma maneira de dizer ao sed para não tentar interpretar a sequência de substituição para caracteres especiais? Tudo o que eu quero é poder substituir uma palavra-chave em um arquivo pelo conteúdo de uma variável, independentemente do conteúdo.

Tal
fonte
Se você deseja inserir caracteres especiais sede fazer com que eles não sejam especiais, basta escapar da barra invertida. VAR='hi\/'não dá esse problema.
Curinga
6
Por que todos os votos negativos? Parece-me uma pergunta perfeitamente razoável
roaima
sed(1)apenas interpreta o que recebe. No seu caso, obtém isso através de uma interpolação de shell. Eu acredito que você não pode fazer o que quiser, mas verifique o manual. Eu sei que no Perl (que faz uma sedsubstituição aceitável , com expressões regulares muito mais ricas), você pode especificar que uma string seja literalmente copiada novamente, verifique o manual.
vonbrand
relacionada stackoverflow.com/questions/407523/...
Ciro Santilli冠状病毒审查六四事件法轮功

Respostas:

4

Há apenas 4 caracteres especiais na peça de substituição: \, &, nova linha e o delimitador ( ref )

$ VAR='abc/def&ghi\foo
next line'

$ repl=$(sed -e 's/[&\\/]/\\&/g; s/$/\\/' -e '$s/\\$//' <<<"$VAR")

$ echo "$repl"
abc\/def\&ghi\\foo\
next line

$ echo ZYX | sed "s/Y/$repl/g"
Zabc/def&ghi\foo
next lineX
Glenn Jackman
fonte
Isso tem o mesmo problema que a solução da Antti - se a sequência de substituição tiver passado de um determinado comprimento, você receberá um erro "A lista de argumentos é muito longa". Além disso, e se a sequência de substituição tiver '[', ']', '*', '.' E outros caracteres desse tipo? Sed realmente não interpretaria isso?
Tal
O lado de substituição de nãos/// é uma expressão regular, é realmente apenas uma string (exceto para barras invertidas e escapes ). Se a string de substituição for tão longa, uma linha de shell não é a sua solução. &
Glenn Jackman
Uma lista muito útil se, por exemplo, sua sequência de substituição for um texto codificado em base64 (por exemplo, substituindo um espaço reservado por uma chave SHA256). Então é só o delimitador com o qual se preocupar.
Heath Raftery
4

Você pode usar Perl em vez de sed com -p(assumir loop sobre entrada) e -e(fornecer programa na linha de comando). Com o Perl, você pode acessar variáveis ​​de ambiente sem interpolá-las no shell. Observe que a variável precisa ser exportada :

export VAR='hi/'
perl -p -e 's/KEYWORD/$ENV{VAR}/g' somefile

Se você não deseja exportar a variável para qualquer lugar, forneça-a apenas para esse processo:

PATTERN="$VAR" perl -p -e 's/KEYWORD/$ENV{PATTERN}/g' somefile

Observe que a sintaxe da expressão regular do Perl é, por padrão, ligeiramente diferente da do sed.

Antti Haapala
fonte
Isso parecia muito promissor, mas ao testá-lo, recebo um erro "Lista de argumentos muito longa" porque minha string de substituição é muito longa, o que faz sentido - usando esse método, estamos usando a string de substituição inteira como parte dos argumentos que fornecemos para perl, então há um limite de quanto tempo pode ser.
Tal
1
Não, ele irá na PATTERN variável de ambiente , não em argumentos. De qualquer forma, esse erro seria o E2BIGque você obteria igualmente se usasse sed.
Antti Haapala
2

A solução mais simples que ainda manipularia a grande maioria dos valores de variáveis ​​corretamente seria usar um caractere não imprimível como delimitador do sedcomando substituto.

Em vivocê pode escapar qualquer caracter de controle digitando Ctrl-V (mais comumente escrito como ^V). Portanto, se você usar algum caractere de controle (eu costumo usar ^Acomo delimitador nesses casos), seu sedcomando só será interrompido se esse caractere não imprimível estiver presente na variável em que você está inserindo.

Então você digitaria "s^V^AKEYWORD^V^A$VAR^V^Ag"e o que receberia (in vi) seria:

sed "s^AKEYWORD^A$VAR^Ag" somefile

Isso funcionará desde $VARque não contenha o caractere não imprimível ^A- o que é extremamente improvável.


Obviamente, se você está passando a entrada do usuário para o valor de $VAR, todas as apostas estão desativadas e é melhor higienizar sua entrada completamente, em vez de depender de caracteres de controle difíceis de digitar para o usuário médio.


Na verdade, há muito mais que ter cuidado do que a string delimitadora. Por exemplo, &quando presente em uma sequência de substituição, significa "todo o texto correspondente". Por exemplo, s/stu../my&/substituiria "stuff" por "mystuff", "stung" por "mystung", etc. Portanto, se você tiver algum caractere na variável que está inserindo como uma seqüência de substituição, mas deseja usar o literal valor da variável apenas, você precisará executar alguns serviços de limpeza de dados antes de poder usar a variável como uma sequência de substituição sed. (A limpeza de dados também pode ser feita com sed.)

Curinga
fonte
Esse é o meu ponto - substituir uma string por outra é uma operação muito simples. Realmente precisa ser tão complicado quanto descobrir quais caracteres o sed não vai gostar e usar o sed para higienizar sua própria entrada? Isso soa ridiculamente e desnecessariamente complicado. Não sou programador profissional, mas tenho certeza de que posso codificar uma pequena função que substitui uma palavra-chave por uma string em praticamente qualquer idioma que eu já tenha encontrado, incluindo o bash - eu só esperava um Linux simples solução usando ferramentas existentes - não acredito que não exista uma por aí.
Tal
1
@ Tal, se a string de substituição tiver "centenas de páginas", como você mencionou em outro comentário ... dificilmente poderá chamá-la de um caso de uso "simples". A resposta aqui é Perl, a propósito - eu apenas não aprendi Perl. A complexidade aqui vem do fato de que você deseja permitir QUALQUER entrada arbitrária como uma sequência de substituição em uma regex .
Curinga
Existem inúmeras outras soluções que você pode usar, muitas delas muito simples. Por exemplo, se sua sequência de substituição for realmente baseada em linhas e não precisar ser inserida no meio de uma linha, use sedo icomando nsert. Mas sednão é uma boa ferramenta para processar grandes quantidades de texto de maneiras complexas. Vou postar outra resposta mostrando como fazer isso awk.
Curinga
1

Você pode usar um ,ou um |e, em vez disso, será considerado um separador e, tecnicamente, você pode usar qualquer coisa

na página do manual

\cregexpc
           Match lines matching the regular expression regexp.  The  c  may
      be any character.

Como você pode ver, você deve começar com um \ antes do seu separador no início, e poderá usá-lo como um separador.

da documentação http://www.gnu.org/software/sed/manual/sed.html#The-_0022s_0022-Command :

The / characters may be uniformly replaced by any other single character 
within any given s command.

The / character (or whatever other character is used in its stead) can appear in 
the regexp or replacement only if it is preceded by a \ character.

Exemplo:

sed -e 'somevar|s|foo|bar|'
echo "Hello all" | sed "s_all_user_"
echo "Hello all" | sed "s,all,user,"

echo "Hello/ World" | sed "s,Hello/,Neo,"

user3566929
fonte
Você está falando sobre permitir o uso de um caractere único e específico na string de substituição - neste caso, "/". Eu estou falando sobre impedir que ele tente interpretar a string de substituição completamente. Não importa qual caractere você use ("/", ",", "|", etc), você sempre corre o risco de que esse caractere apareça na string de substituição. Além disso, o personagem inicial não é o único personagem especial com o qual se importa, é?
Tal
@ Tal não, pode levar qualquer coisa em vez de /e ele ignorará a /felicidade, como acabei de apontar .. na verdade, você pode até procurá-la e substituí-la em uma string >>> eu editei com um exemplo >>> esses as coisas não são tão seguras e você sempre encontrará um cara mais esperto
#
@ Tal Por que você quer impedir que ele interprete? Quero dizer que é o uso de, sedem primeiro lugar, qual é o seu projeto?
precisa saber é o seguinte
Tudo o que preciso é substituir uma palavra-chave por uma string. O sed parece ser a maneira mais comum, de longe, de fazer isso no linux. A cadeia pode ter 100 páginas. Eu não quero tentar higienizar a string para que o sed não surte ao lê-la - eu quero que ele seja capaz de lidar com qualquer caractere da string e, com "handle", quero dizer não tentar encontrar mágica significado dentro.
Tal
1
@ Tal, NÃObash é para manipulação de strings. De todo, de todo. É para manipulação de arquivos e coordenação de comandos . Acontece que ele possui algumas funcionalidades úteis para strings, mas são realmente limitadas e não muito rápidas, se essa é a principal coisa que você está fazendo. Consulte "Por que o uso de um loop de shell para processar o texto é considerado uma má prática?" Algumas ferramentas que são projetados para processamento de texto são, em ordem de mais básico ao mais poderoso: , e Perl. sedawk
Curinga
1

Se for baseado em linhas e tiver apenas uma linha para substituir, recomendo anexar o arquivo com a linha de substituição usando printf, armazenando a primeira linha no sedespaço de espera e soltando-a conforme necessário. Dessa forma, você não precisa se preocupar com caracteres especiais. (A única suposição aqui é que $VARcontém uma única linha de texto sem novas linhas, o que você já disse nos comentários.) Além das novas linhas, o VAR poderia conter qualquer coisa e isso funcionaria independentemente.

VAR=whatever
{ printf '%s\n' "$VAR";cat somefile; } | sed '1{h;d;};/KEYWORD/g'

printf '%s\n'imprimirá o conteúdo $VARcomo uma string literal, independentemente do seu conteúdo, seguido por uma nova linha. ( echofará outras coisas em alguns casos, por exemplo, se o conteúdo de $VARcomeçar com um hífen - ele será interpretado como um sinalizador de opção que está sendo passado para echo.)

Os colchetes são usados ​​para anexar a saída printfao conteúdo da forma somefilecomo ela é passada sed. O espaço em branco que separa os chavetas por si só é importante aqui, assim como o ponto-e-vírgula antes da chaveta de fechamento.

1{h;d;};como um sedcomando irá armazenar a primeira linha de texto em sed's espaço de espera , então délete da linha (em vez de imprimi-lo).

/KEYWORD/aplica as seguintes ações a todas as linhas que contêm KEYWORD. A ação é get, que obtém o conteúdo do espaço de espera e o coloca no lugar do espaço do padrão - em outras palavras, a linha atual inteira. (Isso não serve para substituir apenas parte de uma linha.) A propósito, o espaço de espera não é esvaziado, apenas copiado no espaço do padrão, substituindo o que estiver lá.

Se você deseja ancorar seu regex para que ele não corresponda a uma linha que contém apenas KEYWORD, mas apenas uma linha onde não há mais nada na linha além de KEYWORD, adicione o início da linha anchor ( ^) e o fim da linha anchor ( $) a seu regex:

VAR=whatever
{ printf '%s\n' "$VAR";cat somefile; } | sed '1{h;d;};/^KEYWORD$/g'
Curinga
fonte
Parece ótimo se o seu VAR tiver uma linha. Na verdade, mencionei nos comentários que o VAR "pode ​​ter 100 páginas" em vez de uma linha. Desculpe pela confusão.
19416 Tal
0

Você pode escapar da barra invertida das barras invertidas na sequência de substituição, usando a expansão do parâmetro de substituição de padrão do Bash. É um pouco confuso porque as barras também precisam ser escapadas para o Bash.

$ var='a/b/c';var="${var//\//\\/}";echo 'this is a test' | sed "s/i/$var/g"

resultado

tha/b/cs a/b/cs a test

Você pode colocar a expansão do parâmetro diretamente no seu comando sed:

$ var='a/b/c';echo 'this is a test' | sed "s/i/${var//\//\\/}/g"

mas acho que a primeira forma é um pouco mais legível. E, é claro, se você vai reutilizar o mesmo padrão de substituição em vários comandos sed, faz sentido fazer a conversão apenas uma vez.

Outra opção seria usar um script escrito em awk, perl ou Python, ou um programa C, para fazer suas substituições em vez de usar sed.


Aqui está um exemplo simples no Python que funciona se a palavra-chave a ser substituída for uma linha completa no arquivo de entrada (sem contar a nova linha). Como você pode ver, é essencialmente o mesmo algoritmo do seu exemplo do Bash, mas lê o arquivo de entrada com mais eficiência.

import sys

#Get the keyword and replacement texts from the command line
keyword, replacement = sys.argv[1:]
for line in sys.stdin:
    #Strip any trailing whitespace
    line = line.rstrip()
    if line == keyword:
        line = replacement
    print(line)
PM 2Ring
fonte
Essa é apenas outra maneira de higienizar a entrada, e não é ótima, pois trata apenas de um caractere específico ('/'). Como o Wildcard apontou, há muito mais a ter cuidado do que apenas a string delimitadora.
Tal
Chamada justa. Por exemplo, se o texto de substituição contiver alguma sequência com barra invertida, elas serão interpretadas, o que pode não ser desejável. Uma maneira de contornar isso seria converter os caracteres problemáticos (ou a coisa toda) em \xseqüências de escape no estilo. Ou para usar um programa que pode lidar com entradas arbitrárias, como mencionei no meu último parágrafo.
usar o seguinte comando
@ Tal: Vou adicionar um exemplo simples de Python à minha resposta.
PM 2Ring
O script python funciona muito bem e parece fazer exatamente o que minha função faz, apenas com muito mais eficiência. Infelizmente, se o script principal for bash (como no meu caso), isso requer o uso de um script python externo secundário.
Tal
-1

Foi assim que eu fui:

#Replaces a keyword with a long string
#
#This is normally done with sed, but sed
#tries to interpret the string you are
#replacing the keyword with too hard
#
#stdin - contents to look through
#Arg 1 - keyword to replace
#Arg 2 - what to replace keyword with
replace() {
        KEYWORD="$1"
        REPLACEMENT_STRING="$2"

        while IFS= read -r LINE
        do
                if [[ "$LINE" == "$KEYWORD" ]]
                then
                        printf "%s\n" "$REPLACEMENT_STRING"
                else
                        printf "%s\n" "$LINE"
                fi
        done < /dev/stdin
}

isso funciona muito bem no meu caso, porque minha palavra-chave está em uma linha por si só. Se a palavra-chave estivesse alinhada com outro texto, isso não funcionaria.

Eu realmente gostaria de saber se existe uma maneira fácil de fazer isso que não envolve codificar minha própria solução.

Tal
fonte
1
Se você está realmente preocupado com caracteres especiais e robustez, não deve usar echonada. Use em printfvez disso. E fazer o processamento de texto em um loop de shell é uma má ideia.
Curinga
1
Seria útil se você mencionasse na pergunta que a palavra-chave sempre será uma linha completa. FWIW, o bash readé bastante lento. Destina-se ao processamento de entrada interativa do usuário, não ao processamento de arquivos de texto. É lento porque lê stdin char por char, fazendo uma chamada de sistema para cada char.
usar o seguinte comando
@PM 2Ring Minha pergunta não mencionou que a palavra-chave está em uma linha própria, porque eu não quero uma resposta que funcione em um número tão limitado de casos - eu queria algo que pudesse funcionar facilmente, não importa onde a palavra-chave foi. Eu também nunca disse que meu código é eficiente - se fosse, eu não estaria procurando uma alternativa ...
Tal
@Wildcard A menos que esteja faltando alguma coisa, printf interpreta absolutamente caracteres especiais e muito mais do que o 'eco' padrão. printf "hi\n"fará com que printf imprima uma nova linha enquanto a echo "hi\n"imprime como está.
Tal
@ Tal, o "f" printfsignifica "formato" - o primeiro argumento para printfé um especificador de formato . Se esse especificador é %s\n, que significa "string seguido por nova linha", nada no próximo argumento será interpretado ou traduzido por printf em tudo . (O shell ainda pode interpretá-lo, é claro; é melhor colocar tudo entre aspas simples, se for uma string literal, ou aspas duplas, se você desejar expansão variável.) Veja minha resposta usandoprintf para obter mais detalhes.
Curinga