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 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
.
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"
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 (\$…
).
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"
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 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!
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.