Partita non golosa con regex SED (emula perl. *?)


22

Voglio usare sedper sostituire qualsiasi cosa in una stringa tra la prima ABe la prima occorrenza di AC(compreso) con XXX.

Ad esempio , ho questa stringa (questa stringa è solo per un test):

ssABteAstACABnnACss

e vorrei output simile al seguente: ssXXXABnnACss.


Ho fatto questo con perl:

$ echo 'ssABteAstACABnnACss' | perl -pe 's/AB.*?AC/XXX/'
ssXXXABnnACss

ma voglio implementarlo con sed. Quanto segue (utilizzando il regex compatibile Perl) non funziona:

$ echo 'ssABteAstACABnnACss' | sed -re 's/AB.*?AC/XXX/'
ssXXXss

2
Questo non ha senso. Hai una soluzione funzionante in Perl, ma vuoi usare Sed, perché?
Kusalananda

Risposte:


16

Le regex di Sed corrispondono alla partita più lunga. Sed non ha equivalenti di non avidi.

Ovviamente quello che vogliamo fare è abbinare

  1. AB,
    seguito da
  2. qualsiasi quantità diversa da AC,
    seguita da
  3. AC

Sfortunatamente, sednon posso fare # 2 - almeno non per un'espressione regolare multi-carattere. Naturalmente, per un'espressione regolare a carattere singolo come @(o anche [123]), possiamo fare [^@]*o [^123]*. E così siamo in grado di aggirare le limitazioni del sed modificando tutte le occorrenze di ACa @e poi alla ricerca di

  1. AB,
    seguito da
  2. qualsiasi numero diverso da @,
    seguito da
  3. @

come questo:

sed 's/AC/@/g; s/AB[^@]*@/XXX/; s/@/AC/g'

L'ultima parte cambia le istanze ineguagliate di @back in AC.

Ma, naturalmente, questo è un approccio sconsiderato, perché l'input potrebbe già contenere @caratteri, quindi, abbinandoli, potremmo ottenere falsi positivi. Tuttavia, poiché nessuna variabile di shell avrà mai un carattere NUL ( \x00) al suo interno, NUL è probabilmente un buon carattere da usare nella soluzione precedente invece di @:

$ echo 'ssABteAstACABnnACss' | sed 's/AC/\x00/g; s/AB[^\x00]*\x00/XXX/; s/\x00/AC/g'
ssXXXABnnACss

L'uso di NUL richiede GNU sed. (Per assicurarsi che le funzionalità GNU siano abilitate, l'utente non deve aver impostato la variabile shell POSIXLY_CORRECT.)

Se stai usando sed con il -zflag GNU per gestire l'input separato da NUL, come l'output di find ... -print0, allora NUL non sarà nello spazio del pattern e NUL è una buona scelta per la sostituzione qui.

Sebbene NUL non possa trovarsi in una variabile bash, è possibile includerlo in un printfcomando. Se la tua stringa di input può contenere qualsiasi carattere, incluso NUL, vedi la risposta di Stéphane Chazelas che aggiunge un metodo di escape intelligente.


Ho appena modificato la tua risposta per aggiungere una lunga spiegazione; sentiti libero di tagliarlo o arrotolarlo indietro.
G-Man dice "Ripristina Monica" il

@ G-Man Questa è un'ottima spiegazione! Molto ben fatto. Grazie.
Giovanni 1024,

Puoi echoo printfun '\ 000' bene in bash (o l'input potrebbe provenire da un file). Ma in generale, è probabile che una stringa di testo non abbia NUL.
ilkkachu,

@ilkkachu Hai ragione. Quello che avrei dovuto scrivere è che nessuna variabile di shell o parametro può contenere NUL. Risposta aggiornata
Giovanni 1024,

Non sarebbe molto più sicuro se cambiassi ACdi AC@nuovo?
Michael Vehrs,

7

Alcune sedimplementazioni lo supportano. ssedha una modalità PCRE:

ssed -R 's/AB.*?AC/XXX/g'

AT&T ast sed ha congiunzione e negazione quando si usano regexps aumentati :

sed -A 's/AB(.*&(.*AC.*)!)AC/XXX/g'

Portabilmente, puoi usare questa tecnica: sostituisci la stringa di fine (qui AC) con un singolo carattere che non si presenta né nella stringa di inizio né di fine (come :qui) in modo da poterlo fare s/AB[^:]*://, e nel caso in cui quel carattere possa apparire nell'input , utilizza un meccanismo di escape che non si scontra con le stringhe di inizio e fine.

Un esempio:

sed 's/_/_u/g; # use _ as the escape character, escape it
     s/:/_c/g; # escape our replacement character
     s/AC/:/g; # replace the end string
     s/AB[^:]*:/XXX/g; # actual replacement
     s/:/AC/g; # restore the remaining end strings
     s/_c/:/g; # revert escaping
     s/_u/_/g'

Con GNU sed, un approccio consiste nell'utilizzare newline come personaggio sostitutivo. Poiché sedelabora una riga alla volta, newline non si verifica mai nello spazio del modello, quindi è possibile eseguire:

sed 's/AC/\n/g;s/AB[^\n]*\n/XXX/g;s/\n/AC/g'

Questo in genere non funziona con altre sedimplementazioni perché non supportano [^\n]. Con GNU seddevi assicurarti che la compatibilità POSIX non sia abilitata (come con la variabile d'ambiente POSIXLY_CORRECT).


6

No, i regex sed non hanno una corrispondenza non avida.

Puoi abbinare tutto il testo fino alla prima occorrenza di ACutilizzando "qualsiasi cosa che non contieneAC " seguito da AC, che fa lo stesso di Perl .*?AC. Il fatto è che "tutto ciò che non contiene AC" non può essere espresso facilmente come espressione regolare: esiste sempre un'espressione regolare che riconosce la negazione di un'espressione regolare, ma la regex della negazione si complica rapidamente. E in sed portatile, questo non è affatto possibile, perché la regex di negazione richiede il raggruppamento di un'alternanza che è presente in espressioni regolari estese (ad esempio in awk) ma non in espressioni regolari di base portatili. Alcune versioni di sed, come GNU sed, hanno estensioni a BRE che lo rendono in grado di esprimere tutte le possibili espressioni regolari.

sed 's/AB\([^A]*\|A[^C]\)*A*AC/XXX/'

A causa della difficoltà di negare una regex, questo non si generalizza bene. Quello che puoi fare invece è trasformare temporaneamente la linea. In alcune implementazioni di sed, è possibile utilizzare newline come marker, poiché non possono apparire in una riga di input (e se sono necessari più marker, utilizzare newline seguito da un carattere variabile).

sed -e 's/AC/\
&/g' -e 's/AB[^\
]*\nAC/XXX/' -e 's/\n//g'

Tuttavia, attenzione che backslash-newline non funziona in un set di caratteri con alcune versioni sed. In particolare, ciò non funziona in GNU sed, che è l'implementazione di sed su Linux non incorporato; in GNU sed puoi usare\n invece :

sed -e 's/AC/\
&/g' -e 's/AB[^\n]*\nAC/XXX/' -e 's/\n//g'

In questo caso specifico, è sufficiente sostituire il primo ACcon una nuova riga. L'approccio che ho presentato sopra è più generale.

Un approccio più potente in sed è quello di salvare la linea nello spazio di trattenimento, rimuovere tutto tranne la prima parte "interessante" della linea, scambiare lo spazio di trattenuta e lo spazio del motivo o aggiungere lo spazio del motivo allo spazio di trattenimento e ripetere. Tuttavia, se inizi a fare cose così complicate, dovresti davvero pensare di passare a awk. Awk non ha neanche una corrispondenza non avida, ma puoi dividere una stringa e salvare le parti in variabili.


@ilkkachu No, non lo è. s/\n//grimuove tutte le nuove righe.
Gilles 'SO- smetti di essere malvagio' il

asdf. Giusto, mio ​​cattivo.
ilkkachu,

3

sed - abbinamento non goloso di Christoph Sieghart

Il trucco per ottenere una corrispondenza non avida in sed è quello di abbinare tutti i personaggi escluso quello che termina la partita. Lo so, un gioco da ragazzi, ma ho sprecato minuti preziosi su di esso e gli script di shell dovrebbero essere, dopo tutto, facili e veloci. Quindi nel caso qualcun altro ne avesse bisogno:

Abbinamento goloso

% echo "<b>foo</b>bar" | sed 's/<.*>//g'
bar

Abbinamento non avido

% echo "<b>foo</b>bar" | sed 's/<[^>]*>//g'
foobar


3
Il termine "no-brainer" è ambiguo. In questo caso, non è chiaro che tu (o Christoph Sieghart) ci abbia pensato. In particolare, sarebbe stato carino se tu avessi mostrato come risolvere il problema specifico nella domanda (dove lo zero di più di espressione è seguito da più di un carattere ) . Potresti scoprire che questa risposta non funziona bene in quel caso.
Scott,

La tana del coniglio è molto più profonda di quanto mi sembrasse a prima vista. Hai ragione, quella soluzione alternativa non funziona bene per l'espressione regolare multi-carattere.
gresolio,

0

Nel tuo caso puoi semplicemente negare la chiusura del carattere in questo modo:

echo 'ssABteAstACABnnACss' | sed 's/AB[^C]*AC/XXX/'

2
La domanda dice: "Voglio sostituire qualsiasi cosa tra la prima ABe la prima occorrenza di ACcon XXX..." e fornisce ssABteAstACABnnACsscome input un esempio . Questa risposta funziona per quell'esempio , ma non risponde alla domanda in generale. Ad esempio, ssABteCstACABnnACssdovrebbe anche produrre l'output aaXXXABnnACss, ma il comando passa questa riga invariato.
G-Man dice "Ripristina Monica" il

0

La soluzione è abbastanza semplice .*è avido, ma non è assolutamente avido. Prendi in considerazione la corrispondenza ssABteAstACABnnACsscon regexp AB.*AC. Ciò ACche segue .*deve effettivamente avere una corrispondenza. Il problema è che, poiché .*è avido, il successivo ACcorrisponderà all'ultimo AC anziché al primo. .*mangia il primo ACmentre il letterale ACnella regexp corrisponde all'ultimo in ssABteAstACABnn AC ss. Per evitare che ciò accada, sostituisci semplicemente il primo ACcon qualcosa di ridicolo per differenziarlo dal secondo e da qualsiasi altra cosa.

echo ssABteAstACABnnACss | sed 's/AC/-foobar-/; s/AB.*-foobar-/XXX/'
ssXXXABnnACss

L'avido .*ora si fermerà ai piedi di -foobar-dentro ssABteAst-foobar-ABnnACssperché non c'è nient'altro -foobar-che questo -foobar-e il regexp -foobar- DEVE avere una corrispondenza. Il problema precedente era che il regexp ACaveva due partite, ma poiché .*era avido, ACera stata selezionata l'ultima partita per . Tuttavia, con -foobar-, è possibile solo una partita e questa partita dimostra che .*non è assolutamente avido. La fermata del bus si .*verifica dove rimane solo una corrispondenza per il resto della regexp seguente .*.

Nota che questa soluzione fallirà se ACappare prima della prima ABperché l'errore ACverrà sostituito con -foobar-. Ad esempio, dopo la prima sedsostituzione, ACssABteAstACABnnACssdiventa -foobar-ssABteAstACABnnACss; pertanto, non è possibile trovare una corrispondenza contro AB.*-foobar-. Tuttavia, se la sequenza è sempre ... AB ... AC ... AB ... AC ..., allora questa soluzione avrà successo.


0

Un'alternativa è cambiare la stringa in modo che tu voglia la partita golosa

echo "ssABtCeCAstACABnnACss" | rev | sed -E "s/(.*)CA.*BA(.*)/\1CA+-+-+-+-BA\2/" | rev

Utilizzare revper invertire la stringa, invertire i criteri di corrispondenza, utilizzare sednel solito modo e quindi invertire il risultato ....

ssAB-+-+-+-+ACABnnACss
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.