【问题标题】:How do I convert CSV data into an associative array using Bash 4?如何使用 Bash 4 将 CSV 数据转换为关联数组?
【发布时间】:2019-12-03 13:43:54
【问题描述】:

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

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

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

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 的模板系统来访问它们,如下所示:

{{#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 grepping STDIN 并使用source &lt;(cat); eval $STDIN...在其他功能中)...

有关更多信息,请参阅第二个答案中的我的 cmets。

【问题讨论】:

  • if the above ^ works in a terminal 以上将永远不会在任何终端中工作,因为管道的右侧在子外壳内运行。无法从子外壳更改父级环境。您必须使用一些外部实体,例如。一个临时文件来执行此操作,并在您的父 shell 中读取该文件(并删除它)。

标签: arrays bash shell csv associative-array


【解决方案1】:

方法很简单:

  • 将列标题读入数组
  • 逐行读取文件,在每一行…
    • 创建一个新的关联数组并将其名称注册到数组名称数组中
    • 读取字段并根据列标题进行分配

在最后一步中,我们不能使用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 传递给它,这正是我所需要的。我需要这样称呼它:cat /tmp/file.csv | csv_to_array 但它不起作用 - 当我更改 CSV 文件时,重新运行 func,declare -p 的输出不会改变...参见示例:```# cat /tmp/file.csv | csv_to_Array row1 row2 # declare -p row2 bash: declare: row2: not found ``` 有什么想法吗? (对不起,愚蠢的 SE 不会让我添加我正在使用的 func,太长了)
  • 那是因为管道的右侧在子外壳中运行(并且因为我的脚本需要一个文件,但您使用了标准输入)。数组仅存在于该子外壳内。在csv_to_array 完成后,子shell 被关闭并且所有变量都丢失了。子外壳无法修改其父外壳。这是一个解决方案:将我的脚本打包成一个函数并将第一个分配更改为file="$1"。然后拨打csv_to_array /tmp/file.csv。而已。不需要无用的 cat。
  • 还是不行..我把while read line改成for line in ....还是不行..
  • 无论如何我都需要 func 来处理管道输入...因为这是它会收到的唯一输入...
  • 发现问题。来自help declare:“When used in a function, declare makes NAMEs local, as with the local command. The ‘-g’ option suppresses this behavior.”。我将脚本转换为为您读取标准输入的函数。
【解决方案2】:

以下脚本:

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 的新关联数组,其中的元素是通过将标题名称与从该行读取的值连接起来而创建的。最后从函数输出declare -p 的输出。

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

【讨论】:

  • 感谢您的回答。这是我需要的 99%,但我更喜欢使用 eval 或其他东西将生成的声明内容提供给父 ENV,而不是作为要获取/评估的字符串导出。我必须将source &lt;(cat); eval "$STDIN"; STDIN="${new_array[@]}"; 添加到其他函数的开头,但不应该被允许(这是一个hack)
  • 要明确:“接收”函数(foo2fooX等)都使用read来设置$STDIN,可以是row1 row2 row3,只要有东西像 eval 和 "echo ${row1[@]}" 可以用来访问数组数据...
猜你喜欢
  • 2017-11-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-09-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-09-02
相关资源
最近更新 更多