Пишем shell-совместимую утилиту

Иногда bash‑скрипта хватает. Иногда — нет. Нужна крошечная утилита, которую легко встроить в конвейер, передать ей поток, получить ровно один результат и пойти дальше. В этой главе мы напишем такую утилиту на Си и сразу встроим её в привычный Unix‑поток работы.

Какой должна быть CLI‑утилита

Чтобы инструмент «вписался» в экосистему, держим в голове несколько простых правил:

  • коды возврата: 0 — успех, >0 — ошибка;
  • потоки: читаем из stdin, пишем в stdout, ошибки — в stderr;
  • аргументы в стиле Unix: короткие -o и длинные --output;
  • по умолчанию выводим минимум; детали — по флагам;
  • никаких побочных эффектов: в файлы пишем только по явной просьбе;
  • конвейеры — первоклассные граждане: всё должно работать с |.

Задача: контрольные суммы для прошивок

На микроконтроллерах STM32 есть периферия CRC32 — удобно для проверки целостности, например прошивки. Но есть нюанс: расчёт отличается от стандарта IEEE. Готовой «стандартной» утилиты под этот вариант нет — сделаем свою! Так же в целях демонстрации добавим расчёт контрольной суммы MODBUS.

Назовём программу chsm (checksum). Как ей хочется пользоваться:

  • вход — файл или stdin;
  • вывод — только контрольная сумма, без лишнего шума;
  • по умолчанию шестнадцатеричный формат; десятичный включаем флагом -i;
  • алгоритм выбираем флагом: --algo stm32 (по умолчанию) или --algo rtu (Modbus RTU).

Примеры запуска:

$ ./chsm firmware.bin
58E99ECC
$ ./chsm -i firmware.bin
1491705548
$ ./chsm --algo rtu firmware.bin
1A80
$ echo -en "\xAA\xBB\xCC\xDD" | ./chsm
58E99ECC

Внутренний интерфейс

Сам алгоритм CRC32 разбирать не будем — важен единый интерфейс для разных реализаций. Под капотом каждая реализация должна уметь инициализироваться, «скармливать» порции данных и отдавать результат:

typedef enum {
  eALGO_STM32,
  eALGO_RTU,
} AlgoId;

typedef struct {
  AlgoId id;
  char *name;
  IfaceChecksum *iface;
} Algo;

typedef struct {
  void (*init)();
  void (*accumulate)(const uint8_t *data, uint32_t length);
  uint32_t (*get_crc)();
} IfaceChecksum;

// ...

IfaceChecksum stm32 = {
  .init = stm32_crc_init,
  .accumulate = stm32_crc_acc,
  .get_crc = stm32_crc_get,
};

// ...

Algo algorithms[] = {
  { eALGO_STM32, "stm32", .iface = &stm32 },
  { eALGO_RTU, "rtu", .iface = &rtu },
};

Разбор аргументов

Минимальная логика: -i переключает формат, -a/--algo выбирает реализацию, -h печатает справку.

int opt;
bool print_hex = true;
Algo *algorithm = &algorithms[0];

while ((opt = getopt_long(argc, argv, "ia:h",
        long_options, NULL)) != -1) {
  bool found = false;
  switch (opt) {
    case 'i':
      print_hex = false;
      break;
    case 'a':
      for (uint32_t id = 0;
           id < sizeof(algorithms) / sizeof(Algo);
           id++) {
        if (strcmp(optarg, algorithms[id].name) == 0) {
          found = true;
          algorithm = &algorithms[id];
          break;
        }
      }
      found ?: print_usage();
      break;
    case 'h':
    default:
      print_usage();
  }
}

Откуда брать данные

Сценария два: пользователь указал файл — читаем его; нет — пробуем stdin; если и там пусто (терминал), сообщаем об ошибке.

FILE *file = NULL;

if (optind < argc) {
  file = fopen(argv[optind], "rb");
  if (file == NULL) {
    fprintf(stderr, "%s: %s\n", PROGRAM_NAME, strerror(errno));
    return EXIT_FAILURE;
  }
} else if (!isatty(STDIN_FILENO)) {
  file = stdin;
} else {
  fprintf(stderr, "%s: No input provided.\n", PROGRAM_NAME);
  return EXIT_FAILURE;
}

Считаем и печатаем

Читаем маленькими порциями, накапливаем CRC и печатаем в нужном формате.

uint8_t buffer[4];
uint32_t bytes_read = 0;
algorithm->iface->init();

while (bytes_read = fread(buffer, 1, sizeof(buffer), file),
       bytes_read != 0) {
  algorithm->iface->accumulate(buffer, bytes_read);
}

printf(print_hex ? "%X\n" : "%d\n", algorithm->iface->get_crc());

Быстрая самопроверка

Сравним вывод «из файла» и «из stdin» — должны совпасть:

$ echo -en "\xAA\xBB\xCC\xDD" > firmware.bin
$ test $(./chsm firmware.bin) = $(echo -en "\xAA\xBB\xCC\xDD"|./chsm)
$ echo $?
0

test вернул 0 — значит, утилита ведёт себя одинаково в обоих режимах.

Что получилось

Небольшая, «вежливая», утилита: не засоряет вывод, дружит с конвейерами, понимает файл и поток, возвращает осмысленный код выхода и умеет работать с несколькими алгоритмами. Ровно то, что хочется иметь под рукой в сценариях сборки и проверки прошивок.


Изменено: