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

しかし、一度に1つしかできません。 CSVパラメータを読み取ってプログラムに送信して、たとえばTemplate1,Template2,etcCSVパラメータで生成する方法はありますか?

#!/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でこれを行う非常に簡単な例があります。テキスト::CSVCSVを解析するモジュールです。

コマンドラインオプションの処理は行われません(ただし、以下を使用して簡単に実行できます)。GetSelect::標準または、Getopt::Longであれば十分ですが、基本的(Perlには含まれています)または次のような高度なモジュールです。Getopt::目覚める、インストールが必要ですが、オプションを使用して必要なほとんどすべての操作を実行できます。

これは、単にテンプレートを区切り文書としてスクリプトに含めることです。より複雑なテンプレートが必要な場合は、次を使用してください。テキスト::テンプレートライブラリモジュール。

また、出力を標準出力として印刷します。通常どおりシェルからリダイレクトできます。あるいは、各csv入力行の出力を別々のファイルに保存する必要がある場合は、Perlがファイルを簡単に書き込むためにファイルを開き、出力を文書に印刷することもできます。

約140行(そのうちの1/3はコメント、空白行、使用法メッセージ)のbashスクリプトと比較すると、このPerlスクリプトには合計35行があり、そのうち12行はテンプレート、6行はコメント、8行は中に空白行があります。 Bashのコード約90行と比較すると、実際のコードは9行です。

Bashスクリプトとは異なり、引用や空白の問題がなく、ビルドされたsed(grep、sedと同じ)、trなどの外部プログラムを繰り返し分岐する必要がないため、より速く実行されます。 -Perlの場合)。また、Text::CSV モジュールは、カンマを含むフィールド (TAGS フィールド) を簡単に処理できます。これは正規表現に偽造するのではなく、実際のCSVパーサーを使用する利点の1つです。

#!/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

ちなみに、pythonPythonで書くのはPerlで書くのと同じくらい簡単です。

答え2

このgawkソリューションはGNU Awk v5.1.0でテストされました。

これは、2 つの入力ファイルを受け入れる Bash スクリプトで構成されます。 (yaml_templateOPに付属):

$ 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"の略。
  • 2番目のブロックは、ファイルFNR==1 {...}の各列ヘッダーをdata.csv3番目の配列に配置しますcolhead
  • 3番目と最後のブロックは{...}いくつかのことを行います。
    • テンプレート入力キー(小文字)を繰り返します。
    • 正または空のインデントの場合:
      • data.csvレコード内のフィールドが、,異なるフィールド区切り文字を含む引用符付き文字列で構成されているかどうかを示すフィールド正規表現に基づいて、各ファイルレコードを分割します(ここ)。分割によるコンポーネントは、4番目の配列に配置されますdatafield
      • テンプレート項目に一致する項目を見つけるために、データファイル列ヘッダーに対してネストされたループを実行します。見つかった場合は、yamlテンプレートで指定されたインデントの後にその行を印刷します。
    • 負のインデントの場合、1つ以上の空白行を含むテンプレートの元の順序を尊重して空白行を印刷します。

批判:

  • 2つのネストされたループが使用され、forでさえ計算の観点から理想的ではありませんawk。複雑さは O(n^2) です。ただし、これによりdata.csvyaml テンプレートファイルの項目の順序に応じて、任意の列の順序を持​​つファイルに基づいてソリューションを一般化できます。私は他の解決策を見つけるのに時間を費やしていませんでした。良いことがあるかどうかはわかりませんawk...
  • 合計4つのGNU Awk配列が使用されます。これは、多くの列や多数のレコード(または行)を含むファイルなど、「非常に大きい」または「大きい」ファイルに大量のメモリを占有できます。これはその配列で構成されています(teenofそしてteenaryamlテンプレートにはおよそデータ列と同じくらい多くの項目があり、4つの配列のうち1つだけ(datafield)のみが空になり、各data.csvファイルレコードに対して書き換えられます。つまり、I 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フィールド名表記)があります。

template.yaml

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


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

最後に、GoCSVパイプラインを含むショートシェルスクリプトは次のとおりです。

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"

ほぼ

関連情報