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,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이면 충분하지만 기본적(펄에는 포함되어 있음)이거나 다음과 같은 고급 모듈입니다.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 파서를 사용하는 이점 중 하나입니다.

#!/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에서 테스트되었습니다.

이는 두 개의 입력 파일을 허용하는 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"의 약자.
  • 두 번째 블록은 파일 FNR==1 {...}의 각 열 헤더를 data.csv세 번째 배열에 배치합니다 colhead.
  • 세 번째이자 마지막 블록은 {...}여러 가지 작업을 수행합니다.
    • 템플릿 입력 키(소문자)를 반복합니다.
    • 양수 또는 빈 들여쓰기의 경우:
      • data.csv레코드의 필드가 ,다른 필드 구분 기호를 포함하는 따옴표 붙은 문자열로 구성되어 있는지 알 수 있는 필드 정규식을 기반으로 각 파일 레코드를 분할합니다(여기) . 분할로 인한 구성요소는 네 번째 배열인 에 배치됩니다 datafield.
      • 템플릿 항목과 일치하는 항목을 찾기 위해 데이터 파일 열 헤더에 대해 중첩 루프를 수행합니다. 발견된 경우 yaml 템플릿에 지정된 들여쓰기 뒤에 해당 줄을 인쇄합니다.
    • 음수 들여쓰기의 경우 하나 이상의 빈 줄을 포함하는 템플릿의 원래 순서를 존중하여 빈 줄을 인쇄합니다.

비판:

  • 두 개의 중첩 루프가 사용되며 for조차도 계산 관점에서는 이상적이지 않습니다 awk. 복잡도는 O(n^2)입니다. 그러나 이렇게 하면 data.csvyaml 템플릿 파일의 항목 순서에 따라 임의의 열 순서가 있는 파일을 기반으로 솔루션을 일반화할 수 있습니다. 나는 다른 해결책을 찾는 데 시간을 소비하지 않았습니다. 더 나은 것이 있는지 확실하지 않습니다 awk...
  • 총 4개의 GNU Awk 배열이 사용되는데, 이는 많은 열과 많은 레코드(또는 행)가 있는 파일과 같이 "매우 큰" 또는 "큰" 파일에 많은 메모리를 차지할 수 있습니다. 이는 해당 배열에서 구성됩니다( teenof그리고 teenaryaml 템플릿에는 대략 데이터 열만큼 많은 항목이 있으며, 4개의 배열 중 하나만( 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"

거의

관련 정보