Criação de uma matriz a partir de um arquivo de texto no Bash

86

Um script pega um URL, analisa-o em busca dos campos obrigatórios e redireciona sua saída para ser salva em um arquivo, file.txt . A saída é salva em uma nova linha cada vez que um campo é encontrado.

arquivo.txt

A Cat
A Dog
A Mouse 
etc... 

Quero pegar file.txte criar um array a partir dele em um novo script, onde cada linha passa a ser sua própria variável de string no array. Até agora tentei:

#!/bin/bash

filename=file.txt
declare -a myArray
myArray=(`cat "$filename"`)

for (( i = 0 ; i < 9 ; i++))
do
  echo "Element [$i]: ${myArray[$i]}"
done

Quando executo este script, o espaço em branco resulta em palavras divididas e em vez de

Saída desejada

Element [0]: A Cat 
Element [1]: A Dog 
etc... 

Eu acabo recebendo isso:

Saída real

Element [0]: A 
Element [1]: Cat 
Element [2]: A
Element [3]: Dog 
etc... 

Como posso ajustar o loop abaixo de modo que toda a string em cada linha corresponda um a um com cada variável na matriz?

user2856414
fonte
5
É disso que se trata o Bash FAQ 001 . Além disso, esta seção do tópico array na FAQ 005 do Bash .
Etan Reisner
1
Eu vincularia isso como uma duplicata de stackoverflow.com/questions/11393817/… , mas a resposta aceita é péssima.
Charles Duffy
Etan, muito obrigado por uma resposta tão rápida e precisa! Eu tentei pesquisar minha pergunta nos fóruns, mas não pensei em procurar o FAQ sobre stackoverflow. O comando mapfile atendeu exatamente às minhas necessidades! Obrigado novamente :) Resposta na seção 2.1 .
user2856414
2
(Configure o link na direção oposta, pois temos uma resposta mais aceita aqui do que lá).
Charles Duffy

Respostas:

107

Use o mapfilecomando:

mapfile -t myArray < file.txt

O erro está usando for- a maneira idiomática de repetir as linhas de um arquivo é:

while IFS= read -r line; do echo ">>$line<<"; done < file.txt

Veja BashFAQ / 005 para mais detalhes.

glenn jackman
fonte
5
Uma vez que este está sendo promovido como o q canônica & a, você também pode incluir o que é mencionado no link: while IFS= read -r; do lines+=("$REPLY"); done <file.
fedorqui 'SO pare de prejudicar'
10
mapfile não existe em versões bash anteriores a 4.x
ericslaw
14
O Bash 4 tem cerca de 5 anos agora. Melhoria.
glenn jackman
5
Apesar do bash 4 ter sido lançado em 2009, o comentário de @ericslaw continua relevante porque muitas máquinas ainda são fornecidas com o bash 3.x (e não serão atualizadas, desde que o bash seja lançado sob GPLv3). Se você está interessado em portabilidade, é importante notar
De Novo
12
o problema não é que um desenvolvedor não possa instalar uma versão atualizada, é que um desenvolvedor deve estar ciente de que um script usando mapfilenão será executado como esperado em muitas máquinas sem etapas adicionais. Macs @ericslaw continuarão a ser fornecidos com o bash 3.2.57 em um futuro próximo. Versões mais recentes usam uma licença que exigiria que a apple compartilhasse ou permitisse coisas que ela não deseja compartilhar ou permitir.
De Novo
23

mapfilee readarray(que são sinônimos) estão disponíveis no Bash versão 4 e superior. Se você tiver uma versão mais antiga do Bash, poderá usar um loop para ler o arquivo em uma matriz:

arr=()
while IFS= read -r line; do
  arr+=("$line")
done < file

Caso o arquivo tenha uma última linha incompleta (falta de nova linha), você pode usar esta alternativa:

arr=()
while IFS= read -r line || [[ "$line" ]]; do
  arr+=("$line")
done < file

Relacionado:

codeforester
fonte
Acho que preciso colocar parênteses IFS= read -r line || [[ "$line" ]]para que funcione. Caso contrário, funciona muito bem!
Tatiana Racheva
@TatianaRacheva: não é aquele ponto-e-vírgula que faltava antes do?
codeforester
9

Você também pode fazer isso:

oldIFS="$IFS"
IFS=$'\n' arr=($(<file))
IFS="$oldIFS"
echo "${arr[1]}" # It will print `A Dog`.

Nota:

A expansão do nome do arquivo ainda ocorre. Por exemplo, se houver uma linha com um literal, *ele se expandirá para todos os arquivos na pasta atual. Portanto, use-o apenas se o seu arquivo estiver livre desse tipo de cenário.

Jahid
fonte
Existe alguma maneira de definir IFSapenas temporariamente (para que ele recupere seu valor original após este comando), enquanto ainda persiste a atribuição para arr?
Hugues
1
Observe que a expansão do nome do arquivo ainda ocorre; por exemploIFS=$'\n' arr=($(echo 'a 1'; echo '*'; echo 'b 2')); printf "%s\n" "${arr[@]}"
Hugues
@Hugues: yap, a expansão do nome do arquivo ainda ocorre. Vou adicionar um pouco de informação ... obrigado ...
Jahid
Desculpe, eu discordo. IFS=... commandnão muda IFSno shell atual. No entanto, IFS=... other_variable=...(sem nenhum comando) muda ambos IFSe other_variableno shell atual.
Hugues
1
Obrigado! Isso funciona; é uma pena que não haja uma maneira mais simples, pois gosto da arr=notação (em comparação com mapfile/ readarray).
Hugues
4

Você pode simplesmente ler cada linha do arquivo e atribuí-la a um array.

#!/bin/bash
i=0
while read line 
do
        arr[$i]="$line"
        i=$((i+1))
done < file.txt
Prateek Joshi
fonte
1
Como você acessa o array?
hola
4

Use mapfile ou leia -a

Sempre verifique seu código usando shellcheck . Freqüentemente, você receberá a resposta correta. Neste caso, SC2207 cobre a leitura de um arquivo que tem valores separados por espaço ou separados por nova linha em uma matriz.

Não faça isso

array=( $(mycommand) )

Arquivos com valores separados por novas linhas

mapfile -t array < <(mycommand)

Arquivos com valores separados por espaços

IFS=" " read -r -a array <<< "$(mycommand)"

A página shellcheck fornecerá a razão de por que isso é considerado a prática recomendada.

Cameron Lowell Palmer
fonte
0

Esta resposta diz para usar

mapfile -t myArray < file.txt

Eu fiz um shim para mapfilese você quiser usar mapfileno bash <4.x por qualquer motivo. Ele usa o existentemapfile comando se você estiver em bash> = 4.x

Atualmente, apenas opções -de -tfuncionam. Mas isso deve ser suficiente para o comando acima. Eu só testei no macOS. No macOS Sierra 10.12.6, o bash do sistema é 3.2.57(1)-release. Portanto, o calço pode ser útil. Você também pode simplesmente atualizar seu bash com homebrew, construir o bash você mesmo, etc.

Ele usa essa técnica para definir variáveis ​​em uma pilha de chamadas.

dosentmatter
fonte