Como posso usar curingas inversas ou negativas quando a correspondência de padrões em um shell unix / linux?

Digamos que eu queira copiar o conteúdo de um diretório, excluindo arquivos e pastas cujos nomes contenham a palavra ‘Música’.

cp [exclude-matches] *Music* /target_directory 

O que deve acontecer no lugar de [excluir correspondências] para conseguir isso?

No Bash você pode fazer isso ativando a opção extglob , como esta (substitua ls por cp e adicione o diretório de destino, é claro)

 ~/foobar> shopt -s extglob ~/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 

Mais tarde você pode desativar o extglob com

 shopt -u extglob 

A opção de shell extglob oferece uma correspondência de padrões mais poderosa na linha de comando.

Você liga-o com o shopt -s extglob e desliga-o com shopt -u extglob .

No seu exemplo, você inicialmente faria:

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

Os operadores globais completos disponíveis são (excerto do man bash ):

Se a opção de shell extglob estiver ativada usando o shopt builtin, vários operadores de correspondência de padrões estendidos serão reconhecidos. Na descrição a seguir, uma lista de padrões é uma lista de um ou mais padrões separados por um |. Padrões compostos podem ser formados usando um ou mais dos seguintes sub-padrões:

  • ? (lista de padrões)
    Corresponde a zero ou uma ocorrência dos padrões fornecidos
  • * (lista de padrões)
    Corresponde a zero ou mais ocorrências dos padrões fornecidos
  • + (lista de padrões)
    Corresponde a uma ou mais ocorrências dos padrões fornecidos
  • @ (lista de padrões)
    Corresponde a um dos padrões fornecidos
  • ! (lista de padrões)
    Corresponde a qualquer coisa, exceto um dos padrões fornecidos

Então, por exemplo, se você quisesse listar todos os arquivos no diretório atual que não são arquivos .c ou .h , você faria:

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

É claro que a globalização normal da shell funciona, então o último exemplo também pode ser escrito como:

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

Não no bash (que eu conheço), mas:

 cp `ls | grep -v Music` /target_directory 

Eu sei que isso não é exatamente o que você estava procurando, mas vai resolver o seu exemplo.

Se você quiser evitar o custo de mem de usar o comando exec, acredito que você pode fazer melhor com xargs. Eu acho que o seguinte é uma alternativa mais eficiente para

 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/ 

Você também pode usar um loop bastante simples:

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

Minha preferência pessoal é usar o comando grep and the while. Isso permite escrever scripts poderosos e legíveis, garantindo que você faça exatamente o que deseja. Além disso, usando um comando de eco, você pode executar uma operação a seco antes de executar a operação real. Por exemplo:

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

imprimirá os arquivos que você acabará copiando. Se a lista estiver correta, o próximo passo é simplesmente replace o comando echo pelo comando copy da seguinte forma:

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

Uma solução para isso pode ser encontrada com o find.

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

O Find tem algumas opções, você pode ser bastante específico sobre o que você inclui e exclui.

Edit: Adam nos comentários observou que isso é recursivo. encontre opções mindepth e maxdepth podem ser úteis para controlar isso.

No bash, uma alternativa ao shopt -s extglob é a variável GLOBIGNORE . Não é realmente melhor, mas acho mais fácil de lembrar.

Um exemplo que pode ser o que o autor original queria:

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

Quando terminar, unset GLOBIGNORE para poder rm *techno* no diretório de origem.

Os seguintes trabalhos listam todos os arquivos *.txt no diretório atual, exceto aqueles que começam com um número.

Isso funciona em bash , dash , zsh e todos os outros shells compatíveis com 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. Na linha um, o padrão /some/dir/*.txt fará com que o loop for itere todos os arquivos em /some/dir cujo nome termina com .txt .

  2. Na linha dois, uma declaração de caso é usada para eliminar arquivos indesejados. – A expressão ${FILE##*/} retira qualquer componente do nome do dir principal do nome do arquivo (here /some/dir/ ) para que os padrões possam corresponder apenas ao nome base do arquivo. (Se você está apenas eliminando nomes de arquivos com base em sufixos, pode encurtar isso para $FILE .)

  3. Na linha três, todos os arquivos correspondentes à linha padrão do case [0-9]* ) serão ignorados (a instrução continue para a próxima iteração do loop for ). – Se você quiser, pode fazer algo mais interessante aqui, por exemplo, como pular todos os arquivos que não começam com uma letra (a – z) usando [!az]* , ou você pode usar vários padrões para pular vários tipos de nomes de arquivos, por exemplo [0-9]*|*.bak para pular arquivos arquivos .bak e arquivos que não iniciam com um número.

Um truque que eu ainda não vi aqui e que não usa extglob , find ou grep é tratar duas listas de arquivos como conjuntos e “diff” usando o comm :

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

comm é preferível ao diff porque não tem extra extra.

Isso retorna todos os elementos do conjunto 1, ls , que não estão também no conjunto 2, ls *Music* . Isso requer que os dois conjuntos estejam na ordem de sorting para funcionar corretamente. Não há problema para expansão de ls e glob, mas se você estiver usando algo como find , invoque o sort .

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

Potencialmente útil.

isso faria exatamente excluindo ‘Music’

 cp -a ^'Music' /target 

isso e aquilo para excluir coisas como Música? * ou *?

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