Comment échapper automatiquement les métacaractères du shell avec la command `find`?

J'ai un tas de files XML sous une arborescence de directorys que je voudrais déplacer vers les dossiers correspondants avec le même nom dans la même arborescence.

Voici la structure de l'échantillon (en coquille):

touch foo.xml bar.xml "[ foo ].xml" "( bar ).xml" mkdir -p foo bar "foo/[ foo ]" "bar/( bar )" 

Donc, mon approche ici est la suivante:

 find . -name "*.xml" -exec sh -c ' DST=$( find . -type d -name "$(basename "{}" .xml)" -print -quit ) [ -d "$DST" ] && mv -v "{}" "$DST/"' ';' 

qui donne la sortie suivante:

 './( bar ).xml' -> './bar/( bar )/( bar ).xml' mv: './bar/( bar )/( bar ).xml' and './bar/( bar )/( bar ).xml' are the same file './bar.xml' -> './bar/bar.xml' './foo.xml' -> './foo/foo.xml' 

Mais le file avec des crochets ( [ foo ].xml ) n'a pas été déplacé comme s'il avait été ignoré.

J'ai vérifié et basename (par exemple basename "[ foo ].xml" ".xml" ) convertit le file correctement, mais find des problèmes avec des parenthèses. Par exemple:

 find . -name '[ foo ].xml' 

ne finda pas le file correctement. Cependant, en échappant aux parenthèses ( '\[ foo \].xml' ), cela fonctionne bien, mais cela ne résout pas le problème, car il fait partie du script et je ne connais pas les files ayant ces particularités ?) personnages. Testé avec BSD et GNU find .

Existe-t-il un moyen universel d'échapper aux noms de files lors de l'utilisation avec le paramètre -name , afin que je puisse corriger ma command pour supporter les files avec les métacaractères?

C'est tellement plus facile avec zsh globs ici:

 for f (**/*.xml(.)) (mv -v -- $f **/$f:r:t(/[1])) 

Ou si vous voulez inclure des files xml cachés et regarder dans des directorys cachés comme find :

 for f (**/*.xml(.D)) (mv -v -- $f **/$f:r:t(D/[1])) 

Mais attention, les files .xml , ..xml ou ...xml deviendront un problème, vous pouvez donc les exclure:

 setopt extendedglob for f (**/(^(|.|..)).xml(.D)) (mv -v -- $f **/$f:r:t(D/[1])) 

Avec les outils GNU, une autre approche pour éviter d'avoir à scanner toute l'arborescence de directorys de chaque file serait de la scanner une fois et de searchr tous les directorys et files xml , d'save où ils se trouvent et de faire le déplacement à la fin:

 (export LC_ALL=C find . -mindepth 1 -name '*.xml' ! -name .xml ! \ -name ..xml ! -name ...xml -type f -printf 'F/%P\0' -o \ -type d -printf 'D/%P\0' | awk -v RS='\0' -F / ' { if ($1 == "F") { root = $NF sub(/\.xml$/, "", root) F[root] = substr($0, 3) } else D[$NF] = substr($0, 3) } END { for (f in F) if (f in D) printf "%s\0%s\0", F[f], D[f] }' | xargs -r0n2 mv -v -- ) 

Votre approche a un certain nombre de problèmes si vous voulez autoriser n'importe quel nom de file arbitraire:

  • l'incorporation de {} dans le code shell est toujours incorrecte. Que faire s'il y a un file appelé $(rm -rf "$HOME").xml par exemple? La bonne façon est de passer ces {} comme arguments au script shell en ligne ( -exec sh -c 'use as "$1"...' sh {} \; ).
  • Avec GNU find (implicite ici car vous utilisez -quit ), *.xml correspondrait uniquement aux files constitués d'une séquence de caractères valides suivis de .xml , ce qui exclut les noms de files qui contiennent des caractères non valides dans la locale courante (par exemple noms de file dans le mauvais jeu de caractères). Le correctif pour cela est de fixer la locale à C où chaque octet est un caractère valide (cela signifie que les messages d'erreur seront affichés en anglais cependant).
  • Si l'un de ces files xml est de type directory ou lien symbolique, cela entraînerait des problèmes (affecte l'parsing des directorys ou casse les liens symboliques lorsqu'il est déplacé). Vous voudrez peut-être append un -type f pour seulement déplacer des files réguliers.
  • La substitution de command ( $(...) ) supprime tous les caractères de fin de ligne de fin. Cela poserait des problèmes avec un file appelé foo␤.xml par exemple. Travailler autour de cela est possible mais une douleur: base=$(basename "$1" .xml; echo .); base=${base%??} base=$(basename "$1" .xml; echo .); base=${base%??} . Vous pouvez au less replace basename par les opérateurs ${var#pattern} . Et évitez la substitution de commands si possible.
  • votre problème avec les noms de files contenant des caractères generics ( ? , [ , * et backslash, ils ne sont pas particuliers au shell, mais à la correspondance de motifs ( fnmatch() ). Vous auriez besoin de leur échapper avec une barre oblique inverse.
  • le problème avec .xml , ..xml , ...xml mentionné ci-dessus.

Donc, si nous abordons tout ce qui précède, nous nous retrouvons avec quelque chose comme:

 LC_ALL=C find . -type f -name '*.xml' ! -name .xml ! -name ..xml \ ! -name ...xml -exec sh -c ' for file do base=${file##*/} base=${base%.xml} escaped_base=$(printf "%s\n" "$base" | sed "s/[[*?\\\\]/\\\\&/g"; echo .) escaped_base=${escaped_base%??} find . -name "$escaped_base" -type d -exec mv -v "$file" {\} \; -quit done' sh {} + 

Phew…

Maintenant, ce n'est pas tout. Avec -exec ... {} + , nous courons aussi peu que possible. Si nous avons de la chance, nous n'en exécuterons qu'un, mais si ce n'est pas le cas, après la première invocation sh , nous aurons déplacé un certain nombre de files xml , puis nous continuerons à en chercher d'autres et findons peut-être les files que nous avons déplacés à nouveau au premier tour (et très probablement essayer de les déplacer où ils sont).

A part ça, c'est fondamentalement la même approche que les zsh. Quelques autres différences notables:

  • avec zsh one, la list des files est sortingée (par nom de directory et nom de file), de sorte que le directory de destination est plus ou less cohérent et prévisible. Avec find , il est basé sur l'ordre brut des files dans les directorys.
  • avec zsh , vous obtiendrez un message d'erreur si aucun directory correspondant pour déplacer le file n'est trouvé, pas avec l'approche de find ci-dessus.
  • Avec find , vous obtiendrez des messages d'erreur si certains directorys ne peuvent pas être parcourus, pas avec le zsh .

Une dernière note d'avertissement. Si la raison pour laquelle vous obtenez des files avec des noms de files douteux est que l'arborescence des directorys est accessible en écriture par un adversaire, méfiez-vous alors qu'aucune des solutions ci-dessus n'est sûre si l'adversaire peut renommer des files sous cette command.

Par exemple, si vous utilisez LXDE, l'attaquant peut créer un foo/lxde-rc.xml malveillant, créer un dossier lxde-rc , détecter quand vous exécutez votre command et replace lxde-rc par un lien symbolique vers votre ~/.config/openbox/ pendant la window de course (qui peut être rendue aussi volumineuse que nécessaire à bien des égards) entre find trouvant que lxde-rc et mv font le rename("foo/lxde-rc.xml", "lxde-rc/lxde-rc.xml") ( foo pourrait aussi être changé en ce lien symbolique vous faisant déplacer votre lxde-rc.xml ailleurs).

Travailler autour de cela est probablement impossible en utilisant des utilitaires standard ou même GNU, vous devez l'écrire dans un langage de programmation approprié, en effectuant une traversée de directory sécurisée et en utilisant les appels système renameat() .

Toutes les solutions ci-dessus échouent également si l'arborescence des directorys est suffisamment profonde pour que la limite de la longueur des paths donnés à l'appel système rename() effectué par mv soit atteinte (ce qui entraîne l'échec de la ENAMETOOLONG rename() avec ENAMETOOLONG ). Une solution utilisant renameat() contournerait également le problème.

Lorsque vous utilisez un script en ligne avec find ... -exec sh -c ... , vous devez transmettre le résultat de search au shell via le paramètre de position, vous n'avez donc pas besoin d'utiliser {} partout dans votre script en ligne.

Si vous avez bash ou zsh , vous pouvez transmettre la sortie basename via printf '%q' :

 find . -name "*.xml" -exec bash -c ' for f do BASENAME="$(printf "%q" "$(basename -- "$f" .xml)")" DST=$(find . -type d -name "$BASENAME" -print -quit) [ -d "$DST" ] && mv -v -- "$f" "$DST/" done ' bash {} + 

Avec bash , vous pouvez utiliser printf -v BASENAME , et cette approche ne fonctionnera pas correctement si le nom du file contient des caractères de contrôle ou des caractères non-ascii.

Si vous voulez qu'il fonctionne correctement, vous devez écrire une fonction shell pour ne s'échapper que [ , * ? et backslash.

La bonne nouvelle:

 find . -name '[ foo ].xml' 

n'est pas interprété par le shell, il est transmis de cette façon au programme find. Find interprète cependant l'argument -name comme un model glob et cela doit être pris en count.

Si vous aimez appeler find -exec \; ou mieux find -exec + , il n'y a pas de shell impliqué.

Si vous souhaitez traiter le résultat de la find par le shell, je recommand de désactiver le nom de file dans le shell en appelant set -f avant le code en question et de le rallumer en appelant set +f plus tard.

Voici un pipeline relativement simple, compatible POSIX. Il parsing la hiérarchie deux fois, d'abord pour les directorys, puis pour les files réguliers * .xml. Une ligne vide entre les balayages signale AWK de la transition.

Le composant AWK mappe les noms de base aux directorys de destination (s'il existe plusieurs directorys avec le même nom de base, seul le premier parcours est mémorisé). Pour chaque file * .xml, il imprime une ligne délimitée par des tabulations avec deux champs: 1) le path du file et 2) le directory de destination correspondant.

 { find . -type d echo find . -type f -name \*.xml } | awk -F/ ' !NF { ++i; next } !i && !($NF".xml" in d) { d[$NF".xml"] = $0 } i { print $0 "\t" d[$NF] } ' | while IFS=' ' read -rfd; do mv -- "$f" "$d" done 

La valeur atsortingbuée à IFS juste avant la lecture est un caractère de tabulation littéral, pas un espace.

Voici une transcription utilisant le squelette tactile / mkdir de la question d'origine:

 $ touch foo.xml bar.xml "[ foo ].xml" "( bar ).xml" $ mkdir -p foo bar "foo/[ foo ]" "bar/( bar )" $ find . . ./foo ./foo/[ foo ] ./bar.xml ./foo.xml ./bar ./bar/( bar ) ./[ foo ].xml ./( bar ).xml $ ../mv-xml.sh $ find . . ./foo ./foo/[ foo ] ./foo/[ foo ]/[ foo ].xml ./foo/foo.xml ./bar ./bar/( bar ) ./bar/( bar )/( bar ).xml ./bar/bar.xml