722

I want to read a file and save it in variable, but I need to keep the variable and not just print out the file. How can I do this? I have written this script but it isn't quite what I needed:

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

In my script, I can give the file name as a parameter, so, if the file contains "aaaa", for example, it would print out this:

aaaa
11111-----

But this just prints out the file onto the screen, and I want to save it into a variable! Is there an easy way to do this?

Martin Tournoij
  • 26,737
  • 24
  • 105
  • 146
kaka
  • 7,425
  • 4
  • 19
  • 23
  • 2
    It seems to be a plain text. If it was a binary file, you would need [this](http://unix.stackexchange.com/questions/10801/how-to-use-bash-script-to-read-binary-file-content), as the result of `cat` or `$( – Aquarius Power Dec 20 '15 at 21:27
  • 2
    Related: [What is the difference between “$(cat file)”, “$( – codeforester Dec 10 '18 at 06:23

9 Answers9

1487

In cross-platform, lowest-common-denominator sh you use:

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

In bash or zsh, to read a whole file into a variable without invoking cat:

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

Invoking cat in bash or zsh to slurp a file would be considered a Useless Use of Cat.

Note that it is not necessary to quote the command substitution to preserve newlines.

See: Bash Hacker's Wiki - Command substitution - Specialities.

Nameless One
  • 1,615
  • 2
  • 23
  • 39
Alan Gutierrez
  • 15,339
  • 1
  • 18
  • 17
  • 4
    Ok but it's bash, not sh; it may not fit all cases. – moala Apr 15 '13 at 11:25
  • @moala Yes. That's bothered me ever since I started to use FreeBSD again; remembering the limits of pure `/bin/sh`. – Alan Gutierrez Apr 18 '13 at 00:18
  • 27
    Wouldn't ``value="`cat config.txt`"`` and ``value="$( – Martin von Wittich Aug 21 '14 at 13:29
  • 1
    @AlanGutierrez : What about if config.txt is Config.cpp and contain backslashes; double quotes and quotes? – user2284570 Oct 13 '14 at 03:27
  • 1
    Regarding newlines. The double quotes are not necessary to preserve newlines in the before the last non-empty line. Trailing newlines are stripped. The double quotes that some have suggested do not change that. Other answers here address that issue, how to preserve trailing newlines. I've thought about amending my answer to include that, but it feels like I'm stealing someone else's answer. – Alan Gutierrez Sep 30 '16 at 18:20
  • 22
    Note that using `cat` as above is not always considered a useless use of `cat`. For example, `< invalid-file 2>/dev/null` will result in an error message that can't be routed to `/dev/null`, whereas `cat invalid-file 2>/dev/null` does get properly routed to `/dev/null`. – Dejay Clayton Dec 20 '16 at 20:33
  • 33
    For new shell scripters like me, note the cat version uses back ticks, not single quotes! Hopefully this will save someone a half hour it took me to figure it out. – ericksonla Feb 06 '17 at 18:10
  • 24
    For new bashers like me: Note that `value=$( – ArtHare Jan 30 '18 at 14:25
  • 6
    @DejayClayton use `{ var=$(<"$file"); } 2>/dev/null` to disable warnings, see https://unix.stackexchange.com/questions/428500/how-to-make-bash-substitution-filename-silent/428529#428529 – rudimeier Mar 06 '18 at 16:57
  • 5
    Backticks as the "lowest common demoninator" are catering for shells that are many decades old. I'd say using `$(...)` for *any* shell run as `/bin/sh` works everywhere. – Benjamin W. Nov 22 '18 at 19:11
  • 1
    You can also `value = $(cat /etc/filename)` – GeneCode Dec 14 '18 at 02:19
  • And? Symbol `\` is replaced with a list of directories – Vyachaslav Gerchicov Dec 12 '19 at 15:35
  • 1
    Is `value=\`cat config.txt\`` better than `value="$(cat config.txt)"`? If so, how? Why? Can you explain it in your answer please? I don't understand the backtick usage like this in bash and have never used it before. – Gabriel Staples Mar 24 '20 at 08:27
  • @GabrielStaples It's not better. On the contrary, it's worse—even if you add missing quotes—due to its potential to confuse backticks with apostrophes and inability to easily nest substitutions like `$(cat /path/to/$(printf %03x.txt $myNum))`. – Ruslan May 06 '20 at 07:12
  • On the other hand, when writing bash tasks in an AzureDevOps pipeline where `$(....)` can also mean an ADO variable substitution, it _might_ be prefereable to use backticks. – Ed Randall Apr 07 '21 at 16:32
  • 1
    @MartinvonWittich No, as there is no word splitting performed. To quote `man bash`/SIMPLE COMMAND EXPANSION: “The text after the = in each variable assignment undergoes tilde expansion, parameter expansion, command substitution, arithmetic expansion, and quote removal before being assigned to the variable.” Double quotes get removed as usual, but they have no effect whatsoever. – dessert Jan 21 '22 at 13:46
112

Two important pitfalls

which were ignored by other answers so far:

  1. Trailing newline removal from command expansion
  2. NUL character removal

Trailing newline removal from command expansion

This is a problem for the:

value="$(cat config.txt)"

type solutions, but not for read based solutions.

Command expansion removes trailing newlines:

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

Outputs:

0000000 61
0000001

This breaks the naive method of reading from files:

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

POSIX workaround: append an extra char to the command expansion and remove it later:

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

Outputs:

0000000 61 0a 0a
0000003

Almost POSIX workaround: ASCII encode. See below.

NUL character removal

There is no sane Bash way to store NUL characters in variables.

This affects both expansion and read solutions, and I don't know any good workaround for it.

Example:

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

Outputs:

0000000 61 00 62
0000003

0000000 61 62
0000002

Ha, our NUL is gone!

Workarounds:

  • ASCII encode. See below.

  • use bash extension $"" literals:

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

    Only works for literals, so not useful for reading from files.

Workaround for the pitfalls

Store an uuencode base64 encoded version of the file in the variable, and decode before every usage:

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

Output:

0000000 61 00 0a
0000003

uuencode and udecode are POSIX 7 but not in Ubuntu 12.04 by default (sharutils package)... I don't see a POSIX 7 alternative for the bash process <() substitution extension except writing to another file...

Of course, this is slow and inconvenient, so I guess the real answer is: don't use Bash if the input file may contain NUL characters.

Community
  • 1
  • 1
Ciro Santilli OurBigBook.com
  • 347,512
  • 102
  • 1,199
  • 985
112

If you want to read the whole file into a variable:

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

If you want to read it line-by-line:

while read line; do    
    echo $line    
done < file.txt
brain
  • 2,507
  • 1
  • 13
  • 12
  • 3
    @brain : What if the file is Config.cpp and contain backslashes; double quotes and quotes? – user2284570 Oct 13 '14 at 03:27
  • 6
    You should double-quote the variable in `echo "$value"`. Otherwise, the shell will perform whitespace tokenization and wildcard expansion on the value. – tripleee Feb 04 '16 at 13:12
  • 6
    @user2284570 Use `read -r` instead of just `read` -- always, unless you specifically require the weird legacy behavior you are alluding to. – tripleee Feb 04 '16 at 13:13
29

This works for me:

v=$(cat <file_path>)
echo $v
Zoe
  • 27,060
  • 21
  • 118
  • 148
angelo.mastro
  • 1,680
  • 17
  • 14
17

With bash you may use read like this:

#!/usr/bin/env bash

{ IFS= read -rd '' value <config.txt;} 2>/dev/null

printf '%s' "$value"

Notice that:

  • The last newline is preserved.

  • The stderr is silenced to /dev/null by redirecting the whole commands block, but the return status of the read command is preserved, if one needed to handle read error conditions.

Léa Gris
  • 17,497
  • 4
  • 32
  • 41
5

As Ciro Santilli notes using command substitutions will drop trailing newlines. Their workaround adding trailing characters is great, but after using it for quite some time I decided I needed a solution that didn't use command substitution at all.

My approach now uses read along with the printf builtin's -v flag in order to read the contents of stdin directly into a variable.

# Reads stdin into a variable, accounting for trailing newlines. Avoids
# needing a subshell or command substitution.
# Note that NUL bytes are still unsupported, as Bash variables don't allow NULs.
# See https://stackoverflow.com/a/22607352/113632
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 IFS='' read -r _line; do
     _contents+=("$_line"$'\n')
   done
   # include $_line once more to capture any content after the last newline
   printf -v "$1" '%s' "${_contents[@]}" "$_line"
}

This supports inputs with or without trailing newlines.

Example usage:

$ read_input file_contents < /tmp/file
# $file_contents now contains the contents of /tmp/file
dimo414
  • 47,227
  • 18
  • 148
  • 244
  • Great! I just wonder, why not to use something like `_contents="${_contents}${_line}\n "` to preserve newlines? – Eenoku Mar 27 '19 at 13:14
  • 1
    Are you asking about the `$'\n'`? That's necessary, otherwise you're appending literal `\ ` and `n` characters. Your code block also has an extra space at the end, not sure if that's intentional, but it'd indent every subsequent line with an extra whitespace. – dimo414 Mar 27 '19 at 17:23
2

All the given solutions are quite slow, so:

mapfile -d '' content </etc/passwd  # Read file into an array
content="${content[*]%$'\n'}"       # Remove trailing newline

Would be nice to optimise it even more but I can't think of much

Update: Found a faster way

read -rd '' content </etc/passwd

This will return exit code of 1 so if you need it to be always 0:

read -rd '' content </etc/passwd || :
Ari157
  • 95
  • 4
  • 16
  • you are right, the $(cat sth) take about 5ms slower compare to "read" command, i think its because of the $() syntax – kkocdko Jan 30 '23 at 00:20
  • @kkocdko indeed, you should avoid forks and outside commands like the plague – Ari157 Jan 30 '23 at 13:58
2

I use:

NGINX_PID=`cat -s "/sdcard/server/nginx/logs/nginx.pid" 2>/dev/null`

if [ "$NGINX_PID" = "" ]; then
  echo "..."
  exit
fi
-1

You can access 1 line at a time by for loop

#!/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

Copy the entire content to File (say line.sh ) ; Execute

chmod +x line.sh
./line.sh
Prakash D
  • 403
  • 4
  • 6
  • 1
    Your `for` loop does not loop over lines, it loops over words. In the case of `/etc/passwd`, it happens that each line contains only one word. However, other files may contain multiple words per line. – mpb May 01 '20 at 18:10