Comment puis-je utiliser des caractères génériques inverses ou négatifs lors de la correspondance de motifs dans un shell Unix / Linux?

325

Supposons que je souhaite copier le contenu d'un répertoire à l'exclusion des fichiers et dossiers dont les noms contiennent le mot «Musique».

cp [exclude-matches] *Music* /target_directory

Qu'est-ce qui devrait remplacer [exclude-matches] pour accomplir cela?

utilisateur4812
la source

Réponses:

375

Dans Bash , vous pouvez le faire en permettant à l' extgloboption comme celui - ci (remplacer lsavec cpet ajoutez le répertoire cible, bien sûr)

~/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

Vous pouvez ultérieurement désactiver extglob avec

shopt -u extglob
Vinko Vrsalovic
la source
14
J'aime cette fonctionnalité:ls /dir/*/!(base*)
Erick Robertson
6
Comment inclure tout ( ) et exclure également! (B )?
Elijah Lynn
4
Comment associeriez-vous, disons, tout à partir de f, sauf foo?
Noldorin
8
Pourquoi est-ce désactivé par défaut?
weberc2
3
shopt -o -u histexpand si vous avez besoin de rechercher des fichiers contenant des points d'exclamation - activé par défaut, extglob est désactivé par défaut afin qu'il n'interfère pas avec histexpand, dans les documents, il explique pourquoi il en est ainsi. correspond à tout ce qui commence par f sauf foo: f! (oo), bien sûr, 'food' correspondrait toujours (vous auriez besoin de f! (oo *) pour arrêter les choses qui commencent par 'foo' ou, si vous voulez vous débarrasser de certaines choses se terminant par '.foo' utilisent! ( .foo), ou préfixées: myprefix! ( .foo) (correspond à myprefixBLAH mais pas myprefixBLAH.foo)
osirisgothra
227

L' extgloboption shell vous donne une correspondance de motifs plus puissante dans la ligne de commande.

Vous l'allumez avec shopt -s extglobet l'éteignez avecshopt -u extglob .

Dans votre exemple, vous feriez initialement:

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

Le complet est disponible poste a pris fin glob opérateurs bing sont (extrait man bash):

Si l'option shell extglob est activée à l'aide de la fonction intégrée shopt, plusieurs opérateurs de correspondance de modèle étendus sont reconnus. Une liste de modèles est une liste d'un ou plusieurs modèles séparés par un |. Les motifs composites peuvent être formés en utilisant un ou plusieurs des sous-motifs suivants:

  • ? (liste de motifs)
    Correspond à zéro ou une occurrence des motifs donnés
  • * (liste de modèles)
    Correspond à zéro ou plusieurs occurrences des modèles donnés
  • + (liste de motifs)
    Correspond à une ou plusieurs occurrences des motifs donnés
  • @ (pattern-list)
    Correspond à l'un des modèles donnés
  • ! (liste de motifs)
    Correspond à tout sauf un des motifs donnés

Ainsi, par exemple, si vous vouliez lister tous les fichiers du répertoire courant qui ne le sont pas .cou les .hfichiers, vous feriez:

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

Bien sûr, le globing shell normal fonctionne, donc le dernier exemple pourrait également s'écrire:

$ ls -d !(*.[ch])
tzot
la source
1
Quelle est la raison de -d?
Big McLargeHuge
2
@Koveras pour le cas où l'un des fichiers .cou .hest un répertoire.
tzot
@DaveKennedy C'est pour lister tout dans le répertoire courant D, mais pas le contenu des sous-répertoires qui peuvent être contenus dans le répertoire D.
spurra
23

Pas en bash (que je sache), mais:

cp `ls | grep -v Music` /target_directory

Je sais que ce n'est pas exactement ce que vous cherchiez, mais cela résoudra votre exemple.

ejgottl
la source
Par défaut, ls mettra plusieurs fichiers par ligne, ce qui ne donnera probablement pas les bons résultats.
Daniel Bungert
10
Uniquement lorsque stdout est un terminal. Lorsqu'il est utilisé dans un pipeline, ls affiche un nom de fichier par ligne.
Adam Rosenfield
ls ne place plusieurs fichiers par ligne qu'en sortie vers un terminal. Essayez-le vous-même - "ls | less" n'aura jamais plusieurs fichiers par ligne.
SpoonMeiser
3
Cela ne fonctionnera pas pour les noms de fichiers contenant des espaces (ou d'autres caractères d'espacement blancs).
tzot
7

Si vous voulez éviter le coût mem de l'utilisation de la commande exec, je pense que vous pouvez faire mieux avec xargs. Je pense que ce qui suit est une alternative plus efficace à

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/
Steve
la source
6

En bash, une alternative à shopt -s extglobest la GLOBIGNOREvariable . Ce n'est pas vraiment mieux, mais je trouve ça plus facile à retenir.

Un exemple qui pourrait être ce que l'affiche originale voulait:

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

Une fois terminé, unset GLOBIGNOREpour pouvoir rm *techno*dans le répertoire source.

mivk
la source
5

Vous pouvez également utiliser une forboucle assez simple :

for f in `find . -not -name "*Music*"`
do
    cp $f /target/dir
done
mipadi
la source
1
Cela fait une recherche récursive, qui est un comportement différent de ce que souhaite OP.
Adam Rosenfield
1
utiliser -maxdepth 1pour non récursif?
avtomaton
J'ai trouvé que c'était la solution la plus propre sans avoir à activer / désactiver les options du shell. L'option -maxdepth serait recommandée dans ce post pour avoir le résultat requis par l'OP, mais tout dépend de ce que vous essayez d'accomplir.
David Lapointe,
L'utilisation findde backticks se brisera de manière désagréable si elle trouve des noms de fichiers non triviaux.
tripleee
5

Ma préférence personnelle est d'utiliser grep et la commande while. Cela permet d'écrire des scripts puissants mais lisibles en vous assurant de faire exactement ce que vous voulez. De plus, en utilisant une commande d'écho, vous pouvez effectuer un essai à sec avant d'effectuer l'opération réelle. Par exemple:

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

imprimera les fichiers que vous finirez par copier. Si la liste est correcte, l'étape suivante consiste à remplacer simplement la commande echo par la commande copy comme suit:

ls | grep -v "Music" | while read filename
do
cp "$filename" /target_directory
done
Abid H. Mujtaba
la source
1
Cela fonctionnera tant que les noms de vos fichiers n'ont pas d'onglets, de nouvelles lignes, plus d'un espace dans une rangée ou des barres obliques inverses. Bien qu'il s'agisse de cas pathologiques, il est bon d'en être conscient. Dans bashvous pouvez utiliser while IFS='' read -r filename, mais les sauts de ligne restent un problème. En général, il est préférable de ne pas utiliser lspour énumérer les fichiers; des outils comme findsont beaucoup mieux adaptés.
Jeudi
Sans outils supplémentaires:for file in *; do case ${file} in (*Music*) ;; (*) cp "${file}" /target_directory ; echo ;; esac; done
Jeudi
mywiki.wooledge.org/ParsingLs énumère un certain nombre de raisons supplémentaires pour lesquelles vous devriez éviter cela.
tripleee
5

Un truc que je ne l' ai pas vu ici encore qui n'utilise pas extglob, findou grepest de traiter deux listes de fichiers comme des ensembles et « diff » en les utilisant comm:

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

commest préférable diffcar il ne contient pas de cruches supplémentaires

Cela renvoie tous les éléments de l'ensemble 1,, lsqui ne sont pas également dans l'ensemble 2 ls *Music*,. Cela nécessite que les deux ensembles soient triés pour fonctionner correctement. Pas de problème pour lset l'expansion globale, mais si vous utilisez quelque chose comme find, assurez-vous d'invoquer sort.

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

Potentiellement utile.

James M. Lay
la source
1
L'un des avantages de l'exclusion est de ne pas parcourir le répertoire en premier lieu. Cette solution fait deux traversées de sous-répertoires - un avec l'exclusion et un sans.
Mark Stosberg
Très bon point, @MarkStosberg. Cependant, un avantage supplémentaire de cette technique est que vous pouvez lire les exclusions d'un fichier réel, par exemplecomm -23 <(ls) exclude_these.list
James M. Lay
3

Une solution pour cela peut être trouvée avec find.

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

Find a plusieurs options, vous pouvez être assez précis sur ce que vous incluez et excluez.

Edit: Adam dans les commentaires a noté que c'est récursif. les options mindepth et maxdepth peuvent être utiles pour contrôler cela.

Daniel Bungert
la source
Cela fait une copie récursive, ce qui est un comportement différent. Il génère également un nouveau processus pour chaque fichier, qui peut être très inefficace pour un grand nombre de fichiers.
Adam Rosenfield
Le coût de création d'un processus est d'environ zéro par rapport à tous les E / S générés par la copie de chaque fichier. Je dirais donc que c'est assez bon pour une utilisation occasionnelle.
dland
Quelques solutions pour le processus d'
apparition
utilisez "-maxdepth 1" pour éviter la récursivité.
ejgottl
utilisez des backticks pour obtenir l'analogue de l'expansion du joker du shell: cp find -maxdepth 1 -not -name '*Music*'/ target_directory
ejgottl
2

Les travaux suivants répertorient tous les *.txtfichiers du répertoire en cours, à l'exception de ceux qui commencent par un nombre.

Cela fonctionne bash, dash, zshet tous les autres shells compatibles 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. Dans la première ligne, le motif /some/dir/*.txtprovoquera l' foritération de la boucle sur tous les fichiers /some/dirdont le nom se termine par .txt.

  2. Dans la ligne deux, une déclaration de cas est utilisée pour éliminer les fichiers indésirables. - L' ${FILE##*/}expression retire tout composant de nom de /some/dir/répertoire principal du nom de fichier (ici ) afin que les patters puissent correspondre uniquement avec le nom de base du fichier. (Si vous ne supprimez que les noms de fichiers basés sur des suffixes, vous pouvez les raccourcir à la $FILEplace.)

  3. À la ligne 3, tous les fichiers correspondant au casemodèle [0-9]*) seront ignorés (l' continueinstruction passe à l'itération suivante de la forboucle). - Si vous le souhaitez, vous pouvez faire quelque chose de plus intéressant ici, par exemple, ignorer tous les fichiers qui ne commencent pas par une lettre (a – z) [!a-z]*, ou vous pouvez utiliser plusieurs modèles pour ignorer plusieurs types de noms de fichiers, par exemple [0-9]*|*.bakpour ignorer les fichiers des deux .bakfichiers et les fichiers qui ne commencent pas par un nombre.

zrajm
la source
Ah! Il y avait un bug (je me suis opposé *.txtau lieu de juste *). Fixé maintenant.
zrajm
0

cela le ferait en excluant exactement «Musique»

cp -a ^'Music' /target

ceci et cela pour exclure des choses comme la musique? * ou *? la musique

cp -a ^\*?'complete' /target
cp -a ^'complete'?\* /target
gabreal
la source
La cppage de manuel sur MacOS a une -aoption mais elle fait quelque chose de complètement différent. Quelle plateforme prend en charge cela?
tripleee