从 CSV 文件生成 yaml 模板

从 CSV 文件生成 yaml 模板

我正在尝试使用我的变量从模板创建 yaml 文件。我的 yaml 模板如下所示

number: {{NUMBER}}
  name: {{NAME}}
  region: {{REGION}}
  storenum: {{STORENUM}}
  clients: {{CLIENTS}}
  tags: {{TAGS}}


storename: {{STORENAME}}
employee: {{EMPLOYEE}}
products: {{PRODUCTS}}

但我的变量在 CSV 文件中;结构就是变量。

Number - Name - Region - Storenum  
StoreX - StoreX - New York - 30  

我现在有一个小脚本可以从具有可变参数的模板创建,并且模板如下所示:script.sh template.yml -f variables.txt。我的结果看起来像这样

number: 37579922
  name: Store1
  region: New York
  storenum: 32
  clients: 100
  tags: stores


storename: Store newyork
employee: 10
products: 200

但我一次只能做一个。有什么方法可以读取 CSV 参数并发送到程序并生成,例如Template1,Template2,etc从 CSV 参数生成?

#!/bin/bash
readonly PROGNAME=$(basename $0)

config_file="<none>"
print_only="false"
silent="false"

usage="${PROGNAME} [-h] [-d] [-f] [-s] -- 

where:
    -h, --help
        Show this help text
    -p, --print
        Don't do anything, just print the result of the variable expansion(s)
    -f, --file
        Specify a file to read variables from
    -s, --silent
        Don't print warning messages (for example if no variables are found)

examples:
    VAR1=Something VAR2=1.2.3 ${PROGNAME} test.txt 
    ${PROGNAME} test.txt -f my-variables.txt
    ${PROGNAME} test.txt -f my-variables.txt > new-test.txt"

if [ $# -eq 0 ]; then
  echo "$usage"
  exit 1    
fi

if [[ ! -f "${1}" ]]; then
    echo "You need to specify a template file" >&2
    echo "$usage"
    exit 1
fi

template="${1}"

if [ "$#" -ne 0 ]; then
    while [ "$#" -gt 0 ]
    do
        case "$1" in
        -h|--help)
            echo "$usage"
            exit 0
            ;;        
        -p|--print)
            print_only="true"
            ;;
        -f|--file)
            config_file="$2"
            ;;
        -s|--silent)
            silent="true"
            ;;
        --)
            break
            ;;
        -*)
            echo "Invalid option '$1'. Use --help to see the valid options" >&2
            exit 1
            ;;
        # an option argument, continue
        *)  ;;
        esac
        shift
    done
fi

vars=$(grep -oE '\{\{[A-Za-z0-9_]+\}\}' "${template}" | sort | uniq | sed -e 's/^{{//' -e 's/}}$//')

if [[ -z "$vars" ]]; then
    if [ "$silent" == "false" ]; then
        echo "Warning: No variable was found in ${template}, syntax is {{VAR}}" >&2
    fi
fi

# Load variables from file if needed
if [ "${config_file}" != "<none>" ]; then
    if [[ ! -f "${config_file}" ]]; then
      echo "The file ${config_file} does not exists" >&2
      echo "$usage"      
      exit 1
    fi

    source "${config_file}"
fi    

var_value() {
    eval echo \$$1
}

replaces=""

# Reads default values defined as {{VAR=value}} and delete those lines
# There are evaluated, so you can do {{PATH=$HOME}} or {{PATH=`pwd`}}
# You can even reference variables defined in the template before
defaults=$(grep -oE '^\{\{[A-Za-z0-9_]+=.+\}\}' "${template}" | sed -e 's/^{{//' -e 's/}}$//')

for default in $defaults; do
    var=$(echo "$default" | grep -oE "^[A-Za-z0-9_]+")
    current=`var_value $var`

    # Replace only if var is not set
    if [[ -z "$current" ]]; then
        eval $default
    fi

    # remove define line
    replaces="-e '/^{{$var=/d' $replaces"
    vars="$vars
$current"
done

vars=$(echo $vars | sort | uniq)

if [[ "$print_only" == "true" ]]; then
    for var in $vars; do
        value=`var_value $var`
        echo "$var = $value"
    done
    exit 0
fi

# Replace all {{VAR}} by $VAR value
for var in $vars; do
    value=$(var_value $var | sed -e "s;\&;\\\&;g" -e "s;\ ;\\\ ;g") # '&' and <space> is escaped 
    if [[ -z "$value" ]]; then
        if [ $silent == "false" ]; then
            echo "Warning: $var is not defined and no default is set, replacing by empty" >&2
        fi
    fi

    # Escape slashes
    value=$(echo "$value" | sed 's/\//\\\//g');
    replaces="-e 's/{{$var}}/${value}/g' $replaces"    
done

escaped_template_path=$(echo $template | sed 's/ /\\ /g')
eval sed $replaces "$escaped_template_path"

答案1

这是一个在 perl 中使用以下命令执行此操作的非常简单的示例文本::CSV解析 CSV 的模块。

没有完成命令行选项处理(尽管这可以很容易地使用获取选择::标准或 Getopt::Long 足够但基本(但包含在 perl 中),或者更高级的模块,如Getopt::清醒,它需要安装,但几乎可以做任何你想用选项做的事情。

这只是将模板作为定界文档嵌入到脚本中。对于更复杂的模板需求,请使用文本::模板库模块。

它还只是将输出打印到标准输出。您可以像往常一样在 shell 中重定向它,或者,如果您需要将每个 csv 输入行的输出存储在单独的文件中,则可以很容易地让 perl 打开一个文件进行写入并将输出打印到该文件。

与大约 140 行 bash 脚本(其中大约三分之一是注释、空行和使用消息)相比,此 Perl 脚本总共有 35 行,其中 12 行是模板,6 行是注释,8 行是空行以提高可读性。即 9 行实际代码与 bash 的大约 90 行代码相比。

与 bash 脚本不同,这不会有任何需要处理的引用或空格问题,并且运行速度会更快,因为它不必重复分叉外部程序,例如sed(相当于 grep、sed 的功能) 、tr 等是 Perl 内置的)。另请注意,Text::CSV 模块可以轻松处理包含嵌入逗号的字段(TAGS 字段) - 这是使用实际 CSV 解析器而不是使用正则表达式进行伪造的好处之一。

#!/usr/bin/perl

use strict;
use Text::CSV;

# open the CSV file for read
my $file = 'data.csv';
open(my $fh, "<", $file) or die "Couldn't open $file: $!\n";

# initialise a csv object
my $csv = Text::CSV->new();

# read the header line
my @headers = $csv->getline($fh);
$csv->column_names(@headers);

# iterate over each line of the CSV file, reading
# each line into a hash (associative array) reference.
while (my $row = $csv->getline_hr($fh)) {
print <<__EOF__;
number: $row->{NUMBER}
  name: $row->{NAME}
  region: $row->{REGION}
  storenum: $row->{STORENUM}
  clients: $row->{CLIENTS}
  tags: $row->{TAGS}


storename: $row->{STORENAME}
employee: $row->{EMPLOYEE}
products: $row->{PRODUCTS}

__EOF__
}
close($fh);

如果data.csv包含以下内容:

NUMBER,NAME,REGION,STORENUM,CLIENTS,TAGS,STORENAME,EMPLOYEE,PRODUCTS
37579922,Store1,New York,32,100,stores,Store newyork,10,200
2,Store2,Somewhere,2,100,"tag1,tag2,tag3",Somewhere Store,5,10
3,Store3,Elsewhere,3,100,"tag1,tag3",Elsewhere Store,3,100

然后运行它会产生以下输出:

$ ./template-example.pl 
number: 37579922
  name: Store1
  region: New York
  storenum: 32
  clients: 100
  tags: stores


storename: Store newyork
employee: 10
products: 200

number: 2
  name: Store2
  region: Somewhere
  storenum: 2
  clients: 100
  tags: tag1,tag2,tag3


storename: Somewhere Store
employee: 5
products: 10

number: 3
  name: Store3
  region: Elsewhere
  storenum: 3
  clients: 100
  tags: tag1,tag3


storename: Elsewhere Store
employee: 3
products: 100

顺便说一句,如果您愿意python,用 python 编写和用 perl 编写一样容易。

答案2

gawk解决方案在 GNU Awk v5.1.0 上进行了测试。

它由一个 Bash 脚本组成,该脚本接受两个输入文件:(yaml_template在 OP 中给出):

$ cat yaml_template
number: {{NUMBER}}
  name: {{NAME}}
  region: {{REGION}}
  storenum: {{STORENUM}}
  clients: {{CLIENTS}}
  tags: {{TAGS}}


storename: {{STORENAME}}
employee: {{EMPLOYEE}}
products: {{PRODUCTS}}

data.csv(由@cas在他的回答中提出):

$ cat data.csv
NUMBER,NAME,REGION,STORENUM,CLIENTS,TAGS,STORENAME,EMPLOYEE,PRODUCTS
37579922,Store1,New York,32,100,stores,Store newyork,10,200
2,Store2,Somewhere,2,100,"tag1,tag2,tag3",Somewhere Store,5,10
3,Store3,Elsewhere,3,100,"tag1,tag3",Elsewhere Store,3,100

Bash 脚本yamlit.sh可执行(使用 cmd $ chmod ug+x yamlit.sh):

$ cat yamlit.sh
#!/usr/bin/env bash
gawk -F"[,:]" '
    FNR==NR {
        match($0,/[^[:blank:]]+/); i++;
        if (RSTART-1 < 0) {$1="";null++; teenar[i] = ""} else {$1 = substr($1,RSTART); teenar[i] = $1};
        teenof[$1] = RSTART - 1;
        next;
    }
    FNR==1 {nteen=i; ncol=split(tolower($0), colhead, ",");next;}
    {
    for (i=1; i<=nteen; i++) {
        offset=teenof[teenar[i]];
        if (offset >= 0) {
            patsplit($0,datafield,"([^,]*)|(\"[^\"]*\")")
            for (j=1; j<=ncol; j++) {
                if (tolower(teenar[i]) == colhead[j]) {
                    printf "%*s: %s\n", length(teenar[i]) + offset, teenar[i], datafield[j];
                    }
                }
            }
        else {print ""}
        }
    printf "\n%s\n", "=========================="
    }' "$1" "$2"

从终端运行该脚本会产生:

$ yamlit.sh yaml_template data.csv
number: 37579922
  name: Store1
  region: New York
  storenum: 32
  clients: 100
  tags: stores


storename: Store newyork
employee: 10
products: 200

==========================
number: 2
  name: Store2
  region: Somewhere
  storenum: 2
  clients: 100
  tags: "tag1,tag2,tag3"


storename: Somewhere Store
employee: 5
products: 10

==========================
number: 3
  name: Store3
  region: Elsewhere
  storenum: 3
  clients: 100
  tags: "tag1,tag3"


storename: Elsewhere Store
employee: 3
products: 100

==========================

解释一句话:

  • 脚本将其输出打印到终端,但输出可以轻松重定向到文件,例如从 CLI: $ yamlit.sh yaml_template data.csv >| yaml_out,
  • 它严格遵守 yaml 模板提供的格式和结构。模板中的任何格式错误都将显示在输出中。这包括任何错误的空格(即错误的缩进)和空行,
  • 它尊重提供模板条目的顺序,而不管数据文件中的列顺序如何data.csv
  • 模板可以非常复杂,并且可以根据需要具有任意数量的嵌套级别,
  • 匹配模板输入键和数据列标题不区分大小写。

可以添加什么?
添加到脚本中的功能很简单,包括:

  • 将每个数据文件记录的“yaml”输出重定向到磁盘上的一个文件或与数据记录(行)一样多的不同输出文件,不包括文件头。
  • data.csv运行检查模板文件中的 yaml 模板条目键之间是否存在数据不匹配

代码:

  • 第一个块FNR==NR {...}计算:
    • 模板文件中的(空白和非空白)行数,
    • 每行的缩进,保存在数组: 中teenof,是“TEMplate ENtry OFfset”的缩写。负值表示模板文件中的空行。
    • 另一个数组中的模板条目键(记录的第一个字段):teenar,“TEmplate ENtry ARray”的缩写。
  • 第二个块将文件FNR==1 {...}的每个列标题放置data.csv在第三个数组中colhead
  • 第三个也是最后一个块{...}做了几件事:
    • 循环遍历模板输入键(小写),
    • 对于正缩进或空缩进:
      • data.csv根据字段正则表达式分割每个文件记录,该正则表达式能够辨别记录中的任何字段是否由包含其他字段分隔符(此处,)的带引号的字符串组成。分割产生的组件被放置在第四个数组中:datafield
      • 它对数据文件列标题执行嵌套循环以查找与模板条目的匹配项;如果找到,它将打印相应的行,并遵循 yaml 模板指定的缩进。
    • 对于负缩进,它会打印一个空行,从而尊重模板包含一个或多个空行的原始序列。

批判:

  • 使用了两个嵌套循环,从计算的角度来看,即使 for 也awk远非理想。复杂度为 O(n^2)。然而,这使得data.csv根据相对于 yaml 模板文件的条目顺序具有任意列排序的文件来概括解决方案成为可能。我没有花时间寻找其他解决方案。不确定是否有更好的awk...
  • 总共使用了 4 个 GNU Awk 数组,对于“非常大”或“大量”的文件,例如具有许多列和许多记录(或行)的文件可能会占用大量内存。这将由相应的数组(teenofteenar)构建而成从 yaml 模板中,大概有与数据列一样多的条目,在这 4 个数组中,只有一个 ( datafield) 被清空并为每个data.csv文件记录重建,也就是说,我不确定是否会压缩 Gawk 数组。使用新的内存重新分配内存的效率就像 C 中删除/重新声明的数组变量一样,我将其留给比我更有知识的人来评论。

HTH。

答案3

这是一种奇怪的方法来使用GoCSV,以及它利用 Go 的模板引擎这一事实。

我根据您的模板和您提供的示例数据制作了一个模拟 CSV:

存储-x.csv

Number,Name,Region,Storenum,Clients,Tags,Storename,Employee,Products
StoreX,StoreX,New York,30,Foo,store-x,Store X,Alice,X stuff

将模板与 GoCSV 一起使用几乎与原始 YAML 模板相同:

  • 字段名称采用标题大小写以匹配 CSV
  • 字段名称有一个前导.(Go/GoCSV 的字段名称表示法)

模板.yaml

number: {{.Number}}
  name: {{.Name}}
  region: {{.Region}}
  storenum: {{.Storenum}}
  clients: {{.Clients}}
  tags: {{.Tags}}


storename: {{.Storename}}
employee: {{.Employee}}
products: {{.Products}}

最后,一个带有 GoCSV 管道的简短 shell 脚本:

make_yaml.sh

#!/bin/sh

csv_data=$1

yaml_tmpl=$(cat template.yaml)

cat "$csv_data"                                   \
| gocsv add --name 'YAML' --template "$yaml_tmpl" \  # 1.
| gocsv select -c 'YAML'                          \  # 2.
| gocsv behead                                       # 3.
  1. 添加一个名为YAML使用 CSV 数据填充您的模板
  2. 只选择新的YAML柱子
  3. 去掉标题

...你只剩下 YAML:

% sh make_yaml.sh data.csv

"number: StoreX
  name: StoreX
  region: New York
  storenum: 30
  clients: Foo
  tags: store-x


storename: Store X
employee: Alice
products: X stuff"

几乎

相关内容