
Комментарии 6
Весь смысл скриптовых языков в том, что они понятны широкой массе админов и прочих девопсов. Вы же изобрели новый язык. С моей точки зрения пользы от такого решения очень мало, а вот вреда предостаточно. Представьте человека, эдакого бородатого сисадмина из 90-х, который на bash-е не одну собаку Гришковца сьел, откроет он такой скрипт и прифигеет. Если Вас не устраивает базовый функционал bash, то пишите на Perl или на Lua. Зачем новый язык то изобредать ? Чтобы добавить проблем будущим поколениям разработчиков которые будут обслуживать Ваш код ?
Но было бы интересно почитать как это всё работает - как добавляется новый синтаксис в bash.
Дело скорее даже не в том, что админ прифигеет. Он то как раз разберётся, на то он и есть. Дело скорее в переносимости. Один и тот же скрипт BASH будет одинаково работать и на Linux и на Mac и на, прости господи, WSL, если в нём не использовать всякую стороннюю фигню.
Отчасти вы правы. Пока скрипт состоит из чистого bash, он действительно переносим. Но "одинаково работает на Linux и на Mac" начинает расходиться с реальностью уже в "мелочах". Во время разработки kbool один и тот же набор тестов гонялся на Ubuntu, Debian, Fedora, Alpine (с одним busybox и с GNU-пакетами), FreeBSD, macOS и Windows (Git-bash, MSYS2). Сейчас то же самое проверяют 13 конфигураций CI. Несколько примеров того, что вылезло:
Сам bash. На macOS
/bin/bash- это 3.2 из 2007 года, то есть без namerefs,${x@a}иEPOCHREALTIME. На FreeBSD/bin/bashвообще нет, поэтому#!/bin/bashтам выдаёт not found. Между 5.1 и 5.2 тоже есть разница: в 5.1unset a["$k"]раскрывает ключ дважды, и ключ@очищает весь массив.Локаль. При
ru_RUbash печатаетEPOCHREALTIMEс запятой (1788676658,546051), а$(( ))читает её как оператор запятая. Любой замер времени в скрипте без "всякой сторонней фигни" ломается просто от русской локали."Стандартные" утилиты.
stat -c,date -d,touch -d @EPOCH,realpath -m,sed -iу GNU и BSD разные. На macOSgrep -Zвместо нулевого разделителя включает распаковку. В Debian и Ubuntu по умолчанию стоит mawk, и у негоnan == 0истинно.Регулярки.
\<,\>и\bв POSIX ERE glibc, musl, FreeBSD libc, macOS и newlib пишут по-разному, а где-то их нет совсем. Один и тот же[[ =~ ]]на одной системе находит совпадение, на другой нет, а на третьей это синтаксическая ошибка.Windows. В текстовом режиме утилиты cygwin отбрасывают CR из CRLF, а утилиты Linux и BSD его оставляют. Для одного и того же файла
grepиawkвыдают разные строки.
Каждое найденное расхождение либо закрыто внутри библиотеки (проверка версии bash, определение версии stat, перезапись регулярки под местную libc и т. д.), либо, где закрыть нельзя, явно задокументировано как ограничение. На каждый случай есть тест, который гоняется на всех строках CI. Полный список граблей переносимости тут https://github.com/Keksov/kbool/blob/main/docs/portability.md
Так что идея kbool как раз в обратном: ядро не тянет ни одной GNU-утилиты, только bash и POSIX. А те грабли, на которые наступает чистый bash-скрипт при переезде с Linux на Mac, уже собраны и обезврежены один раз, и каждому не приходится собирать их самому.
Отвечу сначала на главное, потом на вопрос про устройство.
Это не новый язык. Парсер bash я не трогал и ничего в него не добавлял. class, public, var, proc, end, build – это обычные функции bash. Например, строка var Name вызывает функцию var с аргументом Name, и та записывает его в описание класса. Поэтому любой такой скрипт остаётся валидным bash: bash -n его пропускает, а set -x показывает, что происходит. Тела методов – тоже обычные функции bash, с привычным синтаксисом и подсветкой в редакторе.
Насчёт бородатого админа вы во многом правы. Скрипту на 50 строк классы не нужны, и я никому не предлагаю переписывать на kbool backup.sh. Если есть выбор, взять Perl, Python или Lua – нормальное решение. kbool для случая, когда выбора нет: bash уже стоит, а скриптов накопились тысячи строк. Тогда кто-то всё равно начинает изобретать "объекты" на ассоциативных массивах и соглашениях об именах, и каждый делает это по-своему. Я хотел сделать это один раз, с тестами, в форме, знакомой паскалистам. Админу из 90-х придётся прочитать документацию, как и для любой другой библиотеки.
Как это работает. Используются четыре давние возможности bash:
Точка в имени функции. Вне POSIX-режима
TDog.Speak() { ... }– обычная функция с таким именем.declare -f. Bash отдаёт исходный текст уже разобранной функции.build TDogзабирает так тела методов, делает в них пару текстовых замен (inheritedпревращается в вызов метода родителя), сохраняет текст в переменной и удаляет временную функцию.Namerefs (
local -n, bash 4.3+). Поля объекта хранятся в ассоциативном массивеrex_data. Перед выполнением тела метода создаётсяlocal -n Name='rex_data[Name]', поэтому$Nameвнутри метода читает и пишет поле объекта.Динамическая область видимости и
eval. Эти локальные ссылки видны коду, выполненному черезevalвнутри той же функции.
Всё это в 7 строках, которые можно запустить:
TDog.Speak() { echo "$Name barks"; Name=Max; }
body=$(declare -f TDog.Speak); body=${body#*\{}; body=${body%\}}
unset -f TDog.Speak
declare -A rex_data=([Name]=Rex)
rex.Speak() { local -n Name='rex_data[Name]'; eval "$body"; }
rex.Speak # Rex barks
echo "${rex_data[Name]}" # Max – метод изменил поле объекта
В kbool вся эта магия происходит в функции build. Он забирает тела методов, переписывает inherited, собирает списки полей и методов с учётом предков и генерирует функцию TDog.new вместе с шаблоном экземпляра. До build класс – это только описание, после него им можно пользоваться. Дальше TDog.new rex по этому шаблону создаёт массив rex_data, переменную rex_class=TDog и однострочные обёртки rex.Speak() { kk._exec rex Speak TDog "$@"; }. Обёртка передаёт вызов общему рантайму. Тот кладёт в стек кадр (экземпляр и класс, где определён метод), привязывает ссылки на все поля и выполняет тело.
Виртуальность $this.Method получается так: метод ищется по классу экземпляра. inherited работает статически: метод ищется от родителя того класса, чьё тело сейчас выполняется, как в Delphi. Результат метод возвращает в переменной RESULT, а не через stdout, потому что $( ) – это форк, а на Windows он стоит около 16 мс на вызов.
Сложность не в самой идее, а в мелочах. Нужно, чтобы вызванный метод не видел локальные переменные вызывающего, чтобы unset внутри метода не стёр чужое поле, и чтобы set -a не выгрузил в окружение тело метода. Из-за таких историй в kklass около 6,5 тысяч строк и много тестов.
Выглядит прикольно, но Паскаль не сказать, чтоб сильно популярен сейчас. Не проще библиотеку написать на чем-то вроде ba.sh (https://github.com/mnorin/ba.sh)? Это по сути OOP code style для баша. Те самые соглашения и массивы, но с минимальным оверхэдом. Плюс работает на 3 баше. И, в общем, сразу понятно, что там к чему
kbool: классы в духе Free Pascal, модули и библиотека классов для bash