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.