Archivo Squash con registros clave-valor a CSV

Archivo Squash con registros clave-valor a CSV

Quiero escribir un script de análisis de datos. Los datos de ejemplo son:

name: John Doe
description: AM
email: [email protected]
lastLogon: 999999999999999
status: active
name: Jane Doe
description: HR
email: [email protected]
lastLogon: 8888888888
status: active
...
name: Foo Bar
description: XX
email: [email protected]
status: inactive

Los pares clave-valor siempre están en el mismo orden ( name, description, email, lastLogon, status), pero es posible que falten algunos campos. Tampoco se garantiza que el primer registro esté completo.

El resultado esperado son valores separados por delimitadores (por ejemplo, CSV):

John Doe,AM,[email protected],999999999999999,active
Jane Doe,HR,[email protected],8888888888,active
...
Foo Bar,XX,[email protected],n/a,inactive

Mi solución es utilizar un readbucle while. La parte principal de mi guión:

while read line; do
    grep -q '^name:' <<< "$line" && status=''
    case "${line,,}" in
        name*) # capture value ;;
        desc*) # capture value ;;
        email*) # capture value ;;
        last*) # capture value ;;
        status*) # capture value ;;
    esac

    if test -n "$status"; then
        printf '%s,%s,%s,%s,%s\n' "${name:-n\a}" ... etc ...
        unset name ... etc ...
    fi
done < input.txt

Esto funciona. Pero obviamente, muy lento. El tiempo de ejecución con 703 líneas de datos:

real    0m37.195s
user    0m2.844s
sys     0m22.984s

Estoy pensando en el awkenfoque, pero no tengo suficiente experiencia en su uso.

Respuesta1

El siguiente awkprograma debería funcionar. Lo ideal sería guardarlo en un archivo separado (por ejemplo squash_to_csv.awk):

#!/bin/awk -f

BEGIN {
    FS=": *"
    OFS=","
    recfields=split("name,description,email,lastLogon,status",fields,",")
}

function printrec(record) {
    for (i=1; i<=recfields; i++) {
    if (record[i]=="") record[i]="n/a"
    printf "%s%s",record[i],i==recfields?ORS:OFS;
    record[i]="";
    }
}
    
$1=="name" && (FNR>1) { printrec(current) }

{
    for (i=1; i<=recfields;i++) {
        if (fields[i]==$1) {
            current[i]=$2
            break
        }
    }
}

END {
    printrec(current)
}

Luego puedes llamar a esto como

awk -f squash_to_csv.awk input.dat
John Doe,AM,[email protected],999999999999999,active
Jane Doe,HR,[email protected],8888888888,active
Foo Bar,XX,[email protected],n/a,inactive

Esto realizará alguna inicialización en el BEGINbloque:

  • establezca el separador del campo de entrada en "a :seguido de cero o más espacios"
  • establezca el separador de campo de salida en,
  • inicializar una serie de nombres de campos (adoptamos un enfoque estático y codificamos la lista)

Si se encuentra el namecampo, comprobará si está en la primera línea del archivo ysi no, imprima los datos recopilados anteriormente. Luego comenzará a recopilar el siguiente registro de la matriz current, comenzando con el namecampo que acaba de encontrar.

Para todas las demás líneas (para simplificar, asumo que no hay líneas vacías ni de comentarios, pero, de nuevo, este programa debería simplemente ignorarlas silenciosamente), el programa verifica cuál de los campos se menciona en la línea y almacena el valor en la posición apropiada en la currentmatriz utilizada para el registro actual.

La función printrectoma dicha matriz como parámetro y realiza la salida real. Los valores faltantes se sustituyen por n/a(o cualquier otra cadena que desee utilizar). Después de imprimir, los campos se borran para que la matriz esté lista para el siguiente conjunto de datos.

Al final, también se imprime el último registro.

Nota

  1. Si la parte "valor" del archivo también puede incluir :combinaciones de espacio, puede reforzar el programa reemplazando
    current[i]=$2
    
    por
    sub(/^[^:]*: */,"")
    current[i]=$0
    
    que establecerá el valor en "todo después de la primera :combinación de espacios" en la línea, eliminando ( sub) todo hasta incluir la primera :combinación de espacios en la línea.
  2. Si alguno de los campos puede contener el carácter separador de salida (en su ejemplo ,), tendrá que tomar las medidas adecuadas para escapar de ese carácter o citar la salida, según el estándar que desee cumplir.
  3. Como señaló correctamente, se desaconsejan los bucles de shell como herramientas para el procesamiento de texto. Si está interesado en leer más, puede consultarestas preguntas y respuestas.

Respuesta2

$ cat tst.awk
BEGIN {
    OFS = ","
    numTags = split("name description email lastLogon status",tags)
}
{
    tag = val = $0
    sub(/ *:.*/,"",tag)
    sub(/[^:]+: */,"",val)
}
(tag == "name") && (NR>1) { prt() }
{ tag2val[tag] = val }
END { prt() }

function prt(   tagNr,tag,val) {
    for ( tagNr=1; tagNr<=numTags; tagNr++ ) {
        tag = tags[tagNr]
        val = ( tag in tag2val ? tag2val[tag] : "n/a" )
        printf "%s%s", val, (tagNr<numTags ? OFS : ORS)
    }
    delete tag2val
}

$ awk -f tst.awk file
John Doe,AM,[email protected],999999999999999,active
Jane Doe,HR,[email protected],8888888888,active
Foo Bar,XX,[email protected],n/a,inactive

Si también desea imprimir una línea de encabezado, simplemente agregue esto al final de la BEGINsección:

for ( tagNr=1; tagNr<=numTags; tagNr++ ) {
    tag = tags[tagNr]
    printf "%s%s", tag, (tagNr<numTags ? OFS : ORS)
}

información relacionada