join теряет совпадения: одинаковая сортировка ключей и LC_ALL
Два TSV-файла отсортированы, общий идентификатор есть в обоих, но join сообщает, что вход не упорядочен, или выдаёт неожиданные пропуски. Слово «отсортированы» здесь слишком неопределённо: важны ключ, разделитель и правила сравнения.
join сопоставляет соседние группы одинаковых ключей. Он не ищет каждую запись по всему второму файлу. Значит, оба входа должны идти в порядке, который совпадает с его сравнением. Числовая сортировка идентификаторов и лексикографическое сравнение — разные порядки; локаль также может менять положение букв и знаков.
Подготовим обе стороны одним правилом
У нас отдельные строки с идентификатором и числом заказов, а во втором файле — идентификатор и регион. В значениях нет табуляции или переводов строки. LC_ALL=C делает сравнение байтовым и одинаковым на всех трёх шагах.
#!/usr/bin/env bash
folder=$(mktemp -d) || exit 1
export LC_ALL=C
printf 'u2 7
u10 3
u1 5
' > "$folder/orders.tsv"
printf 'u1 North
u2 South
u9 West
' > "$folder/users.tsv"
sort -t $' ' -k1,1 "$folder/orders.tsv" > "$folder/orders.sorted"
sort -t $' ' -k1,1 "$folder/users.tsv" > "$folder/users.sorted"
join -t $' ' "$folder/orders.sorted" "$folder/users.sorted"
printf 'Unmatched orders:
'
join -t $' ' -v 1 "$folder/orders.sorted" "$folder/users.sorted"
Основной результат содержит u1 с 5 и North, затем u2 с 7 и South. Отдельный вывод показывает u10 с 3: в справочнике такого пользователя нет. Исчезновение строки в обычном внутреннем соединении теперь объясняется конкретным отсутствующим ключом.
При байтовом порядке u10 идёт перед u2. Это правильно для строковых идентификаторов. Не добавляйте -n к sort только потому, что часть ключа похожа на число. Если ключ действительно числовой, нормализуйте его представление и выберите согласованные правила для всего конвейера.
Разделитель тоже часть формата
Без -t join по умолчанию рассматривает пробельные границы полей. Для TSV нужен именно символ табуляции; запись $'\t' создаёт его средствами Bash. Ключ второго столбца потребовал бы согласованных -k2,2 у sort и -1 2 либо -2 2 у join. Изменение только одного шага нарушает договор.
Заголовок TSV нельзя оставлять среди строк данных: он участвует в сортировке и может случайно соединиться с заголовком второй стороны. Вынесите заголовки отдельно и добавляйте после обработки. Для полноценного CSV с кавычками, запятыми внутри значений и переносами строк нужен CSV-парсер: join не интерпретирует такой формат.
Проверьте уникальность справочника
Если один ключ встречается дважды с обеих сторон, соединение создаёт комбинации совпадений. Сортировка не исправляет неоднозначность. Для справочника сначала проверьте повторяющиеся ключи через cut и uniq после сортировки, затем решите, какую запись считать актуальной.
Сохраняйте количество входных, совпавших и несовпавших записей рядом с результатом. Так изменение справочника будет видно до того, как итоговый отчёт станет выглядеть подозрительно маленьким. Для отладки достаточно взять один потерянный ключ и сравнить его байтовое написание в обоих входах: пробел в конце является частью значения при явной табуляции.
Чтобы соединять такие команды в повторяемые обработки, пригодятся потоки, текстовые утилиты и проверка статусов. Они входят в программу Bash Linux + Git вместе с авторскими задачами. Откройте курс и соберите собственный рабочий конвейер обработки файлов.
Продолжить на практике
- Bash Linux + Git: от новичка до уверенного пользователя ↗
Освоим два важнейших инструмента разработчика: применение bash-скриптов в практических задачах и git в его самых разных проявлениях.
