Catturare gruppi da un GreEx RegEx


380

Ho questo piccolo script in sh(Mac OSX 10.6) per sfogliare una serie di file. Google ha smesso di essere utile a questo punto:

files="*.jpg"
for f in $files
    do
        echo $f | grep -oEi '[0-9]+_([a-z]+)_[0-9a-z]*'
        name=$?
        echo $name
    done

Fino ad ora (ovviamente, per voi guru della shell) $namevale solo 0, 1 o 2, a seconda se si greptrova che il nome file corrisponde all'argomento fornito. Quello che mi piacerebbe è catturare ciò che è dentro le parentesi ([a-z]+)e archiviarlo in una variabile .

Mi piacerebbe usare grepsolo, se possibile . Altrimenti, per favore niente Python o Perl, ecc. sedO qualcosa del genere - Sono nuovo a sgusciare e vorrei attaccarlo dall'angolo purista * nix.

Inoltre, come un super cool , sono curioso di sapere come posso concatenare la stringa nella shell? Il gruppo che ho catturato era la stringa "somename" memorizzata in $ name e volevo aggiungere la stringa ".jpg" alla fine, posso cat $name '.jpg'?

Per favore, spiega cosa sta succedendo, se hai tempo.


30
Grep è davvero unix più puro di sed?
Martin Clayton,

3
Ah, non intendevo suggerirlo. Speravo solo che si potesse trovare una soluzione usando uno strumento che sto cercando di imparare qui. Se non è possibile risolvere usando grep, allora sedsarebbe grandioso, se è possibile risolvere usando sed.
Isaac,

2
Avrei dovuto mettere un :) su quel btw ...
Martin Clayton,

Psh, il mio cervello è troppo fritto oggi haha.
Isaac,

2
@martinclayton Sarebbe un argomento interessante. Penso davvero che sed, (o ed per essere precisi) sarebbe più vecchio (e quindi più puro? Forse?) Unix perché grep deriva il suo nome dall'espressione ed g (lobal) / re (espressione gular) / p (rint).
sfogliando il

Risposte:


500

Se stai usando Bash, non devi nemmeno usare grep:

files="*.jpg"
regex="[0-9]+_([a-z]+)_[0-9a-z]*"
for f in $files    # unquoted in order to allow the glob to expand
do
    if [[ $f =~ $regex ]]
    then
        name="${BASH_REMATCH[1]}"
        echo "${name}.jpg"    # concatenate strings
        name="${name}.jpg"    # same thing stored in a variable
    else
        echo "$f doesn't match" >&2 # this could get noisy if there are a lot of non-matching files
    fi
done

È meglio mettere la regex in una variabile. Alcuni schemi non funzioneranno se inclusi letteralmente.

Questo utilizza =~quale è l'operatore di corrispondenza regex di Bash. I risultati della partita vengono salvati in un array chiamato $BASH_REMATCH. Il primo gruppo di acquisizione è memorizzato nell'indice 1, il secondo (se presente) nell'indice 2, ecc. L'indice zero è la corrispondenza completa.

Dovresti essere consapevole che senza ancore, questa regex (e quella che usa grep) corrisponderà a uno dei seguenti esempi e altro, che potrebbe non essere quello che stai cercando:

123_abc_d4e5
xyz123_abc_d4e5
123_abc_d4e5.xyz
xyz123_abc_d4e5.xyz

Per eliminare il secondo e il quarto esempio, rendi il tuo regex in questo modo:

^[0-9]+_([a-z]+)_[0-9a-z]*

che dice che la stringa deve iniziare con una o più cifre. Il carato rappresenta l'inizio della stringa. Se aggiungi un segno di dollaro alla fine della regex, in questo modo:

^[0-9]+_([a-z]+)_[0-9a-z]*$

quindi anche il terzo esempio verrà eliminato poiché il punto non è tra i caratteri nella regex e il simbolo del dollaro rappresenta la fine della stringa. Nota che anche il quarto esempio fallisce questa corrispondenza.

Se hai GNU grep(circa 2.5 o versioni successive, penso, quando l' \Koperatore è stato aggiunto):

name=$(echo "$f" | grep -Po '(?i)[0-9]+_\K[a-z]+(?=_[0-9a-z]*)').jpg

L' \Koperatore (look-behind a lunghezza variabile) fa corrispondere il modello precedente, ma non include la corrispondenza nel risultato. L'equivalente a lunghezza fissa è (?<=)- il modello sarebbe incluso prima della parentesi di chiusura. È necessario utilizzare \Kse quantificatori possono corrispondere a stringhe di lunghezza diversa (ad esempio +, *, {2,4}).

L' (?=)operatore abbina schemi a lunghezza fissa o variabile e viene chiamato "look-ahead". Inoltre non include la stringa corrispondente nel risultato.

Al fine di rendere la corrispondenza senza distinzione tra maiuscole e minuscole, (?i)viene utilizzato l' operatore. Colpisce i modelli che lo seguono, quindi la sua posizione è significativa.

Potrebbe essere necessario modificare la regex in base alla presenza di altri caratteri nel nome file. Noterai che in questo caso, mostro un esempio di concatenazione di una stringa mentre viene catturata la sottostringa.


48
In questa risposta voglio dare un voto alla riga specifica che dice "È meglio mettere la regex in una variabile. Alcuni schemi non funzioneranno se inclusi alla lettera".
Brandin,

5
@FrancescoFrassinelli: un esempio è un modello che include spazi bianchi. È scomodo scappare e non puoi usare le virgolette poiché ciò lo costringe da una regex a una stringa normale. Il modo corretto per farlo è utilizzare una variabile. Le citazioni possono essere utilizzate durante il compito rendendo le cose molto più semplici.
In pausa fino a nuovo avviso.

5
/Koperatore rocce.
Razz,

2
@Brandon: funziona. Quale versione di Bash stai usando? Fammi vedere cosa stai facendo che non funziona e forse posso dirti perché.
In pausa fino a nuovo avviso.

2
@mdelolmo: la mia risposta include informazioni su grep. È stato anche accettato dall'OP e votato abbastanza. Grazie per il downvote.
In pausa fino a nuovo avviso.

145

Questo non è davvero possibile con puro grep, almeno non in generale.

Ma se il tuo modello è adatto, potresti essere in grado di utilizzare greppiù volte all'interno di una pipeline per ridurre prima la linea in un formato noto, quindi estrarre solo il bit desiderato. (Sebbene gli strumenti piacciano cute sedsiano molto migliori in questo).

Supponiamo per amor di discussione che il tuo modello fosse un po 'più semplice: [0-9]+_([a-z]+)_potresti estrarlo in questo modo:

echo $name | grep -Ei '[0-9]+_[a-z]+_' | grep -oEi '[a-z]+'

Il primo greprimuove tutte le righe che non corrispondono al tuo patern generale, il secondo grep(che ha --only-matchingspecificato) mostrerà la parte alfa del nome. Questo funziona solo perché il modello è adatto: "porzione alfa" è abbastanza specifica da estrarre ciò che vuoi.

(A parte: Personalmente userei grep+ cutper ottenere ciò che stai cercando:. echo $name | grep {pattern} | cut -d _ -f 2Questo arriva cutad analizzare la linea in campi dividendo il delimitatore _, e restituisce solo il campo 2 (i numeri dei campi iniziano da 1).

La filosofia di Unix è quella di avere strumenti che facciano una cosa, e la facciano bene, e combinandoli per raggiungere compiti non banali, quindi direi che grep+ sedetc è un modo più Unixy di fare le cose :-)


3
for f in $files; do name=echo $ f | grep -oEi '[0-9] + _ ([az] +) _ [0-9a-z] *' | cut -d _ -f 2 ;Ah!
Isacco

2
non sono d'accordo con quella "filosofia". se puoi usare le funzionalità integrate della shell senza chiamare comandi esterni, il tuo script sarà molto più veloce nelle prestazioni. ci sono alcuni strumenti che si sovrappongono in funzione. ad esempio grep, sed e awk. tutti eseguono manipolazioni di stringhe, ma awk si distingue soprattutto perché può fare molto di più. In pratica, tutti quei concatenamenti di comandi, come i doppi greps sopra o grep + sed, possono essere abbreviati eseguendoli con un processo awk.
ghostdog74,

7
@ ghostdog74: Qui non c'è alcun argomento sul fatto che concatenare molte piccole operazioni insieme sia generalmente meno efficiente di fare tutto in un unico posto, ma sostengo la mia affermazione che la filosofia Unix è un sacco di strumenti che lavorano insieme. Ad esempio, tar archivia solo i file, non li comprime e, poiché viene emesso su STDOUT per impostazione predefinita, è possibile reindirizzarlo attraverso la rete con netcat o comprimerlo con bzip2, ecc. Che secondo me rafforza la convenzione e il generale l'etica secondo cui gli strumenti Unix dovrebbero essere in grado di lavorare insieme in tubi.
RobM,

il taglio è fantastico - grazie per la punta! Per quanto riguarda l'argomento strumenti vs efficienza, mi piace la semplicità degli strumenti concatenati.
ether_joe,

oggetti di scena per l'opzione grep o, che è molto utile
chiliNUT

96

Mi rendo conto che una risposta era già stata accettata per questo, ma da un "angolo purista rigorosamente * nix" sembra che sia lo strumento giusto per il lavoro pcregrep, che non sembra essere stato ancora menzionato. Prova a cambiare le linee:

    echo $f | grep -oEi '[0-9]+_([a-z]+)_[0-9a-z]*'
    name=$?

al seguente:

    name=$(echo $f | pcregrep -o1 -Ei '[0-9]+_([a-z]+)_[0-9a-z]*')

per ottenere solo i contenuti del gruppo di acquisizione 1.

Il pcregrep strumento utilizza la stessa sintassi con cui hai già utilizzato grep, ma implementa le funzionalità di cui hai bisogno.

Il parametro -o funziona esattamente come la grepversione se è nudo, ma accetta anche un parametro numerico pcregrep, che indica quale gruppo di acquisizione si desidera mostrare.

Con questa soluzione è necessario un minimo di modifica nello script. È sufficiente sostituire un'utilità modulare con un'altra e modificare i parametri.

Nota interessante: è possibile utilizzare più argomenti -o per restituire più gruppi di acquisizione nell'ordine in cui vengono visualizzati sulla riga.


3
pcregrepnon è disponibile per impostazione predefinita in Mac OS Xcui viene utilizzato l'OP
grebneke

4
Il mio pcregrepnon sembra capire la cifra dopo il -o: "Lettera d'opzione sconosciuta '1' in" -o1 ". Inoltre, nessuna menzione di quella funzionalità quando si guardapcregrep --help
Peter Herdenborg

1
@WAF scusa, suppongo che avrei dovuto includere tali informazioni nel mio commento. Sono su CentOS 6.5 e la versione pcregrep è apparentemente molto antiche: 7.8 2008-09-05.
Peter Herdenborg,

2
sì, molto aiuto, ad esempioecho 'r123456 foo 2016-03-17' | pcregrep -o1 'r([0-9]+)' 123456
zhuguowei

5
pcregrep8.41 (installato con apt-get install pcregrepon Ubuntu 16.03) non riconosce lo -Eiswitch. Funziona perfettamente senza di essa, però. Su macOS, con pcregrepinstallato tramite homebrew(anche 8.41) come menzionato sopra @anishpatel, almeno su High Sierra lo -Eswitch non è riconosciuto.
Ville,

27

Credo che non sia possibile solo in grep

per sed:

name=`echo $f | sed -E 's/([0-9]+_([a-z]+)_[0-9a-z]*)|.*/\2/'`

Prenderò una pugnalata al bonus però:

echo "$name.jpg"

2
Sfortunatamente, quella sedsoluzione non funziona. Stampa semplicemente tutto nella mia directory.
Isaac,

aggiornato, produrrà una riga vuota se non c'è una corrispondenza, quindi assicurati di controllarlo
cobbal

Ora emette solo righe vuote!
Isaac,

questa sed ha un problema. Il primo gruppo di parentesi di cattura comprende tutto. Naturalmente \ 2 non avrà nulla.
ghostdog74,

ha funzionato per alcuni semplici casi di test ... \ 2 ottiene il gruppo interno
cobbal

16

Questa è una soluzione che utilizza gawk. È qualcosa che trovo che devo usare spesso, quindi ho creato una funzione per questo

function regex1 { gawk 'match($0,/'$1'/, ary) {print ary['${2:-'1'}']}'; }

da usare basta

$ echo 'hello world' | regex1 'hello\s(.*)'
world

Ottima idea, ma non sembra funzionare con gli spazi nella regexp - devono essere sostituiti con \s. Sai come ripararlo?
Adam Ryczkowski,

4

Un suggerimento per te: puoi usare l'espansione dei parametri per rimuovere la parte del nome dall'ultimo trattino basso in poi, e allo stesso modo all'inizio:

f=001_abc_0za.jpg
work=${f%_*}
name=${work#*_}

Quindi nameavrà il valore abc.

Consulta i documenti per gli sviluppatori Apple , cerca "Espansione parametri".


questo non verificherà la presenza di ([az] +).
ghostdog74,

@levislevis: è vero, ma, come commentato dall'OP, fa quello che era necessario.
Martin Clayton,

2

se hai bash, puoi usare il globbing esteso

shopt -s extglob
shopt -s nullglob
shopt -s nocaseglob
for file in +([0-9])_+([a-z])_+([a-z0-9]).jpg
do
   IFS="_"
   set -- $file
   echo "This is your captured output : $2"
done

o

ls +([0-9])_+([a-z])_+([a-z0-9]).jpg | while read file
do
   IFS="_"
   set -- $file
   echo "This is your captured output : $2"
done

Sembra intrigante. Potresti forse aggiungere una piccola spiegazione ad esso? Oppure, se sei così propenso, link a una risorsa particolarmente penetrante che lo spiega? Grazie!
Isacco
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.