Comment collecter correctement un tableau de lignes dans zsh

42

Je pensais que ce qui suit regrouperait la sortie de my_commanddans un tableau de lignes:

IFS='\n' array_of_lines=$(my_command);

de sorte que cela $array_of_lines[1]ferait référence à la première ligne de la sortie de my_command, $array_of_lines[2]à la seconde, etc.

Cependant, la commande ci-dessus ne semble pas bien fonctionner. Il semble également séparer la sortie my_commandautour du caractère n, comme je l'ai vérifié print -l $array_of_lines, ce qui, je crois, affiche les éléments d'un tableau ligne par ligne. J'ai aussi vérifié cela avec:

echo $array_of_lines[1]
echo $array_of_lines[2]
...

Dans une seconde tentative, j'ai pensé que l'ajout evalpourrait aider:

IFS='\n' array_of_lines=$(eval my_command);

mais j'ai eu exactement le même résultat que sans.

Enfin, en suivant la réponse sur les éléments List avec des espaces dans zsh , j'ai également essayé d'utiliser des indicateurs de développement de paramètres au lieu d' IFSindiquer à zsh comment scinder l'entrée et collecter les éléments dans un tableau, à savoir:

array_of_lines=("${(@f)$(my_command)}");

Mais j'ai toujours le même résultat (le dédoublement se produit n)

Avec cela, j'ai les questions suivantes:

Q1. Quels sont les " moyens " appropriés pour collecter le résultat d'une commande dans un tableau de lignes?

Q2. Comment puis-je spécifier IFSde scinder uniquement sur les nouvelles lignes?

Q3. Si j'utilise les indicateurs d'expansion de paramètre comme dans ma troisième tentative ci-dessus (en utilisant @f) pour spécifier le fractionnement, zsh ignore-t-il la valeur IFS? Pourquoi ça n'a pas fonctionné dessus?

Amelio Vazquez-Reina
la source

Réponses:

71

TL, DR:

array_of_lines=("${(@f)$(my_command)}")

Première erreur (→ Q2): IFS='\n'définit IFSles deux caractères \et n. Pour définir IFSune nouvelle ligne, utilisez IFS=$'\n'.

Deuxième erreur: pour définir une variable à une valeur de tableau, vous avez besoin des parenthèses autour des éléments: array_of_lines=(foo bar).

Cela fonctionnerait, sauf qu'il supprime les lignes vides, car les espaces consécutifs comptent pour un seul séparateur:

IFS=$'\n' array_of_lines=($(my_command))

Vous pouvez conserver les lignes vides sauf à la toute fin en doublant le caractère d'espacement en IFS:

IFS=$'\n\n' array_of_lines=($(my_command))

Pour que les lignes vides se terminent également, vous devez ajouter quelque chose à la sortie de la commande, car cela se produit dans le remplacement de la commande lui-même, et non lors de son analyse.

IFS=$'\n\n' array_of_lines=($(my_command; echo .)); unset 'array_of_lines[-1]'

(en supposant que la sortie de my_commandne se termine pas par une ligne non délimitée; notez également que vous perdez le statut de sortie de my_command)

Notez que tous les extraits ci-dessus laissent une IFSvaleur différente de celle par défaut, de sorte qu'ils risquent de perturber le code suivant. Pour conserver le paramètre IFSlocal, placez le tout dans une fonction dans laquelle vous déclarez IFSlocal (en veillant également à préserver l'état de sortie de la commande):

collect_lines() {
  local IFS=$'\n\n' ret
  array_of_lines=($("$@"; ret=$?; echo .; exit $ret))
  ret=$?
  unset 'array_of_lines[-1]'
  return $ret
}
collect_lines my_command

Mais je recommande de ne pas jouer avec IFS; à la place, utilisez l' findicateur d'expansion pour fractionner les nouvelles lignes (→ Q1):

array_of_lines=("${(@f)$(my_command)}")

Ou pour conserver les lignes vides qui suivent:

array_of_lines=("${(@f)$(my_command; echo .)}")
unset 'array_of_lines[-1]'

La valeur de IFSimporte peu ici. Je suppose que vous avez utilisé une commande qui se divise IFSpour imprimer $array_of_linesdans vos tests (→ Q3).

Gilles, arrête de faire le mal
la source
7
C'est tellement compliqué! "${(@f)...}"est le même que ${(f)"..."}, mais d'une manière différente. (@)Les guillemets doubles à l'intérieur signifient "donner un mot par élément de tableau" et (f)"se séparer en tableau par une nouvelle ligne". PS: S'il vous plaît lien vers les docs
moutons volants
3
@flyingsheep, non ${(f)"..."}, les lignes vides seront sautées, "${(@f)...}"préservées. C'est la même distinction entre $argvet "$argv[@]". Cette "$@"chose pour préserver tous les éléments d’un tableau provient du shell Bourne à la fin des années 70.
Stéphane Chazelas
4

Deux problèmes: d’abord, apparemment, les guillemets doubles n’interprètent pas non plus les échappements de barre oblique inversée (désolé pour cela :). Utilisez des $'...'citations. Et selon man zshparam, pour collecter des mots dans un tableau, vous devez les placer entre parenthèses. Donc ça marche:

% touch 'a b' c d 'e f'
% IFS=$'\n' arr=($(ls)); print -l $arr
a b
c
d
e f
% print $arr[1]
a b

Je ne peux pas répondre à votre Q3. J'espère que je n'aurai jamais à savoir de telles choses ésotériques :).

Angus
la source
-2

Vous pouvez également utiliser tr pour remplacer newline par space:

lines=($(mycommand | tr '\n' ' '))
select line in ("${lines[@]}"); do
  echo "$line"
  break
done
Jimchao
la source
3
Et si les lignes contiennent des espaces?
don_crissti
2
Ça n'a aucun sens. SPC et NL ont la valeur par défaut de $IFS. Traduire l'un en l'autre ne fait aucune différence.
Stéphane Chazelas
Mes modifications étaient-elles raisonnables? Je ne pouvais pas le faire fonctionner tel quel,
John P
J'admets que je l'ai édité sans vraiment comprendre le but recherché, mais je pense que la traduction est un bon début pour la séparation basée sur la manipulation de chaînes. Je ne traduirais pas en espaces et ne me séparerais pas d'eux, à moins que le comportement attendu ne soit plus semblable à celui echooù les entrées sont toutes plus ou moins des tas de mots séparés par ceux qui s'en soucient.
John P