如何在shell中将文件读入变量?

730
我想读取一个文件并将其保存在变量中,但我需要保留变量而不仅仅是打印出文件。 如何做到这一点?我编写了这个脚本,但它不完全是我所需要的:
#!/bin/sh
while read LINE  
do  
  echo $LINE  
done <$1  
echo 11111-----------  
echo $LINE  

在我的脚本中,我可以将文件名作为参数。例如,如果文件包含“aaaa”,那么它会打印出这个:

aaaa
11111-----

但是这只是把文件打印到屏幕上,我想将它保存到变量中! 有没有简单的方法可以做到这一点?


2
看起来这是一个纯文本。如果它是一个二进制文件,你需要使用这个链接,因为cat$(<someFile)的结果会导致输出不完整(大小小于实际文件)。 - Aquarius Power
9个回答

1502
在跨平台、最低公共分母的 sh 中,您可以使用:
#!/bin/sh
value=`cat config.txt`
echo "$value"

在或中,读取整个文件到一个变量中而不调用的方法是:
#!/bin/bash
value=$(<config.txt)
echo "$value"

在编程中,使用bashzsh中的cat命令来读取文件被认为是无用的Cat用法
请注意,为了保留换行符,不需要对命令替换进行引用。
参见:Bash黑客维基-命令替换-特殊之处

28
如果config.txt包含空格,那么value="`cat config.txt`"value="$(<config.txt)"更安全,这样做不会改变原来的意思。 - Martin von Wittich
23
请注意,像上面那样使用 cat 并不总是被认为是无用的。例如,< invalid-file 2>/dev/null 会导致无法将错误信息路由到 /dev/null,而 cat invalid-file 2>/dev/null 则可以正确地路由到 /dev/null - Dejay Clayton
35
对于像我一样的新脚本编写者,请注意cat命令的版本使用反引号,而不是单引号!希望这能为其他人节省掉我花费半个小时才弄明白的时间。 - ericksonla
27
对于像我这样的新手:请注意,value=$(<config.txt)是正确的,但value = $(<config.txt)是错误的。注意那些空格。 - ArtHare
6
дҪҝз”Ё{ var=$(<"$file"); } 2>/dev/nullжқҘзҰҒз”ЁиӯҰе‘ҠпјҢеҸӮиҖғhttps://unix.stackexchange.com/questions/428500/how-to-make-bash-substitution-filename-silent/428529#428529гҖӮ - rudimeier
显示剩余12条评论

116

两个重要的陷阱

到目前为止其他答案忽略了这两个陷阱:

  1. 从命令扩展中删除尾随换行符
  2. 删除NUL字符

从命令扩展中删除尾随换行符

这是一个问题,适用于:

value="$(cat config.txt)"

适用于类型解决方案,但不适用于基于read的解决方案。

命令扩展会删除尾随的换行符:

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

输出:

0000000 61
0000001

这会破坏从文件中读取的朴素方法:

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

POSIX解决方法:在命令扩展的末尾添加一个额外的字符,然后再将其删除:

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

输出:

0000000 61 0a 0a
0000003

几乎符合 POSIX 标准的解决方法:使用 ASCII 编码。见下文。

删除 NUL 字符

在 Bash 中没有明智的方式来存储 NUL 字符到变量中

这会影响扩展和 read 解决方案,我不知道任何好的解决方法。

示例:

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

输出:

0000000 61 00 62
0000003

0000000 61 62
0000002

哈,我们的NUL不见了!

解决方法:

  • 使用ASCII编码。请参见下文。

  • 使用bash扩展$""字面量:

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

    只适用于字面值,因此无法从文件中读取。

    解决此问题的方法

    将文件的 uuencode base64 编码版本存储在变量中,并在每次使用前解码:

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

    输出:

    0000000 61 00 0a
    0000003
    

    uuencode和udecode是POSIX 7的一部分,但在Ubuntu 12.04中默认情况下没有(需要安装sharutils软件包)... 我没有看到bash中有关于<()替换扩展的POSIX 7替代方案,除了写入另一个文件...

    当然,这样做效率低下且不方便,所以我的建议是:如果输入文件可能包含NUL字符,请不要使用Bash。


2
只有这一个适合我,因为我需要换行。 - Jason Livesay
1
@CiroSantilli:如果FILE是Config.cpp并且包含反斜杠、双引号和引号,怎么办? - user2284570
@user2284570 我不知道,但很容易找到:S="$(printf "\\\'\"")"; echo $S。输出:\'"。所以它可以工作 =) - Ciro Santilli OurBigBook.com
@CiroSantilli:5511行?你确定没有自动化的方法吗? - user2284570
@user2284570,我不明白,哪里有5511行?陷阱来自于$()扩展,我的例子表明$()扩展与\''"一起使用。 - Ciro Santilli OurBigBook.com
显示剩余3条评论

112

如果您想将整个文件读入变量中:

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

如果您想逐行读取它:

while read line; do    
    echo $line    
done < file.txt

3
@brain:如果文件名是Config.cpp并且包含反斜杠、双引号和引号怎么办? - user2284570
6
echo "$value"中应该使用双引号引用变量,否则shell将对该值执行空格分词和通配符扩展。 - tripleee
6
请使用read -r,而不是仅仅使用read,除非你需要特定的旧行为。这样能够使代码更加规范,并避免出现一些奇怪的问题。 - tripleee

29
这对我有效:
v=$(cat <file_path>)
echo $v

<file_path> 未被识别为 cmdlet、函数、脚本文件或可操作程序的名称。 - john k
13
真的吗? <file_path> 意思是 在此输入您的文件路径 - angelo.mastro
4
这将吞噬多行文本文件中的换行符。 - ucipass
@johnktejik 这不是针对 Windows 的答案,而是针对 Linux 的答案。 - Ari157

18

使用bash时,您可以像这样使用read

#!/usr/bin/env bash

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

printf '%s' "$value"

请注意:

  • 最后一个换行符被保留。

  • 通过重定向整个命令块,使stderr被静音为/dev/null,但读取命令的返回状态被保留,如果需要处理读取错误条件。


5

正如Ciro Santilli注意到的那样,使用命令替换会丢失尾随的换行符。他们的解决方法是添加尾随字符,但是在使用了一段时间后,我决定需要一种完全不使用命令替换的解决方案。

我的方法现在使用read以及printf内置的-v标志,直接将stdin的内容读入变量中。

# 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://dev59.com/zWs05IYBdhLWcg3wIedj#22607352
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"
}

这支持带有或不带有尾随换行符的输入。
示例用法:
$ read_input file_contents < /tmp/file
# $file_contents now contains the contents of /tmp/file

太好了!我只是在想,为什么不使用像 _contents="${_contents}${_line}\n " 这样的东西来保留换行符呢? - Eenoku
1
你是在询问 $'\n' 吗?这是必要的,否则你将会添加 \ n 字符。你的代码块末尾还有一个额外的空格,不确定这是否是有意为之,但是它会导致每一行都多了一个空格的缩进。 - dimo414

2

我使用:

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

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

2

所有给出的解决方案都非常缓慢,因此:

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

希望能更加优化它,但我想不出更多的方法。

更新:找到了一种更快的方法。

read -rd '' content </etc/passwd

这将返回1的退出代码,如果您希望它始终为0

read -rd '' content </etc/passwd || :

你是对的,$(cat sth) 命令比 "read" 命令慢大约 5 毫秒,我认为这是因为 $() 语法的原因。 - kkocdko
@kkocdko 确实,你应该像避开瘟疫一样避免使用fork和外部命令。 - Ari157

-1

你可以通过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

将整个内容复制到文件中(比如说line.sh);执行

chmod +x line.sh
./line.sh

1
你的 for 循环不是按行循环,而是按单词循环。在 /etc/passwd 的情况下,每行只包含一个单词。但是,其他文件可能每行包含多个单词。 - mpb

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接