从管道中读取shell变量的值
1. 概述
在本教程中,我们将介绍为 shell 变量赋值的不同方法。我们将通过管道传递不同的命令并将最终处理的值分配给一个变量。
最后,我们将看看哪种方法更适合我们的用例。
2. 使用read和heredoc
作为管道的替代方案,我们可以使用heredoc将值读入变量。**heredoc 是我们用来指定输入源的重定向机制。**它以标签开始和结束。标签内的内容用作命令的标准输入。
让我们看看一个简单的heredoc是什么样子的:
command << TAG
command_2
TAG
请注意,最后一个TAG旁边没有尾随空格,我们可以用**我们选择的任何单词替换TAG标记。**在我们的例子中,我们想从heredoc中读取值并将它们分配给 shell 变量:
#!/bin/sh
IFS= read platform << EOF
$(uname -a)
EOF
echo "$platform"
IFS (内部字段分隔符)环境变量包含我们可以用来分割字符串的分隔符。我们可以将它设置为我们需要的任何分隔符。在这种情况下,我们将其设置为空,因为我们想防止分词。之后,我们使用read关键字将值读入platform变量。分配给platform变量的值将是heredoc中命令的输出——这是一个简单的uname 命令。
我们将 heredoc标签命名为EOF,它代表“文件结束”。这是我们在编写bash脚本时经常使用的约定。 现在,让我们看看platform变量的值是多少:
$ ./uname.sh
Linux laptop 5.15.5-arch1-1 #1 SMP PREEMPT Thu, 25 Nov 2021 22:09:33 +0000 x86_64 GNU/Linux
我们可以将管道附加到heredoc中的命令,它的功能相同。
3. 使用带有while循环的read
如果我们要读取包含大量数据的文件,我们可以使用*while循环 并将文件逐行read到变量中。然后我们可以在while*循环的范围内使用该变量:
#!/bin/sh
cat packages.txt | while read pkg; do
echo "$pkg"
done
执行脚本应该会逐行输出package.txt文件的内容:
$ ./listpackages.sh
alacritty 0.9
bash 5.1
firefox 94.0
gcc 11.1
mpv 0.34
nvim 0.5
rust 1.56
sway 1.6
循环将一直执行,直到到达 EOF。**请注意,我们不能在while循环之外使用pkg变量。**如果循环自行结束,pkg将包含一个尾随换行符。否则,它将包含从文件中读取的最后一个值。
或者,我们可以删除cat 语句并以这种方式重写脚本:
while read pkg version; do
echo "Package: $pkg Version: $version";
done < packages.txt
在这个例子中,我们对每一行进行了分词。我们将一行的第一个单词放入pkg变量中,并将其对应的版本放入version变量中。在循环结束时,我们使用重定向运算符将 packages.txt文件的内容提供给循环。
4. 使用输入文件描述符操作符“<”
*<*重定向器或输入文件描述符运算符用于指定输入源。**使用此运算符,我们可以快速为变量分配一个简单的值。**简单来说,我们的意思是单行。但是,如果我们想使用重定向运算符将多行值分配给变量,我们的脚本将出现意外行为。
假设我们要将uname.sh脚本的 shebang 保存到shebang变量中。我们可以使用以下命令来实现:
$ read shebang < <(cat uname.sh)
$ echo "$shebang"
#!/bin/sh
“<”运算符是一个输入文件描述符,我们使用它来为命令提供输入。在这种情况下,“< <”是两个单独的重定向。**语法“ <(command) ”被称为进程替换。进程替换类似于将一个命令 的标准输出作为输入传递给另一个命令。**因此,我们可以通过第一个“<”重定向运算符将cat进程的输出重定向到shebang变量。执行命令后,shebang变量应包含uname.sh文件的第一行。
类似地,我们可以在进程替换中使用管道并将评估值分配给变量:
$ read shell < <(cat uname.sh | cut -d'/' -f3)
$ echo "$shell"
sh
**使用进程替换而不是命令替换运算符“$”的优点是它在行可用时立即返回。**相反,命令替换运算符会等待输出完成,然后才能对其进行处理。
5. 使用 Bash 的lastpipe特性
管道read以将值分配给变量,shell 创建一个新的子 shell,在其中执行管道命令。因此,值丢失,变量无法使用。但是,当我们在最新版本的bash中使用lastpipe选项时,我们可以将命令的输出(例如echo)通过管道传输到read。然后read语句会将输出分配给一个变量。
使用lastpipe功能,最后一个命令在当前 shell 中执行,而不是在新的子 shell 中执行。因此,环境保留了价值。默认情况下,此选项被禁用。我们可以使用shopt 打开它:
$ shopt -s lastpipe
如果我们在交互式 shell 上,我们还应该使用set 命令禁用当前 shell 的作业控制:
$ set +m
但是,我们不需要在脚本中禁用作业控制,因为它默认禁用作业控制。
我们现在准备好使用管道read。让我们重写我们的示例以从stdout读取:
$ uname -a | read kernel_version;
现在,当我们回显kernel_version变量时,它应该包含uname命令的输出:
$ echo "$kernel_version"
Linux laptop 5.15.5-arch1-1 #1 SMP PREEMPT Thu, 25 Nov 2021 22:09:33 +0000 x86_64 GNU/Linux
6. 命令替换
**命令评估或命令替换方法可能是将评估值分配给变量的最简单且简洁的方法。**我们可以使用命令评估运算符“$()”或反引号将我们的命令放入其中:
$ ip_address="$(curl -Ls ifconfig.me)"
让我们分解一下:
- 我们在命令评估括号内使用curl命令从 ifconfig.me ping 回我们的 IP 地址
- 然后将命令的输出或响应分配给ip_address变量
$ echo ip_address
39.112.32.90
我们还在表达式中使用管道:
$ ff_ver="$(pacman -Ss firefox | grep "extra/firefox[[:space:]]")"
$ echo "$ff_ver"
extra/firefox 95.0-1 [installed: 94.0.2-2]
请注意,我们应该始终在表达式周围使用双引号以防止扩展。
此外,如果我们在zsh上,我们可以有嵌套的替换:
$ echo $(expr length $(curl -Ls ifconfig.me))
12
7. 使用哪一个?
上述方法几乎总是有一个用例。对于文件,我们可以使用while循环并将文件的内容读入变量。对于简单的任务,我们可以使用输入重定向运算符。如果我们想将大量数据流输入到变量中,我们可以使用heredoc或herestring 。
如果我们使用bash,我们可以使用lastpipe选项,因为它简洁且高效。lastpipe的缺点是我们无法将脚本移植到其他 shell,例如dash。尽管如此,我们应该更喜欢它在不需要将我们的脚本移植到其他平台的内部使用。**
除此之外,对于简单的命令和表达式,我们应该坚持命令评估或命令替换方法,因为它简单易读。有时,可读性胜过效率。此外,它符合 POSIX 标准,因此几乎可以与任何 shell 一起使用。