Vous ne pouvez pas utiliser le "mapfile" de bash ... mais pourquoi?

11

Je veux juste obtenir tous les fichiers d’un répertoire donné dans un tableau bash (en supposant qu’aucun des fichiers n’ait une nouvelle ligne dans le nom):

Alors:

myarr=()
find . -maxdepth 1  -name "mysqldump*" | mapfile -t myarr; echo "${myarr[@]}"

Résultat vide!

Si je fais la maniʻere détournée d'utiliser un fichier, temporaire ou non:

myarr=()
find . -maxdepth 1  -name "mysqldump*" > X
mapfile -t myarray < X
echo "${myarray[@]}"

Résultat!

Mais pourquoi ne mapfilelit-il pas correctement à partir d'un tuyau?

David Tonhofer
la source
Excellentes réponses tout autour, merci à tous. Il est intéressant de noter que la stratégie d’exécution du pipeline (chaque partie exécutée dans un processus spearate) fuit "vers le haut" et modifie la signification apparente du code, mettant essentiellement en silence "local" devant chaque variable apparaissant dans le tuyau. Dans un langage qui est autre chose que de la colle folle pour d’autres programmes, ce serait un bogue, espérons-le.
David Tonhofer
2
Si vous attribuez le code à shellcheck , vous recevez des avertissements: SC2030 : "La modification de var est locale (vers le sous-shell causé par le pipeline)" et SC2031 : "var a été modifié dans un sous-shell. Ce changement peut être perdu." . Excellent.
David Tonhofer
Pourquoi utiliser findet mapfileici du tout et pas simplement myarr=(mysqldump*)? Cela fonctionnera même avec les noms de fichiers avec des espaces et des nouvelles lignes.
BlackJack
1
Je viens de remarquer qu'il faut activer l' nullgloboption ( shopt -s nullglob) pour myarr=(mysqldump*)ne pas se retrouver avec le tableau ('mysqldump*')si aucun fichier ne correspond.
David Tonhofer

Réponses:

23

De man 1 bash:

Chaque commande d'un pipeline est exécutée en tant que processus séparé (c'est-à-dire dans un sous-shell).

Ces sous-shell héritent des variables du shell principal mais ils sont indépendants. Cela signifie que mapfiledans votre commande d'origine, elle fonctionne seule myarr. Ensuite echo(en dehors du tuyau) imprime vide myarr(qui est celle du shell principal myarr).

Cette commande fonctionne différemment:

find . -maxdepth 1 -name "mysqldump*" | { mapfile -t myarr; echo "${myarr[@]}"; }

Dans ce cas mapfile, echoopérez de la même manière myarr(ce qui n'est pas le shell principal myarr).

Pour changer le shell principal, myarrvous devez exécuter mapfileexactement le shell principal. Exemple:

myarr=()
mapfile -t myarr < <(find . -maxdepth 1 -name "mysqldump*")
echo "${myarr[@]}"
Kamil Maciorowski
la source
Ajout du lien vers "processus de substitution" comme indiqué dans la réponse d'Attie, au cas où un visiteur aurait un moment TL; DR.
David Tonhofer
10

Bash exécute les commandes d'un pipeline dans un environnement de sous-shell. Par conséquent, les affectations de variables, etc., qui y sont effectuées ne sont pas visibles pour le reste du shell.

Dash (Debian /bin/sh) et busybox shsont similaires, alors que zsh et ksh exécutent la dernière partie du shell principal. Dans Bash, vous pouvez shopt -s lastpipefaire de même, mais cela ne fonctionne que lorsque le contrôle du travail est désactivé, donc pas dans les shells interactifs par défaut.

Alors:

$ bash -c 'x=a; echo b | read x; echo $x'
a
$ bash -c 'shopt -s lastpipe; x=a; echo b | read x; echo $x'
b

( readet mapfileont le même problème.)

Alternativement (et comme mentionné par Attie), utilisez la substitution de processus , qui fonctionne comme un tube généralisé et est prise en charge par Bash, ksh et zsh.

$ bash -c 'x=a; read x < <(echo b); echo $x'
b

POSIX ne spécifie pas si les parties d'un pipeline sont exécutées dans des sous-coques ou non, donc on ne peut pas vraiment dire que l'une des coques serait "fausse".

ilkkachu
la source
1
Si vous désactivez le contrôle des tâches de bash, vous pouvez également utiliser lastpipe dans un shell interactif:set +m; shopt -s lastpipe; x=a; echo b | read x; echo $x; set -m
Cyrus
@Cyrus, ah oui, j'avais oublié les détails, merci
ilkkachu
8

Comme l'a souligné Kamil, chaque élément du pipeline est un processus distinct.

Vous pouvez utiliser ce qui suit la substitution de processus pour obtenir de findfonctionner dans un processus différent, avec l' mapfileinvocation qui reste dans votre interprète en cours, ce qui permet l' accès à la myarrsuite:

myarr=()
mapfile -t myarr < <( find . -maxdepth 1  -name "mysqldump*" )
echo "${myarr[@]}"

b < <( a )agira de la même manière a | ben ce qui concerne la manière dont le pipeline est câblé - la différence est qu’il best exécuté " ici ".

Attie
la source