Что такое Irnix
Irnix вдохновлялся парадигмой ООП, поэтому его можно охарактеризовать как систему организации объектов, что превращает файловую систему в объекты с методами и контрактами. Это позволяет организовывать скрипты, проверять вызов метода на валидность, иметь одну точку вызова а также легко подменять реализации объектов.
Irnix помогает создавать жесткую структуру, путем механизмов проверки контрактов (сигнатур методов), тем самым помогая обеспечить правильный вызов методов объектов и помогает избежать неправильного поведения в pipeline.
Быстрый старт
Создайте директорию object в директории ~/.local/share/irnix и создайте в ней файл method:
~/.local/share/irnix
└── object
└── method
Содержимое файла method:
#!/bin/bash
echo "Hello $1"
Выдайте права для исполнения файла method:
chmod +x ~/.local/share/irnix/object/method
Вызовите метод:
irnix e object.method -- World!
Вывод:
Hello World!
Концепты
В этой главе мы более глубоко погрузимся в то, какие сущности есть в Irnix и какую роль они играют.
Вот краткое описание всех из них:
- namespace (именное пространство) — директория, с которой начинается именования объектов и их методов.
- object (объект) — множество методов и контрактов.
- method (метод) — исполняемый файл, название которого — это название метода, а содержимое которого, это инструкция выполнения (например скрипт на python, bash, ruby или обычный бинарный файл).
Именные пространства (namespaces)
Namespace это некая директория, с которой начинается именования остальных объектов, и все содержимое этой директории Irnix будет воспринимать, как объекты, методы и контракты. По умолчанию это директория ~/.local/share/irnix, но ее можно изменить, путем изменения переменной окружения IRNIX_NAMESPACE или напрямую используя флаг -n (--namespace):
IRNIX_NAMESPACE=dir irnix e object.method -- World!
irnix e -n dir object.method -- World!
Irnix соблюдает следующую иерархию:
-nIRNIX_NAMESPACE~/.local/share/irnix
Это означает, что в этом случае:
IRNIX_NAMESPACE=dir1 irnix -n dir2 e object.method -- World!
Irnix выберет dir2 в качестве namespace.
Объекты (objects)
В Irnix объекты, это сущности, что содержат другие объекты, методы и контракты, о которых мы поговорим позже.
Irnix считает объектом все директории, что расположены в namespace. Для примера возьмем стандартный namespace и создадим в нем директорию object:
~/.local/share/irnix
└── object
Теперь Irnix распознает object как объект, у которого пока что нет методов.
Объекты также могут владеть другими объектами. Внутри файловой системы это выглядит как просто еще одна директория внутри другой. Например создадим еще один объект, но в этот раз назовем его object2:
~/.local/share/irnix
└── object
└── object2
Именования объектов
Названия объектов (директорий) не должны содержать .. Они могут начинаться на числа и содержать _ или -.
Любая директория, что начинается с . в namespace не будет восприниматься как объект, поэтому в таких папках удобно хранить какую-то дополнительную информацию и служебные файлы для методов. Такие директории даже могут содержать исполняемые файлы, которые Irnix в любом случае напрямую вызвать не сможет, так как вызов будет выглядеть так: object..secret_object.method и Irnix просто не поймет его.
Методы (methods)
Irnix воспринимает любой исполняемый файл объекта как метод. Например, возьмем object из предыдущего примера и добавим файл method:
~/.local/share/irnix
└── object
└── method
С содержимым:
#!/bin/python
print("Hello World!")
Который должен быть исполняемым:
chmod +x ~/.local/share/irnix/object/method
drwxr-xr-x ~/.local/share/irnix
drwxr-xr-x └── object
.rwxr-xr-x └── method
Теперь метод можно вызвать командой execute (или просто e), передав имя относительно namespace (в данном случае ~/.local/share/irnix):
irnix e object.method
Irnix запустит исполняемый файл через системный вызов execve. Это означает, что метод может быть как и бинарным файлом, так и скриптом, что начинается с "shebang". Если же файл не начинается с #! и не является бинарным, но при этом вы запускаете irnix в bash или zsh, то скрипт попытается выполнится как bash скрипт.
По сути это означает, что у одного объекта все методы могут быть написанными на разных языках программирования: один на Ruby, другой на Python или Bash, а третий вообще бинарный файл, что скомпилировал компилятор Rust. Это позволяет объединить разные методы в одном объекте, вызывать их из одного места, и для каждой задачи выбирать самый удобный язык программирования, при этом следуя философии UNIX ("Пишите программы, которые делают что-то одно и делают это хорошо").
Передача аргументов
Все что идет после разделителя -- Irnix воспринимает как аргументы, которые необходимо передать методу во время вызова. Возьмем в качестве примера method из главы Быстрый старт:
irnix e object.method -- World!
В этом случае Irnix передаст все аргументы после разделителя -- ровно в таком виде, в каком они были переданы.
Вы также можете передавать флаги, например:
irnix e object.method -- --flag value -f value arg1 arg2 --flag2=value
Именования методов
Названия методов (файлов) не могут содержать .. Они могут начинаться с чисел и содержать _, - или содержать пробелы.
Также файлы не должны иметь расширения (например .sh или .py). Это связано с тем, как Irnix парсит вызов метода. Если попытаться вызвать такой метод: object.method.py, то Irnix будет считать, что method это объект внутри объекта object, что имеет метод py. Irnix просто не найдет такого объекта и выведет сообщение об ошибке. Но это можно обойти, просто создав ссылку на файл. Подробнее про то, как можно использовать ссылки в связке с Irnix будет рассказано в главе "Работа с ссылками"
Детали вызова
Во время вызова Irnix не анализирует весь объект целиком, а только вызываемый метод. Например в случае вызова object.subobject.method Irnix будет работать только со следующими файлами (в данном примере со стандартным namespace, так как фактический путь к методу зависит от того, какой namespace указан в переменных окружения или в флагах):
~/.local/share/irnix/object/subobject/.self(если существует)~/.local/share/irnix/object/subobject/method
Это означает, что Irnix делает проверки только в рамках одного вызова.
Контракты и интерфейсы
Контракты и интерфейсы — это те механизмы, которые превращают Irnix не просто в еще один способ запускать скрипты, а в целую строгую и гибкую систему.
- Контракты — это способ строго задать сигнатуру метода.
- Интерфейсы — абсолютная копия ООП интерфейсов. Они позволяют описать некий абстрактный объект с методами, а также легко подменять конкретную реализацию.
Контракты (contracts)
Контракты помогают описать сигнатуру метода, в которую входит:
- Имя метода.
- Взаимодействие с
stdinиstdout. - Описание опциональных и обязательных аргументов.
- Описание опциональных и обязательных флагов.
Контракты помогают обеспечить правильный и ожидаемый вызов. Если при существовании контракта его нарушить, то Irnix просто не вызовет метод и выведет сообщение об ошибке.
Все контракты описываются в файле .self прямо в директории объекта. Такое именование связано с тем, что название методов не может содержать ., о чем мы говорили в главе "Методы".
Рассмотрим пример из главы "Быстрый старт" и добавим новый файл .self
~/.local/share/irnix
└── object
├── .self
└── method
С содержимым:
method: message! stdout!
В данном случае method — это название метода, message это просто название для аргумента, которое может быть любым, а ! обозначает то, что аргумент обязателен. stdout! означает, что при выполнении в любом случае ожидается вывод в stdout.
После того, как мы добавили контракт, Irnix не позволит нам вызвать его неправильно. Если попробовать передать stdin:
echo text | irnix e object.method
То Irnix не запустит метод и выведет ошибку (Контракт "method" не подразумевает функциональность для stdin, но stdin был передан.):
The contract "method" does not imply functionality for stdin, but stdin was passed.
Если не передавать аргументов:
irnix e object.method
(Предоставлено меньше аргументов, чем требуется по контракту. Контракт требует 1 обязательный аргумент и 0 дополнительных.)
The arguments provided are fewer than required by the contract. The contract requires 1 arguments and 0 optional ones.
Если передать слишком много аргументов:
irnix e object.method -- message message
(Слишком много аргументов. Контракт требует 1 аргумент и 0 дополнительных.)
Too many arguments. The contract requires 1 arguments and 0 optional ones.
Вы можете использовать любое количество контрактов в файлах .self.
method: arg! arg! arg?
method: arg!
method:
Таким образом Irnix защищает метод от неправильного вызова, что уменьшает вероятность появления багов.
Компоненты сигнатуры
- Название метода
stdin- Обязательные аргументы
- Опциональные аргументы
- Обязательные флаги
- Опциональные флаги
stdout
Название метода
Для описания сигнатуры необходимо указать название метода, что должно совпадать с фактическим названием метода. Например, если мы хотим описать сигнатуру для метода start, то контракт должен начинаться с start::
start:
Важно, чтобы после названия метода сразу был символ : без пробела.
Stdin
У stdin может быть три состояния:
- Не указано: означает, что метод не принимает
stdin, что не пустойstdinможет приводить к ошибке, или что функционал не предусмотрен иstdinбудет проигнорирован. Если при этом передатьstdin, то Irnix не запустит метод и вернет ошибку. stdin!: означает, что метод для работы требует не пустойstdin. Если во время вызова не передать что-то вstdin, то Irnix выведет ошибку.stdin?: означает, чтоstdinопционален, и может быть пустым или непустым.
Например, возьмем в качестве примера объект logger с методом log, который ожидает на вход stdin:
log: stdin!
echo message | irnix e logger.log
Stdout
У stdout может быть три состояния:
- Не указано: означает, что метод никогда ничего не выводит. Если при этом использовать его в pipeline, то Irnix выведет ошибку о том, что вы пытаетесь перенаправить вывод из метода, который ничего не выводит.
stdout!: означает, что метод всегда что-то выводит.stdout?: означает, что метод иногда может выводить что-то, а иногда нет.
Например, возьмем в качестве примера объект say с методом hello, что просто выводит строку Hello!:
hello: stdout!
irnix e say.hello
Аргументы
Вы можете указать аргументы в любом порядке. Важно лишь количество обязательных и опциональных аргументов. Название аргумента также не имеет значения, и Irnix не обращает на них внимания. Поэтому вы можете придумать любое название для аргумента, чтобы улучшить читаемость контракта.
arg!— обязательный аргумент.arg?— опциональный аргумент.
При проверке аргументов контракта во время вызова Irnix обращает внимание только на количество аргументов. Например если вы добавили в контракт 3 обязательных и еще два опциональных аргумента, то Irnix будет считать валидным вызов с количеством от трех включительно, до пяти включительно аргументов. Если указать всего два, или например шесть, то Irnix выведет сообщение об ошибке и не запустит метод.
Возьмем пример из Быстрый старт и опишем для него контракт:
method: message! stdout!
Это некий метод с названием method, который требует один обязательный аргумент, и всегда что-то выводит. При вызове метода, Irnix проверит контракт и убедится, что вызов верный. А если мы будем использовать этот метод в pipeline, то Irnix проверит, гарантирован ли вывод:
irnix e object.method -- World! | logm
Флаги
Флаги как и аргументы могут быть как и обязательными так и опциональными. Irnix воспринимает флагами в контрактах все, что начинается с -. Например:
method: arg! --flag! -s? stdout!
В данном примере --flag обязателен, а -s опционален.
Значения флагов
Вы также можете указать, должен ли флаг содержать значение, путем добавления = перед знаком ! или ?:
method: arg! --flag=! -f?
В таком случае Irnix будет проверять, что после флага есть аргумент, который будет восприниматься как значение флага. При этом вы можете также указывать значения используя =, например --flag=value:
irnix e object.method -- -f --flag value arg
irnix e object.method -- -f --flag=value arg
Вы также можете использовать вспомогательные символы для того, чтобы сделать контракт более читаемым, например (, ), ,, ->.
Следующие примеры абсолютно эквивалентны:
method_name: stdin! -> (arg1!, arg2?, -f?, --flag=?) -> stdout!
method_name: stdin! -> arg1!, arg2?, -f?, --flag=? -> stdout!
method_name: stdin! -> arg1! arg2? -f? --flag=? -> stdout!
method_name: stdin! arg1! arg2? -f? --flag=? stdout!
В Irnix работает перезапись контрактов. Это означает, что если вы для одного и того же метода напишете много контрактов, то использоваться будет последний указанный. В данном случае будет использоваться контракт method_name: stdin! arg1! arg2? -f? --flag? stdout!, а остальные будут проигнорированы.
Коды ошибок (экспериментальная функция)
Вы также можете указать коды ошибок в контракте, просто написав числа, например в таком виде 2 42 50, в таком 2, 42, 50 или таком [2, 42, 50]. Но на данный момент это экспериментальная функция, и в будущих версиях поддержка кодов ошибок может быть прекращена.
На данный момент нет никаких проверок для кодов ошибок, а при сравнении контрактов интерфейсов и объектов множества кодов ошибок сравниваются напрямую, а также сравнивается порядок. Поэтому пока использование кодов ошибок не рекомендуется, но все же такая возможность есть.
Интерфейсы (interfaces)
Интерфейсы — это мощный механизм, позволяющий абстрактно описать объект и легко подменять конкретные реализации.
Irnix считает интерфейсом любой объект, имя которого начинается с __ и заканчивается символами __, например __interface_object__. Для таких объектов действует особое правило: директория такого объекта должна содержать только два файла: контракт в файле .self, а также ссылка на конкретную реализацию.
Например, давайте создадим некий __logger__, который имеет метод log, что принимает stdin, форматирует сообщение и всегда выводит его:
~/.local/share/irnix
└── __logger__
└── .self
Содержимое .self:
log: stdin! stdout!
Если попытаться сейчас вызвать метод, то Irnix выведет сообщение об ошибке, говорящее о том, что в директории интерфейса должно быть ровно два файла (.self и ссылка на конкретную реализацию).
Давайте создадим конкретную реализацию и назовем ее logm:
~/.local/share/irnix
├── __logger__
│ └── .self
└── logm
├── .self
├── log_level -> /bin/logm
└── log -> /bin/logm
А в .self добавим уже два метода:
log: stdin! stdout!
log_level: stdin! level! stdout!
log принимает stdin, форматирует его и всегда выводит что-то в stdout. А метод log_level работает также как и log, но также принимает один аргумент, а именно уровень.
Важно отметить, что в реализации logm все методы это ссылки на один и тот же бинарный файл logm, который просто форматирует сообщение и выводит его. Более подробно о том, как можно использовать ссылки в Irnix и добиваться высокой гибкости мы рассмотрим в главе "Работа с ссылками".
Теперь просто создадим жесткую ссылку на директорию logm в директории интерфейса:
~/.local/share/irnix
├── __logger__
│ ├── .self
│ └── logm -> ../logm
└── logm
├── .self
├── log_level -> /bin/logm
└── log -> /bin/logm
После чего метод можно спокойно вызывать как метод на обычном объекте:
echo message | irnix e __logger__.log
Во время запуска Irnix сравнивает оба контракта в __logger__ и в реализации logm для метода log, и если они совпадают, значит реализация подходит. Далее все как обычно, Irnix проверяет вызов и вызывает конкретную реализацию метода log.
Вы также можете не создавать ссылок, а создать реализацию объекта прямо в директории интерфейса:
~/.local/share/irnix
└── __logger__
├── .self
└── logm
├── .self
├── log_level -> /bin/logm
└── log -> /bin/logm
Но тогда вы теряете в гибкости, ведь реализацию будет уже не так просто заменить на другую. Ведь основной смысл использования таких интерфейсов заключается в том, что вы можете легко подменить реализацию, и при этом не меняя то, как команда будет вызвана.
Если при этом попытаться вызвать метод __logger__.log_level, то Irnix выведет следующее сообщение:
(Вызываемый метод "log_level" не указан в контракте интерфейса. Однако он указан в контракте объекта: "/home/illia/.local/share/irnix/logm")
The called method "log_level" is not specified in the interface contract. However, it is specified in the objects contract: "/home/illia/.local/share/irnix/logm"
Irnix понимает, что на самом деле такой метод существует, и он может его вызвать (и это было бы чем-то вроде приведения типов), но этого не происходит и это сделано намерено. Такое поведение позволяет избежать багов, ведь если поменять реализацию на новую, ее методы могут не полностью совпадать со старой реализацией.
Строгая классификация
В Irnix есть строгая классификация того, что считается методом, объектом и namespace, а что нет:
- Все что содержит точку в названии не считается объектом или методом (файлы
.selfхоть и считаются контрактами, но это и не метод и не объект). - namespace не считается объектом, поэтому методы расположенные в namespace невозможно запустить.
Первое ограничение помогает создавать директории и файлы, которые невозможно запустить напрямую через Irnix. Это могут быть некие объекты, которые небезопасно запускать без соблюдения контракта, который будет проверен во время запуска метода. Для того, чтобы создать такой объект, достаточно просто добавить в название точку, например .config или important_script.sh.
Оба эти ограничения связаны с темой будущего развития Irnix, а также с тем, что используя ссылки их можно легко обойти, при этом не нарушая концепцию объектов, методов и namespace. Более подробно об этом рассказано в главе "Работа с ссылками". Это может измениться в будущем, но пока Irnix старается сохранять обратную совместимость путем таких строгих классификаций.
Работа с ссылками
Мы уже видели примеры того, как можно использовать ссылки в связке с Irnix. Сейчас мы более подробно и глубоко разберем тему использования ссылок.
Ссылки помогают переиспользовать код, а также переименовывать сущности.
Например, мы можем представить, что у нас есть некая библиотека объектов Irnix, которая представляет из себя обычную директорию ~/library со скриптами и контрактами. Предположим она выглядит примерно так:
~/library
├── method1
├── method2
├── method3
├── ...
└── methodn
Мы можем создать ссылку на эту директорию в namespace, а также назвать ее как угодно, тем самым выбрав название для нашего объекта:
ln -s ~/library object
~/.local/share/irnix
└── object -> ~/library
После чего мы можем использовать методы из директории ~/library:
irnix e object.method2 -- 'Hello World!'
Таким образом можно эффективно переиспользовать код. Вот еще одни пример: мы можем копировать тело метода путем создания ссылок:
ln method method2
ln -s method method3
~/.local/share/irnix
└── object
├── method
├── method2 -> method
└── method3 -> method
И такие методы можно использовать как обычные, и Irnix вызовет один и тот же файл method:
irnix e object.method
irnix e object.method2
irnix e object.method3
В главе Строгая классификация мы разобрали, какие сущности не считаются объектами или методами. Сейчас мы рассмотрим ряд примеров, которые помогают явно создать из них методы и объекты.
Возьмем в качестве примера некий файл script.sh, что располагается внутри объекта object:
~/.local/share/irnix
└── object
└── script.sh
Такой скрипт напрямую вызвать не выйдет, так как script.sh не считается методом. Но мы можем явно сделать метод, что ссылается на script.sh:
cd ~/.local/share/irnix/object
ln -s script.sh method
~/.local/share/irnix
└── object
├── method -> script.sh
└── script.sh
После для такого метода можно составить контракт а также вызывать, при условии что сам файл script.sh имеет право на исполнение:
irnix e object.method
Возьмем в качестве второго примера стандартный namespace ~/.local/share/irnix и превратим его в объект, путем создания ссылки:
ln -s . self
~/.local/share/irnix
└── self -> .
Теперь self имитирует объект, так как является директорией и при этом не содержит в названии точек. Если добавить метод method, то его можно будет вызвать:
irnix e self.method
~/.local/share/irnix
├── method
└── self -> .
Таким образом мы превращаем namespace в объект self и не оставляем метод method сиротой без объекта. Если попытаться просто вызвать метод (irnix e method), то Irnix выведет сообщение об ошибке.
Команда methods
Эта команда выводит список доступных методов, учитывая ссылки.
irnix e methods
self.logm.log
self.__logger__.logm.log
self.waybar.reload
self.headphones.disconnect
self.headphones.connect
self.build
logm.log
__logger__.log
waybar.reload
headphones.disconnect
headphones.connect
В этом случае self это ссылка на текущий namespace.
Эта команда позволяет создавать очень мощные вещи. Например, давайте добавим метод build прямо в namespace, а также ссылку self на этот же namespace:
cd ~/.local/share/irnix
ln -s . self
~/.local/share/irnix
├── build
└── self -> .
Также создадим директорию .build в namespace:
mkdir ~/.local/share/irnix/.build
Содержимое build:
#!/bin/bash
DIR=$HOME/.local/share/irnix/.build/
for method in `irnix methods`; do
printf "#!/bin/bash\nirnix e $method -- \$*" > "$DIR$method"
chmod +x "$DIR$method"
done
Как видите, наш метод build создает скрипты вида:
#!/bin/bash
irnix e object.method -- $*
..с названиями, что соответствуют методам, а также выдает права на исполнение. Каждый скрипт вызывает внутри себя Irnix, передавая аргументы. Если добавить этот путь в PATH (например в файле .bashrc или .zshenv):
export PATH="$HOME/.local/share/irnix/.build:$PATH"
...и запустить метод build:
irnix e self.build
...то мы сможем вызывать методы прямо в терминале, без того чтобы писать каждый раз irnix e!
Мы также можем добавить контракт для метода build, для безопасности:
~/.local/share/irnix
├── .self
├── build
└── self -> .
.self:
build:
В итоге мы получаем возможность легко вызывать наши методы, что расположены в .build:
~/.local/share/irnix
├── .build
│ ├── __logger__.log
│ ├── headphones.connect
│ ├── headphones.disconnect
│ ├── logm.log
│ ├── self.__logger__.logm.log
│ ├── self.build
│ ├── self.headphones.connect
│ ├── self.headphones.disconnect
│ ├── self.logm.log
│ ├── self.waybar.reload
│ └── waybar.reload
├── .self
├── __logger__
│ ├── .self
│ └── logm -> ../logm
├── build
├── headphones
│ ├── .self
│ ├── connect
│ └── disconnect
├── logm
│ ├── .self
│ └── log -> /bin/logm
├── self -> .
└── waybar
├── .self
└── reload
Мы также можем доработать наш скрипт, и очищать папку перед созданием скриптов:
#!/bin/bash
DIR=$HOME/.local/share/irnix/.build/
rm $DIR*
for method in `irnix methods`; do
printf "#!/bin/bash\nirnix e $method -- \$*" > "$DIR$method"
chmod +x "$DIR$method"
done
После этого всего методы можно будет использовать, например так:
echo message | __logger__.log warn
Такой способ делает вызов короче, при этом неявно используя Irnix под капотом.
Логгер
В этом примере мы создадим логгер внутри нашей системы, что форматирует сообщения, добавляя к нему время и уровень
Давайте создадим интерфейс __logger__ с простым методом log:
cd ~/.local/share/irnix
mkdir __logger__
touch __logger__/.self
~/.local/share/irnix
├── .self
└── __logger__
└── .self
.self:
log: stdin! level? stdout!
Метод log читает сообщение из stdin, форматирует его и выводит отформатированное сообщение.
stdin!добавляет проверку, чтобыstdinбы не пустым.level?необязательный аргумент, позволяющий указать уровень.stdout!означает, что метод обязательно в любом случае что-то выводит.
Нашим следующим шагом будет добавление конкретной реализации, которой будет являться logm. Для этого создадим директорию logm в namespace и добавим метод log:
cd ~/.local/share/irnix
mkdir logm
cd logm
ln /bin/logm log
cat ../__logger__/.self > .self
~/.local/share/irnix
├── __logger__
│ ├── .self
│ └── logm -> ../logm
└── logm
├── .self
└── log -> /bin/logm
В данном случае метод log это ссылка на бинарный файл logm, который форматирует сообщение.
echo "message" | irnix e __logger__.log
Sun, 2 Nov 2025 19:58:33 +0000 INFO message
Теперь мы можем вызывать логгер в других скриптах (например в телах других методов) и при этом иметь возможность заменить логгер на другой, при этом не изменяя вызовы.
Например мы можем изменить реализацию log на простой bash скрипт:
#!/bin/bash
echo "$(date +%s): $(</dev/stdin)"