Como ler um arquivo em uma variável no shell?

489

Quero ler um arquivo e salvá-lo na variável, mas preciso manter a variável e não apenas imprimir o arquivo. Como posso fazer isso? Eu escrevi este script, mas não é exatamente o que eu precisava:

#!/bin/sh
while read LINE  
do  
  echo $LINE  
done <$1  
echo 11111-----------  
echo $LINE  

No meu script, posso fornecer o nome do arquivo como parâmetro, portanto, se o arquivo contiver "aaaa", por exemplo, ele imprimirá o seguinte:

aaaa
11111-----

Mas isso apenas imprime o arquivo na tela e quero salvá-lo em uma variável! Existe uma maneira fácil de fazer isso?

kaka
fonte
1
Parece ser um texto simples. Se fosse um arquivo binário, você precisaria disso , como resultado catou $(<someFile)resultará em uma saída incompleta (o tamanho é menor que o arquivo real).
Aquarius Power

Respostas:

1052

Na plataforma cruzada, o menor denominador comum shvocê usa:

#!/bin/sh
value=`cat config.txt`
echo "$value"

Em bashou zsh, para ler um arquivo inteiro em uma variável sem chamar cat:

#!/bin/bash
value=$(<config.txt)
echo "$value"

Invocando catem bashou zshslurp um arquivo seria considerado um uso inútil de gato .

Observe que não é necessário citar a substituição de comando para preservar novas linhas.

Veja: Wiki do Bash Hacker - Substituição de comandos - Especialidades .

Alan Gutierrez
fonte
4
Ok, mas é bash, não sh; pode não caber em todos os casos.
Moala
14
Não seria value="`cat config.txt`"e value="$(<config.txt)"seria mais seguro no caso de o config.txt conter espaços?
Martin von Wittich
13
Observe que usar catcomo acima nem sempre é considerado um uso inútil de cat. Por exemplo, < invalid-file 2>/dev/nullresultará em uma mensagem de erro que não pode ser roteada para /dev/null, enquanto cat invalid-file 2>/dev/nullé roteada corretamente para /dev/null.
DeJay Clayton
16
Para novos scripts de shell como eu, observe que a versão cat usa tick back, não aspas simples! Espero que isso poupe a alguém meia hora que me levou a descobrir isso.
Ericksonla
7
Para iniciantes como eu: observe que value=$(<config.txt)é bom, mas value = $(<config.txt)é ruim. Cuidado com esses espaços.
ArtHare 30/01/19
88

Se você quiser ler o arquivo inteiro em uma variável:

#!/bin/bash
value=`cat sources.xml`
echo $value

Se você quiser ler linha por linha:

while read line; do    
    echo $line    
done < file.txt
cérebro
fonte
2
@brain: E se o arquivo for Config.cpp e contiver barras invertidas; aspas e aspas duplas?
user2284570
2
Você deve dobrar-citar a variável em echo "$value". Caso contrário, o shell executará tokenização de espaço em branco e expansão de curinga no valor.
Tripleee
3
@ user2284570 Use em read -rvez de apenas read- sempre, a menos que você exija especificamente o estranho comportamento herdado ao qual você está se referindo.
Tripleee
74

Duas armadilhas importantes

que foram ignoradas por outras respostas até agora:

  1. Trailing remoção de nova linha da expansão de comando
  2. Remoção de caracteres NUL

Trailing remoção de nova linha da expansão de comando

Este é um problema para o:

value="$(cat config.txt)"

soluções de tipo, mas não para readsoluções baseadas.

A expansão de comandos remove as novas linhas finais:

S="$(printf "a\n")"
printf "$S" | od -tx1

Saídas:

0000000 61
0000001

Isso quebra o método ingênuo de ler arquivos:

FILE="$(mktemp)"
printf "a\n\n" > "$FILE"
S="$(<"$FILE")"
printf "$S" | od -tx1
rm "$FILE"

Solução alternativa POSIX: acrescente um caractere extra à expansão do comando e remova-o posteriormente:

S="$(cat $FILE; printf a)"
S="${S%a}"
printf "$S" | od -tx1

Saídas:

0000000 61 0a 0a
0000003

Solução alternativa quase POSIX: codificação ASCII. Ver abaixo.

Remoção de caracteres NUL

Não há uma maneira sadia do Bash para armazenar caracteres NUL em variáveis .

Isso afeta a expansão e as readsoluções, e não conheço nenhuma solução alternativa para isso.

Exemplo:

printf "a\0b" | od -tx1
S="$(printf "a\0b")"
printf "$S" | od -tx1

Saídas:

0000000 61 00 62
0000003

0000000 61 62
0000002

Ha, nosso NUL se foi!

Soluções alternativas:

  • Codificação ASCII. Ver abaixo.

  • use $""literais de extensão bash :

    S=$"a\0b"
    printf "$S" | od -tx1

    Funciona apenas para literais, portanto, não é útil para ler arquivos.

Solução alternativa para as armadilhas

Armazene uma versão codificada em uuencode base64 do arquivo na variável e decodifique antes de cada uso:

FILE="$(mktemp)"
printf "a\0\n" > "$FILE"
S="$(uuencode -m "$FILE" /dev/stdout)"
uudecode -o /dev/stdout <(printf "$S") | od -tx1
rm "$FILE"

Resultado:

0000000 61 00 0a
0000003

uuencode e udecode são POSIX 7, mas não no Ubuntu 12.04 por padrão ( sharutilspacote) ... Não vejo uma alternativa POSIX 7 para a <()extensão de substituição do processo bash, exceto a gravação em outro arquivo ...

Claro, isso é lento e inconveniente, então acho que a resposta real é: não use Bash se o arquivo de entrada puder conter caracteres NUL.

Ciro Santilli adicionou uma nova foto
fonte
2
Obrigado apenas este funcionou para mim porque eu precisava de novas linhas.
Jason Livesay
1
@CiroSantilli: E se FILE for Config.cpp e contiver barras invertidas; aspas e aspas duplas?
user2284570
@ user2284570 Eu não sabia, mas é fácil de descobrir: S="$(printf "\\\'\"")"; echo $S. Saída: \'". Por isso, funciona =)
Ciro Santilli # 13/14
@CiroSantilli: Em 5511 linhas? Tem certeza de que não há uma maneira automatizada?
user2284570
@ user2284570 Não entendo, onde existem 5511 linhas? As armadilhas vêm da $()expansão, meu exemplo mostra que a $()expansão funciona \'".
Ciro Santilli escreveu:
3

isso funciona para mim: v=$(cat <file_path>) echo $v

angelo.mastro
fonte
2

Como Ciro Santilli observa, o uso de substituições de comandos eliminará as novas linhas à direita. A solução alternativa para adicionar caracteres à direita é ótima, mas depois de usá-lo por um bom tempo, decidi que precisava de uma solução que não usasse a substituição de comandos.

Minha abordagem agora usa readjunto com a flagprintf do builtin para ler o conteúdo do stdin diretamente em uma variável.-v

# Reads stdin into a variable, accounting for trailing newlines. Avoids needing a subshell or
# command substitution.
read_input() {
  # Use unusual variable names to avoid colliding with a variable name
  # the user might pass in (notably "contents")
  : "${1:?Must provide a variable to read into}"
  if [[ "$1" == '_line' || "$1" == '_contents' ]]; then
    echo "Cannot store contents to $1, use a different name." >&2
    return 1
  fi

  local _line _contents
   while read -r _line; do
     _contents="${_contents}${_line}"$'\n'
   done
   _contents="${_contents}${_line}" # capture any content after the last newline
   printf -v "$1" '%s' "$_contents"
}

Isso suporta entradas com ou sem novas linhas à direita.

Exemplo de uso:

$ read_input file_contents < /tmp/file
# $file_contents now contains the contents of /tmp/file
dimo414
fonte
Ótimo! Eu me pergunto, por que não usar algo como _contents="${_contents}${_line}\n "para preservar novas linhas?
Eenoku
1
Você está perguntando sobre o $'\n'? Isso é necessário, caso contrário, você anexará literal \ e ncaracteres. Seu bloco de código também possui um espaço extra no final, não tenho certeza se isso é intencional, mas recuava todas as linhas subseqüentes com um espaço em branco extra.
dimo414
Bem, obrigado pela explicação!
Eenoku
-3

Você pode acessar 1 linha por vez usando o loop for

#!/bin/bash -eu

#This script prints contents of /etc/passwd line by line

FILENAME='/etc/passwd'
I=0
for LN in $(cat $FILENAME)
do
    echo "Line number $((I++)) -->  $LN"
done

Copie todo o conteúdo para o arquivo (diga line.sh); Executar

chmod +x line.sh
./line.sh
Prakash D
fonte
Seu forloop não passa por linhas, passa por cima de palavras. No caso de /etc/passwd, acontece que cada linha contém apenas uma palavra. No entanto, outros arquivos podem conter várias palavras por linha.
mpb