Como posso usar curingas inversos ou negativos ao fazer a correspondência de padrões em um shell unix / linux?

325

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 exclusões] para fazer isso?

user4812
fonte

Respostas:

375

No Bash, você pode fazer isso ativando a extglobopção, assim (substitua lspor cpe adicione o diretório de destino, é claro)

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

Mais tarde, você pode desativar o extglob com

shopt -u extglob
Vinko Vrsalovic
fonte
14
Eu gosto deste recurso:ls /dir/*/!(base*)
Erick Robertson
6
Como você inclui tudo ( ) e também exclui! (B )?
Eliash Lynn
4
Como você corresponderia, digamos, tudo o que começou f, exceto foo?
Noldorin
8
Por que isso está desativado por padrão?
Weberc2
3
shopt -o -u histexpand se você precisar procurar por arquivos com pontos de exclamação - ativado por padrão, o extglob está desativado por padrão para não interferir com o histexpand, nos documentos que explicam por que isso acontece. corresponder a tudo o que começa com f, exceto foo: f! (oo), é claro que 'comida' ainda corresponderia (você precisaria de f! (oo *) para interromper as coisas que começam em 'foo' ou, se você quiser se livrar ! de certas coisas que terminam em uso '.foo' ( ! .foo), ou prefixado: myprefix ( .foo) (corresponde myprefixBLAH mas não myprefixBLAH.foo)
osirisgothra
227

A extglobopção shell oferece uma combinação de padrões mais poderosa na linha de comando.

Você liga shopt -s extglobe desliga com shopt -u extglob.

No seu exemplo, você faria inicialmente:

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

O completo disponível ext terminou glob operadores bing são (excerto man bash):

Se a opção extglob shell estiver ativada usando o shopt interno, vários operadores estendidos de correspondência de padrões serão reconhecidos. 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 subpadrõ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

Portanto, por exemplo, se você quiser listar todos os arquivos no diretório atual que não são .cou .harquivos, faça:

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

Obviamente, o globing normal de shell funciona, então o último exemplo também pode ser escrito como:

$ ls -d !(*.[ch])
tzot
fonte
1
Qual é o motivo de -d?
Big McLargeHuge
2
@Koveras para o caso de um dos arquivos .cou .hser um diretório.
tzot 21/09/2015
@DaveKennedy É para listar tudo no diretório atual D, mas não o conteúdo dos subdiretórios que podem estar contidos no diretório D.
Spurra
23

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

cp `ls | grep -v Music` /target_directory

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

ejgottl
fonte
O ls padrão colocará vários arquivos por linha, o que provavelmente não dará os resultados corretos.
Daniel Bungert
10
Somente quando stdout é um terminal. Quando usado em um pipeline, ls imprime um nome de arquivo por linha.
Adam Rosenfield
ls coloca apenas vários arquivos por linha se estiver enviando para um terminal. Tente você mesmo - "ls | less" nunca terá vários arquivos por linha.
SpoonMeiser 19/10/08
3
Não funcionará para nomes de arquivos que contenham espaços (ou outros caracteres de espaço em branco).
tzot 18/10/09
7

Se você quiser evitar o custo de mem do uso do comando exec, acredito que você pode fazer melhor com o 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/
Steve
fonte
6

Em festa, uma alternativa para shopt -s extglobé a GLOBIGNOREvariável . Não é realmente melhor, mas acho mais fácil lembrar.

Um exemplo que pode ser o que o pôster original queria:

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

Quando terminar, unset GLOBIGNOREserá possível rm *techno*no diretório de origem.

mivk
fonte
5

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

for f in `find . -not -name "*Music*"`
do
    cp $f /target/dir
done
mipadi
fonte
1
Isso faz uma descoberta recursiva, que é um comportamento diferente do que o OP deseja.
Adam Rosenfield
1
usar -maxdepth 1para não recursivo?
Avtomaton #
Eu achei que essa era a solução mais limpa sem precisar ativar / desativar as opções de shell. A opção -maxdepth seria recomendada nesta postagem para obter o resultado necessário para o OP, mas tudo depende do que você está tentando realizar.
David Lapointe
O uso findde backticks quebrará de maneira desagradável se encontrar algum nome de arquivo não trivial.
Tripleee
5

Minha preferência pessoal é usar o grep e o comando while. Isso permite escrever scripts poderosos e legíveis, garantindo que você acabe fazendo exatamente o que deseja. Além disso, usando um comando de eco, você pode executar uma execuçã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, a próxima etapa é simplesmente substituir o comando echo pelo comando copy da seguinte maneira:

ls | grep -v "Music" | while read filename
do
cp "$filename" /target_directory
done
Abid H. Mujtaba
fonte
1
Isso funcionará desde que os nomes dos arquivos não tenham guias, novas linhas, mais de um espaço em uma linha ou barras invertidas. Enquanto esses são casos patológicos, é bom estar ciente da possibilidade. Em bashvocê pode usar while IFS='' read -r filename, mas as novas linhas ainda são um problema. Em geral, é melhor não usar lspara enumerar arquivos; ferramentas como findsão muito mais adequadas.
Thedward
Sem ferramentas adicionais:for file in *; do case ${file} in (*Music*) ;; (*) cp "${file}" /target_directory ; echo ;; esac; done
Thedward 14/03/13
mywiki.wooledge.org/ParsingLs lista uma série de razões adicionais pelas quais você deve evitar isso.
Tripleee
5

Um truque que eu não vi aqui ainda que não usa extglob, findou grepé tratar duas listas de arquivos como conjuntos e "diff" -los usando comm:

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

commé preferível ao diffcontrário, porque não possui crosta extra.

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

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

Potencialmente útil.

James M. Lay
fonte
1
Um dos benefícios da exclusão é não percorrer o diretório em primeiro lugar. Essa solução faz duas travessias de subdiretórios - um com exclusão e outro sem.
Mark Stosberg
Muito bom ponto, @ MarkStosberg. Embora um dos benefícios adicionais dessa técnica seja que você pode ler exclusões de um arquivo real, por exemplocomm -23 <(ls) exclude_these.list
James M. Lay
3

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 bem específico sobre o que incluir e excluir.

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

Daniel Bungert
fonte
Isso faz uma cópia recursiva, que é um comportamento diferente. Também gera um novo processo para cada arquivo, que pode ser muito ineficiente para um grande número de arquivos.
Adam Rosenfield
O custo de gerar um processo é aproximadamente zero em comparação com todas as E / S que a cópia de cada arquivo gera. Então, eu diria que isso é bom o suficiente para uso ocasional.
dland 19/10/08
Algumas soluções alternativas para a geração do processo: stackoverflow.com/questions/186099/…
Vinko Vrsalovic 19/10/2008
use "-maxdepth 1" para evitar recursão.
ejgottl
uso backticks para obter o analógico da expansão wild card shell: cp find -maxdepth 1 -not -name '*Music*'/ target_directory
ejgottl
2

O trabalho a seguir lista todos os *.txtarquivos no diretório atual, exceto aqueles que começam com um número.

Isso funciona em bash, dash, zshe 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/*.txtfará com que o forloop itere sobre todos os arquivos em /some/dircujo nome termina .txt.

  2. Na linha dois, uma declaração de caso é usada para eliminar arquivos indesejados. - A ${FILE##*/}expressão retira qualquer componente principal do nome do diretório do nome do arquivo (aqui /some/dir/) para que os padrões possam corresponder apenas ao nome da base do arquivo. (Se você estiver eliminando apenas os nomes de arquivos com base em sufixos, poderá abreviá-los $FILE.)

  3. Na linha três, todos os arquivos correspondentes ao casepadrão [0-9]*) serão ignorados (a continueinstrução pula para a próxima iteração do forloop). - 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 [!a-z]*, ou você pode usar vários padrões para pular vários tipos de nomes de arquivos, por exemplo, [0-9]*|*.bakpular arquivos de ambos os .bakarquivos e arquivos que não começam com um número.

zrajm
fonte
Doh! Houve um erro (eu fiz a correspondência em *.txtvez de apenas *). Corrigido agora.
Zrajm
0

isso faria excluindo exatamente 'Música'

cp -a ^'Music' /target

isso e aquilo para excluir coisas como Music? * ou *? Music

cp -a ^\*?'complete' /target
cp -a ^'complete'?\* /target
gabreal
fonte
A cppágina de manual no MacOS tem uma -aopção, mas faz algo totalmente diferente. Qual plataforma suporta isso?
tripleee