Hacer que la secuencia de comandos BASH `for` maneje nombres de archivo con espacios (o solución)

12

Si bien he estado usando BASH durante varios años, mi experiencia con las secuencias de comandos BASH es relativamente limitada.

Mi código es el siguiente. Debe tomar toda la estructura de directorios desde el directorio actual y replicarla en $OUTDIR.

for DIR in `find . -type d -printf "\"%P\"\040"`
do
  echo mkdir -p \"${OUTPATH}${DIR}\"        # Using echo for debug; working script will simply execute mkdir
  echo Created $DIR
done

El problema es que aquí hay una muestra de la estructura de mi archivo:

$ ls
Expect The Impossible-Stellar Kart
Five Iron Frenzy - Cheeses...
Five Score and Seven Years Ago-Relient K
Hello-After Edmund
I Will Go-Starfield
Learning to Breathe-Switchfoot
MMHMM-Relient K

Tenga en cuenta los espacios: -S Y fortoma los parámetros palabra por palabra, por lo que la salida de mi script se ve así:

Creating directory structure...
mkdir -p "/myfiles/multimedia/samjmusicmp3test/Learning"
Created Learning
mkdir -p "/myfiles/multimedia/samjmusicmp3test/to"
Created to
mkdir -p "/myfiles/multimedia/samjmusicmp3test/Breathe-Switchfoot"
Created Breathe-Switchfoot

Pero lo necesito para tomar nombres de archivos completos (una línea a la vez) de la salida de find. También he intentado hacer findponer comillas dobles alrededor de cada nombre de archivo. Pero esto no ayuda.

for DIR in `find . -type d -printf "\"%P\"\040"`

Y salida con esta línea cambiada:

Creating directory structure...
mkdir -p "/myfiles/multimedia/samjmusicmp3test/"""
Created ""
mkdir -p "/myfiles/multimedia/samjmusicmp3test/"Learning"
Created "Learning
mkdir -p "/myfiles/multimedia/samjmusicmp3test/to"
Created to
mkdir -p "/myfiles/multimedia/samjmusicmp3test/Breathe-Switchfoot""
Created Breathe-Switchfoot"

Ahora, necesito alguna forma en la que pueda iterar de esta manera, porque también deseo ejecutar un comando más complicado que involucre gstreamera cada archivo en una estructura similar. ¿Cómo debería estar haciendo esto?

Editar: Necesito una estructura de código que me permita ejecutar múltiples líneas de código para cada directorio / archivo / bucle. Lo siento si no estaba claro.

Solución: inicialmente intenté:

find . -type d | while read DIR
do
  mkdir -p "${OUTPATH}${DIR}"
  echo Created $DIR
done

Esto funcionó bien en su mayor parte. Sin embargo, más tarde descubrí que, dado que la tubería da como resultado que el bucle while se ejecute en una subshell, las variables establecidas en el bucle no estuvieron disponibles más tarde, lo que dificultó la implementación de un contador de errores. Mi solución final (de esta respuesta en SO ):

while read DIR
do
  mkdir -p "${OUTPATH}${DIR}"
  echo Created $DIR
done < <(find . -type d)

Esto más tarde me permitió incrementar condicionalmente las variables dentro del ciclo que permanecerían disponibles más adelante en el script.

Samuel Jaeschke
fuente
¿Por qué_necesitarías_una_espacio_en_un_nombre_archivo?
Kevin Panko
Es cierto, no es mi preferencia. Sin embargo, para eliminar espacios, primero debe manejar archivos con espacios;)
Samuel Jaeschke
1
En realidad, los nombres de archivo deben permitir espacios. Permitiría cualquier cosa menos /caracteres no imprimibles. Pero todo está permitido excepto /y, \0por lo tanto, debe permitirlos.
Kevin Panko

Respuestas:

11

Necesitas canalizarlo finden un whilebucle.

find ... | while read -r dir
do
    something with "$dir"
done

Además, no necesitará usar -printfen este caso.

Puede hacer esta prueba contra archivos con nuevas líneas en sus nombres, si lo desea, utilizando un delimitador de nullbyte (que es el único carácter que no puede aparecer en una ruta de archivo * nix):

find ... -print0 | while read -d '' -r dir
do
    something with "$dir"
done

También encontrarás que usar en $()lugar de backticks es más versátil y fácil. Se pueden anidar mucho más fácilmente y las citas se pueden hacer mucho más fácilmente. Este ejemplo artificial ilustrará estos puntos:

echo "$(echo "$(echo "hello")")"

Intenta hacer eso con backticks.

Pausado hasta nuevo aviso.
fuente
2
Además, en lugar de hacerlo "$dir", es preferible usarlo "${dir}": es fácil distinguir entre $ {dir} name y $ {dirname}, pero $ dirname podría interpretarse de cualquier manera.
James Polley
Lo importante aquí es que readlee una línea completa ${dir}, por lo que el IFS no importa.
James Polley
1
Gracias por encontrar el error tipográfico $ / ". Las llaves no son necesarias si no hay nada que siga al nombre de la variable.
Pausa hasta nuevo aviso.
44
Esto manejará los nombres de ruta con espacios (U + 0020), pero aún no podrá manejar adecuadamente los nombres de ruta con saltos de línea (U + 000A). Prefiero find … -print0 | xargs -0 …porque el delimitador que usa corresponde exactamente al único carácter que no está permitido en los nombres de nombres POSIX: NUL (U + 0000).
Chris Johnsen
2
¡Perfecto! Justo lo que estaba buscando. Nunca se me había ocurrido que podrías ser capaz de canalizarte while. @Chris Johnsen: Cierto, pero incluso los programas de extracción de música no tienden a incluir avances de línea en sus nombres de archivo. Y si lo hacen, quiero saber (es decir: algo va mal) y deshacerse de ellos de inmediato ...
Samuel Jaeschke
8

Vea esta respuesta que escribí hace unos días para obtener un ejemplo de un script que maneja nombres de archivos con espacios.

Sin embargo, hay una forma un poco más complicada (pero más concisa) de lograr lo que estás tratando de hacer:

find . -type d -print0 | xargs -0 -I {} mkdir -p ../theredir/{}

-print0le dice a find que separe los argumentos con un valor nulo; el -0 a xargs le dice que espere argumentos separados por nulos. Esto significa que maneja bien los espacios.

-I {}le dice a xargs que reemplace la cadena {}con el nombre del archivo. Esto también implica que solo se debe usar un nombre de archivo por línea de comando (xargs normalmente rellenará tantos como quepan en la línea)

El resto debería ser obvio.

James Polley
fuente
La sugerencia de Dennis Williamson es, sin embargo (aparte de los errores tipográficos) mucho más legible y, por lo tanto, preferible en casi todos los sentidos.
James Polley
Funciona, para mkdir, pero lo siento, debería haber sido más claro: deseo ejecutar una serie de comandos para cada archivo. Verá, para mi rutina similar más adelante, deseo generar un nombre de archivo de salida basado en el nombre de archivo de entrada (que implica quitar la extensión .ogg y agregar .mp3) y luego usar estas variables múltiples en mi pipline al invocar gst-launch.
Samuel Jaeschke
5

El problema que está encontrando es que la declaración for responde al hallazgo como argumentos separados. El delimitador del espacio. Debe usar la variable IFS de bash para no dividirse en el espacio.

Aquí hay un enlace que explica cómo hacer esto.

La variable interna IFS

Una forma de evitar este problema es cambiar la variable interna IFS (Separador de campo interno) de Bash para que divida los campos por algo que no sea el espacio en blanco predeterminado (espacio, tabulación, nueva línea), en este caso, una coma.

#!/bin/bash
IFS=$';'

for I in `find -type d -printf \"%P\"\;`
do
   echo "== $I =="
done

Establezca su búsqueda para generar su delimitador de campo después del% P y establezca su IFS adecuadamente. Elegí punto y coma, ya que es muy poco probable que se encuentre en sus nombres de archivo.

La otra alternativa es llamar a mkdir desde find directamente a través de -execsi puede omitir el bucle for por completo. Eso si no necesita hacer ningún análisis adicional.

Darren Hall
fuente
¿Qué pasa si el nombre del archivo contiene el IFS? Entonces tienes que elegir uno diferente. Pero entonces, ¿qué
pasa
3
Puede elegir /POSIX y :sistemas de archivos DOS. Hay caracteres ilegales para diferentes sistemas de archivos que puede elegir para el IFS. Algo más complicado y es mejor usar Perl.
Darren Hall
2
El problema con el uso de / es que es el delimitador de directorio y finddevuelve nombres de archivo con rutas que incluyen una barra inclinada. Intente cambiar el punto y coma en su secuencia de comandos a una barra diagonal y el eco imprimirá el directorio y el nombre del archivo en líneas separadas.
Pausado hasta nuevo aviso.
Eso también parece bastante útil. He optado por la tubería a la whileopción, pero esto también parece bastante viable. Sí, en mi estructura similar más tarde necesitaba analizar más. (El nombre de archivo de entrada sería .ogg, que se pasaría como filesrcen la tubería gst, pero se generaría un equivalente que termina en .mp3 basado en el directorio de salida y también se pasa a la tubería como filesink, y esto, por supuesto, debe hacerse para cada archivo, junto con algunos echopara el usuario.)
Samuel Jaeschke
4

Si el cuerpo de su ciclo es más que un solo comando, es posible usar xargs para manejar un script de shell:

export OUTPATH=/some/where/else/
find . -type d -print0 | xargs -0 bash -c 'for DIR in "$@"; do
  printf "mkdir -p %q\\n" "${OUTPATH}${DIR}"        # Using echo for debug; working script will simply execute mkdir
  echo Created $DIR
done' -

Asegúrese de incluir el guión final (o alguna otra 'palabra') si el shell es de la variedad Bourne / POSIX (se usa para establecer $ 0 en el script del shell). Además, se debe tener cuidado con las citas, ya que el script de shell se está escribiendo dentro de una cadena entre comillas en lugar de directamente en el indicador.

Chris Johnsen
fuente
Otro concepto interesante. Gracias, estoy seguro de que encontraré un uso para esto más tarde :)
Samuel Jaeschke
1

en su pregunta actualizada que tiene

mkdir -p \"${OUTPATH}${DIR}\"

esto debería ser

mkdir -p "${OUTPATH}${DIR}"
usuario23307
fuente
Gracias. Fijo. También leía a FILENAME en lugar de DIR - copiar y pegar: P
Samuel Jaeschke
1
find . -type d -exec mkdir -p "{}\040" ';' -exec echo "Created {}\040" ';'
Vouze
fuente
0

o para hacer todo mucho menos complicado:

% rsync -av --include='*/' --exclude='*' SRC DST

esto replica la estructura de directorios de SRC en DST.

akira
fuente
No, necesito una estructura iterativa como esta, lo que me permite ejecutar múltiples líneas de código para cada archivo. "Ahora, necesito una forma en la que pueda iterar de esta manera, porque también deseo ejecutar un comando más complicado que involucre a gstreamer en cada archivo en una siguiente estructura similar". Lo siento si no estaba claro.
Samuel Jaeschke
el comando que le di resuelve el problema que solicitó, no importa si esto es solo una parte de una 'tubería' más grande de su lado. para alguien que tenga el problema como se describe en la pregunta, el enfoque rsync funcionará. por lo tanto, no hay que lamentar la posible falta de claridad :)
akira
Sí. No, quiero decir que usaría una estructura similar while... do... donemás tarde para hacer un procesamiento similar desde find, lo que requeriría que se ejecutaran varias líneas de código en cada archivo (modificar cadena, echo, gst-launch, etc. ) y rsyncno lograría esto. Es por eso que especifiqué que necesitaba poder ejecutar un conjunto de comandos más complicado dentro de una estructura similar. Mi secuencia de comandos utiliza esta estructura de bucle dos veces, por lo que para la pregunta publiqué la que tiene menos grosería en el medio.
Samuel Jaeschke
0

Si tiene instalado GNU Parallel http: // www.gnu.org/software/parallel/, puede hacer esto:

find . -type d | parallel echo making {} ";" mkdir -p /tmp/outdir/{} ";" echo made {}

Mire el video de introducción de GNU Parallel para obtener más información: http://www.youtube.com/watch?v=OpaiGYxkSuQ

Ole Tange
fuente