将多行字符串转换为数组

61

我有这个脚本:

nmapout=`sudo nmap -sP 10.0.0.0/24`
names=`echo "$nmapout" | grep "MAC" | grep -o '(.\+)'`
echo "$names"

现在,$names 变量包含以换行符分隔的字符串:

>_
 (Netgear)
 (Hon Hai Precision Ind. Co.)
 (Apple)

我尝试使用子字符串方法进行数组转换:

names=(${names//\\n/ })
echo "${names[@]}"

但问题在于,我无法通过索引访问它们(即${names[$i]等),如果我运行这个循环。

for (( i=0; i<${#names[@]}; i++ ))
do
     echo "$i: ${names[$i]"
     # do some processing with ${names[$i]}
done

我得到了这个输出:

>_
 0: (Netgear)
 1: (Hon
 2: Hai

但是我想要的是:

>_
 0: (Netgear)
 1: (Hon Hai Precision Ind. Co.)
 2: (Apple)

我想不出一个好方法来做到这一点,请注意第二个字符串中有空格。


你为什么想要使用数组?我更喜欢使用逐行读取循环的方式。 - kan
@kan,实际上这只是一个大脚本的一小部分,原始脚本还将索引用于其他目的,这就是为什么我想保留数组的原因。 - ramgorur
相关:如何将以空格为分隔符的字符串转换为bash数组:在Bash中将分隔字符串读入数组 - Gabriel Staples
6个回答

90

设置 IFS(内部字段分隔符)。Shell 使用 IFS 变量来确定字段分隔符。默认情况下,IFS 设置为空格字符。按照下面的示例将其更改为换行符:

#!/bin/bash
names="Netgear
Hon Hai Precision Ind. Co.
Apple"
    
SAVEIFS=$IFS   # Save current IFS (Internal Field Separator)
IFS=$'\n'      # Change IFS to newline char
names=($names) # split the `names` string into an array by the same name
IFS=$SAVEIFS   # Restore original IFS

for (( i=0; i<${#names[@]}; i++ ))
do
    echo "$i: ${names[$i]}"
done

输出

0: Netgear
1: Hon Hai Precision Ind. Co.
2: Apple

3
因为原始的$IFS可能包含特殊字符,最好避免尝试存储它。最好只是用括号将整个内容包裹在子Shell中。 - etheranger
@etheranger,我在Bash脚本方面还是新手,您能否详细解释一下“带括号的子shell”是什么? - ramgorur
1
如果我以非root用户身份执行,会出现“语法错误:'('意外”的错误。 - koppor
12
在第9行可以使用“IFS=$'\n' names=(${names})”来更改IFS变量的值,以便只影响该行。这与将第8行和第9行合并是相同的。 - andrej
对我来说,那个错误是因为我使用了 #!/bin/sh 而不是 #!/bin/bashmy_arr=(...) 是 Bash 特有的数组初始化方式。我敢打赌你的 root 帐户之所以能够工作,是因为它使用 bash 作为默认 shell。 - Hintron
显示剩余7条评论

42

Bash还有一个内置命令readarray,在man页面中易于搜索。它使用换行符(\n)作为默认分隔符,MAPFILE作为默认数组,所以可以这样做:

    names="Netgear
    Hon Hai Precision Ind. Co.
    Apple"

    readarray -t <<<$names

    printf "0: ${MAPFILE[0]}\n1: ${MAPFILE[1]}\n2: ${MAPFILE[2]}\n"
-t选项会移除分隔符('\n'),这样在printf中就可以明确地添加它。输出结果为:
    0: Netgear
    1: Hon Hai Precision Ind. Co.
    2: Apple

2
这是对所提出问题的正确答案。readarray 的设计目的就是为了做到这一点。 - shagamemnon
这确实是针对特定问题的正确答案。 - V_Singh
3
readarray是在bash v4.0中引入的。一些系统(如macOS <11.*)仍在使用bash v3.2。在这种情况下,可以使用基于IFS的解决方案。 - qff

25

让我为Sanket Parmar的回答做出贡献。如果您可以将字符串分割和处理提取到单独的函数中,则无需保存和恢复$IFS — 使用local即可:

#!/bin/bash

function print_with_line_numbers {
    local IFS=$'\n'
    local lines=($1)
    local i
    for (( i=0; i<${#lines[@]}; i++ )) ; do
        echo "$i: ${lines[$i]}"
    done
}

names="Netgear
Hon Hai Precision Ind. Co.
Apple"

print_with_line_numbers "$names"

另请参阅:


7

正如其他人所说,IFS会对您有所帮助。IFS=$'\n' read -ra array <<< "$names" 如果您的变量中包含有空格的字符串,请将其放在双引号之间。 现在,您可以通过${array[@]}轻松地将所有值放入数组中。


9
默认情况下,read 命令使用 \n 作为分隔符,因此您需要在 read 命令中添加 -d '' 参数,否则数组仅包含 $names 的第一行。纠正后的命令为:IFS=$'\n' read -r -d '' -a array <<< "$names"。另外,您忘记在 { 前面加上 $ 符号。 - Toni Dietze
我是新手,您能详细说明一下这个命令中 -r-a 的用法吗? - Hari Bharathi
我有点困惑。你在最初的回答中已经使用了“-r”和“-a”,只是缩写为“-ra”。在我的评论中,我添加了“-d''”。Bash手册很好地解释了所有这些命令行选项(查找“read”内置命令)。 - Toni Dietze
@ToniDietze,感谢你的修正!否则我永远不会想到加上“-d''”,而那部分是必不可少的。我在这里的答案中添加了它。 - Gabriel Staples
值得一提的是,read内置命令在遇到EOF时会返回非零退出状态,因此如果您在shell脚本中设置了-e(如Bash Strict Mode文档所建议的),最好掩盖read的退出代码,例如:read -ra array -d '' <<< "${names}" || true - Bass

7
如何将多行字符串读入常规 bash “索引” 数组
Bash shell 静态代码分析器和检查工具 shellcheck 建议使用 read -rmapfile(参见SC2206)。它们的 mapfile 示例完整,但是 read 示例仅涵盖通过空格拆分字符串的情况,而不是换行符。因此,我从@Toni Dietze's comment here中学到了用于此目的的 read 命令的完整形式。

那么,以下是如何使用两者来按换行符拆分字符串的方法。请注意,<<< 被称为 "herestring"。它类似于 <<,后者是一个 "heredoc",以及 < 读取文件:

# split the multiline string stored in variable `var` by newlines, and
# store it into array `myarray`

# Option 1
# - this technique will KEEP empty lines as elements in the array!
# ie: you may end up with some elements being **empty strings**!
mapfile -t myarray <<< "$multiline_string"

# OR: Option 2 [my preference]
# - this technique will NOT keep empty lines as elements in the array!
# ie: you will NOT end up with any elements which are empty strings!
IFS=$'\n' read -r -d '' -a myarray <<< "$multiline_string"

我最常用的第三种技术不一定是由shellcheck推荐的,但如果使用得当,它比上述两种选项都更易读,并且可以保留HTML标签。我在eRCaGuy_dotfiles/useful_scripts目录中的许多脚本中都使用了它。克隆该repo并运行grep -rn "IFS"以查找我使用该技术的所有位置。
这里是我最初学习它的地方:由@Sanket Parmar回答的此处答案:将多行字符串转换为数组。
# Option 3 [not necessarily recommended by shellcheck perhaps, since you must
# NOT use quotes around the right-hand variable, but it is **much
# easier to read**, and one I very commonly use!]
#
# Convert any multi-line string to an "indexed array" of elements:
#
# See:
# 1. "eRCaGuy_dotfiles/useful_scripts/find_and_replace.sh" for an example 
#    of this.
# 1. *****where I first learned it: https://dev59.com/9mAf5IYBdhLWcg3wdCd1#24628676
SAVEIFS=$IFS   # Save current IFS (Internal Field Separator).
IFS=$'\n'      # Change IFS (Internal Field Separator) to the newline char.
# Split a long string into a bash "indexed array" (via the parenthesis),
# separating by IFS (newline chars); notice that you must intentionally NOT use
# quotes around the parenthesis and variable here for this to work!
myarray=($multiline_string) 
IFS=$SAVEIFS   # Restore IFS

另请参阅:

  1. 我学习“选项3”的地方:由@Sanket Parmar在此处回答:将多行字符串转换为数组
  2. 将文件读入空行数组中
  3. 一个示例,我使用read命令将bash多行字符串读入bash数组中:查找目录中不是目录本身的所有文件

2

在@HariBharathi的回答中添加所需的null byte分隔符。

#!/bin/bash

IFS=$'\n' read -r -d '' -a array <<< "$names"

备注:mapfile/readarray 不同的是,这个命令适用于 macOS bash 3.2。


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