Сверка двух списков в awk: почему NR == FNR ломается на пустом справочнике

Есть approved.txt со списком разрешённых пакетов и requested.txt с пакетами выпуска. Нужно вывести запрошенные имена, которых нет в разрешённом списке. Популярное решение загружает первый файл условием NR == FNR, а затем проверяет второй по ассоциативному массиву. На обычных данных оно работает. Если первый файл пуст, все строки второго внезапно считаются справочником, и проверка ничего не находит.

NR считает записи за весь запуск awk, FNR — записи в текущем файле. Равенство означает «ещё не прочитали ни одной записи из предыдущих файлов», а не «сейчас первый аргумент командной строки». Для пустого первого файла это различие становится решающим. В проверке выпуска пустой список разрешений должен означать, что не разрешён ни один пакет.

Определим файл явно

Для двух обычных файлов с разными именами можно сопоставить FILENAME с ARGV[1]. Тогда роль входа задаётся аргументом, а не числом уже увиденных строк. Пример создаёт данные в текущем каталоге.

printf '%s\n' alpha beta > approved.txt
printf '%s\n' beta gamma > requested.txt
awk '
FILENAME == ARGV[1] { allowed[$0] = 1; next }
!($0 in allowed) { print }
' approved.txt requested.txt

Получится gamma. Если approved.txt сделать пустым, результатом будут beta и gamma. При пустом requested.txt вывода не будет. Так мы проверили именно роли файлов, а не только удачный пример с непустыми входами.

Оператор in проверяет наличие ключа. Это выразительнее проверки значения allowed[$0], особенно если позже справочник начнёт хранить числа, среди которых допустим ноль. Сейчас все значения равны единице, но смысл операции остаётся понятным: существует ли идентификатор.

Что считать одним идентификатором

В примере ключом служит вся строка. Пробел в конце, CR после переноса из Windows или различие регистра создают другой ключ. Не добавляйте автоматическое обрезание, пока не определили контракт справочника. Для имён пакетов регистр и пробелы могут быть недопустимы; тогда лучше отвергать неправильный формат при подготовке, чем незаметно исправлять его во время проверки допуска.

Пустая строка тоже становится ключом. Если списки допускают пустые строки для визуального разделения, добавьте одинаковое правило пропуска для обоих файлов. Обязательно разместите его до загрузки и проверки. Применённая только к одному входу нормализация создаёт другую разновидность потерянных совпадений.

Результат сохраняет порядок requested.txt и повторяет неизвестное имя столько раз, сколько оно встречалось. Для отчёта по событиям это полезно. Если нужен перечень уникальных неизвестных пакетов, добавьте отдельный массив уже выведенных имён. Не смешивайте эти два назначения: число строк отчёта перестанет означать число исходных запросов.

Где заканчивается удобство массива

Справочник целиком хранится в памяти процесса awk. Для небольшого перечня пакетов или разрешённых идентификаторов это обычно удобно, а предварительная сортировка не нужна. Для справочника размером в десятки гигабайт стоит выбрать потоковую сортировку с последующим соединением либо базу данных. Решение определяется объёмом справочника, а не только объёмом второго файла.

Пути approved.txt и requested.txt в этом варианте должны обозначать два разных входа. Для сложной обработки нескольких файлов GNU awk предлагает BEGINFILE и ARGIND; это уже отдельный интерфейс с привязкой к gawk. Приведённый пример использует обычные средства awk и не требует этих расширений.

Проверку отсутствующих имён полезно включить до действий, которые меняют систему. Сначала сформируйте отчёт, оцените его полноту и решите, что означает неизвестный пакет. Так логика допуска останется отделённой от установки.

Откройте программу Linux и Bash: практика автоматизации помогает собрать такие проверки в обслуживаемый скрипт с проверкой входов, корректными аргументами и понятными статусами для следующего шага.

Ещё по теме

Продолжить на практике

  • Linux и Bash для DevOps-инженеров ↗

    Linux для работы с серверами: процессы, права, systemd, сеть, диски и логи. Автоматизируйте повседневные задачи на Bash и разберите типичные сбои сервиса — от занятого порта до переполненного диска.