Não é possível canalizar no "mapfile" do bash ... mas por quê?

13

Eu só quero obter todos os arquivos em um determinado diretório em uma matriz bash (supondo que nenhum dos arquivos tenha uma nova linha no nome):

Assim:

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

Resultado vazio!

Se eu usar a maneira indireta de usar um arquivo, temporário ou não:

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

Resultado!

Mas por que não mapfilelê corretamente de um cachimbo?

David Tonhofer
fonte
Excelentes respostas, obrigado a todos. Interessante como a estratégia de execução do pipeline (cada parte executada em um processo spearate) vaza "para cima" e modifica o significado aparente do código, basicamente colocando silenciosamente "local" na frente de todas as variáveis ​​que aparecem no pipe. Em uma linguagem que é algo diferente de cola maluca para outros programas, isso seria um erro, espero.
David Tonhofer 15/08/19
2
Se você der o código para verificação de shell , você receberá avisos: SC2030 : "A modificação de var é local (para o subshell causado pelo pipeline)" e SC2031 : "var foi modificado em um subshell. Essa alteração pode ser perdida." . Excelente.
David Tonhofer
Por que usar finde mapfileaqui em tudo e não apenas simplesmente myarr=(mysqldump*)? Isso funcionará mesmo com nomes de arquivos com espaços e novas linhas.
precisa
1
Apenas notei que é preciso ativar a nullglobopção ( shopt -s nullglob) para myarr=(mysqldump*)não terminar com a matriz ('mysqldump*')caso nenhum arquivo corresponda.
David Tonhofer

Respostas:

25

De man 1 bash:

Cada comando em um pipeline é executado como um processo separado (isto é, em um subshell).

Tais subshells herdam variáveis ​​do shell principal, mas são independentes. Isso significa que mapfileno seu comando original opera por conta própria myarr. Então echo(estando fora do tubo) imprime vazio myarr(que é o shell principal myarr).

Este comando funciona de maneira diferente:

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

Nesse caso, mapfilee echoopere da mesma forma myarr(que não é o shell principal myarr).

Para alterar o shell principal, myarrvocê deve executar mapfileexatamente no shell principal. Exemplo:

myarr=()
mapfile -t myarr < <(find . -maxdepth 1 -name "mysqldump*")
echo "${myarr[@]}"
Kamil Maciorowski
fonte
Adicionado o link para "substituição de processo", conforme indicado na resposta de Attie, no caso de um visitante ter um momento TL; DR.
David Tonhofer
11

O Bash executa os comandos de um pipeline em um ambiente subshell, para que quaisquer atribuições de variáveis ​​etc. que ocorram nele não sejam visíveis para o restante do shell.

O Dash (Debian /bin/sh) e o busybox shsão semelhantes, enquanto o zsh e o ksh executam a última parte no shell principal. No Bash, você pode shopt -s lastpipefazer o mesmo, mas só funciona quando o controle de trabalho está desativado, portanto, não em shells interativos por padrão.

Assim:

$ 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 mapfiletem o mesmo problema.)

Como alternativa (e conforme mencionado por Attie), use a substituição de processo , que funciona como um pipe generalizado, e é suportada no Bash, ksh e zsh.

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

O POSIX deixa não especificado se as partes de um pipeline são executadas em subconchas ou não, portanto, não se pode realmente dizer que alguma das conchas estaria "errada" nisso.

ilkkachu
fonte
2
Se o controle de trabalho que você desativar de festa você pode usar lastpipe em um shell interativo, também:set +m; shopt -s lastpipe; x=a; echo b | read x; echo $x; set -m
Cyrus
@Cyrus, ah bem, eu tinha esquecido os detalhes, graças
ilkkachu
9

Como Kamil apontou, cada elemento no pipeline é um processo separado.

Você pode usar a seguinte substituição processo para obter finda correr em um processo diferente, com a mapfileinvocação restante em sua intérprete atual, permitindo o acesso a myarrtarde:

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

b < <( a )agirá de maneira semelhante a | bem termos de como o pipeline é conectado - a diferença é que bé executado " aqui ".

Attie
fonte