Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

What Is Irnix

Irnix was inspired by the Object‑Oriented Programming paradigm, therefore it can be described as a system for organizing objects, which turns the file system into objects with methods and contracts.
This allows scripts to be organized, method calls to be validated, a single call point to exist, and object implementations to be easily swapped.

Irnix helps create a strict structure through contract‑checking mechanisms (method signatures), thereby ensuring correct method invocation on objects and helping avoid erroneous behavior in the pipeline.

Quick Start

Create an object directory inside ~/.local/share/irnix and add a file named method there:

~/.local/share/irnix
└── object
    └── method

Contents of the method file:

#!/bin/bash

echo "Hello $1"

Give the file executable permissions:

chmod +x ~/.local/share/irnix/object/method

Invoke the method:

irnix e object.method -- World!

Output:

Hello World!

Concepts

In this chapter we’ll dive deeper into what entities exist in Irnix and what roles they play.

Here is a brief description of each:

  • namespace – a directory from which object and method names are derived.
  • object – a collection of methods and contracts.
  • method – an executable file whose name is the method’s name, and whose contents contain the execution instructions (for example a script in Python, Bash, Ruby, or a plain binary).

Namespaces

A namespace is essentially a directory from which the naming of all other objects starts, and everything inside that directory is interpreted by Irnix as objects, methods, and contracts. By default it is ~/.local/share/irnix, but you can change it by setting the environment variable IRNIX_NAMESPACE or by passing the flag -n (--namespace) directly:

IRNIX_NAMESPACE=dir irnix e object.method -- World!
irnix e -n dir object.method -- World!

Irnix follows this hierarchy:

  1. -n
  2. IRNIX_NAMESPACE
  3. ~/.local/share/irnix

This means that in the following case:

IRNIX_NAMESPACE=dir1 irnix -n dir2 e object.method -- World!

Irnix will choose dir2 as the namespace.

Objects

In Irnix, an object is an entity that contains other objects, methods, and contracts, which we’ll discuss later.

Irnix treats every directory located in a namespace as an object. For example, take the default namespace and create a directory called object inside it:

~/.local/share/irnix
└── object

Now Irnix recognises object as an object that currently has no methods.

Objects can also own other objects. In the file system this simply looks like another directory nested inside one. For instance, create a second object named object2:

~/.local/share/irnix
└── object
    └── object2

Naming Objects

Object (directory) names must not contain .. They may start with numbers and can include _ or -.

Any directory that starts with . in the namespace will not be treated as an object, so such folders are convenient for storing additional information and helper files for methods. These directories may even contain executable files, but Irnix will never invoke them directly because a call would look like: object..secret_object.method, which Irnix simply cannot understand.

Methods

Irnix treats any executable file inside an object as a method. For example, take object from the previous section and add a file named method:

~/.local/share/irnix
└── object
    └── method

With contents:

#!/bin/python

print("Hello World!")

Which must be made executable:

chmod +x ~/.local/share/irnix/object/method
drwxr-xr-x ~/.local/share/irnix
drwxr-xr-x └── object
.rwxr-xr-x     └── method

Now the method can be invoked with the execute command (or simply e) by passing its name relative to the namespace (here ~/.local/share/irnix):

irnix e object.method

Irnix runs the executable via the system call execve. This means that a method can be either a binary or a script that starts with a shebang (#!). If the file does not start with #! and is not a binary, but you run irnix in bash or zsh, the script will attempt to execute as a Bash script.

In essence this allows all methods of a single object to be written in different programming languages: one in Ruby, another in Python or Bash, and a third could even be a Rust‑compiled binary. This lets you group diverse methods within one object, call them from a single place, and choose the most convenient language for each task while following the UNIX philosophy (“Write programs that do one thing well”).

Passing Arguments

Anything that comes after the separator -- Irnix treats as arguments to be passed to the method during invocation. For example, using the method from the Quick Start chapter:

irnix e object.method -- World!

In this case Irnix will pass all arguments after -- exactly as they were provided.

You can also pass flags, for instance:

irnix e object.method -- --flag value -f value arg1 arg2 --flag2=value

Naming Methods

Method names (files) cannot contain .. They may start with numbers and can include _, - or spaces.

Files should also not have extensions (e.g., .sh or .py). This is due to how Irnix parses a method call. If you try to invoke such a method as object.method.py, Irnix will interpret that method is an object inside the object object, which has a method named py. Irnix simply won’t find such an object and will output an error message. This can be circumvented by creating a link to the file instead. More on using links with Irnix will be covered in the chapter "Working with Links".

Call Details

During a call, Irnix does not analyze the entire object; it only processes the invoked method. For example, when calling object.subobject.method, Irnix will operate solely on the following files (in this example using the default namespace, since the actual path to the method depends on which namespace is specified in environment variables or flags):

  • ~/.local/share/irnix/object/subobject/.self (if it exists)
  • ~/.local/share/irnix/object/subobject/method

This means Irnix performs checks only within the scope of a single call.

Contracts and Interfaces

Contracts and interfaces are mechanisms that transform Irnix from merely another way to run scripts into a comprehensive, strict, and flexible system.

  • A contract is a method for strictly defining a method’s signature.
  • An interface is an exact copy of OOP interfaces. It allows you to describe an abstract object with methods and easily swap out the concrete implementation.

Contracts

Contracts help describe the method signature, which includes:

  • The method name.
  • Interaction with stdin and stdout.
  • Description of optional and required arguments.
  • Description of optional and required flags.

Contracts help ensure a correct and expected call. If a contract exists and is violated, Irnix simply will not invoke the method and will output an error message.

All contracts are described in a .self file directly inside the object’s directory. This naming convention is linked to the fact that method names cannot contain .—as we discussed in the chapter "Methods".

Let us consider an example from the chapter "Quick Start" and add a new .self file:

~/.local/share/irnix
└── object
    ├── .self
    └── method

With contents:

method: message! stdout!

In this case method is the name of the method, message is simply a placeholder for an argument that can be any name, and ! indicates that the argument is required. stdout! means that output to stdout is expected in every execution.

After adding the contract, Irnix will prevent us from calling it incorrectly. If we try to pass stdin:

echo text | irnix e object.method

Then Irnix will not run the method and will output an error (the contract “method” does not imply functionality for stdin, but stdin was passed.):

The contract "method" does not imply functionality for stdin, but stdin was passed.

If we do not provide arguments:

irnix e object.method
The arguments provided are fewer than required by the contract. The contract requires 1 arguments and 0 optional ones.

If we pass too many arguments:

irnix e object.method -- message message
Too many arguments. The contract requires 1 arguments and 0 optional ones.

You can use any number of contracts in .self files.

method: arg! arg! arg?
method: arg!
method:

Thus Irnix protects the method from incorrect invocation, reducing the likelihood of bugs.

Signature Components

  • Method name
  • stdin
  • Required arguments
  • Optional arguments
  • Required flags
  • Optional flags
  • stdout

Method Name

To describe a signature you must specify the method name, which should match the actual method name. For example, if we want to describe the signature for the method start, the contract should begin with start::

start:

It is important that immediately after the method name there is a colon : without a space.

Stdin

stdin can have three states:

  • Not specified: means the method does not accept stdin; if non‑empty stdin is provided, it may cause an error or be ignored. If stdin is passed in this case, Irnix will not run the method and will return an error.
  • stdin!: indicates that the method requires non‑empty stdin. If nothing is sent to stdin during invocation, Irnix will output an error.
  • stdin?: means stdin is optional and may be empty or non‑empty.

For example, consider object logger with method log, which expects stdin as input:

log: stdin!
echo message | irnix e logger.log

Stdout

stdout can have three states:

  • Not specified: means the method never outputs anything. If it is used in a pipeline, Irnix will output an error stating that you are attempting to redirect output from a method that does not produce any output.
  • stdout!: indicates that the method always produces some output.
  • stdout?: means the method sometimes produces output and sometimes does not.

For example, consider object say with method hello, which simply outputs the string Hello!:

hello: stdout!
irnix e say.hello

Arguments

You can specify arguments in any order. What matters is only the number of required and optional arguments. The name of an argument also does not matter, and Irnix ignores it. Therefore you may invent any name for an argument to improve the readability of the contract.

  • arg! — a mandatory argument.
  • arg? — an optional argument.

When checking the contract’s arguments at call time, Irnix pays attention only to the count of arguments. For example, if you add three required and two optional arguments to the contract, Irnix will consider a call valid with a number from three (inclusive) up to five (inclusive). If you provide only two, or for instance six, Irnix will output an error message and will not run the method.

Let’s take an example from Quick Start and describe its contract:

method: message! stdout!

This is a method named method that requires one mandatory argument and always outputs something. When calling the method, Irnix checks the contract and ensures the call is correct. If we use this method in a pipeline, Irnix will check whether output is guaranteed:

irnix e object.method -- World! | logm

Flags

Flags, like arguments, can be either required or optional. Irnix treats everything that starts with - as a flag in contracts. For example:

method: arg! --flag! -s? stdout!

In this example --flag is mandatory, and -s is optional.

Flag values

You can also indicate whether a flag should contain a value by adding = before the ! or ? symbol:

method: arg! --flag=! -f?

In that case Irnix will check that after the flag there is an argument, which will be interpreted as the flag’s value. At the same time you can also specify values using =, for example --flag=value:

irnix e object.method -- -f --flag value arg
irnix e object.method -- -f --flag=value arg

You can also use helper symbols to make the contract more readable, such as (, ), ,, ->.

The following examples are absolutely equivalent:

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!

In Irnix contract overriding works. This means that if you write many contracts for the same method, only the last specified one will be used. In this case the contract method_name: stdin! arg1! arg2? -f? --flag? stdout! will be used, and the others will be ignored.

Error codes (experimental feature)

You can also specify error codes in a contract by simply writing numbers, for example in the form 2 42 50, as 2, 42, 50, or [2, 42, 50]. However, at the moment this is an experimental feature, and support for error codes may be discontinued in future versions.

At present there are no checks for error codes, and when comparing interface contracts with object contracts the sets of error codes are compared directly, as well as their order. Therefore using error codes is not recommended at the moment, but such a possibility still exists.

Interfaces

Interfaces are a powerful mechanism that allows an object to be described abstractly and concrete implementations to be swapped in easily.

Irnix considers any object whose name starts with __ and ends with __, e.g. __interface_object__, to be an interface. For such objects there is a special rule: the directory of that object must contain only two files: a contract in a .self file, and a link to a concrete implementation.

For example, let us create a hypothetical __logger__ that has a method log, which takes stdin, formats the message, and always outputs it:

~/.local/share/irnix
└── __logger__
    └── .self

Contents of .self:

log: stdin! stdout!

If you try to call the method now, Irnix will output an error message stating that the interface directory must contain exactly two files (.self and a link to the concrete implementation).

Let us create a concrete implementation and name it logm:

~/.local/share/irnix
├── __logger__
│   └── .self
└── logm
    ├── .self
	├── log_level -> /bin/logm
    └── log -> /bin/logm

And in .self add two methods:

log: stdin! stdout!
log_level: stdin! level! stdout!

log takes stdin, formats it, and always outputs something to stdout. The method log_level works the same as log, but also takes one argument – the level.

It is important to note that in the implementation logm all methods are links to the same binary file logm, which simply formats the message and outputs it. We will discuss in more detail how links can be used in Irnix and how to achieve high flexibility in the chapter "Working with Links".

Now just create a hard link from the logm directory into the interface directory:

~/.local/share/irnix
├── __logger__
│   ├── .self
│   └── logm -> ../logm
└── logm
    ├── .self
	├── log_level -> /bin/logm
    └── log -> /bin/logm

After that the method can be called safely as a method on a regular object:

echo message | irnix e __logger__.log

During startup Irnix compares both contracts in __logger__ and in the implementation logm for the method log, and if they match, it means the implementation is suitable. Then everything proceeds as usual: Irnix verifies the call and invokes the specific implementation of the method log.

You can also avoid creating links and create the object’s implementation directly inside the interface directory:

~/.local/share/irnix
└── __logger__
    ├── .self
	└── logm
	    ├── .self
		├── log_level -> /bin/logm
	    └── log -> /bin/logm

But then you lose flexibility, because the implementation will no longer be as easy to replace with another one. The main purpose of using such interfaces is that you can easily swap out the implementation without changing how the command is invoked.


If at this point you try to call the method __logger__.log_level, Irnix will output the following message:

The called method "log_level" is not specified in the interface contract. However, it is specified in the object's contract: "/home/illia/.local/share/irnix/logm"

Irnix understands that such a method actually exists and could invoke it (and this would be something like a type cast), but this does not happen and it is intentional. This behavior helps avoid bugs, because if you replace the implementation with a new one, its methods may not fully match those of the old implementation.

Strict Classification

In Irnix there is a strict classification of what is considered a method, an object, and a namespace, and what is not:

  • Anything that contains a dot in its name is not treated as an object or a method (files ending with .self are indeed contracts, but they are neither a method nor an object).
  • A namespace is not considered an object, therefore methods located inside a namespace cannot be executed.

The first restriction helps create directories and files that cannot be run directly through Irnix. These can be some objects that are unsafe to execute without observing the contract that will be checked at method startup. To create such an object it is enough simply to add a dot in its name, for example .config or important_script.sh.

Both of these restrictions relate to the future development of Irnix, as well as to the fact that they can be easily bypassed using links without violating the concept of objects, methods, and namespaces. More details are explained in the chapter "Working with Links". This may change in the future, but for now Irnix strives to maintain backward compatibility through such strict classifications.

Working with Links

We have already seen examples of how links can be used in conjunction with Irnix. Now we will examine the topic of link usage more thoroughly and deeply.

Links help reuse code, as well as rename entities.

For instance, we might imagine that we have some library of Irnix objects, which is simply a regular directory ~/library containing scripts and contracts. Suppose it looks roughly like this:

~/library
├── method1
├── method2
├── method3
├── ...
└── methodn

We can create a link to this directory in a namespace, and we may name it whatever we wish, thereby selecting the name for our object:

ln -s ~/library object
~/.local/share/irnix
└── object -> ~/library

Afterwards we can use methods from the ~/library directory:

irnix e object.method2 -- 'Hello World!'

Thus code can be reused efficiently. Here is another example: we can copy a method body by creating links:

ln method method2
ln -s method method3
~/.local/share/irnix
└── object
    ├── method
    ├── method2 -> method
    └── method3 -> method

Such methods can be used like ordinary ones, and Irnix will invoke the same file method:

irnix e object.method
irnix e object.method2
irnix e object.method3

In the chapter Strict Classification we covered which entities are not considered objects or methods. Now we’ll look at a number of examples that explicitly turn them into methods and objects.

Take as an example some file script.sh located inside the object object:

~/.local/share/irnix
└── object
    └── script.sh

Such a script cannot be called directly, because script.sh is not considered a method. But we can explicitly make it a method that points to script.sh:

cd ~/.local/share/irnix/object
ln -s script.sh method
~/.local/share/irnix
└── object
	├── method -> script.sh
    └── script.sh

Afterwards a contract can be written for such a method and it can be called, provided that the file script.sh itself has execute permission:

irnix e object.method

Take as a second example the standard namespace ~/.local/share/irnix and turn it into an object by creating a link:

ln -s . self
~/.local/share/irnix
└── self -> .

Now self mimics an object, because it is a directory and does not contain dots in its name. If we add a method method, it can be invoked:

irnix e self.method
~/.local/share/irnix
├── method
└── self -> .

Thus we turn the namespace into an object self and do not leave the method method orphaned without an object. If you try to call just the method (irnix e method), Irnix will output an error message.

Command methods

This command lists available methods, taking links into account.

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

In this case self is a symlink the current namespace.


This command allows you to create very powerful things. For example, let’s add the method build directly into the namespace, and also a link self to that same namespace:

cd ~/.local/share/irnix
ln -s . self
~/.local/share/irnix
├── build
└── self -> .

Also create a .build directory in the namespace:

mkdir ~/.local/share/irnix/.build

Contents of 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

As you can see, our method build creates scripts of the form:

#!/bin/bash

irnix e object.method -- $*

…with names that correspond to methods, and also gives them execute permissions. Each script internally calls Irnix, passing arguments. If you add this path to PATH (e.g., in .bashrc or .zshenv):

export PATH="$HOME/.local/share/irnix/.build:$PATH"

…and run the method build:

irnix e self.build

…you will be able to call methods directly from the terminal, without having to type irnix e each time!

We can also add a contract for the method build, for safety:

~/.local/share/irnix
├── .self
├── build
└── self -> .

.self:

build:

In the end we get the ability to easily invoke our methods that are located in .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

We can also refine our script to clear the folder before creating scripts:

#!/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

After that, all methods can be used, for example:

echo message | __logger__.log warn

This approach shortens the call, while implicitly using Irnix under the hood.

Logger

In this example we will create a logger inside our system that formats messages by adding to them the time and level.

Let us create an interface __logger__ with a simple method log:

cd ~/.local/share/irnix
mkdir __logger__
touch __logger__/.self
~/.local/share/irnix
├── .self
└── __logger__
    └── .self

.self:

log: stdin! level? stdout!

The method log reads a message from stdin, formats it, and outputs the formatted message.

  • stdin! adds a check so that stdin is not empty.
  • level? is an optional argument allowing one to specify the level.
  • stdout! means that the method must output something in any case.

Our next step will be adding a concrete implementation, which will be logm. To do this we create a directory logm in the namespace and add the method 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

In this case the method log is a link to the binary file logm, which formats the message.

echo "message" | irnix e __logger__.log
Sun, 2 Nov 2025 19:58:33 +0000 INFO message

Now we can call the logger in other scripts (for example, in the bodies of other methods) and at the same time have the ability to replace the logger with another one, without changing the calls.

For instance, we can change the implementation of log to a simple Bash script:

#!/bin/bash

echo "$(date +%s): $(</dev/stdin)"