Sostituisci più stringhe in un unico passaggio


11

Sto cercando un modo per sostituire le stringhe segnaposto in un file modello con valori concreti, con strumenti Unix comuni (bash, sed, awk, forse perl). È importante che la sostituzione avvenga in un unico passaggio, ovvero ciò che è già stato scansionato / sostituito non deve essere preso in considerazione per un'altra sostituzione. Ad esempio, questi due tentativi falliscono:

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

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

Il risultato corretto in questo caso è ovviamente BA.

In generale, la soluzione dovrebbe essere equivalente alla scansione dell'input da sinistra a destra per una corrispondenza più lunga con una delle stringhe di sostituzione fornite e per ciascuna corrispondenza, eseguire una sostituzione e continuare da quel punto in avanti nell'input (nessuna delle già letto input né i sostituti eseguiti devono essere considerati per le partite). In realtà, i dettagli non contano, solo che i risultati della sostituzione non vengono mai considerati per un'altra sostituzione, in tutto o in parte.

NOTA Sto solo cercando soluzioni generiche corrette. Non proporre soluzioni che falliscono per determinati input (file di input, ricerca e sostituzione di coppie), per quanto improbabili possano sembrare.


Presumo che siano più lunghi di un personaggio? Per questo potresti usare tr AB BA.
Kevin,

3
E francamente, non sarei sorpreso se qualcuno considerasse la tua nota un po 'maleducata.
peterph,

1
Come prevedete di "ottenere soluzioni corrette" solo quando non avete fornito input o output di esempio?
jasonwryan,

1
Temo che dovrai farlo esattamente come lo descrivi - analizzalo dall'inizio e sostituiscilo man mano che procedi - cioè non con espressioni regolari.
peterph,

2
Questa è una domanda giusta, ma la risposta è che hai bisogno di un parser per macchine a stati , che è ciò che fornisce la risposta di rici (in vero stile hacker, penso). In altre parole, stai sottovalutando la complessità del compito, ala "Voglio analizzare genericamente (HT | X) ML con espressioni regolari" -> La risposta è NO. Non puoi (solo) usare sed. Non puoi (solo) usare awk. AFAIK non esiste uno strumento esistente che lo farà immediatamente. Per l'exploit di Sans rici, dovresti scrivere del codice.
Riccioli d'oro

Risposte:


10

OK, una soluzione generale. La seguente funzione bash richiede 2kargomenti; ogni coppia è composta da un segnaposto e un sostituto. Sta a te citare le stringhe in modo appropriato per passarle nella funzione. Se il numero di argomenti è dispari, verrà aggiunto un argomento vuoto implicito, che eliminerà effettivamente le occorrenze dell'ultimo segnaposto.

Né i segnaposto né i sostituti possono contenere caratteri NUL, ma puoi usare C- \escape standard come \0se hai bisogno di NULs (e di conseguenza ti viene richiesto di scrivere \\se vuoi un \).

Richiede gli strumenti di costruzione standard che dovrebbero essere presenti su un sistema simile a 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"
}

Partiamo dal presupposto che \è già sfuggito, se necessario, negli argomenti, ma dobbiamo evitare doppie virgolette, se presenti. Ecco cosa fa il secondo argomento del secondo printf. Poiché l' lexazione predefinita è ECHO, non dobbiamo preoccuparci.

Esempio di esecuzione (con tempistiche per lo scettico; è solo un laptop economico-o di materie prime):

$ 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

Per input più grandi potrebbe essere utile fornire un flag di ottimizzazione cce, per l'attuale compatibilità con Posix, sarebbe meglio usarlo c99. Un'implementazione ancora più ambiziosa potrebbe tentare di memorizzare nella cache gli eseguibili generati invece di generarli ogni volta, ma non sono esattamente costosi da generare.

modificare

Se hai tcc , puoi evitare il fastidio di creare una directory temporanea e goderti il ​​tempo di compilazione più veloce che ti aiuterà su input di dimensioni normali:

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

Non sono sicuro che si tratti di uno scherzo o no;)
Ambroz Bizjak,

3
@ambrozbizjak: funziona, è veloce per input di grandi dimensioni e accettabilmente veloce per input di piccole dimensioni. Potrebbe non utilizzare gli strumenti a cui stavi pensando ma sono strumenti standard. Perché sarebbe uno scherzo?
rici,

4
+1 Per non essere uno scherzo! : D
Riccioli d'oro

Sarebbe POSIX portatile come fn() { tcc ; } <<CODE\n$(gen code)\nCODE\n. Posso chiedere però - questa è una risposta fantastica e l'ho votata non appena l'ho letta - ma non capisco cosa sta succedendo alla shell array? Cosa fa "${@//\"/\\\"}"questo?
Mikeserv,

@mikeserv: «Per ogni argomento come valore tra virgolette (" $ @ "), sostituisci tutte le occorrenze (//) di una citazione (\") con (/) una barra rovesciata (\\) seguita da una virgoletta (\ ") ». Vedi Espansione dei parametri nel manuale di 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

Qualcosa del genere sostituirà sempre ogni occorrenza delle stringhe di destinazione una sola volta mentre si verificano sednel flusso a un morso per linea. Questo è il modo più veloce che posso immaginare che lo faresti. Inoltre, non scrivo C. Ma questo lo gestisce in modo affidabile delimitatori null se lo desideri. Vedi questa risposta per come funziona. Ciò non ha alcun problema con caratteri shell speciali simili o simili, ma è specifico per le impostazioni internazionali ASCII o, in altre parole, odnon genererà caratteri multibyte sulla stessa riga e ne farà solo uno per. Se questo è un problema, ti consigliamo di aggiungerlo iconv.


+1 Perché dici che sostituisce solo "la prima occorrenza delle tue stringhe target"? Nell'output sembra che li sostituisca tutti. Non sto chiedendo di vederlo, ma questo potrebbe essere fatto in questo modo senza codificare i valori?
Riccioli d'oro

@goldilocks - Sì - ma non appena si verificano. Forse dovrei riformularlo. E sì - potresti semplicemente aggiungere un punto centrale sede salvare fino a un valore nullo o qualcosa del genere, quindi far sedscrivere questo script; o metterlo in una funzione shell e dargli i valori di un morso per riga come "/$1/"... "/$2/"- forse scriverò anche quelle funzioni ...
Mikeserv

Questo non sembra funzionare nel caso in cui i segnaposto sono PLACE1, PLACE2e PLA. PLAvince sempre. OP dice: "equivalente alla scansione dell'input da sinistra a destra per una corrispondenza più lunga con una delle stringhe di sostituzione fornite" (enfasi aggiunta)
rici

@rici - grazie. Quindi dovrò fare i delimitatori null. Di nuovo in un lampo.
Mikeserv,

@rici - Stavo per pubblicare un'altra versione, che gestirà ciò che descrivi, ma guardandolo di nuovo e non penso che dovrei. Dice più a lungo per una delle stringhe di sostituzione fornite. Questo fa quello. Non vi è alcuna indicazione che una stringa sia un sottoinsieme di un'altra, solo che il valore sostituito può essere. Inoltre, non penso che l'iterazione su un elenco sia un modo valido per risolvere il problema. Dato il problema, come ho capito, questa è una soluzione funzionante.
Mikeserv,

1

Una perlsoluzione Anche se alcuni hanno affermato che non è possibile, ne ho trovato uno, ma in generale una semplice corrispondenza e sostituzione non è possibile e anche peggiora a causa del backtracking di un NFA il risultato può essere inaspettato.

In generale, e questo va detto, il problema produce risultati diversi che dipendono dall'ordine e dalla lunghezza delle tuple di sostituzione. vale a dire:

A B
AA CC

e l'input AAArisulta in BBBo CCB.

Ecco il codice:

#!/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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.