Substitua várias strings em uma única passagem


11

Estou procurando uma maneira de substituir seqüências de caracteres de espaço reservado em um arquivo de modelo por valores concretos, por 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 digitalizado / 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 à varredura da entrada da esquerda para a direita para uma correspondência mais longa com uma das seqüências de substituição especificadas e, para cada correspondência, executando uma substituição e continuando a partir desse ponto na entrada (nenhuma das entrada já lida nem as substituições realizadas devem ser consideradas para 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.

NOTA Estou procurando apenas soluções genéricas corretas. Não proponha soluções que falhem para determinadas entradas (arquivos de entrada, pesquisa e substituição de pares), por mais improváveis ​​que pareçam.


Eu suponho que eles tenham mais de um personagem? Para isso você pode usar tr AB BA.
Kevin

3
E, francamente, não ficaria surpreso se alguém considerasse sua nota um pouco rude.
Peterph

1
Como você espera "obter apenas soluções corretas" quando não forneceu entrada ou saída de amostra?
jasonwryan

1
Receio que você precise fazer exatamente o que está descrevendo - analise desde o início e substitua à medida que avança - ou seja, não com expressões regulares.
peterph

2
Essa é uma pergunta justa, mas a resposta é que você precisa de um analisador de máquina de estado , que é o que a resposta de rici fornece (no verdadeiro estilo hacker, eu acho). Em outras palavras, você está subestimando a complexidade da tarefa, ala "Quero analisar genericamente (HT | X) ML com expressões regulares" -> A resposta é NÃO. Você não pode (apenas) usar sed. Você não pode (apenas) usar o awk. AFAIK não existe uma ferramenta existente que faça isso imediatamente. Sem a exploração de Sans Rici, você precisará escrever algum código.
Goldilocks

Respostas:


10

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

Nem os espaços reservados nem as substituições podem conter caracteres NUL, mas você pode usar \escapes C padrão , como \0se você precisar de NULs (e, consequentemente, precisar escrever \\se quiser \).

Requer as ferramentas de compilação padrão que devem estar presentes em um sistema semelhante ao 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 \já está escapado, se necessário, nos argumentos, mas precisamos escapar de aspas duplas, se presente. É isso que o segundo argumento para o segundo printf faz. Como a lexação padrão é ECHO, não precisamos nos preocupar com isso.

Execução de exemplo (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 cce, para compatibilidade atual com o Posix, seria melhor usar c99. 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ê possui o tcc , pode evitar o incômodo de criar um diretório temporário e aproveitar o tempo de compilação mais rápido, o que ajudará em 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

Eu não tenho certeza se isso é uma piada ou não;)
Ambroz Bizjak

3
@ambrozbizjak: Funciona, é rápido para entradas grandes e aceitavelmente rápido para entradas pequenas. Pode não usar as ferramentas que você estava pensando, mas são ferramentas padrão. Por que seria uma piada?
rici 18/06

4
+1 Por não ser uma piada! : D
goldilocks

Isso seria como POSIX portátil fn() { tcc ; } <<CODE\n$(gen code)\nCODE\n. No entanto, posso perguntar - essa é uma resposta incrível e eu a votei assim que a li - mas não entendo o que está acontecendo com o array de shell? O que "${@//\"/\\\"}"isso faz?
mikeserv

@mikeserv: «Para cada argumento como um valor entre aspas (" $ @ "), substitua todas as ocorrências (//) de uma citação (\") por (/) uma barra invertida (\\) seguida por uma citação (\ ") ». Consulte Expansão de parâmetros no manual do bash.
rici

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 assim sempre substituirá cada ocorrência de suas sequências de destino apenas uma vez, uma vez que elas ocorrem sedno fluxo a uma mordida por linha. Esta é a maneira mais rápida que consigo imaginar. Então, novamente, eu não escrevo C. Mas isso não lidar de forma confiável delimitadores nulos se assim desejar. Veja esta resposta para saber como funciona. Isso não tem problemas com nenhum caractere shell especial ou similar contido - mas é específico da localidade ASCII ou, em outras palavras, odnão produzirá caracteres de vários bytes na mesma linha e fará apenas um por. Se este for um problema, você deverá adicionar iconv.


+1 Por que você diz que substitui apenas "a ocorrência mais antiga de suas sequências de destino"? Na saída, parece que substitui todos eles. Não estou pedindo para vê-lo, mas isso poderia ser feito dessa maneira sem codificar os valores?
precisa

@goldilocks - Sim - mas apenas assim que ocorrerem. Talvez eu deva reformular isso. E sim - você pode simplesmente adicionar um meio sede economizar até um valor nulo ou algo assim, e então sedescrever esse script; ou colocá-lo em uma função shell e dar-lhe os valores em uma mordida por linha como "/$1/"... "/$2/"- talvez eu vou escrever essas funções também ...
mikeserv

Isso não parece funcionar no caso em que os espaços reservados estão PLACE1, PLACE2e PLA. PLAganha sempre. OP diz: "equivalente a digitalização da entrada da esquerda para a direita para um jogo mais longo para uma das cadeias de substituição dadas" (grifo nosso)
rici

@rici - obrigado. Então terei que fazer os delimitadores nulos. De volta em um flash.
mikeserv

@rici - Eu estava prestes a publicar outra versão, que irá lidar com o que você descreve, mas olhando novamente e acho que não deveria. Ele diz mais tempo para uma das cordas de substituição fornecidas. Isso faz isso. Não há indicação de que uma sequência seja um subconjunto de outra, apenas que o valor substituído pode ser. Também não acho que a iteração em uma lista seja uma maneira válida de resolver o problema. Dado o problema como eu o entendo, esta é uma solução funcional.
mikeserv

1

Uma perlsolução Mesmo que alguns tenham declarado que isso não é possível, eu encontrei um, mas geralmente não é possível uma correspondência simples e a substituição e até piora por causa do retorno de uma NFA, o resultado pode ser inesperado.

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

A B
AA CC

e a entrada AAAresulta em BBBou 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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.