如何使用 Bash 4 将 CSV 数据转换为关联数组?

How do I convert CSV data into an associative array using Bash 4?

文件 /tmp/file.csv 包含以下内容:

name,age,gender
bob,21,m
jane,32,f

CSV 文件将始终包含 headers.. 但可能包含不同数量的字段:

id,title,url,description
1,foo name,foo.io,a cool foo site
2,bar title,http://bar.io,a great bar site
3,baz heading,https://baz.io,some description

在任何一种情况下,我都想将我的 CSV 数据转换为关联数组的数组..

我需要什么

所以,我想要一个 Bash 4.3 函数,它将 CSV 作为管道输入并将数组发送到标准输出:

/tmp/file.csv:

name,age,gender
bob,21,m
jane,32,f

需要在我的模板系统中使用,像这样:

{{foo | csv_to_array | foo2}}

^ 这是固定的 API,我必须使用该语法。foo2 必须接收数组作为标准输入。

csv_to_array func 必须做它的事情,这样之后我就可以这样做:

$ declare -p row1; declare -p row2; declare -p new_array;

它会给我这个:

declare -A row1=([gender]="m" [name]="bob" [age]="21" )
declare -A row2=([gender]="f" [name]="jane" [age]="32" )
declare -a new_array=([0]="row1" [1]="row2")

..一旦我有了这个数组结构(关联数组名称的索引数组),我就有了一个 shell-based 模板系统来访问它们,就像这样:

{{#new_array}}
  Hi {{item.name}}, you are {{item.age}} years old.
{{/new_array}}

但我正在努力生成我需要的数组..

我尝试过的事情:

我已经尝试以此为起点来获取我需要的数组结构:

while IFS=',' read -r -a my_array; do
    echo ${my_array[0]} ${my_array[1]} ${my_array[2]}
done <<< $(cat /tmp/file.csv)

(来自 Shell: CSV to array

..还有这个:

cat /tmp/file.csv | while read line; do
  line=( ${line//,/ } )
  echo "0: ${line[0]}, 1: ${line[1]}, all: ${line[@]}" 
done

(来自 https://www.reddit.com/r/commandline/comments/1kym4i/bash_create_array_from_one_line_in_csv/cbu9o2o/

但我在从另一端得到我想要的东西方面并没有真正取得任何进展...

编辑:

接受了第二个答案,但我不得不破解我正在使用的库来使任一解决方案都起作用..

我很乐意查看其他答案,这些答案不将声明命令导出为字符串,在当前环境中为运行,而是以某种方式将声明命令的结果数组提升到当前环境(当前环境是函数 运行 来自的任何地方)。

示例:

$ cat file.csv | csv_to_array
$ declare -p row2 # gives the data 

所以,要明确一点,如果上面的 ^ 在终端中工作,它将在我正在使用的库中工作,而无需我必须添加的 hack(这涉及为 ^declare -a 和在其他函数中使用 source <(cat); eval $STDIN...)...

有关更多信息,请参阅我对第二个答案的评论。

方法很简单:

  • 将第 headers 列读入数组
  • 逐行读取文件,每一行...
    • 创建一个新的关联数组并将其名称注册到数组名称数组中
    • 读取字段,按列赋值headers

在最后一步中,我们不能使用 read -amapfile 或类似的东西,因为它们只会创建以数字作为索引的常规数组,但我们需要一个关联数组,所以我们必须手动创建数组.

但是,由于 bash 的怪癖,实现有点复杂。

以下函数解析 stdin 并相应地创建数组。 我冒昧地将您的数组 new_array 重命名为 rowNames.

#! /bin/bash
csvToArrays() {
    IFS=, read -ra header
    rowIndex=0
    while IFS= read -r line; do
        ((rowIndex++))
        rowName="row$rowIndex"
        declare -Ag "$rowName"
        IFS=, read -ra fields <<< "$line"
        fieldIndex=0
        for field in "${fields[@]}"; do
            printf -v quotedFieldHeader %q "${header[fieldIndex++]}"
            printf -v "$rowName[$quotedFieldHeader]" %s "$field"
        done
        rowNames+=("$rowName")
    done
    declare -p "${rowNames[@]}" rowNames
}

在管道中调用函数无效。 Bash 在子 shell 的管道中执行命令,因此您无法访问 someCommand | csvToArrays 创建的数组。相反,将函数调用为以下之一

csvToArrays < <(someCommand) # when input comes from a command, except "cat file"
csvToArrays < someFile       # when input comes from a file

Bash 像这样的脚本往往很慢。这就是为什么我懒得从内部循环中提取 printf -v quotedFieldHeader … 的原因,即使它会一遍又一遍地做同样的工作。
我认为整个模板化的东西和所有相关的东西在 python、perl 或类似的语言中会更容易编程和更快地执行。

以下脚本:

csv_to_array() {
    local -a values
    local -a headers
    local counter

    IFS=, read -r -a headers
    declare -a new_array=()
    counter=1
    while IFS=, read -r -a values; do
        new_array+=( row$counter )
        declare -A "row$counter=($(
            paste -d '' <(
                printf "[%s]=\n" "${headers[@]}"
            ) <(
                printf "%q\n" "${values[@]}"
            )
        ))"
        (( counter++ ))
    done
    declare -p new_array ${!row*}
}

foo2() {
    source <(cat)
    declare -p new_array ${!row*} |
    sed 's/^/foo2: /'
}

echo "==> TEST 1 <=="

cat <<EOF |
id,title,url,description
1,foo name,foo.io,a cool foo site
2,bar title,http://bar.io,a great bar site
3,baz heading,https://baz.io,some description
EOF
csv_to_array |
foo2 

echo "==> TEST 2 <=="

cat <<EOF |
name,age,gender
bob,21,m
jane,32,f
EOF
csv_to_array |
foo2 

将输出:

==> TEST 1 <==
foo2: declare -a new_array=([0]="row1" [1]="row2" [2]="row3")
foo2: declare -A row1=([url]="foo.io" [description]="a cool foo site" [id]="1" [title]="foo name" )
foo2: declare -A row2=([url]="http://bar.io" [description]="a great bar site" [id]="2" [title]="bar title" )
foo2: declare -A row3=([url]="https://baz.io" [description]="some description" [id]="3" [title]="baz heading" )
==> TEST 2 <==
foo2: declare -a new_array=([0]="row1" [1]="row2")
foo2: declare -A row1=([gender]="m" [name]="bob" [age]="21" )
foo2: declare -A row2=([gender]="f" [name]="jane" [age]="32" )

输出来自 foo2 函数。

csv_to_array 函数首先读取标头。然后对于每个读取的行,它将新元素添加到 new_array 数组中,并创建一个名为 row$index 的新关联数组,其中的元素是通过连接 headers 名称和从该行读取的值创建的。最后,declare -p 的输出从函数输出。

foo2 函数获取标准输入,因此数组进入它的作用域。然后它再次输出这些值,在每一行前面加上 foo2:.