Come posso usare i caratteri jolly inversi o negativi quando il pattern matching in una shell unix / linux?


325

Supponiamo che io voglia copiare il contenuto di una directory escludendo file e cartelle i cui nomi contengono la parola "Musica".

cp [exclude-matches] *Music* /target_directory

Cosa dovrebbe andare al posto di [exclude-match] per raggiungere questo obiettivo?

Risposte:


375

In Bash puoi farlo abilitando l' extglobopzione, in questo modo (sostituisci lscon cpe aggiungi la directory di destinazione, ovviamente)

~/foobar> shopt extglob
extglob        off
~/foobar> ls
abar  afoo  bbar  bfoo
~/foobar> ls !(b*)
-bash: !: event not found
~/foobar> shopt -s extglob  # Enables extglob
~/foobar> ls !(b*)
abar  afoo
~/foobar> ls !(a*)
bbar  bfoo
~/foobar> ls !(*foo)
abar  bbar

In seguito puoi disabilitare extglob con

shopt -u extglob

14
Mi piace questa caratteristica:ls /dir/*/!(base*)
Erick Robertson,

6
Come includi tutto ( ) ed escludi anche! (B )?
Elijah Lynn il

4
Come abbineresti, diciamo, tutto a partire da f, tranne foo?
Noldorin,

8
Perché questo è disabilitato di default?
weberc2,

3
shopt -o -u histexpand se hai bisogno di cercare file con punti esclamativi al loro interno - attivata per impostazione predefinita, extglob è disattivata per impostazione predefinita in modo che non interferisca con histexpand, nei documenti spiega perché è così. abbina tutto ciò che inizia con f tranne foo: f! (oo), ovviamente 'food' corrisponderebbe comunque (avresti bisogno di f! (oo *) per fermare le cose che iniziano in 'foo' o, se vuoi liberarti di certe cose che terminano con '.foo' use! ( .foo), o con prefisso: myprefix! ( .foo) (corrisponde a myprefixBLAH ma non a myprefixBLAH.foo)
osirisgothra,

227

L' extglobopzione shell ti offre una corrispondenza dei pattern più potente nella riga di comando.

Lo accendi con shopt -s extglobe lo spegni con shopt -u extglob.

Nel tuo esempio, inizialmente faresti:

$ shopt -s extglob
$ cp !(*Music*) /target_directory

La piena disposizione ext conclusa glob operatori Bing sono (estratto da man bash):

Se l'opzione shell extglob è abilitata utilizzando lo shopt built-in, vengono riconosciuti diversi operatori di corrispondenza dei modelli estesi. Un elenco di schemi è un elenco di uno o più schemi separati da un |. I pattern compositi possono essere formati usando uno o più dei seguenti sub-pattern:

  • ? (elenco di schemi)
    Corrisponde a zero o una ricorrenza di determinati schemi
  • * (elenco di schemi)
    Corrisponde a zero o più occorrenze di determinati schemi
  • + (elenco di schemi)
    Corrisponde a una o più occorrenze di determinati schemi
  • @ (elenco
    dei modelli ) Corrisponde a uno dei modelli indicati
  • ! (elenco dei modelli)
    Corrisponde a qualsiasi cosa tranne uno dei modelli indicati

Quindi, ad esempio, se si desidera elencare tutti i file nella directory corrente che non lo sono .co i .hfile, è necessario:

$ ls -d !(*@(.c|.h))

Ovviamente, il normale shell globing funziona, quindi l'ultimo esempio potrebbe anche essere scritto come:

$ ls -d !(*.[ch])

1
Qual è il motivo per -d?
Big McLargeHuge,

2
@Koveras nel caso in cui uno dei file .co .hsia una directory.
tzot,

@DaveKennedy Serve per elencare tutto nella directory corrente D, ma non il contenuto delle sottodirectory che potrebbero essere contenute nella directory D.
spurra,

23

Non in bash (che io conosco), ma:

cp `ls | grep -v Music` /target_directory

So che questo non è esattamente quello che stavi cercando, ma risolverà il tuo esempio.


Predefinito ls metterà più file per riga, il che probabilmente non darà i risultati giusti.
Daniel Bungert,

10
Solo quando stdout è un terminale. Se utilizzato in una pipeline, ls stampa un nome file per riga.
Adam Rosenfield,

Mette solo più file per riga se viene inviato a un terminale. Provalo tu stesso - "ls | less" non avrà mai più file per riga.
SpoonMeiser,

3
Non funzionerà con nomi di file contenenti spazi (o altri caratteri bianchi di spazio).
Tzot

7

Se vuoi evitare il costo in mem dell'utilizzo del comando exec, credo che tu possa fare di meglio con xargs. Penso che la seguente sia un'alternativa più efficiente a

find foo -type f ! -name '*Music*' -exec cp {} bar \; # new proc for each exec



find . -maxdepth 1 -name '*Music*' -prune -o -print0 | xargs -0 -i cp {} dest/

6

In bash, un'alternativa a shopt -s extglobè la GLOBIGNOREvariabile . Non è davvero meglio, ma trovo più facile da ricordare.

Un esempio che potrebbe essere quello che voleva il poster originale:

GLOBIGNORE="*techno*"; cp *Music* /only_good_music/

Al termine, unset GLOBIGNOREper poterlo accedere alla rm *techno*directory di origine.


5

Puoi anche usare un forloop piuttosto semplice :

for f in `find . -not -name "*Music*"`
do
    cp $f /target/dir
done

1
Questo fa una scoperta ricorsiva, che è un comportamento diverso da quello che OP vuole.
Adam Rosenfield,

1
usare -maxdepth 1per non ricorsivo?
Avtomaton,

Ho scoperto che questa è la soluzione più pulita senza dover abilitare / disabilitare le opzioni della shell. L'opzione -maxdepth sarebbe raccomandata in questo post per avere il risultato richiesto dall'OP, ma tutto dipende da cosa stai cercando di ottenere.
David Lapointe,

L'uso findnei backtick si interromperà in modo spiacevole se trova nomi di file non banali.
triplo il

5

La mia preferenza personale è usare grep e il comando while. Ciò consente di scrivere script potenti ma leggibili, garantendo di finire per fare esattamente ciò che si desidera. Inoltre, utilizzando un comando echo, è possibile eseguire una corsa a secco prima di eseguire l'operazione effettiva. Per esempio:

ls | grep -v "Music" | while read filename
do
echo $filename
done

stamperà i file che finirai per copiare. Se l'elenco è corretto, il passo successivo è semplicemente sostituire il comando echo con il comando copia come segue:

ls | grep -v "Music" | while read filename
do
cp "$filename" /target_directory
done

1
Funzionerà fintanto che i nomi dei tuoi file non hanno tab, newline, più di uno spazio di fila o barre rovesciate. Mentre quelli sono casi patologici, è bene essere consapevoli della possibilità. In bashpuoi usare while IFS='' read -r filename, ma poi le nuove righe sono ancora un problema. In generale è meglio non usare lsper enumerare i file; strumenti come findsono molto più adatti.
Il

Senza strumenti aggiuntivi:for file in *; do case ${file} in (*Music*) ;; (*) cp "${file}" /target_directory ; echo ;; esac; done
Il

mywiki.wooledge.org/ParsingLs elenca una serie di motivi aggiuntivi per cui dovresti evitarlo.
triplo il

5

Un trucco non ho visto qui ancora che non usa extglob, findo grepè per il trattamento di due liste di file come set e "diff" utilizzando comm:

comm -23 <(ls) <(ls *Music*)

commè preferibile diffperché non ha extra cruft.

Questo restituisce tutti gli elementi del set 1 ls, che non sono anche nel set 2 ls *Music*,. Ciò richiede che entrambi i set siano in ordine per funzionare correttamente. Nessun problema per l' lsespansione globale, ma se stai usando qualcosa del genere find, assicurati di invocare sort.

comm -23 <(find . | sort) <(find . | grep -i '.jpg' | sort)

Potenzialmente utile.


1
Uno dei vantaggi dell'esclusione è di non attraversare la directory in primo luogo. Questa soluzione esegue due attraversamenti di sottodirectory: uno con l'esclusione e uno senza.
Mark Stosberg,

Ottimo punto, @MarkStosberg. Tuttavia, un vantaggio marginale di questa tecnica è che potresti leggere le esclusioni da un file reale, ad esempiocomm -23 <(ls) exclude_these.list
James M. Lay

3

Una soluzione per questo può essere trovata con find.

$ mkdir foo bar
$ touch foo/a.txt foo/Music.txt
$ find foo -type f ! -name '*Music*' -exec cp {} bar \;
$ ls bar
a.txt

Trova ha alcune opzioni, puoi essere abbastanza specifico su cosa includere ed escludere.

Modifica: Adam nei commenti ha notato che questo è ricorsivo. trova opzioni mindepth e maxdepth possono essere utili per controllare questo.


Questo fa una copia ricorsiva, che è un comportamento diverso. Genera anche un nuovo processo per ogni file, che può essere molto inefficiente per un gran numero di file.
Adam Rosenfield,

Il costo di generazione di un processo è approssimativamente pari a zero rispetto a tutto l'IO generato dalla copia di ciascun file. Quindi direi che è abbastanza buono per un uso occasionale.
dland

Alcune soluzioni alternative per la deposizione delle uova processo: stackoverflow.com/questions/186099/...
Vinko Vrsalovic

utilizzare "-maxdepth 1" per evitare la ricorsione.
ejgottl,

usa i backtick per ottenere l'analogo dell'espansione jolly della shell: cp find -maxdepth 1 -not -name '*Music*'/ target_directory
ejgottl

2

Le seguenti opere elencano tutti i *.txtfile nella directory corrente, ad eccezione di quelli che iniziano con un numero.

Questo funziona in bash, dash, zshe tutte le altre shell compatibili POSIX.

for FILE in /some/dir/*.txt; do    # for each *.txt file
    case "${FILE##*/}" in          #   if file basename...
        [0-9]*) continue ;;        #   starts with digit: skip
    esac
    ## otherwise, do stuff with $FILE here
done
  1. Nella riga uno il modello /some/dir/*.txtfarà sì che il forciclo esegua l'iterazione su tutti i file /some/dircon il nome che termina .txt.

  2. Nella riga due viene utilizzata un'istruzione case per eliminare i file indesiderati. - L' ${FILE##*/}espressione rimuove qualsiasi componente principale /some/dir/del nome della directory dal nome file (qui ) in modo che gli schemi possano corrispondere solo al nome base del file. (Se stai solo eliminando i nomi dei file in base ai suffissi, puoi invece accorciarli $FILE.)

  3. Nella riga tre, tutti i file che corrispondono al casemodello [0-9]*) verranno ignorati (l' continueistruzione passa alla successiva iterazione del forciclo). - Se lo desideri, puoi fare qualcosa di più interessante qui, ad esempio saltare tutti i file che non iniziano con una lettera (a-z) [!a-z]*o puoi usare più pattern per saltare diversi tipi di nomi di file, ad esempio [0-9]*|*.bakper saltare i file di entrambi i .bakfile e file che non iniziano con un numero.


Doh! Si è verificato un errore (ho confrontato *.txtinvece che semplicemente *). Riparato ora.
zrajm,

0

questo lo farebbe escludendo esattamente la "musica"

cp -a ^'Music' /target

questo e quello per escludere cose come la musica? * o *? la musica

cp -a ^\*?'complete' /target
cp -a ^'complete'?\* /target

La cppagina di manuale su MacOS ha -aun'opzione ma fa qualcosa di completamente diverso. Quale piattaforma supporta questo?
triplo il
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.