Manière compatible POSIX de travailler avec une liste de noms de fichiers éventuellement avec des espaces

14

J'ai vu des guides de script Bash suggérant l'utilisation d'un tableau pour travailler avec des noms de fichiers contenant des espaces. DashAsBinSh suggère cependant que les tableaux ne sont pas portables, donc je recherche une manière compatible POSIX de travailler avec des listes de noms de fichiers pouvant contenir des espaces.

Je cherche à modifier l'exemple de script ci-dessous afin qu'il echo

foo/target/a.jar
foo/target/b.jar
bar/target/lol whitespace.jar

Voici le script

#!/usr/bin/env sh

INPUT="foo/target/a.jar
foo/target/b.jar
bar/target/b.jar
bar/target/lol whitespace.jar"
# this would be produced by a 'ls' command
# We can execute the ls within the script, if it helps

dostuffwith() { echo $1; };

F_LOCATIONS=$INPUT
ALL_FILES=$(for f in $F_LOCATIONS; do echo `basename $f`; done)
ALL_FILES=$(echo "$ALL_FILES" | sort | uniq)

for f in $ALL_FILES
do
    fpath=$(echo "$F_LOCATIONS" | grep -m1 $f)
    dostuffwith $fpath
done
Eero Aaltonen
la source
Même chose possible sur SO: stackoverflow.com/questions/6499486/…
Ciro Santilli 新疆 改造 中心 法轮功 六四 事件

Réponses:

8

Coquilles ont une matrice POSIX: les paramètres de position ( $1, $2, etc., collectivement refered comme "$@").

set -- 'foo/target/a.jar' 'foo/target/b.jar' 'bar/target/b.jar' 'bar/target/lol whitespace.jar'
set -- "$@" '/another/one at the end.jar'

for jar do
  dostuffwith "$jar"
done

Cela n'est pas pratique car il n'y en a qu'un et cela détruit toute autre utilisation des paramètres de position. Les paramètres de position sont locaux à une fonction, ce qui est parfois une bénédiction et parfois une malédiction.

Si les noms de vos fichiers ne contiennent pas de sauts de ligne, vous pouvez utiliser des sauts de ligne comme séparateur. Lorsque vous développez la variable, désactivez d'abord la globalisation avecset -f et définissez la liste des caractères de fractionnement de champ IFSpour qu'elle ne contienne qu'une nouvelle ligne.

INPUT="foo/target/a.jar
foo/target/b.jar
bar/target/b.jar
bar/target/lol whitespace.jar"

set -f; IFS='
'                           # turn off variable value expansion except for splitting at newlines
for jar in $INPUT; do
  set +f; unset IFS
  dostuffwith "$jar"        # restore globbing and field splitting at all whitespace
done
set +f; unset IFS           # do it again in case $INPUT was empty

Les éléments de votre liste étant séparés par des sauts de ligne, vous pouvez utiliser de nombreuses commandes de traitement de texte, en particulier sort.

N'oubliez pas de toujours mettre des guillemets doubles autour des substitutions de variables, sauf lorsque vous souhaitez explicitement que le fractionnement de champ se produise (ainsi que la globalisation, sauf si vous l'avez désactivé).

Gilles 'SO- arrête d'être méchant'
la source
Bonne réponse et explication. Je vais marquer cela comme accepté car cela fait fonctionner l' sort | uniqétape d' origine comme prévu.
Eero Aaltonen
5

Puisque votre $INPUTvariable utilise des sauts de ligne comme séparateurs, je vais supposer que vos fichiers n'auront pas de retours à la ligne dans les noms. En tant que tel, oui, il existe un moyen simple d'itérer sur les fichiers et de préserver les espaces.

L'idée est d'utiliser le readshell intégré. Normalement, il readse divisera sur n'importe quel espace, et ainsi les espaces le briseront. Mais vous pouvez définir IFS=$'\n'et il se divisera uniquement sur les nouvelles lignes. Vous pouvez donc parcourir chaque ligne de votre liste.

Voici la plus petite solution que j'ai pu trouver:

INPUT="foo/target/a.jar
foo/target/b.jar
bar/target/b.jar
bar/target/lol whitespace.jar"

dostuffwith() {
    echo "$1"
}

echo "$INPUT" | awk -F/ '{if (!seen[$NF]++) print }' | \
while IFS=$'\n' read file; do
  dostuffwith "$file"
done

Fondamentalement, il envoie "$ INPUT" à awklaquelle dédoublonne en fonction du nom du fichier (il se divise /puis imprime la ligne si le dernier élément n'a pas été vu auparavant). Ensuite, une fois que awk a généré la liste des chemins de fichiers, nous utilisons while readpour parcourir la liste.

Patrick
la source
$ checkbashisms bar.sh bashisme possible dans la ligne 14 de bar.sh (<<< ici chaîne)
Eero Aaltonen
1
@EeroAaltonen Il a été modifié pour ne pas utiliser l'héritage. Notez cependant qu'avec ce changement, la whileboucle, et donc dostuffwithest exécutée dans un sous-shell. Ainsi, toutes les variables ou modifications apportées au shell en cours d'exécution seront perdues à la fin de la boucle. La seule alternative est d'utiliser un hérédoc complet, ce qui n'est pas si désagréable, mais j'ai pensé que ce serait préférable.
Patrick
J'attribue des points plus basés sur la lisibilité que la petitesse. Cela fonctionne certainement et déjà +1 pour cela.
Eero Aaltonen
IFS="\n"se divise en barre oblique inverse et n caractères. Mais read file, il n'y a pas de division. IFS="\n"est toujours utile dans la mesure où il supprime les caractères vierges de $ IFS qui autrement auraient été supprimés au début et à la fin de l'entrée. Pour lire une ligne, la syntaxe canonique est IFS= read -r line, cependant IFS=anything read -r line(à condition que rien ne contienne des blancs) fonctionnera également.
Stéphane Chazelas
Oups. Je ne sais pas comment j'ai réussi celui-là. Fixé.
Patrick