Wait, what?
Inspired by this awesome article.
According to wikipedia, OOP is a programming paradigm or technique based on the concept of "objects". The object structure contain data and behaviour.
Data is the object's state, which should be isolated and must be private. Behaviour is the set of messages that allow internal state (data) management.
Thus, objects can communicate to each other by sending messages.
In short, OOP can be implemented by applying some techniques such as dynamic dispatch, closures or late binding.
Behaviour (messages, functions, methods) can be declared in a "template structure" but must be executed later at run time. For that reason, it's very important that the programming language is able to support lexical scope. Otherwise, the behaviour can't be implemented then there's no OOP.
Bash is a UNIX shell and a type of shell script which covers the traits of a scripting language. As such, it holds simple structures that follow a "top-down" execution line.
Because of its simplicity, this kind of language has no lexical scope hence is not possible to implement OOP in a proper way.
However, we could make use of some techniques that would allow to simulate a very simple OOP use-case.
In this very Gist, let's demonstrate how to implement OOP in Bash.
First of all, we're going to create an Object function that will simulate the template Object.
Object() {
}
Pretty simple. Now, we can call this function with some arguments. Let's say we want to create our objects using the following syntax:
Object account leandroAccount name=Leandro balance=500
That would be great, wouldn't it? Explaining the arguments:
$1
: the type of object,account
$2
: the variable holding the object$3
: a key-pair structure that should be parsed then saved in the object's internal state
Okay, time to implement the Object
function. In bash, there's no "internal state" of functions. Indeed, there's a local scope but it can't be used across different "objects" we want to create. Remember that Leandro is not the only account in this world, right? What should we do then?
Use global state. It's weird, I know, but it's the only way to define an object's state in Bash. But we can workaround by prepending the object name at every attribute, for instance:
leandroAccount_name
leandroAccount_balance
carlosAccount_name
And so on...
Object () {
# e.g account
kind=$1
# e.g leandroAccount
self=$2
shift
shift
# iterates over the remaining args
for arg in "$@"; do
# e.g name=Leandro becomes ARG_KEY=name ARG_VALUE=Leandro
read ARG_KEY ARG_VALUE <<< $(echo "$arg" | sed -E "s/(\w+)=(.*?)/\1 \2/")
if [[ ! -z "$ARG_KEY" ]] && [[ ! -z "$ARG_VALUE" ]]; then
# declare the object's state!!!!
# e.g export leandroAccount_balance=100
export ${self}_$ARG_KEY="$ARG_VALUE"
fi
done
}
Super nice! Now let's check it:
Object account leandroAccount name=Leandro balance=500
echo $leandroAccount_name # prints Leandro
echo $leandroAccount_balance # prints 500
Object account carlosAccount name=Carlitos balance=800
echo $carlosAccount_name # prints Carlitos
echo $carlosAccount_balance # prints 800
So far, so good, isn't it?
Okay, but where are the behaviour? Of course, we can implement behaviour using Bash functions. Suppose we want to call functions as follows:
$leandroAccount_fn_display
Hello, Leandro. Your balance is 100
However, calling the function solely relying on that Bash is able to "remember" the object scope, is not possible in Bash because, as we already said, Bash has no lexical scope support.
But we can implement a workaround. What if we pass the scope (object) as an argument to the function? Let's create the function first:
display() {
self=$1
name=${self}_name
balance=${self}_balance
echo "Hello, ${!name}. Your balance is ${!balance}"
}
And now, we can create the object using the function as argument:
## Note that we're using a different syntax for functions, by prepending a "fn_", otherwise it would conflict with attributes
Object account leandroAccount name=Leandro balance=500 fn_display
Uh oh, we have to parse the fn
argument in the Object
function, just adding the elif
clause:
...
## Parse argument when matching functions
## e.g fn_display -> FUNC=display
read FUNC <<< $(echo "$arg" | sed -E "s/fn_(\w+)$/\1/")
...
elif [[ ! -z "$FUNC" ]] && [[ "$FUNC" != "$self" ]]; then
export ${kind}_fn_$FUNC=$FUNC
fi
...
At this time we are all set, we can already call the function passing the object to it:
Object account leandroAccount name=Leandro balance=500 fn_display
Object account carlosAccount name=Carlitos balance=800 fn_display
$account_fn_display leandroAccount
$account_fn_display carlosAccount
#### Result ####
Hello, Leandro. Your balance is 100
Hello, Carlitos. Your balance is 800
Let's implement a deposit function? Super easy now:
deposit() {
self=$1
currentBalance=${self}_balance
amount=$2
export ${self}_balance=$(($currentBalance + $amount))
}
And then:
Object account leandroAccount name=Leandro balance=100 fn_display fn_deposit
$account_fn_deposit leandroAccount 50
$account_fn_display leandroAccount
## Result
Hello, Leandro. Your balance is 150
How about grouping the functions into a single wrapping function called Account
which in turn calls the Object
function?
Account() {
display() {
self=$1
name=${self}_name
balance=${self}_balance
echo "Hello, ${!name}. Your balance is ${!balance}"
}
deposit() {
self=$1
currentBalance=${self}_balance
amount=$2
export ${self}_balance=$(($currentBalance + $amount))
}
Object account "$@"
Object account $1 fn_display
Object account $1 fn_deposit
}
Creating objects:
Account accountA name=Leandro balance=100
Account accountB name=John balance=500
$account_fn_deposit accountA 50
$account_fn_display accountA
$account_fn_display accountB
That's it. This Gist is a very simple simulation of OOP in Bash.
Check out the complete Gist below.