Non è possibile reindirizzare il "mapfile" di bash ... ma perché?


13

Voglio solo ottenere tutti i file in una determinata directory in un array bash (supponendo che nessuno dei file abbia una nuova riga nel nome):

Così:

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

Risultato vuoto!

Se faccio il modo rotonda di utilizzare un file, temporaneo o altro:

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

Risultato!

Ma perché non mapfilelegge correttamente da una pipe?



Ottime risposte a tutti, grazie a tutti. Interessante il modo in cui la strategia di esecuzione della pipeline (ogni parte in esecuzione in un processo spearate) perde "verso l'alto" e modifica il significato apparente del codice, fondamentalmente mettendo "localmente" davanti a ogni variabile che appare nella pipa. In un linguaggio che è qualcosa di diverso dalla colla folle per altri programmi, sarebbe un bug, si spera.
David Tonhofer,

2
Se dai il codice al shellcheck , ricevi avvisi: SC2030 : "La modifica di var è locale (alla subshell causata dalla pipeline)" e SC2031 : "var è stata modificata in una subshell. Quella modifica potrebbe andare persa." . Eccellente.
David Tonhofer,

Perché usare finde mapfilequi a tutti e non solo semplicemente myarr=(mysqldump*)? Funzionerà anche con i nomi di file con spazi e nuove righe.
BlackJack,

1
Ho appena notato che si deve attivare l' nullglobopzione on ( shopt -s nullglob) per myarr=(mysqldump*)non finire con l'array ('mysqldump*')nel caso in cui nessun file corrisponda.
David Tonhofer,

Risposte:


25

Da man 1 bash:

Ogni comando in una pipeline viene eseguito come un processo separato (cioè in una subshell).

Tali subshells ereditano le variabili dalla shell principale ma sono indipendenti. Ciò significa che mapfilenel comando originale funziona da solo myarr. Quindi echo(trovandosi all'esterno del tubo) stampa vuota myarr(che è la shell principale myarr).

Questo comando funziona in modo diverso:

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

In questo caso mapfilee echooperare sullo stesso myarr(che non è la shell principale myarr).

Per cambiare la shell principale myarrdevi eseguire mapfileesattamente nella shell principale. Esempio:

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

Aggiunto il link al "processo di sostituzione" come indicato nella risposta di Attie, nel caso in cui un visitatore abbia un momento TL; DR.
David Tonhofer,

11

Bash esegue i comandi di una pipeline in un ambiente subshell, quindi eventuali assegnazioni di variabili ecc. Che avvengono al suo interno non sono visibili al resto della shell.

Dash (Debian's /bin/sh) e busybox shsono simili, mentre zsh e ksh eseguono l'ultima parte nella shell principale. In Bash, puoi usare shopt -s lastpipelo stesso, ma funziona solo quando il controllo del lavoro è disabilitato, quindi non nelle shell interattive per impostazione predefinita.

Così:

$ 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

( reade mapfilehanno lo stesso problema.)

In alternativa (e come menzionato da Attie), utilizzare la sostituzione di processo , che funziona come una pipe generalizzata ed è supportata in Bash, ksh e zsh.

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

POSIX lo lascia non specificato se le parti di una pipeline funzionano o meno in subshells, quindi non si può davvero dire che una delle shell sarebbe "sbagliata" in questo.


2
Se disabiliti il ​​controllo del lavoro di bash puoi usare lastpipe anche in una shell interattiva:set +m; shopt -s lastpipe; x=a; echo b | read x; echo $x; set -m
Cyrus

@Ciro, ah giusto, avevo dimenticato i dettagli, grazie
ilkkachu il

9

Come ha sottolineato Kamil, ogni elemento della pipeline è un processo separato.

È possibile utilizzare la seguente sostituzione del processo per iniziare findl'esecuzione in un processo diverso, con l' mapfileinvocazione che rimane nell'interprete corrente, consentendo l'accesso in myarrseguito:

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

b < <( a )agirà in modo simile a | bin termini di come viene cablata la pipeline - la differenza è che bviene eseguita " qui ".

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.