¿Cómo puedo usar comodines inversos o negativos cuando coinciden los patrones en un shell de Unix / Linux?

325

Digamos que quiero copiar el contenido de un directorio excluyendo archivos y carpetas cuyos nombres contienen la palabra 'Música'.

cp [exclude-matches] *Music* /target_directory

¿Qué debería ir en lugar de [excluir-coincidencias] para lograr esto?

usuario4812
fuente

Respuestas:

375

En Bash puede hacerlo habilitando la extglobopción, de esta manera (reemplazar lscon cpy agregar el directorio de destino, por supuesto)

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

Luego puede deshabilitar extglob con

shopt -u extglob
Vinko Vrsalovic
fuente
14
Me gusta esta característica:ls /dir/*/!(base*)
Erick Robertson
66
¿Cómo incluye todo ( ) y también excluye! (B )?
Elijah Lynn el
44
¿Cómo combinarías, digamos, todo empezando f, excepto foo?
Noldorin
8
¿Por qué está deshabilitado por defecto?
weberc2
3
shopt -o -u histexpand si necesita buscar archivos con signos de exclamación en ellos: activado de forma predeterminada, extglob está desactivado de forma predeterminada para que no interfiera con histexpand, en los documentos explica por qué esto es así. coincide con todo lo que comienza con f excepto foo: f! (oo), por supuesto, 'comida' aún coincidiría (necesitarías f! (oo *) para detener las cosas que comienzan en 'foo' o, si quieres deshacerte de ciertas cosas que terminan en '.foo' use! ( .foo), o prefijado: myprefix! ( .foo) (coincide con myprefixBLAH pero no myprefixBLAH.foo)
osirisgothra
227

La extglobopción de shell le ofrece una coincidencia de patrones más potente en la línea de comandos.

Lo enciendes shopt -s extgloby lo apagas con shopt -u extglob.

En su ejemplo, inicialmente haría:

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

La disposición completa ext terminó glob operadores de Bing son (extracto de man bash):

Si la opción de shell extglob está habilitada usando el shopt incorporado, se reconocen varios operadores de coincidencia de patrones extendidos. Una lista de patrones es una lista de uno o más patrones separados por |. Los patrones compuestos se pueden formar utilizando uno o más de los siguientes subpatrones:

  • ? (lista de patrones)
    Coincide con cero o una ocurrencia de los patrones dados
  • * (lista de patrones)
    Coincide con cero o más ocurrencias de los patrones dados
  • + (lista de patrones)
    Coincide con una o más apariciones de los patrones dados
  • @ (lista de patrones)
    Coincide con uno de los patrones dados
  • ! (lista de patrones)
    Coincide con cualquier cosa excepto uno de los patrones dados

Entonces, por ejemplo, si quisiera enumerar todos los archivos en el directorio actual que no son .co .harchivos, haría:

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

Por supuesto, el gloging de shell normal funciona, por lo que el último ejemplo también podría escribirse como:

$ ls -d !(*.[ch])
tzot
fuente
1
¿Cuál es la razón de -d?
Big McLargeHuge
2
@Koveras para el caso de que uno de los archivos .co .hsea ​​un directorio.
tzot
@DaveKennedy Es para enumerar todo en el directorio actual D, pero no el contenido de los subdirectorios que pueden estar contenidos en el directorio D.
spurra
23

No en bash (que yo sepa), pero:

cp `ls | grep -v Music` /target_directory

Sé que esto no es exactamente lo que estabas buscando, pero resolverá tu ejemplo.

ejgottl
fuente
El valor predeterminado de ls colocará varios archivos por línea, lo que probablemente no dará los resultados correctos.
Daniel Bungert
10
Solo cuando stdout es un terminal. Cuando se usa en una tubería, ls imprime un nombre de archivo por línea.
Adam Rosenfield
ls solo coloca múltiples archivos por línea si se envía a un terminal. Pruébelo usted mismo: "ls | less" nunca tendrá varios archivos por línea.
SpoonMeiser
3
No funcionará para nombres de archivo que contengan espacios (u otros caracteres de espacio en blanco).
tzot
7

Si desea evitar el costo de usar el comando exec, creo que puede hacerlo mejor con xargs. Creo que lo siguiente es una alternativa más eficiente 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/
Steve
fuente
6

En bash, una alternativa a shopt -s extglobes la GLOBIGNOREvariable . No es realmente mejor, pero me resulta más fácil recordarlo.

Un ejemplo que puede ser lo que quería el póster original:

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

Cuando haya terminado, unset GLOBIGNOREpara poder rm *techno*en el directorio de origen.

mivk
fuente
5

También puedes usar un forbucle bastante simple :

for f in `find . -not -name "*Music*"`
do
    cp $f /target/dir
done
mipadi
fuente
1
Esto hace un hallazgo recursivo, que es un comportamiento diferente de lo que OP quiere.
Adam Rosenfield
1
uso -maxdepth 1para no recursivo?
avtomaton
Encontré que esta es la solución más limpia sin tener que habilitar / deshabilitar las opciones de shell. La opción -maxdepth se recomendaría en esta publicación para obtener el resultado que necesita el OP, pero todo depende de lo que intente lograr.
David Lapointe
El uso finden backticks se romperá de manera desagradable si encuentra nombres de archivos no triviales.
tripleee
5

Mi preferencia personal es usar grep y el comando while. Esto le permite a uno escribir scripts potentes pero legibles para asegurarse de que termine haciendo exactamente lo que quiere. Además, mediante el uso de un comando de eco puede realizar una ejecución en seco antes de llevar a cabo la operación real. Por ejemplo:

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

imprimirá los archivos que terminará copiando. Si la lista es correcta, el siguiente paso es simplemente reemplazar el comando echo con el comando copy de la siguiente manera:

ls | grep -v "Music" | while read filename
do
cp "$filename" /target_directory
done
Abid H. Mujtaba
fuente
1
Esto funcionará siempre que sus nombres de archivo no tengan pestañas, líneas nuevas, más de un espacio en una fila o barras diagonales inversas. Si bien esos son casos patológicos, es bueno ser consciente de la posibilidad. En bashpuede utilizar while IFS='' read -r filename, pero entonces nuevas líneas siguen siendo un problema. En general, es mejor no usar lspara enumerar archivos; herramientas como findson mucho más adecuadas.
Thedward
Sin herramientas adicionales:for file in *; do case ${file} in (*Music*) ;; (*) cp "${file}" /target_directory ; echo ;; esac; done
Thedward
mywiki.wooledge.org/ParsingLs enumera una serie de razones adicionales por las que debe evitar esto.
tripleee
5

Un truco que no he visto aquí sin embargo que no utiliza extglob, findo grepes para tratar dos listas de archivos como conjuntos y "diff" utilizando comm:

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

commes preferible diffporque no tiene cruft extra.

Esto devuelve todos los elementos del conjunto 1 ls, que no están también en el conjunto 2 ls *Music*,. Esto requiere que ambos conjuntos estén ordenados para funcionar correctamente. No hay problema para la lsexpansión global, pero si está utilizando algo como find, asegúrese de invocar sort.

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

Potencialmente útil.

James M. Lay
fuente
1
Uno de los beneficios de la exclusión es no recorrer el directorio en primer lugar. Esta solución realiza dos recorridos de subdirectorios: uno con exclusión y otro sin exclusión.
Mark Stosberg
Muy buen punto, @MarkStosberg. Aunque, un beneficio adicional de esta técnica es que puede leer las exclusiones de un archivo real, por ejemplocomm -23 <(ls) exclude_these.list
James M. Lay
3

Una solución para esto se puede encontrar 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

Find tiene bastantes opciones, puede ser bastante específico sobre lo que incluye y excluye.

Editar: Adam en los comentarios señaló que esto es recursivo. Las opciones de búsqueda mindepth y maxdepth pueden ser útiles para controlar esto.

Daniel Bungert
fuente
Esto hace una copia recursiva, que es un comportamiento diferente. También genera un nuevo proceso para cada archivo, que puede ser muy ineficiente para una gran cantidad de archivos.
Adam Rosenfield
El costo de generar un proceso es aproximadamente cero en comparación con todas las E / S que genera la copia de cada archivo. Entonces diría que esto es lo suficientemente bueno para un uso ocasional.
Dland
Algunas soluciones para el proceso de generación: stackoverflow.com/questions/186099/…
Vinko Vrsalovic
use "-maxdepth 1" para evitar la recursividad.
ejgottl
use las teclas de retroceso para obtener el análogo de la expansión del comodín de shell: cp find -maxdepth 1 -not -name '*Music*'/ target_directory
ejgottl
2

Los siguientes trabajos enumeran todos los *.txtarchivos en el directorio actual, excepto aquellos que comienzan con un número.

Esto funciona en bash, dash, zshy todas las otras conchas compatibles con 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. En la línea uno, el patrón /some/dir/*.txthará que el forbucle itere sobre todos los archivos /some/dircuyo nombre termine con .txt.

  2. En la línea dos se usa una declaración de caso para eliminar archivos no deseados. - La ${FILE##*/}expresión elimina cualquier componente de nombre de directorio principal del nombre de archivo (aquí /some/dir/) para que los patrones puedan coincidir solo con el nombre base del archivo. (Si solo está eliminando nombres de archivos basados ​​en sufijos, puede acortar esto en su $FILElugar).

  3. En la línea tres, se omitirán todos los archivos que coincidan con el casepatrón [0-9]*) (la continueinstrucción salta a la siguiente iteración del forbucle). - Si lo desea, puede hacer algo más interesante aquí, por ejemplo, como omitir todos los archivos que no comienzan con una letra (a – z) [!a-z]*, o puede usar múltiples patrones para omitir varios tipos de nombres de archivo, por ejemplo, [0-9]*|*.bakomitir archivos de ambos .bakarchivos y archivos que no comienzan con un número.

revs zrajm
fuente
Doh! Hubo un error (lo comparé en *.txtlugar de solo *). Corregido ahora.
zrajm 01 de
0

esto lo haría excluyendo exactamente 'Música'

cp -a ^'Music' /target

esto y aquello por excluir cosas como Música? * o *? Música

cp -a ^\*?'complete' /target
cp -a ^'complete'?\* /target
gabreal
fuente
La cppágina del manual en MacOS tiene una -aopción, pero hace algo completamente diferente. ¿Qué plataforma admite esto?
tripleee