web-dev-qa-db-fra.com

Pourquoi les caractères de nouvelle ligne se perdent lors de l'utilisation de la substitution de commandes?

J'ai un fichier texte nommé links.txt qui ressemble à ceci

link1
link2
link3

Je veux parcourir ce fichier ligne par ligne et effectuer une opération sur chaque ligne. Je sais que je peux le faire en utilisant la boucle while mais comme j'apprends, j'ai pensé utiliser une boucle for. J'ai utilisé la substitution de commandes comme celle-ci

a=$(cat links.txt)

Ensuite, j'ai utilisé la boucle comme ceci

for i in $a; do ###something###;done

Je peux aussi faire quelque chose comme ça

for i in $(cat links.txt); do ###something###; done

Maintenant, ma question est quand j'ai substitué la sortie de la commande cat dans une variable a, les nouveaux caractères de ligne entre link1 link2 et link3 sont supprimés et remplacés par des espaces

echo $a

les sorties

link1 link2 link3

puis j'ai utilisé la boucle for. Est-ce toujours qu'une nouvelle ligne est remplacée par un espace quand on fait une substitution de commande ??

Cordialement

35
user3138373

Les sauts de ligne ont été perdus, car le shell avait effectué fractionnement de champ après la substitution de commande.

Dans POSIX Substitution de commandes section:

Le shell doit étendre la substitution de commande en exécutant la commande dans un environnement de sous-shell (voir Environnement d'exécution Shell) et en remplaçant la substitution de commande (le texte de la commande plus le "$ ()" ou les guillemets) qui l'entourent par la sortie standard de la commande, en supprimant séquences d'un ou plusieurs caractères à la fin de la substitution. Les caractères incorporés avant la fin de la sortie ne doivent pas être supprimés; cependant, ils peuvent être traités comme des délimiteurs de champ et éliminés lors de la division du champ, en fonction de la valeur de l'IFS et de la cotation en vigueur . Si la sortie contient des octets nuls, le comportement n'est pas spécifié.

Valeur par défaut IFS (au moins dans bash):

$ printf '%q\n' "$IFS"
$' \t\n'

Dans votre cas, vous ne définissez pas IFS ou n'utilisez pas de guillemets doubles, donc le caractère de retour à la ligne sera éliminé lors du fractionnement du champ.

Vous pouvez conserver les retours à la ligne, par exemple en définissant IFS sur vide:

$ IFS=
$ a=$(cat links.txt)
$ echo "$a"
link1
link2
link3
28
cuonglm

Les retours à la ligne sont échangés à certains moments car ce sont des caractères spéciaux. Afin de les conserver, vous devez vous assurer qu'ils sont toujours interprétés, en utilisant des guillemets:

$ a="$(cat links.txt)"
$ echo "$a"
link1
link2
link3

Maintenant, puisque j'ai utilisé des guillemets chaque fois que je manipulais les données, les caractères de nouvelle ligne (\n) a toujours été interprété par le Shell, et est donc resté. Si vous oubliez de les utiliser à un moment donné, ces caractères spéciaux seront perdus.

Le même comportement se produira si vous utilisez votre boucle sur des lignes contenant des espaces. Par exemple, étant donné le fichier suivant ...

mypath1/file with spaces.txt
mypath2/filewithoutspaces.txt

La sortie dépendra de l'utilisation ou non de guillemets:

$ for i in $(cat links.txt); do echo $i; done
mypath1/file
with
spaces.txt
mypath2/filewithoutspaces.txt

$ for i in "$(cat links.txt)"; do echo "$i"; done
mypath1/file with spaces.txt
mypath2/filewithoutspaces.txt

Maintenant, si vous ne voulez pas utiliser de guillemets, il existe une variable Shell spéciale qui peut être utilisée pour changer le séparateur de champ Shell (IFS). Si vous définissez ce séparateur sur le caractère de nouvelle ligne, vous vous débarrasserez de la plupart des problèmes.

$ IFS=$'\n'; for i in $(cat links.txt); do echo $i; done
mypath1/file with spaces.txt
mypath2/filewithoutspaces.txt

Par souci d'exhaustivité, voici un autre exemple, qui ne repose pas sur la substitution de sortie de commande. Après un certain temps, j'ai découvert que cette méthode était considérée comme plus fiable par la plupart des utilisateurs en raison du comportement même de l'utilitaire read.

$ cat links.txt | while read i; do echo $i; done

Voici un extrait de la page de manuel de read:

L'utilitaire de lecture doit lire une seule ligne à partir de l'entrée standard.

Puisque read obtient son entrée ligne par ligne, vous êtes sûr qu'il ne se cassera pas chaque fois qu'un espace apparaîtra. Passez-lui simplement la sortie de cat à travers un tube, et il itérera très bien sur vos lignes.

Edit: Je peux voir dans d'autres réponses et commentaires que les gens hésitent beaucoup à utiliser cat. Comme jasonwryan l'a dit dans son commentaire, une manière plus plus appropriée de lire un fichier dans Shell est d'utiliser la redirection de flux (<), comme vous pouvez le voir dans la réponse de val0x00ff ici . Cependant, comme la question n'est pas "comment lire/traiter un fichier dans la programmation Shell", ma réponse se concentre plus sur le comportement des guillemets, et non le reste.

40
John WH Smith

Pour ajouter mon accent, for boucles itèrent sur mots. Si votre dossier est:

one two
three four

Ensuite, cela émettra quatre lignes:

for Word in $(cat file); do echo "$Word"; done

Pour parcourir les lignes d'un fichier, procédez comme suit:

while IFS= read -r line; do
    # do something with "$line" <-- quoted almost always
done < file
6
glenn jackman

Vous pouvez utiliser read depuis bash. Recherchez également le mapfile

while read -r link
  do
   printf '%s\n' "$link"
  done < links.txt

Ou en utilisant mapfile

mapfile -t myarray < links.txt
for link in "${myarray[@]}"; do printf '%s\n' "$link"; done
4
Valentin Bajrami