Ejecutar comando en todos los archivos en un directorio

290

¿Podría alguien proporcionar el código para hacer lo siguiente? Suponga que hay un directorio de archivos, todos los cuales deben ejecutarse a través de un programa. El programa emite los resultados a la salida estándar. Necesito un script que vaya a un directorio, ejecute el comando en cada archivo y concatene la salida en un archivo de salida grande.

Por ejemplo, para ejecutar el comando en 1 archivo:

$ cmd [option] [filename] > results.out
el maestro
fuente
3
Me gustaría agregar a la pregunta. ¿Se puede hacer usando xargs? por ejemplo, ls <directory> | xargs cmd [options] {filenames put in here automatically by xargs} [more arguments] > results.out
Ozair Kafray
2
Puede, pero probablemente no quieras usarls para conducir xargs. Si cmdestá escrito de manera competente, tal vez simplemente pueda hacerlo cmd <wildcard>.
tripleee

Respuestas:

425

El siguiente código bash pasará $ file al comando donde $ file representará cada archivo en / dir

for file in /dir/*
do
  cmd [option] "$file" >> results.out
done

Ejemplo

el@defiant ~/foo $ touch foo.txt bar.txt baz.txt
el@defiant ~/foo $ for i in *.txt; do echo "hello $i"; done
hello bar.txt
hello baz.txt
hello foo.txt
Andrew Logvinov
fuente
23
Si no existen archivos /dir/, entonces el ciclo todavía se ejecuta una vez con un valor de '*' para $file, lo que puede ser indeseable. Para evitar esto, habilite nullglob mientras dure el ciclo. Agregue esta línea antes del ciclo shopt -s nullgloby esta línea después del ciclo shopt -u nullglob #revert nullglob back to it's normal default state.
Stew-au
43
+1, y me costó toda mi colección de fondos de pantalla. todos después de mí, usan comillas dobles. "$ file"
Behrooz
Si el archivo de salida es el mismo dentro del bucle, es mucho más eficiente redirigir fuera del bucle done >results.out(y probablemente luego pueda sobrescribir en lugar de agregar, como he supuesto aquí).
tripleee
¿Cómo se obtienen archivos de resultados individuales con nombres personalizados para sus archivos de entrada?
Timothy Swan el
1
tenga cuidado al usar este comando para una gran cantidad de archivos en dir. Use find -exec en su lugar.
kolisko
181

Qué tal esto:

find /some/directory -maxdepth 1 -type f -exec cmd option {} \; > results.out
  • -maxdepth 1El argumento evita que find descienda recursivamente en cualquier subdirectorio. (Si desea que se procesen dichos directorios anidados, puede omitir esto).
  • -type -f especifica que solo se procesarán archivos sin formato.
  • -exec cmd option {}le dice que se ejecute cmdcon el especificado optionpara cada archivo encontrado, con el nombre de archivo sustituido por{}
  • \; denota el final del comando.
  • Finalmente, la salida de todas las cmdejecuciones individuales se redirige a results.out

Sin embargo, si le importa el orden en que se procesan los archivos, es mejor que escriba un bucle. Creo que findprocesa los archivos en orden de inodo (aunque podría estar equivocado al respecto), lo que puede no ser lo que desea.

Jim Lewis
fuente
1
Esta es la forma correcta de procesar archivos. El uso de un bucle for es propenso a errores debido a muchas razones. También se puede ordenar mediante el uso de otros comandos como staty sort, que por supuesto depende de los criterios de clasificación.
tuxdna
1
si quisiera ejecutar dos comandos, ¿cómo los vincularía después de la -execopción? ¿Tengo que envolverlos entre comillas simples o algo así?
Frei
findes siempre la mejor opción porque puede filtrar por patrón de nombre de archivo con la opción -namey puede hacerlo en un solo comando.
João Pimentel Ferreira
3
@frei la respuesta a su pregunta está aquí: stackoverflow.com/a/6043896/1243247 pero básicamente solo agrega -execopciones:find . -name "*.txt" -exec echo {} \; -exec grep banana {} \;
João Pimentel Ferreira
2
¿Cómo puede hacer referencia al nombre del archivo como opción?
Toskan
54

Estoy haciendo esto en mi frambuesa pi desde la línea de comandos ejecutando:

for i in *;do omxplayer "$i";done
robgraves
fuente
7

Las respuestas aceptadas / altamente votadas son geniales, pero carecen de algunos detalles esenciales. Esta publicación cubre los casos sobre cómo manejar mejor cuando falla la expansión del nombre de ruta del shell (glob), cuando los nombres de archivo contienen líneas nuevas incrustadas / símbolos de guión y mueven la redirección de salida del comando fuera del ciclo for al escribir los resultados en un expediente.

Cuando se ejecuta la expansión de glob shell, *existe la posibilidad de que la expansión falle si no hay archivos presentes en el directorio y se pasará una cadena glob no expandida al comando que se ejecutará, lo que podría tener resultados no deseados. El bashshell proporciona una opción de shell extendida para este uso nullglob. Entonces, el bucle se convierte básicamente de la siguiente manera dentro del directorio que contiene sus archivos

 shopt -s nullglob

 for file in ./*; do
     cmdToRun [option] -- "$file"
 done

Esto le permite salir de forma segura del ciclo for cuando la expresión ./*no devuelve ningún archivo (si el directorio está vacío)

o de una manera compatible con POSIX ( nullglobes bashespecífico)

 for file in ./*; do
     [ -f "$file" ] || continue
     cmdToRun [option] -- "$file"
 done

Esto le permite ir dentro del ciclo cuando la expresión falla por una vez y la condición [ -f "$file" ]verifica si la cadena no expandida ./*es un nombre de archivo válido en ese directorio, lo que no sería. Entonces, en caso de falla de esta condición, el uso continuevuelve al forbucle que no se ejecutará posteriormente.

También tenga en cuenta el uso de --justo antes de pasar el argumento del nombre de archivo. Esto es necesario porque, como se señaló anteriormente, los nombres de archivo de shell pueden contener guiones en cualquier parte del nombre de archivo. Algunos de los comandos de shell interpretan eso y los tratan como una opción de comando cuando el nombre no es cita correctamente y ejecuta el comando pensando si se proporciona el indicador.

Las --señales de la gama de opciones de línea de comandos en ese caso, que significa que el comando no deben analizar todas las cadenas más allá de este punto como opciones de órdenes, pero sólo como nombres de archivo.


La doble cita de los nombres de archivo resuelve correctamente los casos en que los nombres contienen caracteres globales o espacios en blanco. Pero los nombres de archivo * nix también pueden contener nuevas líneas en ellos. Por lo tanto, limitamos los nombres de archivo con el único carácter que no puede ser parte de un nombre de archivo válido: el byte nulo ( \0). Dado que bashinternamente utiliza Ccadenas de estilo en las que se utilizan los bytes nulos para indicar el final de la cadena, es el candidato adecuado para esto.

Entonces, usando la printfopción de shell para delimitar archivos con este byte NULL usando la -dopción de readcomando, podemos hacer a continuación

( shopt -s nullglob; printf '%s\0' ./* ) | while read -rd '' file; do
    cmdToRun [option] -- "$file"
done

Los nullgloby printfse envuelven, lo (..)que significa que básicamente se ejecutan en un subconjunto (shell secundario), porque para evitar la nullglobopción de reflejarse en el shell principal, una vez que el comando sale. La -d ''opción de readcomando no es compatible con POSIX, por lo que necesita un bashshell para que esto se haga. Usando el findcomando esto se puede hacer como

while IFS= read -r -d '' file; do
    cmdToRun [option] -- "$file"
done < <(find -maxdepth 1 -type f -print0)

Para findimplementaciones que no son compatibles -print0(aparte de las implementaciones de GNU y FreeBSD), esto se puede emular usandoprintf

find . -maxdepth 1 -type f -exec printf '%s\0' {} \; | xargs -0 cmdToRun [option] --

Otra solución importante es mover la redirección fuera del ciclo for para reducir una gran cantidad de E / S de archivo. Cuando se usa dentro del bucle, el shell debe ejecutar llamadas al sistema dos veces para cada iteración del bucle for, una para abrir y otra para cerrar el descriptor de archivo asociado con el archivo. Esto se convertirá en un cuello de botella en su rendimiento para ejecutar iteraciones grandes. La sugerencia recomendada sería moverlo fuera del ciclo.

Extendiendo el código anterior con estas correcciones, podría hacer

( shopt -s nullglob; printf '%s\0' ./* ) | while read -rd '' file; do
    cmdToRun [option] -- "$file"
done > results.out

que básicamente pondrá el contenido de su comando para cada iteración de su entrada de archivo en stdout y cuando finalice el bucle, abra el archivo de destino una vez para escribir el contenido de stdout y guardarlo. La findversión equivalente de la misma sería

while IFS= read -r -d '' file; do
    cmdToRun [option] -- "$file"
done < <(find -maxdepth 1 -type f -print0) > results.out
Inian
fuente
1
+1 para verificar que el archivo existe. Si busca en un directorio no existente, $ file contiene la cadena de expresiones regulares "/ invald_dir / *" no es un nombre de archivo válido.
cdalxndr
3

Una forma rápida y sucia que hace el trabajo a veces es:

find directory/ | xargs  Command 

Por ejemplo, para encontrar el número de líneas en todos los archivos en el directorio actual, puede hacer:

find . | xargs wc -l
Rahul
fuente
8
@Hubert ¿Por qué tienes nuevas líneas en tus nombres de archivo?
musicin3d
2
no es una cuestión de "por qué", es una cuestión de corrección: los nombres de archivo no tienen que incluir caracteres imprimibles, ni siquiera tienen que ser secuencias UTF-8 válidas. Además, lo que es una nueva línea depende mucho de la codificación, una codificación ♀ es la nueva línea de otra. Consulte la página de códigos 437
Hubert Kario el
2
vamos, en serio? esto funciona el 99.9% del tiempo, y él dijo "rápido y sucio"
Edoardo
No soy fanático de los scripts de Bash "rápidos y sucios" (también conocidos como "rotos"). Tarde o temprano termina en cosas como el famoso "Movido ~/.local/share/steam. Ran steam. Eliminó todo en el sistema propiedad del usuario". informe de error.
reducción de actividad
Esto tampoco funcionará con archivos que tengan espacios en el nombre.
Shamas S - Restablece a Monica el
2

Necesitaba copiar todos los archivos .md de un directorio a otro, así que esto es lo que hice.

for i in **/*.md;do mkdir -p ../docs/"$i" && rm -r ../docs/"$i" && cp "$i" "../docs/$i" && echo "$i -> ../docs/$i"; done

Lo cual es bastante difícil de leer, así que vamos a analizarlo.

primer CD en el directorio con sus archivos,

for i in **/*.md; para cada archivo en su patrón

mkdir -p ../docs/"$i"cree ese directorio en una carpeta de documentos fuera de la carpeta que contiene sus archivos. Lo que crea una carpeta adicional con el mismo nombre que ese archivo.

rm -r ../docs/"$i" eliminar la carpeta adicional que se crea como resultado de mkdir -p

cp "$i" "../docs/$i" Copie el archivo real

echo "$i -> ../docs/$i" Echo lo que hiciste

; done Vivir feliz para siempre

Eric Wooley
fuente
Nota: para **que funcione, se globstardebe configurar la opción de shell:shopt -s globstar
Hubert Kario, el
2

Puedes usar xarg

ls | xargs -L 1 -d '\n' your-desired-command

-L 1 hace pasar 1 artículo a la vez

-d '\n'La salida de lsmake se divide según la nueva línea.

Al Mamun
fuente
1

Basado en el enfoque de @Jim Lewis:

Aquí hay una solución rápida usando findy también ordenando archivos por su fecha de modificación:

$ find  directory/ -maxdepth 1 -type f -print0 | \
  xargs -r0 stat -c "%y %n" | \
  sort | cut -d' ' -f4- | \
  xargs -d "\n" -I{} cmd -op1 {} 

Para ordenar ver:

http://www.commandlinefu.com/commands/view/5720/find-files-and-list-them-sorted-by-modification-time

esmoquin
fuente
esto no funcionará si los archivos tienen nuevas líneas en sus nombres
Hubert Kario
1
@HubertKario Es posible que desee leer más acerca -print0de findy -0para los xargsque utilizan el carácter nulo en lugar de cualquier espacio en blanco (incluyendo saltos de línea).
tuxdna
sí, usar -print0es algo que ayuda, pero toda la tubería necesita usar algo como esto, y sortno lo es
Hubert Kario
1

Creo que la solución simple es:

sh /dir/* > ./result.txt
yovie
fuente
2
¿Entendiste la pregunta correctamente? Esto simplemente intentará ejecutar cada archivo en el directorio a través del shell, como si fuera un script.
rdas
1

Máxima profundidad

Descubrí que funciona bien con la respuesta de Jim Lewis, solo agregue un poco como esto:

$ export DIR=/path/dir && cd $DIR && chmod -R +x *
$ find . -maxdepth 1 -type f -name '*.sh' -exec {} \; > results.out

Orden de clasificación

Si desea ejecutar en orden de clasificación, modifíquelo así:

$ export DIR=/path/dir && cd $DIR && chmod -R +x *
find . -maxdepth 2 -type f -name '*.sh' | sort | bash > results.out

Solo por un ejemplo, esto se ejecutará con el siguiente orden:

bash: 1: ./assets/main.sh
bash: 2: ./builder/clean.sh
bash: 3: ./builder/concept/compose.sh
bash: 4: ./builder/concept/market.sh
bash: 5: ./builder/concept/services.sh
bash: 6: ./builder/curl.sh
bash: 7: ./builder/identity.sh
bash: 8: ./concept/compose.sh
bash: 9: ./concept/market.sh
bash: 10: ./concept/services.sh
bash: 11: ./product/compose.sh
bash: 12: ./product/market.sh
bash: 13: ./product/services.sh
bash: 14: ./xferlog.sh

Profundidad ilimitada

Si desea ejecutar en profundidad ilimitada por ciertas condiciones, puede usar esto:

export DIR=/path/dir && cd $DIR && chmod -R +x *
find . -type f -name '*.sh' | sort | bash > results.out

luego coloque encima de cada archivo en los directorios secundarios de esta manera:

#!/bin/bash
[[ "$(dirname `pwd`)" == $DIR ]] && echo "Executing `realpath $0`.." || return

y en algún lugar del cuerpo del archivo principal:

if <a condition is matched>
then
    #execute child files
    export DIR=`pwd`
fi
Chetabahana
fuente