Skip to content

Instantly share code, notes, and snippets.

@leonletto
Forked from leandronsp/001-README.md
Created December 9, 2022 18:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save leonletto/f742913bad7dddcee3dccf16e887e988 to your computer and use it in GitHub Desktop.
Save leonletto/f742913bad7dddcee3dccf16e887e988 to your computer and use it in GitHub Desktop.
OOP in Bash script

OOP in Bash script

Wait, what?

Inspired by this awesome article.

Prelude about OOP

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.

Implementing OOP

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 script

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.

Experimentation

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

Conclusion

That's it. This Gist is a very simple simulation of OOP in Bash.

Check out the complete Gist below.

#!/bin/bash
Object() {
## Object type, e.g account, user, company
kind=$1
## Variable that holds an instance of object
self=$2
## Shift twice across all arguments, because $1 = kind and $2 = self
shift
shift
## Iterates over remaining arguments
for arg in "$@"; do
## Parse argument when matching attributes
## e.g name=Leandro -> ARG_KEY=name ARG_VALUE=Leandro
read ARG_KEY ARG_VALUE <<< $(echo "$arg" | sed -E "s/(\w+)=(.*?)/\1 \2/")
## Parse argument when matching functions
## e.g fn_display -> FUNC=display
read FUNC <<< $(echo "$arg" | sed -E "s/fn_(\w+)$/\1/")
## Declare (export) variables of attributes and functions
## eg. attribute: accountA_name, accountB_name, accountC_balance
## eg. function: account_fn_display, person_fn_walk
if [[ ! -z "$ARG_KEY" ]] && [[ ! -z "$ARG_VALUE" ]]; then
export ${self}_$ARG_KEY="$ARG_VALUE"
elif [[ ! -z "$FUNC" ]] && [[ "$FUNC" != "$self" ]]; then
export ${kind}_fn_$FUNC=$FUNC
fi
done
}
Account() {
## Instance function
display() {
## The first argument should be the object instance
## e.g accountA
self=$1
## Attributes, e.g accountA_name
name=${self}_name
balance=${self}_balance
## Remaining arguments passed to the function
greeting=$2
## Do something...
echo "$greeting, ${!name}. Your balance is ${!balance}"
}
## Instance function
deposit() {
self=$1
currentBalance=${self}_balance
amount=$2
export ${self}_balance=$(($currentBalance + $amount))
}
## Build instance attributes and functions based on Object prototype
Object account "$@"
Object account $1 fn_display
Object account $1 fn_deposit
}
## Create different account objects
Account accountA name=Leandro balance=100
Account accountB name=John balance=500
## Call functions on account objects
$account_fn_deposit accountA 50
$account_fn_display accountA Ola
$account_fn_display accountB Hello
### Result ###
Ola, Leandro. Your balance is 150
Hello, John. Your balance is 500
#!/bin/bash
Object() {
kind=${FUNCNAME[1]}
for arg in "$@"; do
IFS='=' read ARG_KEY ARG_VALUE <<< $arg
case "$ARG_KEY" in
"ref") [[ ! -z "$ARG_VALUE" ]] && self="$ARG_VALUE" ;;
"fn") [[ ! -z "$ARG_VALUE" ]] && export ${kind}_fn_$ARG_VALUE=$ARG_VALUE ;;
"subtype") [[ ! -z "$ARG_VALUE" ]] && export ${self}_subtype="$ARG_VALUE" ;;
*) export ${self}_$ARG_KEY="$ARG_VALUE" ;;
esac
done
}
Animal() {
scream() {
self=$1
subtype=${self}_subtype
${!subtype} scream $self
}
Object "$@" fn=scream subtype=${FUNCNAME[1]}
}
Dog() {
scream() {
self=$1
echo "${self} woofing!"
}
$1 $2 2> /dev/null
Animal "$@"
}
Cat() {
scream() {
self=$1
echo "${self} meowing!"
}
$1 $2 2> /dev/null
Animal "$@"
}
######
Dog ref=doguinho
Dog ref=dogao
Cat ref=gatinho
$Animal_fn_scream doguinho
$Animal_fn_scream dogao
$Animal_fn_scream gatinho
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment