Пишем 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 — значит, утилита ведёт себя одинаково в обоих режимах.
Что получилось
Небольшая, «вежливая», утилита: не засоряет вывод, дружит с конвейерами, понимает файл и поток, возвращает осмысленный код выхода и умеет работать с несколькими алгоритмами. Ровно то, что хочется иметь под рукой в сценариях сборки и проверки прошивок.
