Kodumaro :: Implementing OOP on Fish (just for fun)

Released on March 3rd, 2019
The shadows behind the code.

Fish is a great command-line shell targeted on programmers. It has some cool programming-like features hereat.

But, despite it’s not a merely batch prompt, it’s still only a shell, not a programming language, and lacks some basic resources, like closures and object-oriented programming.

We can emulate those behaviours with some smart workarounds.

For OOP, we can mimic the F♯ approach: the class is in fact a function, which body is, at the same time, the class’ and the constructor’s one.

Thus the methods are defined inside the constructor body.

But we have another problem yet: Fish has no closures, and the local variables are collected as soon as the function ends.

The way to work around it is creating global variables (and functions) with the name bound to the instance identity.

On Fish, dunder-started functions and variables are weakly private, similar to Python private methods. So we can prefix the instance attributes (and methods) with dunder (__) and the instance id.

We’re gonna use the uuidgen tool. to generate the id.

The following example is the classic Person class. It has a name and a birth.

The constructor

The Person function should create the instance reference and pass the parameters to it.

The reference should be a dunder class name followed by the instance id. The sed call is for removing the dashes from global id (not supported by environment variables). The arguments can be --name, --surname, and --birth. Let’s make it work:

function Person -d'Person class'
  set -l id (uuidgen)
  set -l self __Person_(echo $id | sed 's!-!!g')
  argparse n/name= s/surname= b/birth= -- $argv

OK, now the parameters can be retrieved from _flag_name, _flag_surname, and _flag_birth.

Accessor methods

We can create three getters (read accessor methods) called id, fullname, and birth. For that we use alias:

  alias $self.id="echo -n $id"
  alias $self.fullname="echo -n $_flag_name $_flag_surname"
  alias $self.birth="echo -n $_flag_birth"

Instance methods

For sample purpose, we can create a method for serialisation. As Fish doesn’t support closures, all private info must use the Fish approach for weakly private data, and the method must be built by eval tool:

  eval "function $self.string
    printf '%s (%s): %s' ($self.fullname) ($self.id) ($self.birth)
  end"

The $self variable is expanded on the method creation. Every other variable (which is not right expanded) must be protected by a backslash (\$…).

Mutable attributes

Again Fish doesn’t support closures, thereat it’s necessary to use global variables. For example, a person can have metadata:

  set -g "$self"_metadata

Its access method is a bit more complicated. As explained above, it must deal with non-expanded variables:

  eval "function $self.metadata -a metadata
    test -n \"\$metadata\"
    and set $self""_metadata \"\$metadata\"
    echo -n \$$self""_metadata
  end"

Returning the instance

At the constructor block’s end, it’s necessary to return the instance global id. The return statement returns the status code, so it’s not what we want.

To return string values, we need to echo them:

  echo -n $self
end

The destructor

The Fish garbage collector cannot clean up global variables, not even weakly private when their references die. So we need to do it explicitly. Therefore we created a destructor block.

For the destructor, outside the class, we create a delete function. Inside it, we have to erase all functions related to the supplied instance ids – and the variables too, if we got some.

Let’s iterate over the supplied instance global ids and search for their methods:

function delete -d'garbage collector for class instances'
  for instance in $argv
    for funcname in (functions --all)
      string match "$instance.*" -- "$funcname" >/dev/null
      and functions --erase "$funcname"
    end

It also must erase the attributes stored in environment variables:

    set | awk '{ print $1; }' | while read envvar
      string match "$instance\_*" -- "$envvar" >/dev/null
      and set --erase "$envvar"
    end
  end
end

This is the end!

Using the class

Now an example of using the class:

begin
  set -l person_a (Person -nJohn  -sDoe      -b1970-01-01)
  set -l person_b (Person -nPedro -sde\ Lara -b1925-02-25)
  $person_a.metadata nobody >/dev/null
  $person_b.metadata showman >/dev/null
  printf '%64s, %s\n' ($person_a.string) ($person_a.metadata)
  printf '%64s, %s\n' ($person_b.string) ($person_b.metadata)
  delete $person_a $person_b
end

The output must be something like:

     John Doe (DE93A63C-8F1D-454F-BD01-C9EF0E1683AF): 1970-01-01, nobody
Pedro de Lara (B3169DFE-DD8F-49B1-9601-4E05F2CE40F9): 1925-02-25, showman

See yah!


Also in Medium.

Concept | Shell

DEV Profile 👩‍💻👨‍💻