8 votes

Existe-t-il un "convertisseur d'échappement" pour les noms de fichiers et de répertoires ?

Le jour est venu où j'ai dû écrire un BASH script qui parcourt des arbres de répertoires arbitraires et regarde des fichiers arbitraires et tente de déterminer quelque chose concernant une comparaison entre eux. Je pensais qu'il s'agirait d'un simple travail de quelques heures des sommets ! Le processus - pas vraiment !

Mon problème est que parfois, des idiots -ahem!- m'excusent, Très bon utilisateur choisit de mettre des espaces dans les noms de répertoires et de fichiers. Cela fait échouer mon script.

La solution idéale En dehors de la menace de la guillotine pour ceux qui insistent sur l'utilisation d'espaces à ces endroits (sans parler des gars qui mettent cela dans le code des systèmes d'exploitation ! Y a-t-il quelque chose de ce genre dans une distribution Unix/Linux standard ?

Notez que le simple for file in * ne fonctionne pas si bien lorsqu'on essaie de comparer des arbres de répertoires, car elle ne permet pas d'établir des comparaisons. UNIQUEMENT fonctionne sur "le répertoire courant" - et, dans ce cas comme dans beaucoup d'autres, le fait de passer constamment d'un CD à un autre répertoire pose ses propres problèmes. En faisant mes devoirs, j'ai donc trouvé cette question Gérer les caractères spéciaux dans la boucle for...in de bash et la solution proposée ici s'accroche aux espaces dans les noms de répertoires, mais peut simplement être contournée comme ceci :

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

VEUILLEZ NOTER : Le code ci-dessus n'est pas particulièrement merveilleux car les variables utilisées à l'intérieur de la boucle while sont INACCESSIBLES en dehors de cette boucle while. Cela est dû au fait qu'un sous-shell implicite est créé lorsque la sortie de la commande ls est acheminée. C'est un facteur de motivation essentiel pour ma requête !

...OK, le code ci-dessus est utile dans de nombreuses situations, mais "l'échappement" des caractères serait également très efficace. Par exemple, le code ci-dessus pourrait contenir :

dir\ with\ spaces

Est-ce que cela existe déjà et que je l'ai juste négligé ?

Si non, quelqu'un a-t-il une proposition facile pour en écrire une - peut-être avec sed ou lex ? (Je suis loin d'être compétent avec l'un ou l'autre).

4voto

Dennis Williamson Points 105818

Créez un nom de fichier vraiment méchant pour le test :

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

[ Edit : Il y a des chances que j'aie voulu que touch à être :

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

qui met plus de caractères laids dans le nom du fichier. La sortie sera un peu différente. ]

Alors exécutez ceci :

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

Le résultat devrait ressembler à ceci :

\--\[./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

Il s'agit d'une version condensée de Pascal Thivent sed monstre, plus la gestion des retours de chariot et des nouvelles lignes et peut-être un peu plus.

Le premier passage par sed fusionne plusieurs lignes en une seule délimitée par " \n "pour les noms de fichiers comportant des nouvelles lignes. La deuxième passe remplace n'importe quel caractère d'une liste de caractères précédés d'une barre oblique inversée. La dernière partie remplace les retours chariot par " \r ".

Une chose à noter est que, comme vous le savez, while traitera les espaces et for ne le fera pas mais en envoyant la sortie de find avec une terminaison nulle et en fixant le délimiteur de read à null, vous pouvez également gérer les nouvelles lignes dans les noms de fichiers. Le site -r les causes de l'option read pour accepter les backslashes sans les interpréter.

Editar:

Une autre façon d'échapper aux caractères spéciaux, cette fois sans utiliser la fonction sed utilise la fonction de citation et de création de variables du langage Bash. printf builtin (ceci illustre également l'utilisation de la substitution de processus plutôt que d'un pipe) :

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

La variable $name sera disponible en dehors de la boucle, puisque l'utilisation de la substitution de processus empêche la création d'un sous-shell autour de la boucle.

2voto

Pascal Thivent Points 295221

J'ai trouvé ceci Comment échapper les noms de fichiers dans le shell bash scripts. en googlant que je cite ci-dessous :

Après s'être battu avec Bash pendant un certain temps un certain temps, j'ai découvert que le code suivant fournit une bonne base pour l'échappement des caractères spéciaux. Bien sûr, il n'est pas complet, mais les caractères les plus importants sont filtrés.

Si quelqu'un a une meilleure solution, qu'il me le fasse savoir. Cela fonctionne et c'est lisible mais pas joli.

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

Vous pourriez peut-être l'utiliser comme point de départ.

2voto

fgm Points 5930

L'extrait suivant traite tous les noms de fichiers (y compris les blancs, les guillemets, les nouvelles lignes, ...) :

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 )

Voir aussi BashFAQ .

2voto

Gordon Davisson Points 22534

Il y a un problème assez sérieux avec l'approche de l'échappement : les échappements nécessaires dépendent du contexte dans lequel la variable va être développée, et dans le cas habituel il n'y a pas d'échappement qui fonctionne. Par exemple, si vous voulez faire quelque chose de simple comme :

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

...cela ne fonctionnera pas (ls recherche 4 fichiers : "a", "b\", "c" et "d") parce que l'interpréteur de commandes ne fait pas attention aux échappatoires lorsqu'il divise les mots $files. Vous pouvez utiliser eval ls $files mais cela échouerait pour des choses comme les tabulations dans les noms de fichiers.

El while ... read ... done < <(find ... -print0) L'approche suggérée par fgm fonctionne bien (et grâce à la flexibilité des modèles de recherche de find, elle est très puissante), mais c'est aussi un tas de solutions de rechange plutôt désordonnées pour divers problèmes possibles ; si vous n'avez pas besoin de la puissance de find, il n'est pas difficile de faire les choses avec 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 (comme vous le mentionnez dans la question) vous êtes intéressé par la comparaison des deux arborescences de répertoires, boucler à travers l'une d'entre elles n'est pas tout à fait ce que vous voulez ; il serait préférable de mettre leur contenu dans des tableaux, comme ceci :

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...

(Notez que, selon les informations disponibles, l "${pathlist2[@]##*/}" n'est pas standard, mais semble être supportée à la fois dans bash et zsh depuis un certain temps).

1voto

#!/bin/bash

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

Notez que le <( ) ne fonctionnera pas lorsque bash est invoqué en tant que /bin/sh .

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X