¿Cómo leer un archivo en una variable en shell?

489

Quiero leer un archivo y guardarlo en variable, pero necesito mantener la variable y no solo imprimir el archivo. ¿Cómo puedo hacer esto? He escrito este script pero no es exactamente lo que necesitaba:

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

En mi script, puedo dar el nombre del archivo como parámetro, por lo tanto, si el archivo contiene "aaaa", por ejemplo, imprimirá esto:

aaaa
11111-----

Pero esto solo imprime el archivo en la pantalla, ¡y quiero guardarlo en una variable! ¿Hay una forma fácil de hacer esto?

kaka
fuente
1
Parece ser un texto simple. Si se tratara de un archivo binario, necesitaría esto , como resultado de cato $(<someFile)dará como resultado una salida incompleta (el tamaño es menor que el archivo real).
Acuario Power

Respuestas:

1052

En multiplataforma, el mínimo común denominador shutiliza:

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

En basho zsh, para leer un archivo completo en una variable sin invocar cat:

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

Invocando caten basho zshsorber un archivo sería considerado un uso inútil de gato .

Tenga en cuenta que no es necesario citar la sustitución del comando para preservar las nuevas líneas.

Ver: Bash Hacker's Wiki - Sustitución de comandos - Especialidades .

Alan Gutiérrez
fuente
44
Ok, pero es bash, no sh; Es posible que no se ajuste a todos los casos.
moala
14
¿No sería value="`cat config.txt`"y value="$(<config.txt)"sería más seguro en caso de que config.txt contenga espacios?
Martin von Wittich
13
Tenga en cuenta que el uso catcomo anteriormente no siempre se considera un uso inútil de cat. Por ejemplo, < invalid-file 2>/dev/nulldará como resultado un mensaje de error al que no se puede enrutar /dev/null, mientras cat invalid-file 2>/dev/nullque se enruta correctamente /dev/null.
Dejay Clayton
16
Para los nuevos scripters de shell como yo, tenga en cuenta que la versión cat usa ticks de retroceso, ¡no comillas simples! Espero que esto le ahorre a alguien media hora que me llevó resolverlo.
ericksonla
77
Para nuevos bashers como yo: Tenga en cuenta que value=$(<config.txt)es bueno, pero value = $(<config.txt)es malo. Cuidado con esos espacios.
ArtHare
88

Si desea leer todo el archivo en una variable:

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

Si quieres leerlo línea por línea:

while read line; do    
    echo $line    
done < file.txt
cerebro
fuente
2
@brain: ¿Qué sucede si el archivo es Config.cpp y contiene barras invertidas? comillas dobles y comillas?
user2284570
2
Deberías poner entre comillas la variable echo "$value". De lo contrario, el shell realizará la tokenización de espacios en blanco y la expansión de comodines en el valor.
tripleee
3
@ user2284570 Use en read -rlugar de solo read, siempre, a menos que requiera específicamente el extraño comportamiento heredado al que se refiere.
tripleee
74

Dos trampas importantes

que fueron ignoradas por otras respuestas hasta ahora:

  1. Eliminación de línea nueva posterior a la expansión del comando
  2. Eliminación de caracteres NUL

Eliminación de línea nueva posterior a la expansión del comando

Este es un problema para:

value="$(cat config.txt)"

soluciones de tipo, pero no para readsoluciones basadas.

La expansión de comandos elimina las nuevas líneas finales:

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

Salidas:

0000000 61
0000001

Esto rompe el método ingenuo de lectura de archivos:

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

Solución alternativa de POSIX: agregue un carácter adicional a la expansión del comando y elimínelo más tarde:

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

Salidas:

0000000 61 0a 0a
0000003

Solución alternativa casi POSIX: codificación ASCII. Vea abajo.

Eliminación de caracteres NUL

No hay una forma sensata de Bash de almacenar caracteres NUL en variables .

Esto afecta tanto a la expansión como a las readsoluciones, y no conozco una buena solución para ello.

Ejemplo:

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

Salidas:

0000000 61 00 62
0000003

0000000 61 62
0000002

Ja, nuestro NUL se ha ido!

Soluciones alternativas:

  • Codificación ASCII. Vea abajo.

  • usar $""literales de extensión bash :

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

    Solo funciona para literales, por lo que no es útil para leer archivos.

Solución para las trampas

Almacene una versión codificada uuencode base64 del archivo en la variable y 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"

Salida:

0000000 61 00 0a
0000003

uuencode y udecode son POSIX 7 pero no en Ubuntu 12.04 por defecto ( sharutilspaquete) ... No veo una alternativa POSIX 7 para la <()extensión de sustitución del proceso bash, excepto escribir en otro archivo ...

Por supuesto, esto es lento e inconveniente, así que supongo que la respuesta real es: no use Bash si el archivo de entrada puede contener caracteres NUL.

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
fuente
2
Gracias, solo esto funcionó para mí porque necesitaba nuevas líneas.
Jason Livesay
1
@CiroSantilli: ¿Qué pasa si FILE es Config.cpp y contiene barras invertidas? comillas dobles y comillas?
user2284570
@ user2284570 Yo no sé, pero es fácil de averiguar: S="$(printf "\\\'\"")"; echo $S. Salida: \'". Así funciona =)
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
@CiroSantilli: ¿En las líneas 5511? ¿Estás seguro de que no hay una forma automatizada?
user2284570
@ user2284570 No entiendo, ¿dónde hay 5511 líneas? Las trampas provienen de la $()expansión, mi ejemplo muestra que la $()expansión funciona \'".
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
3

esto funciona para mi: v=$(cat <file_path>) echo $v

angelo.mastro
fuente
2

Como señala Ciro Santilli, el uso de sustituciones de comandos eliminará las nuevas líneas finales. Su solución para agregar caracteres finales es excelente, pero después de usarla durante bastante tiempo decidí que necesitaba una solución que no utilizara la sustitución de comandos.

Mi enfoque ahora usa readjunto con la banderaprintf incorporada para leer el contenido de stdin directamente en una variable.-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"
}

Esto admite entradas con o sin líneas nuevas.

Ejemplo de uso:

$ read_input file_contents < /tmp/file
# $file_contents now contains the contents of /tmp/file
dimo414
fuente
¡Excelente! Me pregunto, ¿por qué no usar algo como _contents="${_contents}${_line}\n "preservar nuevas líneas?
Eenoku
1
¿Estás preguntando por el $'\n'? Eso es necesario, de lo contrario, está agregando literales \ y ncaracteres. Su bloque de código también tiene un espacio adicional al final, no estoy seguro si eso es intencional, pero sangraría cada línea posterior con un espacio en blanco adicional.
dimo414
Bueno, gracias por la explicación!
Eenoku
-3

Puede acceder a 1 línea a la vez por bucle

#!/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 el contenido en Archivo (por ejemplo, line.sh); Ejecutar

chmod +x line.sh
./line.sh
Prakash D
fuente
Su forbucle no se repite sobre las líneas, se repite sobre las palabras. En el caso de /etc/passwd, sucede que cada línea contiene solo una palabra. Sin embargo, otros archivos pueden contener varias palabras por línea.
mpb