The Jerusalem PostNew academic plan expands support for reservist students, career soldiers for 2026-27 school yearPunchEkiti recruits 1,522 health workers for PHCsInquirerUndas 2026: Crowds brave the heat at Manila cemeteriesZDF heuteEntdecken Sie das ZDF-NachrichtenstudioEgypt IndependentReunited after 1,711 days: Oleshky evacuees return to Ukraine under Melania Trump-brokered ceasefireRTL BoulevardBijna alle opgepakte relschoppers in Den Haag weer op vrije voetenColliderHBO's 9/10 'Downton Abbey' Replacement Officially Makes Major Comeback Ahead of Season 401netFailles PHP : 10 jours après, près de 30 millions de sites et services web toujours vulnérablesSportstarRanji Trophy 2026/27: Delhi, Services desperate for outright win on fresh pitchTagesschau"Gen Z regelt" will Demos fortsetzenWirtualna PolskaUderzyli w drzewo na łuku drogi. Nie żyje 36-letnia kobietaBBC BusinessShops looted as German cost of living protests turn violent
The Daily Newsstand · Free, Always
Sunday, November 1, 2026

kbool: классы в духе Free Pascal, модули и библиотека классов для bash

Translate

Скрипт на bash начинается с двадцати строк. Через пару лет в нём две тысячи строк, три десятка функций, глобальные переменные с префиксами, чтобы они не столкнулись, source "$(dirname "$0")/lib/…" в начале каждого файла и $(…) вокруг каждого вызова, потому что функция «возвращает» значение через stdout. Переписать всё на Python можно не всегда: скрипт живёт там, где гарантированно есть только bash и базовые утилиты, или его поддерживают люди, которые пишут на bash.

Вторая половина проблемы — переносимость. sed -i, stat -c, date -d, grep -Z, \b в регулярном выражении, nc -l ведут себя по-разному на Ubuntu, на Alpine с busybox, на FreeBSD, на macOS и в Git-bash на Windows. Скрипт, который год работал на одной машине, ломается на соседней.

kbool — попытка дать таким скриптам привычки Free Pascal: модули, которые подключаются по имени через uses, классы, объявленные как class … end, и библиотеку классов со знакомыми именами: TStringList, TDictionary, TIniFile, TRegEx. Вышла первая версия, 0.1.0. Проект разработан с помощью ИИ, и об этом ниже есть отдельный раздел: кто что решал, как проверялся код и какие у этого цифры.

Репозиторий: https://github.com/Keksov/kbool, лицензия MIT.

Что внутри

kbool — это пять git-репозиториев: корневой и четыре подмодуля.

  • kkore — системные модули: загрузчик kk.uses, конфигурация, обработчик ошибок.

  • kklass — классы: поля, свойства с геттерами и сеттерами, конструкторы и деструкторы, наследование с inherited, статические члены, секции видимости.

  • kcl — библиотека классов, 29 модулей: порты классов FPC и Delphi (TList, TStringList, TDictionary, TIniFile, TPath, TRegEx, DateUtils, Math …), обёртки над утилитами командной строки (TGrep, TSed, TAwk, TFind, THead, TTail) и небольшой HTTP-сервер.

  • ktests — тестовый фреймворк и «гейт»: одна команда, которая прогоняет все наборы тестов.

Нужен bash 5.1 или новее. POSIX sh и старые bash не поддерживаются: kbool.sh проверяет версию первым делом и отказывается работать с кодом выхода 2 и одной строкой в stderr, а не падает на середине загрузки.

Класс

Класс объявляется в стиле паскалевского interface: сначала структура, потом тела методов как обычные функции bash, потом build:

source /path/to/kbool/kbool.sh || exit
kk.uses kklass_pascal || exit         # Pascal DSL; ядро kklass он загрузит сам

class TCounter
    public
        var         Name
        var         Count
        constructor Create
        proc        Add
        func        Report
end

TCounter.Create() { Name="$1"; Count=0; }
TCounter.Add()    { Count=$(( Count + ${1:-1} )); }
TCounter.Report() { RESULT="$Name: $Count"; }
build TCounter

TCounter.new basket apples
basket.Add
basket.Add 2
basket.Report; echo "$RESULT"         # => apples: 3
basket.Count                          # => 3
basket.Count = 10
echo "$(basket.Report)"               # => apples: 10
basket.delete

Этот пример взят из README и прогоняется тестами как есть. Несколько деталей:

  • тела методов — настоящие функции, поэтому редактор их подсвечивает, и не нужно экранировать кавычки внутри строк;

  • внутри тела поля — обычные переменные (Name, Count);

  • func отдаёт значение в $RESULT и печатает его только внутри $( ). Почему так, видно в разделе о стоимости: подстановка $( ) — это fork, а fork дорог.

Наследование и вызов родителя выглядят так, как ожидает человек, писавший на Delphi:

# TAnimal (Name, конструктор Create, Speak) объявлен и собран выше
class TDog : TAnimal
    public
        var           Breed
        constructor   Create
        override proc Speak
end
TDog.Create() { inherited; Breed="${2:-mutt}"; }   # конструктор родителя, аргументы передаются
TDog.Speak()  { echo "$Name barks"; inherited Speak; }
build TDog

Все методы виртуальные; override — это проверка при сборке, что у предка такой метод есть.

Библиотека

Модуль подключается по имени; повторное подключение ничего не делает:

source /path/to/kbool/kbool.sh || exit
kk.uses tstringlist tdictionary tinifile || exit

TStringList.new names
names.Add Wirth
names.Add Hejlsberg
names.Sort
names.Get 0; echo "$RESULT"                      # => Hejlsberg

TDictionary.new langs
langs.Add Wirth Pascal
langs.Add Wirth Oberon || echo "duplicate: rc=$?"   # => duplicate: rc=1
langs.TryGetValue Wirth && echo "$RESULT"        # => Pascal

TMemIniFile.new cfg settings.ini
cfg.WriteString main editor lazarus
cfg.UpdateFile
cfg.ReadString main editor vim; echo "$RESULT"   # => lazarus

У всех модулей kcl один контракт: значение возвращается в RESULT, а не в stdout; код возврата 0 — успех, 1 — «нет» или «не найдено» (молча, без сообщений), 2 — неправильный вызов; модули работают под set -eu и не делают fork на горячих путях. Отклонения от FPC перечислены для каждого модуля в его README — например, TRegEx работает на POSIX ERE из bash, а не на PCRE, поэтому ленивых квантификаторов и lookaround нет.

Модули и uses

kk.uses tlist tdictionary загружает каждый модуль один раз, по имени, из пути поиска. Есть файл конфигурации и проверяемые пространства имён, а бухгалтерии вида source "$(dirname …)/…" нет. Файл модуля можно подключить и напрямую, source kcl/tlist/tlist.sh: заголовок модуля сам загрузит kbool.sh, если тот ещё не загружен, и модуль всё равно загрузится один раз, каким бы путём до него ни дошли. В файле с классами на Pascal DSL есть директива uses A B, она делает то же самое, а имена разрешает относительно этого файла.

Модуль сам объявляет, от чего зависит: kk.uses tstringlist подтянет и kklass, и всё, что нужно списку.

Демо: сервис заметок на bash

В examples/notes.sh — HTTP-сервис заметок: около 175 строк кода без комментариев и пустых строк. Он показывает три модуля библиотеки и одну идею: обработчики запросов выполняются в том же процессе bash, что и сервер, поэтому объекты живут между запросами.

Класс, методы которого — обработчики маршрутов:

class TNotes
    public
        constructor Create
        destructor  Destroy
        proc Help
        proc List
        proc Add
        proc Show
        proc Remove
        proc Search
        proc Quit
        proc NotFound
    private
        var  _ini
        func _Ids
end

Заметки хранятся в INI-файле через TMemIniFile, поэтому переживают перезапуск. Поиск ищет слово целиком, без учёта регистра, через TRegEx и \b:

TNotes.Search() {
    local word ids id out=""
    if ! $1.QueryField q || ! [[ "$RESULT" =~ ^[A-Za-z0-9_]+$ ]]; then
        $2.Code = 400
        $2.Write $'use /search?q=WORD (letters, digits, underscore)\n'
        return 0
    fi
    word="$RESULT"
    $this._Ids
    ids="$RESULT"
    for id in $ids; do
        "$_ini.ReadString" notes "$id" ""
        if TRegEx.isMatch "$RESULT" '\b'"$word"'\b' i; then
            out+="$id: $RESULT"$'\n'
        fi
    done
    $2.Write "$out"
}

Маршруты регистрируются в приложении (THttpApplication, наследник порта TCustomApplication):

App.RegisterRoute /             GET    Notes.Help
App.RegisterRoute /notes        GET    Notes.List
App.RegisterRoute /notes        POST   Notes.Add
App.RegisterRoute /notes/:id    GET    Notes.Show
App.RegisterRoute /notes/:id    DELETE Notes.Remove
App.RegisterRoute /search       GET    Notes.Search
App.RegisterRoute /quit         POST   Notes.Quit
App.RegisterRoute /*rest        ALL    Notes.NotFound

examples/notes_session.sh запускает сервис на свободном порту и проигрывает короткую сессию curl. Так она выглядела при проверке «чистой установки» по README на Ubuntu:

$ curl -s localhost:39218/notes -d 'buy milk'
1
$ curl -s localhost:39218/notes -d 'call Alice about the milk'
2
$ curl -s localhost:39218/notes -d 'fix the bike'
3
$ curl -s 'localhost:39218/search?q=milk'
1: buy milk
2: call Alice about the milk
$ curl -s -X DELETE localhost:39218/notes/2
$ curl -s localhost:39218/notes
1: buy milk
3: fix the bike
$ curl -s -X POST localhost:39218/quit
bye
server exited: 0

Ограничения сервера честно записаны в его README: один запрос за раз, без keep-alive, около 10 запросов в секунду последовательно, только текстовые тела, обычный HTTP без аутентификации — держите его на 127.0.0.1. Слушает он через GNU netcat 0.7.1 или socat 1.7.4+.

Переносимость: «зоопарк»

Тесты kbool в CI идут на 13 конфигурациях на каждый push в main и каждый pull request: bash 5.1, 5.2 и 5.3 на Linux (glibc и musl), текущий bash на macOS, FreeBSD и Windows (Git-bash и MSYS2). До CI те же тесты гонялись в лабораторных виртуальных машинах. Вот что они нашли — каждый пункт закреплён тестом, полный список в docs/portability.md.

  • mawk — awk по умолчанию в Debian и Ubuntu. Движок вещественной арифметики модуля Math написан на awk, и в mawk он молча не работал: парсер переполнялся на длинной цепочке else if, на запросы через пайп mawk отвечает только с -W interactive, а nan == 0 у него истинно. Теперь там плоская диспетчеризация, сравнения защищены от NaN, и одна программа работает на gawk, mawk, BWK awk и busybox awk.

  • bash 5.1. declare -f печатает >&"$fd" как &> "$fd". После такого круга команда пишет stdout и stderr в файл с именем дескриптора, а kklass пропускает через declare -f каждое тело метода. Ещё в 5.1 unset a["$k"] раскрывает ключ дважды, и ключ @ очищает весь массив. Код обходит обе формы, а статическая проверка в каждом репозитории их не пропускает.

  • Десятичная запятая. В локали ru_RU или de_DE bash печатает EPOCHREALTIME как 1788676658,546051, а $(( )) читает запятую как оператор. Замеры времени берут только цифры.

  • \<, \>, \b по-разному пишутся (или отсутствуют) в glibc, musl, libc FreeBSD, macOS и newlib в cygwin. TRegEx один раз проверяет C-библиотеку и переписывает шаблон в её написание, сохраняя номера групп.

  • FreeBSD. В pkg-сборке bash /dev/fd/N эмулируется самим bash, а <( ) — это FIFO, писатель которого блокируется до прихода читателя. Быстрый захват тел методов там отключается сам, а FIFO открывается и удаляется явно.

  • Linux под нагрузкой иногда теряет код выхода производителя в <( ) (около 0,3 % быстрых производителей при восьмикратной нагрузке). TPipe возвращает его через отдельный пайп статуса.

  • netcat. У GNU netcat, OpenBSD nc, ncat и busybox nc флаг -c значит разное, а OpenBSD nc терял каждого второго клиента. Сервер работает на GNU netcat или socat, остальные отклоняет по имени.

  • CRLF. Утилиты cygwin в текстовом режиме отбрасывают CR, утилиты Linux и BSD его оставляют. TGrep, TSed и TAwk отбрасывают CR в текстовом режиме везде.

Разделение такое. Ядро (kkore, kklass и модули kcl, кроме обёрток и HTTP-сервера) не требует GNU утилит и даёт одинаковые результаты на всех конфигурациях CI. Обёртки дают поведение GNU-утилиты везде или понятную ошибку: они ищут gsed, gawk, ggrep и т. д., потом обычное имя, если это GNU, а без них каждый вызов возвращает код 2 с причиной, где назван пакет для установки.

Сколько это стоит

Все цифры — из docs/benchmarks.md, медианы трёх прогонов на одной машине (AMD Ryzen 7 8845H): Ubuntu 24.04 в VirtualBox (bash 5.2.21) и Git-bash на Windows-хосте (bash 5.3.15). Сравнивать столбцы между собой не стоит: VM добавляет свои накладные расходы, и версии bash разные.

что

VM

Windows

вызов пустой функции bash

7.018 µs

5.032 µs

один fork, x=$(true)

750.395 µs

16008.55 µs

вызов метода

266.6 µs

212.9 µs

чтение свойства

45.9 µs

34.8 µs

TDictionary.Add

337 µs

235 µs

source kbool.sh

12.6 ms

32.8 ms

+ kk.uses tstringlist

152.4 ms

171.7 ms

+ kk.uses dateutils

431.5 ms

383.7 ms

Главное здесь — строка с fork. Тот же GetItem из словаря, вызванный как v=$(d.GetItem k), стоил 2756 µs на Linux и 31617 µs на Windows, а прямой вызов в том же прогоне — 370 µs и 253 µs. Поэтому члены классов возвращают значение в переменной, а внешняя утилита запускается один раз на вызов, а не на каждую запись.

Обёртки над утилитами добавляют фиксированную цену: от 2.9 до 6.4 ms на вызов. На Linux это делает обёртку в 2–7 раз медленнее прямого вызова утилиты, на Windows, где доминирует запуск процесса, — в 1.19–1.38 раза.

Когда kbool не нужен: если скрипт перемалывает тысячи записей, bash медленный сам по себе. count у THead по 5000 строкам занял 420.83 ms на Linux против 2.94 ms у head -n 5000 | wc -l. Для числа или отфильтрованного файла правильный инструмент — конвейер из grep, awk, sort. kbool окупается там, где скрипт тратит время на логику, структуру и обработку ошибок.

Тесты

Все наборы тестов написаны на ktests и запускаются «гейтом», ktests/tools/gate.sh: одна команда, одна итоговая строка __GATE__:… и код выхода. Каждый набор идёт в своей «песочнице» (свой HOME, временный и рабочий каталоги, окружение «запрещено всё, что не разрешено»), а гейт проверяет, не оставил ли тест после себя процессов. Случай, который на платформе выполнить нельзя, засчитывается как SKIP: <причина> или N/A: <причина>, поэтому число тестов в наборе одинаково на всех конфигурациях, и изменившееся число что-то значит.

Чего нет и что ограничено

  • Только bash 5.1+, не POSIX sh; на macOS нужен bash из Homebrew.

  • TRegEx — POSIX ERE из bash: нет ленивых квантификаторов, lookaround и именованных групп.

  • Обёртки требуют GNU-утилиты.

  • HTTP-сервер — демонстрация, а не веб-сервер для продакшена.

  • Загрузка с модулем стоит 150–430 ms на скрипт: для утилиты, которую вызывают в цикле, это заметно.

  • fpjson запланирован, но не реализован.

Как это сделано: с помощью ИИ, открыто

kbool разработан с помощью ИИ, и это часть истории проекта, а не сноска. Роли:

  • Мейнтейнер (я) решает: границы проекта, каждый вопрос дизайна, у которого больше одного разумного ответа, и что выпускать. В main изменения попадают только через меня; коммиты, написанные с Claude, несут трейлер Co-Authored-By: Claude ….

  • Сессия-супервизор (Claude) планирует фазу. На каждый шаг она пишет карточку задачи: цель, файлы, которые можно трогать, проверки приёмки — и отдаёт её исполнителю: Claude Opus для дизайна, ядра и текстов, Claude Sonnet для хорошо специфицированных задач. Отчёт исполнителя супервизор сверяет с реальным деревом до коммита.

  • Независимый критик — свежая сессия без контекста исполнителя — проверяет изменения ядра.

  • Каждый коммит проходит гейт на двух рантаймах bash: Git-bash 5.3.15 и MSYS2 5.3.9. Полный прогон гейта от 31 октября 2026 года: 30 наборов тестов, 9694 теста на 5.3.15 и 9695 на 5.3.9, все прошли, всего 19389. С 30 октября каждый push в main и каждый pull request дополнительно проходит 13 конфигураций CI.

Гейты действительно ловят проблемы. Первый прогон на Linux (Ubuntu, Debian, Fedora и Debian под de_DE) дал 111 упавших тестовых файлов (по всем машинам и локалям вместе), прогон на Alpine и FreeBSD — 240, первые запуски CI упали на 5 конфигурациях из 13: 72 упавших случая с 14 причинами. Всё это разобрано, исправлено и закреплено тестами, а матрица CI потом прошла дважды подряд на всех 13 конфигурациях.

Ревью находит ошибки и в самой системе проверки: в одном из раундов выяснилось, что прерванный тестовый файл мог отчитаться зелёным, и ktests это исправил. Ревью kklass закрыло 12 найденных проблем; .new после него стал быстрее в 4,8 раза.

Вся история открыта: планы, каждое решение с причиной и 27 машиночитаемых журналов фаз лежат в репозиториях в docs/dev/ (оглавление). Можно не верить тексту на слово, а прочитать, как принималось конкретное решение и каким тестом оно закреплено.

Статус и как попробовать

Версия 0.1.0 — первая, одна для всех пяти репозиториев. В линейке 0.x API может меняться между минорными версиями.

git clone --recurse-submodules https://github.com/Keksov/kbool.git
cd kbool
bash examples/notes_session.sh     # нужны curl и socat (или GNU netcat 0.7.1)
bash ktests/tools/gate.sh all      # все тесты под вашим bash

Ничего не собирается и не устанавливается: скрипт делает source kbool.sh оттуда, где лежит дерево, и называет нужные модули. На macOS понадобится brew install bash (системный /bin/bash 3.2 kbool отклонит), на FreeBSD — pkg install bash.

Особенно интересна обратная связь от тех, кто писал на Free Pascal или Delphi: насколько точно перенесены классы и где поведение расходится с ожиданиями. Issues и pull requests — в https://github.com/Keksov/kbool.

P.S. kbool посвящён памяти Никлауса Вирта (1934–2024), создателя Pascal, Modula-2 и Oberon.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.