No se puede conectar el "archivo de mapa" de bash ... pero ¿por qué?

13

Solo quiero obtener todos los archivos en un determinado directorio en una matriz bash (suponiendo que ninguno de los archivos tenga una nueva línea en el nombre):

Entonces:

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

Resultado vacío!

Si hago la forma indirecta de usar un archivo, temporal o de otra manera:

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

¡Resultado!

Pero, ¿por qué no mapfilelee correctamente de una tubería?

David Tonhofer
fuente
Excelentes respuestas por todas partes, gracias a todos. Es interesante ver cómo la estrategia de ejecución de la tubería (cada parte que se ejecuta en un proceso de filtración) se filtra "hacia arriba" y modifica el significado aparente del código, básicamente colocando en silencio "local" delante de cada variable que aparece en la tubería. En un lenguaje que no sea pegamento loco para otros programas, eso sería un error, con suerte.
David Tonhofer
2
Si le da el código a shellcheck , recibirá advertencias: SC2030 : "La modificación de var es local (a la subshell causada por la canalización)" y SC2031 : "var se modificó en una subshell. Ese cambio podría perderse". . Excelente.
David Tonhofer
¿Por qué usar findy mapfileaquí en absoluto y no simplemente myarr=(mysqldump*)? Esto incluso funcionará con nombres de archivos con espacios y líneas nuevas.
BlackJack
1
Acabo de notar que uno tiene que activar la nullglobopción ( shopt -s nullglob) para myarr=(mysqldump*)no terminar con la matriz ('mysqldump*')en caso de que no coincidan los archivos.
David Tonhofer

Respuestas:

25

De man 1 bash:

Cada comando en una tubería se ejecuta como un proceso separado (es decir, en una subshell).

Tales subcapas heredan variables de la coraza principal pero son independientes. Esto significa que mapfileen su comando original opera por sí solo myarr. Luego echo(estando fuera de la tubería) imprime vacío myarr(que es el caparazón principal myarr).

Este comando funciona de manera diferente:

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

En este caso mapfiley echooperar en el mismo myarr(que no es el shell principal myarr).

Para cambiar el shell principal, myarrdebe ejecutar mapfileexactamente en el shell principal. Ejemplo:

myarr=()
mapfile -t myarr < <(find . -maxdepth 1 -name "mysqldump*")
echo "${myarr[@]}"
Kamil Maciorowski
fuente
Se agregó el enlace a "sustitución de proceso" como se indica en la respuesta de Attie, en caso de que un visitante tenga un momento TL; DR.
David Tonhofer
11

Bash ejecuta los comandos de una tubería en un entorno de subshell, por lo que cualquier asignación de variables, etc. que tenga lugar dentro de ella no será visible para el resto del shell.

Dash (Debian's /bin/sh) así como busybox's shson similares, mientras que zsh y ksh ejecutan la última parte en el shell principal. En Bash, puede shopt -s lastpipehacer lo mismo, pero solo funciona cuando el control de trabajo está deshabilitado, por lo que no se encuentra en shells interactivos de forma predeterminada.

Entonces:

$ 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

( ready mapfiletienen el mismo problema)

Alternativamente (y como lo menciona Attie), use la sustitución de procesos , que funciona como una tubería generalizada y es compatible con Bash, ksh y zsh.

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

POSIX lo deja sin especificar si las partes de una tubería se ejecutan en subcapas o no, por lo que realmente no se puede decir que cualquiera de las cáscaras estaría "mal" en esto.

ilkkachu
fuente
2
Si desactiva el control de trabajo de bash, también puede usar lastpipe en un shell interactivo:set +m; shopt -s lastpipe; x=a; echo b | read x; echo $x; set -m
Cyrus
@ Cyrus, ah cierto, me había olvidado de los detalles, gracias
ilkkachu
9

Como Kamil ha señalado, cada elemento en la tubería es un proceso separado.

Se puede utilizar la siguiente sustitución proceso para conseguir findque se ejecute en un proceso diferente, con la mapfileinvocación que queda en su intérprete actual, permitiendo el acceso a myarrdespués:

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

b < <( a )actuará de manera similar a | ben términos de cómo está conectada la tubería; la diferencia es que bse ejecuta " aquí ".

Attie
fuente