Как получить все поля в внешнем соединении с Unix?

Предположим, что у меня есть два файла: en.csv и sp.csv, каждый из которых содержит ровно две записи, разделенные запятыми:

en.csv:

1,dog,red,car
3,cat,white,boat

sp.csv:

2,conejo,gris,tren
3,gato,blanco,bote

Если я выполняю

join -t, -a 1 -a 2 -e MISSING en.csv sp.csv

вывод, который я получаю:

1,dog,red,car
2,conejo,gris,tren
3,cat,white,boat,gato,blanco,bote

Обратите внимание, что все отсутствующие поля были свернуты. Чтобы получить "правильное" полное внешнее соединение, мне нужно указать формат; Таким образом,

join -t, -a 1 -a 2 -e MISSING -o 0,1.2,1.3,1.4,2.2,2.3,2.4 en.csv sp.csv

дает

1,dog,red,car,MISSING,MISSING,MISSING
2,MISSING,MISSING,MISSING,conejo,gris,tren
3,cat,white,boat,gato,blanco,bote

Одним из недостатков этого способа создания полного внешнего соединения является то, что нужно явно указать формат финальной таблицы, что может быть нелегко сделать в программных приложениях (где идентификация объединенных таблиц известна только при во время выполнения).

Последние версии GNU join устраняют этот недостаток, поддерживая специальный формат auto. Поэтому с такой версией join последняя команда выше может быть заменена гораздо более общей

join -t, -a 1 -a 2 -e MISSING -o auto en.csv sp.csv

Как я могу добиться такого же эффекта с версиями join, которые не поддерживают параметр -o auto?


Фон и сведения

У меня есть оболочка Unix (zsh) script, которая предназначена для обработки нескольких CSV файлов, и делает это, широко используя опцию GNU join -o auto. Мне нужно изменить этот script так, чтобы он работал в средах, где доступная команда join не поддерживает параметр -o auto (как в случае с BSD join, так и для более старых версий GNU join).

Типичное использование этой опции в script выглядит примерно так:

_reccut () {
    cols="1,$1"
    shift
    in=$1
    shift
    if (( $# > 0 )); then
        join -t, -a 1 -a 2 -e 'MISSING' -o auto \
          <( cut -d, -f $cols $in | sort -t, -k1 ) \
          <( _reccut "[email protected]" )
    else
        cut -d, -f $cols $in | sort -t, -k1
    fi
}

Я показываю этот пример, чтобы проиллюстрировать, что было бы трудно заменить -o auto на явный формат, поскольку поля для включения в этот формат неизвестны до выполнения.

Функция _reccut выше в основном извлекает столбцы из файлов и объединяет результирующие таблицы вдоль их первого столбца. Чтобы увидеть, как _reccut в действии, представьте, что помимо файлов, упомянутых выше, у нас также был файл

de.csv

2,Kaninchen,Grau,Zug
1,Hund,Rot,Auto

Затем, например, чтобы отобразить бок о бок столбца 3 из en.csv, столбцы 2 и 4 из sp.csv и столбец 3 команды de.csv будут запускаться:

% _reccut 3 en.csv 2,4 sp.csv 3 de.csv | cut -d, 2-
red,MISSING,MISSING,Rot
MISSING,conejo,tren,Grau
white,gato,bote,MISSING

Ответ 1

Вот решение, которое может работать или не работать для ваших данных. Он подходит к проблеме путем выравнивания записей в файле csv по номеру строки, то есть запись 2 заканчивается в строке 2, запись 3123 на номер строки 3123 и т.д. Отсутствующие записи/строки заполняются полями MISSING, поэтому входные файлы будут искажены, чтобы выглядеть так:

en.csv:

1,dog,red,car
2,MISSING,MISSING,MISSING
3,cat,white,boat

de.csv:

1,Hund,Rot,Auto
2,Kaninchen,Grau,Zug
3,MISSING,MISSING,MISSING

sp.csv:

1,MISSING,MISSING,MISSING
2,conejo,gris,tren
3,gato,blanco,bote

Оттуда легко вырезать интересующие столбцы и просто печатать их бок о бок с помощью paste.

Для этого мы сначала сортируем входные файлы, а затем применяем несколько тупых магов awk:

  • Если на ожидаемом номере строки появится запись, напечатайте ее
  • В противном случае напечатайте как можно больше строк, содержащих количество ожидаемых (это зависит от количества полей первой строки в файле, так же, как и те, что join -o auto) MISSING, пока выравнивание не будет правильным снова
  • Не все входные файлы имеют одинаковое количество записей, поэтому до этого все выполняется поиск максимального значения. Затем больше строк с полями MISSING печатаются до тех пор, пока не будет достигнут максимум.

Код

reccut.sh:

#!/bin/bash

get_max_recnum()
{
    awk -F, '{ if ($1 > max) { max = $1 } } END { print max }' "[email protected]"
}

align_by_recnum()
{
    sort -t, -k1 "$1" \
        | awk -F, -v MAXREC="$2" '
            NR==1 { for(x = 1; x < NF; x++) missing = missing ",MISSING" }
            {
                i = NR
                if (NR < $1)
                {
                    while (i < $1)
                    {
                        print i++ missing
                    }
                    NR+=i
                }
            }1
            END { for(i++; i <= MAXREC; i++) { print i missing } }
            '
}

_reccut()
{
    local infiles=()
    local args=( [email protected] )
    for arg; do
        infiles+=( "$2" )
        shift 2
    done
    MAXREC="$(get_max_recnum "${infiles[@]}")" __reccut "${args[@]}"
}

__reccut()
{
    local cols="$1"
    local infile="$2"
    shift 2

    if (( $# > 0 )); then
        paste -d, \
            <(align_by_recnum "${infile}" "${MAXREC}" | cut -d, -f ${cols}) \
            <(__reccut "[email protected]")
    else
        align_by_recnum "${infile}" "${MAXREC}" | cut -d, -f ${cols}
    fi
}

_reccut "[email protected]"

Run

$ ./reccut.sh 3 en.csv 2,4 sp.csv 3 de.csv
red,MISSING,MISSING,Rot
MISSING,conejo,tren,Grau
white,gato,bote,MISSING