2009-12-19 9 views
8

Llegó el día en que tuve que escribir un script de BASH que recorre árboles de directorios arbitrarios y mira archivos arbitrarios e intenta determinar algo con respecto a una comparación entre ellos. Pensé que sería un simple par de horas tops! proceso - ¡No es así!¿Hay un "convertidor de escape" para los nombres de archivos y directorios disponibles?

Mi cuelgue es que algunas veces un idiota -ejemplo! - discúlpeme, el encantador usuario elige poner espacios en el directorio y los nombres de los archivos. Esto hace que mi script falle.

La solución perfecta, además de amenazar la guillotina para aquellos que insisten en el uso de espacios en esos lugares (por no hablar de los tipos que ponen esto en código de los sistemas operativos!), Podría ser una rutina que 'se escapa' los nombres de archivos y directorios para nosotros, algo así como cómo cygwin tiene rutinas para convertir de unix a dos formatos de archivo. ¿Hay algo como esto en una distribución estándar de Unix/Linux?

Nota que el simple for file in * constructo no funciona tan bien cuando uno está tratando de comparar los árboles de directorios ya que las SOLO trabajos sobre "el directorio actual" - y, en este caso, como en muchos otros, constantemente a CDing varias ubicaciones de directorio trae consigo sus propios problemas. Así pues, en hacer mi tarea, me encontré con esta pregunta Handle special characters in bash for...in loop y la solución propuesta no cuelga en espacios en los nombres de directorio, sino que simplemente se pueden superar de esta manera:

dir="dirname with spaces" 
ls -1 "$dir" | while read x; do 
    echo $x 
done 

POR FAVOR: El código anterior ISN' Es particularmente maravilloso porque las variables utilizadas dentro del ciclo while son INACCESIBLES fuera de ese ciclo while. Esto se debe a que hay una subshell implícita creada cuando la salida del comando ls está canalizada. ¡Este es un factor de motivación clave para mi consulta!

... OK, el código anterior ayuda en muchas situaciones, pero "escapar" de los personajes también sería muy poderoso. Por ejemplo, el directorio anterior puede contener:

dir\ with\ spaces 

¿Esto ya existe y acabo de pasarlo por alto?

Si no es así, ¿alguien tiene una propuesta fácil para escribir una, quizás con sed o lex? (Estoy lejos de ser competente con cualquiera.)

+0

Bash debe tener una internamente, ya que se usa cada vez que tocas "tab", por lo que podría ser un punto de partida. – Ken

+0

Estoy de acuerdo y me encantaría aprovecharlo directamente, ¡si hubiera alguna manera! Hmmm ... Tal vez algunos de nuestros amigos de Código Abierto lo consideren una digna adición al caparazón mismo. Podría argumentarse que las "actualizaciones" originales para permitir espacios (en particular) son incompletas sin dicha herramienta. ... Mientras tanto, ¡no tengo idea de cómo replicar programáticamente la acción de la pestaña! ¿Tú? –

+0

No estoy seguro de entender su necesidad. Por lo general, usar 'find' y un' while' loop es más que suficiente. Tal vez podrías publicar algún código con el que estés teniendo problemas. –

Respuesta

4

Hacer un nombre de archivo muy desagradable para la prueba:

mkdir escapetest 
cd escapetest && touch "m'i;x&e\"d u(p\nmulti)\nlines'\nand\015ca&rr\015re;t" 

[Editar: Lo más probable es que tenía la intención de que touch orden de ser:

touch $'m\'i;x&e\"d u(p\nmulti)\nlines\'\nand\015ca&rr\015re;t' 

que pone a los personajes más feos en el nombre del archivo . La salida se verá un poco diferente. ]

A continuación, ejecute la siguiente:

find -print0 | while read -d '' -r line; do echo -en "--[${line}]--\t\t"; echo "$line"|sed -e ':t;N;s/\n/\\n/;bt' | sed 's/\([ \o47()"&;\\]\)/\\\1/g;s/\o15/\\r/g'; done 

La salida debería tener este aspecto:

 
--[./m'i;x&e"d u(p 
multi) 
lines' 
re;t]--   ./m\'i\;x\&e\"d\ u\(p\\nmulti\)\\nlines\'\\nand\\015ca\&rr\\015re\;t 

Este consiste en una versión condensada del monstruosed de Pascal Thivent, más gastos de envío para su transporte retornos y nuevas líneas y tal vez un poco más.

La primera pasada a través de sed combina varias líneas en una delimitada por "\ n" para los nombres de archivo que tienen líneas nuevas. La segunda pasada reemplaza a cualquiera de una lista de caracteres con una barra invertida precediéndose a sí misma. La última parte reemplaza los retornos de carro con "\ r".

Una cosa a destacar es que, como saben, while se encargará de espacios y se for no, pero mediante el envío de la salida de find con terminación nula y establecer el delimitador de read a null, puede también manejar los saltos de línea en los nombres de archivo . La opción -r hace que read acepte barras diagonales inversas sin interpretarlas.

Editar:

Otra manera de escapar los caracteres especiales, esta vez sin utilizar sed, utiliza la característica de cotización y variable creación de la orden interna Bash printf (esto también se ilustra mediante la sustitución de proceso en lugar de un tubo):

while read -d '' -r file; do echo "$file"; printf -v name "%q" "$file"; echo "$name"; done< <(find -print0) 

la variable $name estará disponible fuera del bucle, puesto que el uso de sustitución proceso evita la creación de una subcapa alrededor del bucle.

+0

No es mío, pero sí, es un monstruo :) –

+0

Gran publicación, gracias. ... Esto es _muy_ bueno y receptivo a la pregunta original. –

0

El comando find trabaja a veces en esta situación:

find . -exec ls {} \; 

por ejemplo

2

encontré este How to escape file names in bash shell scripts mientras googlear que cito a continuación:

Después de luchar con Bash desde hace bastante algún tiempo, descubrí que la siguiente código proporciona una base agradable para escapar de caracteres especiales. De cource no está completo, pero los caracteres más importantes son filtrados.

Si alguien tiene una mejor solución, , por favor avíseme. Funciona y es legible pero no bonito.

FILE_ESCAPED=`echo "$FILE" | \ 
sed s/\\ /\\\\\\\\\\\\\\ /g | \ 
sed s/\\'/\\\\\\\\\\\\\\'/g | \ 
sed s/\&/\\\\\\\\\\\\\\&/g | \ 
sed s/\;/\\\\\\\\\\\\\\;/g | \ 
sed s/\(/\\\\\\\\\\(/g | \ 
sed s/\)/\\\\\\\\\\)/g ` 

Tal vez usted podría utilizarlo como punto de partida.

+0

Gracias por este fragmento de código. Es una versión incompleta de lo que estaba pidiendo, ¡GRACIAS! –

2

El siguiente fragmento maneja todos los nombres de archivo (aquellos incluidos los esbozos, citas, saltos de línea, ...):

startdir="${1:-.}"        # first parameter or working directory 

#------------------------------------------------------------------------------- 
# IFS is undefined 
# read: 
# -r do not allow backslashes to escape any characters 
# -d delimiter is \0 (not a valid character in a filename) 
# done < <(find ...) . redirection from a process substitution 
#------------------------------------------------------------------------------- 
while IFS= read -r -d '' file; do 
    echo "'$file'" 
done < <(find "$startdir" -type f -print0) 

Véase también este BashFAQ.

+0

Gracias por la publicación. OK, esta es otra forma de bucle y no es ni mejor ni peor que el bucle publicado en la pregunta original. Tiene la desventaja de reiniciar IFS y si lo necesita dentro del ciclo, tendrá dolor de cabeza. Y tiene la ventaja de permitir que el guionista libere contenidos varibles del ciclo, una limitación del código presentado en la consulta original. –

2

Hay un problema bastante grave con el enfoque de escape: qué escapes son necesarios depende del contexto en el que se va a expandir la variable, y en el caso habitual no hay escapatoria que funcione.Por ejemplo, si vas a hacer algo tan simple como:

touch a "b c" d 
files="a b\ c d" 
ls $files 

... no va a funcionar (ls busca los archivos 4: "A", "B \", "c", y "d") porque el intérprete de comandos no presta atención a los escapes cuando divide $ archivos en palabras. Podría usar eval ls $files, pero eso fallaría en cosas como pestañas en los nombres de archivo.

El enfoque while ... read ... done < <(find ... -print0) fgm sugirió que funciona sólidamente (y debido a la flexibilidad de los patrones de búsqueda de find, es muy poderoso), pero también es una pila bastante desordenada de soluciones para varios posibles problemas; Si usted no necesita el poder del hallazgo, no es difícil de hacer las cosas con for y *:

shopt -s nullglob # In case of empty directories... 
for filepath in "$dir"/*; do # loop over all files in the specified directory 
    filename="${filepath##*/}" # You just wanted the files' names? No problem. 
    echo "$filename" 
done 

Si (como se menciona en la pregunta) está interesado en la comparación de los dos árboles de directorios, recorriendo uno de ellos no es exactamente lo que quieres; que sería mejor poner su contenido en matrices, así:

shopt -s nullglob 
pathlist1=("$dir1"/*) # Get a list of paths of files in dir1 
filelist1=("${pathlist1[@]##*/}") # Parse off just the filenames 
pathlist2=("$dir2"/*) # Same for dir2 
filelist2=("${pathlist2[@]##*/}") 
# now compare filelist1 with filelist2... 

(. Tenga en cuenta que yo sepa el constructo "${pathlist2[@]##*/}" no es estándar, pero parece que se ha apoyado tanto en bash y zsh desde hace un tiempo)

+1

Publicación muy reflexiva y creativa, gracias. Un punto aquí es que con tu patrón de escape problemas para comentar, uno podría superar los problemas de los que hablas mediante el uso de comillas además del "escape", al menos eso creo. ... Mi sistema no sabe qué es "shopt", supongo que es una opción de shell. ¡A mi bash no le gusta! ¡Y me temo que aún no entiendo lo que el negocio "$ {pathlist2 [@] ## * /}" está intentando hacer! Más aquí, tal vez? –

+0

Al cotizar además de escapar: lo intenté, las citas solo se tratan como parte del nombre del archivo; aparte de 'eval', no creo que haya una manera de hacerlo.En 'shopt': ¿qué versión de bash estás usando? Está en cada versión que he usado ... Si no la tiene, y no hay archivos que coincidan, el patrón global se expande a sí mismo. Una alternativa es que puedes agregar '[[-e" $ filepath "]] || continue' como la primera línea del bucle 'for'. –

+0

Activado '" $ {pathlist2 [@] ## * /} "': '" $ {pathlist2 [@]} "' se expande a los miembros de la matriz, cada uno como una "palabra" separada. Agregar '## * /' elimina el último "/" en cada entrada; básicamente, es un truco para convertir una matriz de rutas de archivos completas en una matriz de solo los nombres de archivo. –

1
#!/bin/bash 

while read filename; do 
    echo 'I am doing something with "'"$filename"'".' 
done < <(find) 

tenga en cuenta que la notación <() no funcionará cuando bash se llama como /bin/sh.

Cuestiones relacionadas