Обновить

Комментарии 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.1 unset a["$k"] раскрывает ключ дважды, и ключ @ очищает весь массив.

  • Локаль. При ru_RU bash печатает EPOCHREALTIME с запятой (1788676658,546051), а $(( )) читает её как оператор запятая. Любой замер времени в скрипте без "всякой сторонней фигни" ломается просто от русской локали.

  • "Стандартные" утилиты. stat -c, date -d, touch -d @EPOCH, realpath -m, sed -i у GNU и BSD разные. На macOS grep -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:

  1. Точка в имени функции. Вне POSIX-режима TDog.Speak() { ... } – обычная функция с таким именем.

  2. declare -f. Bash отдаёт исходный текст уже разобранной функции. build TDog забирает так тела методов, делает в них пару текстовых замен (inherited превращается в вызов метода родителя), сохраняет текст в переменной и удаляет временную функцию.

  3. Namerefs (local -n, bash 4.3+). Поля объекта хранятся в ассоциативном массиве rex_data. Перед выполнением тела метода создаётся local -n Name='rex_data[Name]', поэтому $Name внутри метода читает и пишет поле объекта.

  4. Динамическая область видимости и 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 баше. И, в общем, сразу понятно, что там к чему

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации