Substitua várias strings em uma única passagem

11

Estou procurando uma maneira de substituir as strings de espaço reservado em um arquivo de modelo por valores concretos, com ferramentas comuns do Unix (bash, sed, awk, talvez perl). É importante que a substituição seja feita em uma única passagem, ou seja, o que já foi escaneado / substituído não deve ser considerado para outra substituição. Por exemplo, essas duas tentativas falham:

echo "AB" | awk '{gsub("A","B");gsub("B","A");print}'
>> AA

echo "AB" | sed 's/A/B/g;s/B/A/g'
>> AA

O resultado correto neste caso é, obviamente, BA.

Em geral, a solução deve ser equivalente a varrer a entrada da esquerda para a direita por uma correspondência mais longa para uma das cadeias de substituição fornecidas, e para cada correspondência, realizando uma substituição e continuando a partir desse ponto na entrada ( nenhuma das entradas já lidas nem as trocas realizadas devem ser consideradas para as correspondências). Na verdade, os detalhes não importam, apenas que os resultados da substituição nunca são considerados para outra substituição, no todo ou em parte.

OBSERVAÇÃO Estou apenas procurando por soluções genéricas corretas. Por favor, não proponha soluções que falhem em certas entradas (arquivos de entrada, busca e substituição de pares), por mais improváveis que possam parecer.

    
por Ambroz Bizjak 19.06.2014 / 00:18

3 respostas

9

OK, uma solução geral. A seguinte função bash requer 2k arguments; cada par consiste em um espaço reservado e um substituto. Cabe a você citar as cordas apropriadamente para passá-las para a função. Se o número de argumentos for ímpar, um argumento vazio implícito será adicionado, o que efetivamente excluirá ocorrências do último espaço reservado.

Nem espaços reservados nem substitutos podem conter caracteres NUL, mas você pode usar C \ -escapes padrão, como NUL , se precisar de \ s (e, consequentemente, será necessário escrever \ se desejar \ ).

Requer as ferramentas de compilação padrão que devem estar presentes em um sistema do tipo posix (lex e cc).

replaceholder() {
  local dir=$(mktemp -d)
  ( cd "$dir"
    { printf %s\n "%option 8bit noyywrap nounput" "%%"
      printf '"%s" {fputs("%s", yyout);}\n' "${@//\"/\\"}"
      printf %s\n "%%" "int main(int argc, char** argv) { return yylex(); }"
    } | lex && cc lex.yy.c
  ) && "$dir"/a.out
  rm -fR "$dir"
}

Assumimos que lex já tenha escapado, se necessário, nos argumentos mas precisamos escapar de aspas duplas, se presentes. Isso é o que o segundo argumento para o segundo printf faz. Como a ação padrão ECHO é cc , não precisamos nos preocupar com isso.

Exemplo de execução (com horários para os céticos; é apenas um laptop barato):

$ time echo AB | replaceholder A B B A
BA

real    0m0.128s
user    0m0.106s
sys     0m0.042s
$ time printf %s\n AB{0000..9999} | replaceholder A B B A > /dev/null

real    0m0.118s
user    0m0.117s
sys     0m0.043s

Para entradas maiores, pode ser útil fornecer um sinalizador de otimização para c99 e, para a compatibilidade atual com Posix, seria melhor usar %code% . Uma implementação ainda mais ambiciosa pode tentar armazenar em cache os executáveis gerados em vez de gerá-los a cada vez, mas eles não são exatamente caros para gerar.

Editar

Se você tiver tcc , poderá evitar o incômodo de criar um diretório temporário e aproveitar o tempo de compilação mais rápido que ajudará a entradas de tamanho normal:

treplaceholder () { 
  tcc -run <(
  {
    printf %s\n "%option 8bit noyywrap nounput" "%%"
    printf '"%s" {fputs("%s", yyout);}\n' "${@//\"/\\"}"
    printf %s\n "%%" "int main(int argc, char** argv) { return yylex(); }"
  } | lex -t)
}

$ time printf %s\n AB{0000..9999} | treplaceholder A B B A > /dev/null

real    0m0.039s
user    0m0.041s
sys     0m0.031s
    
por 19.06.2014 / 01:14
1
printf 'STRING1STRING1\n\nSTRING2STRING1\nSTRING2\n' |
od -A n -t c -v -w1 |
sed 's/ \{1,3\}//;s/\$/&&/;H;s/.*//;x
     /\nS\nT\nR\nI\nN\nG\n1/s//STRING2/
     /\nS\nT\nR\nI\nN\nG\n2/s//STRING1/
     /\n/!{x;d};s/\n//g;s/./\&/g' |
     xargs printf %b

###OUTPUT###

STRING2STRING2

STRING1STRING2
STRING1

Algo como isso sempre substituirá cada ocorrência de suas strings de destino apenas uma vez, pois elas ocorrem em sed no fluxo em uma mordida por linha. Este é o caminho mais rápido que posso imaginar que você faria. Então, novamente, eu não escrevo C. Mas isso faz lidar de forma confiável com delimitadores nulos se você desejar. Consulte esta resposta para saber como funciona. Isso não tem nenhum problema com qualquer caractere de shell especial contido ou similar - mas é específico do código do ASCII, ou, em outras palavras, od não produzirá caracteres de múltiplos bytes na mesma linha e somente faça um por. Se este for um problema, você deverá adicionar iconv .

    
por 19.06.2014 / 07:00
1

Uma solução perl . Mesmo que alguns afirmem que não é possível, eu encontrei um, mas, em geral, uma simples correspondência e substituição não é possível e até piora por causa do retrocesso de um NFA, o resultado pode ser inesperado.

Em geral, e isso deve ser dito, o problema gera resultados diferentes que dependem da ordem e do comprimento das tuplas de substituição. ou seja:

A B
AA CC

e a entrada AAA resulta em BBB ou CCB .

Aqui o código:

#!/usr/bin/perl

$v='if (0) {} ';
while (($a,$b)=split /\s+/, <DATA>) {
  $k.=$a.'|';
  $v.='elsif ($& eq \''.$a.'\') {print \''.$b.'\'} ';
}
$k.='.';
$v.='else {print $&;}';

eval "
while (<>) {
  \$_ =~ s/($k)/{$v}/geco;
}";  
print "\n";


__DATA__
A    B
B    A
abba baab
baab abbc
abbc aaba

Checkerbunny:

$ echo 'ABBabbaBBbaabAAabbc'|perl script
$ BAAbaabAAabbcBBaaba
    
por 23.06.2014 / 13:22