Created
July 10, 2020 11:47
-
-
Save dakkar/7c492052eb114eccfe39d8628af622f9 to your computer and use it in GitHub Desktop.
I seem to have build a perl-like OO system in bash
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
set -e | |
function class() { | |
current_class="$1" | |
} | |
declare -A _inheritance | |
function extends() { | |
_inheritance[$current_class]="$*" | |
} | |
# walk the inheritance tree, depth-first, returning all the found | |
# methods' full-names | |
function _find_methods() { | |
local method="$1";shift | |
local -a found | |
local class | |
for class in "$@"; do | |
local full_name="${class}.${method}" | |
if declare -F "$full_name" >/dev/null; then | |
found+=( "$full_name" ) | |
fi | |
local supers="${_inheritance[$class]}" | |
if [[ -n "$supers" ]]; then | |
found+=( $(_find_methods "$method" $supers) ) | |
fi | |
done | |
echo "${found[@]}" | |
} | |
function _id_from_self() { | |
local -a parts=( $1 ) | |
echo "${parts[2]}" | |
} | |
# objects are values, so they're stored in variables! | |
# | |
# the shape of that value is… a call to _invoke curry-ed on class and | |
# id, so that `$my_obj the-method 1 2` is equivalent to `_invoke | |
# TheClass $the_obj_id the-method 1 2` | |
function _self_from_class_id() { | |
local class="$1" | |
local self_id="$2" | |
echo "_invoke $class $self_id" | |
} | |
function _storage_var_for_object() { | |
local self_id="$(_id_from_self "$1")" | |
local var="_obj_storage_${self_id}" | |
echo "$var" | |
} | |
function _meta_set() { | |
local self="$1";shift | |
local field="$1";shift | |
local -n storage="$(_storage_var_for_object "$self")" | |
storage[$field]="$*" | |
} | |
function _meta_get() { | |
local self="$1";shift | |
local field="$1";shift | |
local -n storage="$(_storage_var_for_object "$self")" | |
echo "${storage[$field]}" | |
} | |
function has() { | |
local name="$1" | |
local getter="${current_class}.get_${name}" | |
local setter="${current_class}.set_${name}" | |
eval " | |
function $getter() { | |
local self=\"\$1\";shift | |
_meta_get \"\$self\" $name | |
} | |
function $setter() { | |
local self=\"\$1\";shift | |
_meta_set \"\$self\" $name \"\$@\" | |
} | |
" | |
} | |
# the method dispatcher | |
function _invoke() { | |
local class="$1";shift | |
local self_id="$1";shift | |
local method="$1";shift | |
local self="$(_self_from_class_id $class $self_id)" | |
local -a method_chain=( $(_find_methods "$method" "$class") ) | |
if [[ ${#method_chain[*]} -eq 0 ]]; then | |
>&2 echo "can't find method '$method' for class '$class'" | |
exit 255 | |
fi | |
# this builds the `next-method` function for calling the next | |
# method in the inheritance tree | |
local next_body='unset -f next-method' | |
local idx | |
for idx in $(eval "echo {${#method_chain[*]}..1..-1}"); do | |
local method_name=${method_chain[$idx]} | |
next_body="next-method() { $next_body; $method_name \"$self\" \"\$@\"; }" | |
done | |
# at this point next_body looks like: | |
# | |
# next-method() { | |
# next-method() { | |
# unset -f next-method | |
# UltraClass.the-method "$self" "$@" | |
# } | |
# SuperClass.the-method "$self" "$@" | |
# } | |
eval "$next_body" | |
local calling="${method_chain[0]}" | |
$calling "$self" "$@" | |
} | |
object_id=0 | |
new() { | |
local class="$1" | |
shift | |
object_id=$[ $object_id + 1 ] | |
local self="$(_self_from_class_id $class $object_id)" | |
declare -gA "$(_storage_var_for_object "$self")" | |
$self BUILD "$@" | |
echo "$self" | |
} | |
# hack: we can't do `foo=$(new Class)` because that would run `new` in | |
# a sub-shell, and the allocations would be invisible to the main | |
# shell | |
function assign() { | |
local -n dest="$1";shift | |
local file="$(tempfile)" || exit 255 | |
"$@" >"$file" | |
dest="$(< "$file")" | |
rm "$file" | |
} | |
####### | |
# the tests | |
class A | |
has x;has y | |
A.BUILD() { | |
local self="$1";shift | |
$self set_x "$1" | |
$self set_y "$2" | |
} | |
A.say() { | |
local self="$1";shift | |
echo "A=($($self get_x) - $($self get_y))" | |
} | |
class B | |
extends A | |
has z | |
B.BUILD() { | |
local self="$1";shift | |
next-method "$@" | |
$self set_z "$3" | |
} | |
B.say() { | |
local self="$1";shift | |
echo "B=($($self get_z))" | |
} | |
assign one new A 1 2 | |
assign two new A 3 4 | |
assign three new B 5 6 7 | |
if [[ "$($one say)" == 'A=(1 - 2)' ]]; then | |
echo 'ok 1' | |
else | |
echo 'not ok 1' | |
fi | |
if [[ "$($two say)" == 'A=(3 - 4)' ]]; then | |
echo 'ok 2' | |
else | |
echo 'not ok 2' | |
fi | |
assign foo $three get_z | |
if [[ "$foo" == '7' ]]; then | |
echo 'ok 3' | |
else | |
echo 'not ok 3' | |
fi | |
if [[ "$($three get_x)" == '5' ]]; then | |
echo 'ok 4' | |
else | |
echo 'not ok 4' | |
fi | |
echo '1..4' |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment