Ich kann Bashs "Mapfile" nicht weiterleiten ... aber warum?

13

Ich möchte nur alle Dateien in einem bestimmten Verzeichnis in ein Bash-Array bringen (vorausgesetzt, keine der Dateien enthält eine neue Zeile im Namen):

Damit:

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

Leeres Ergebnis!

Wenn ich auf Umwegen eine Datei verwende, vorübergehend oder auf andere Weise:

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

Ergebnis!

Aber warum mapfileliest man nicht richtig aus einer Pfeife?

David Tonhofer
quelle
Rundum hervorragende Antworten, vielen Dank an alle. Interessant, wie die Ausführungsstrategie der Pipeline (jeder Teil läuft in einem Spearate-Prozess) "nach oben" leckt und die offensichtliche Bedeutung des Codes ändert, indem im Grunde genommen "lokal" vor jede Variable gestellt wird, die in der Pipe erscheint. In einer Sprache, die für andere Programme etwas anderes als verrückter Kleber ist, wäre das hoffentlich ein Fehler.
David Tonhofer
2
Wenn Sie den Code für Shellcheck eingeben , erhalten Sie Warnungen: SC2030 : "Die Änderung von var ist lokal (für die durch die Pipeline verursachte Unterschale)" und SC2031 : "var wurde in einer Unterschale geändert. Diese Änderung kann verloren gehen." . Ausgezeichnet.
David Tonhofer
Warum findund mapfilehier überhaupt und nicht nur einfach myarr=(mysqldump*)? Dies funktioniert sogar mit Dateinamen mit Leerzeichen und Zeilenumbrüchen.
BlackJack
1
Ich habe gerade bemerkt, dass man die nullglobOption on ( shopt -s nullglob) aktivieren muss, um myarr=(mysqldump*)nicht mit dem Array zu enden, ('mysqldump*')falls keine Dateien übereinstimmen.
David Tonhofer

Antworten:

25

Von man 1 bash:

Jeder Befehl in einer Pipeline wird als separater Prozess ausgeführt (dh in einer Unterschale).

Solche Unterschalen erben Variablen von der Hauptschale, sind jedoch unabhängig. Dies bedeutet, dass mapfilein Ihrem ursprünglichen Befehl von selbst gearbeitet wird myarr. Dann echo(außerhalb des Rohrs) wird leer gedruckt myarr(das ist die Hauptschale myarr).

Dieser Befehl funktioniert anders:

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

In diesem Fall mapfileund echoarbeiten Sie auf der gleichen myarr(die nicht die Hauptschale ist myarr).

Um die Haupt-Shell zu ändern, müssen myarrSie mapfilegenau in der Haupt-Shell laufen . Beispiel:

myarr=()
mapfile -t myarr < <(find . -maxdepth 1 -name "mysqldump*")
echo "${myarr[@]}"
Kamil Maciorowski
quelle
Der Link zu "Prozesssubstitution" wurde hinzugefügt, wie in Atties Antwort angegeben, falls ein Besucher einen TL; DR-Moment hat.
David Tonhofer
11

Bash führt die Befehle einer Pipeline in einer Subshell-Umgebung aus, sodass alle darin enthaltenen Variablenzuweisungen usw. für den Rest der Shell nicht sichtbar sind.

Dash (Debian /bin/sh) und Busybox shsind ähnlich, während zsh und ksh den letzten Teil in der Haupt-Shell ausführen. In Bash können Sie shopt -s lastpipedasselbe tun, aber es funktioniert nur, wenn die Jobsteuerung deaktiviert ist, also nicht standardmäßig in interaktiven Shells.

Damit:

$ 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

( readund mapfilehaben das gleiche Problem.)

Alternativ (und wie von Attie erwähnt) verwenden Sie die Prozessersetzung , die wie eine verallgemeinerte Pipe funktioniert und in Bash, ksh und zsh unterstützt wird.

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

POSIX lässt es nicht spezifiziert, ob die Teile einer Pipeline in Subshells ausgeführt werden oder nicht, daher kann nicht wirklich gesagt werden, dass eine der Shells darin "falsch" wäre.

ilkkachu
quelle
2
Wenn Sie die Jobsteuerung von bash deaktivieren, können Sie lastpipe auch in einer interaktiven Shell verwenden:set +m; shopt -s lastpipe; x=a; echo b | read x; echo $x; set -m
Cyrus
@ Cyrus, ah richtig, ich hatte die Details vergessen, danke
ilkkachu
9

Wie Kamil betont hat, ist jedes Element in der Pipeline ein separater Prozess.

Sie können die folgende Prozessersetzung verwenden , um findin einem anderen Prozess ausgeführt zu werden, wobei der mapfileAufruf in Ihrem aktuellen Interpreter verbleibt und anschließend Zugriff myarrgewährt wird:

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

b < <( a )verhält sich ähnlich a | bwie die Pipeline verdrahtet - der Unterschied besteht darin, dass b" hier " ausgeführt wird.

Attie
quelle