Dec 22, 2012

Object Oriented Programming in Bash

Bash is a very common *nix shell, and it's programming language is purely procedural and focused on command execution. Object Oriented Programming (OOP) is a programming paradigm that represents the elements of a problem as entities with a set of properties and actions that it can execute. If you use Bash to write very simple and short scripts, procedural programming is just fine, you don't need more. But if your program becomes more and more bigger, a monster program (> 1000 lines), then you need a better way to structure your program to make it easy to maintain and read. Of course, Bash lacks OOP features, but with some tricks you can simulate it with a few lines added, and I'll show you how.
ATTENTION: You need to have solid concepts of OOP and Bash before reading it, I will not explain nothing of that here.

First at all create an script in which you will define your class, and give it execution permissions.
touch vector.sh
chmod +x vector.sh
Then copy the following code inside the script:
#!/bin/bash

# Base class. (1)
function Vector()
{
    # A pointer to this Class. (2)
    base=$FUNCNAME
    this=$1

    # Inherited classes (optional). (3)
    export ${this}_inherits="Class1 Class2 Class3" # (3.1)

    for class in $(eval "echo \$${this}_inherits")
    do
        for property in $(compgen -A variable ${class}_)
        do
            export ${property/#$class\_/$this\_}="${property}" # (3.2)
        done

        for method in $(compgen -A function ${class}_)
        do
            export ${method/#$class\_/$this\_}="${method} ${this}"
        done
    done

    # Declare Properties. (4)
    export ${this}_x=$2
    export ${this}_y=$3
    export ${this}_z=$4

    # Declare methods. (5)
    for method in $(compgen -A function)
    do
        export ${method/#$base\_/$this\_}="${method} ${this}"
    done
}

# Human readable representation of the vector. (6)
function Vector_show()
{
    # (7)
    base=$(expr "$FUNCNAME" : '\([a-zA-Z][a-zA-Z0-9]*\)')
    this=$1

    x=$(eval "echo \$${this}_x")
    y=$(eval "echo \$${this}_y")
    z=$(eval "echo \$${this}_z")

    echo "$this ($x, $y, $z)"
}

# Adds two vectors.
function Vector_add()
{
    base=$(expr "$FUNCNAME" : '\([a-zA-Z][a-zA-Z0-9]*\)')
    this=$1
    other=$2

    # Get it's components
    x1=$(eval "echo \$${this}_x")
    y1=$(eval "echo \$${this}_y")
    z1=$(eval "echo \$${this}_z")

    x2=$(eval "echo \$${other}_x")
    y2=$(eval "echo \$${other}_y")
    z2=$(eval "echo \$${other}_z")

    # Add it's components
    x=$(($x1 + $x2))
    y=$(($y1 + $y2))
    z=$(($z1 + $z2))

    # Create a new vector. (8)
    Vector 'vector3' $x $y $z

    $vector3_show
}

Explanation

  1. This is function is an equivalent of the constructor of a class. It must have the same name of the class. The name of a class can't contains underscores (_). We will use this function to create instances of the "Vector" class.
  2. Here you can get a reference of the base class of the object and a reference of the object created. The first argument of the constructor must be always the name of the object to create.
  3. This block of code simulates multiple inheritance, is optional and you can remove it if you don't need.
    1. Every property and method of the class can be accessed as ${this}_methodProperty_name, or you can also call to it's base class as ${base}_methodProperty_name. Multiple inheritance works overwriting the methods and properties (m&p) of the preceding classes as follows:
      Class1 < Class2 < Class3 < Base
      • Class2 overwrites m&p of Class1.
      • Class3 overwrites m&p of Class1 and Class2.
      • Base overwrites m&p of Class1 and Class2 and Class3.
    2. This will export the m&p of the base class as a m&p of the object. See bellow.
  4. Every method and property of the class is exported as a global variable in the form of objectName_methodProperty_name. You can initialize it's properties through the constructor's parameters, or with a default value, or leave it uninitialized.
  5. This will find every function with the pathern BaseClass_* and export it as a global variable replacing BaseClass with the object's name, the value of the variable will be a string with the base class method and the object name as first parameter that serves as a reference to it.
  6. The methods of the class are defined as BaseClass_methodName. Method names can contains underscores, but camelCase is recommended.
  7. Obtains a reference to the base class (not obligatory) and the object, remember, the first argument is the name of the object.
  8. Of course, you can create new objects inside the methods of the same class.
We are ready to use our class. Create an script from which you will use your class, give it execution permissions, and copy the following code inside it:
#!/bin/bash

# Import the class definition file.
. vector.sh

function main()
{
    # Create the vectors objects. (1)
    Vector 'vector1' 1 2 3
    Vector 'vector2' 7 5 3

    # Show it's properties. (2)
    echo "vector1 ($vector1_x, $vector1_y, $vector1_z)"
    echo "vector2 ($vector2_x, $vector2_y, $vector2_z)"

    # Call to it's methods.
    $vector1_show
    $vector2_show

    $vector1_add vector2
}

main

Explanation

  1. Objects are declared as Class objectName [arg1 arg2 arg3 ...].
  2. Now you can access to it's methods and properties as $objectName_methodsProperty_name [arg1 arg2 arg3 ...].
The result of the script execution is:
$ ./main.sh
vector1 (1, 2, 3)
vector2 (7, 5, 3)
vector1 (1, 2, 3)
vector2 (7, 5, 3)
vector3 (8, 7, 6)
OOP in Bash unlocked. Achievement reached :D

27 comments:

  1. your font makes this site unreadable.

    ReplyDelete
    Replies
    1. Hi, thanks for your advice, I have changed the font from Impact 12px to Verdana 16px. that seems to fix the problem :)

      Delete
  2. Hi,
    Thanks a lot for explaining this topic!
    I have some trouble trying to understand how these lines work:
    export ${property/#$class\_/$this\_}="${property}" # Why not '${property} ${this}' as in the line below?
    export ${method/#$class\_/$this\_}="${method} ${this}"
    $(eval "echo \$${this}_inherits") # Curious about '\$$' syntax
    It'll be great if someone will kindly explain or point me to some relevant readings. :)

    ReplyDelete
    Replies
    1. I have some trouble trying to understand how these lines work:
      export ${property/#$class\_/$this\_}="${property}" # Why not '${property} ${this}' as in the line below?
      export ${method/#$class\_/$this\_}="${method} ${this}"


      Because a method needs to know who is calling for applying it's operations to the right object, while a property already contains a reference to it's owner object.

      $(eval "echo \$${this}_inherits") # Curious about '\$$' syntax

      The first \$ is just the literal character $ inside the string, the second $ is part of ${this} that will be replaced for the contents of the this variable.

      In the example, you must read the expression:

      eval "echo \$${this}_inherits"

      as follows:

      1) Replace ${this} by Vector:

      eval "echo \$Vector_inherits"

      2) Since $Vector_inherits is a dynamic variable and I need to read it's contents, I eval the expression inside the quotes: eval "...", that is echo $Vector_inherits.

      Delete
  3. Thanks for the explanation.

    ReplyDelete
  4. This comment has been removed by the author.

    ReplyDelete
  5. Can you please explain , why are we declaring the variable base as ==> base=$(expr "$FUNCNAME" : '\([a-zA-Z][a-zA-Z0-9]*\)') at line number 42 and 55 , as we are not using this variable inside those functions in vector.sh??

    Also could you provide some more examples where we are actually making use of the inherited classes ==> export ${this}_inherits="Class1 Class2 Class3" # (3.1)

    I am very interested to learn more on OOPS in bash script. Can you help me in knowing some good book/site/material ??

    ReplyDelete
    Replies
    1. $base is declared in case you want to access to some method or property from the base class, I didn't used because it was not required for the example.

      Anyways, you are right, the example was too simple, I'll try to make it more complete in the future, right now I'm very busy.

      About books talking about this topic, mmh..., thats really hard to find. OOP isn't natively supported by Bash, so what presented here was a pure hackish way to do it. As @Daniel Lemke, linked here, there are many ways to mimic OOP in Bash, just search for it ;).

      Delete
  6. Oh god, this looks more like structural programming thinly disguised as OOP. This is a better example:
    http://lab.madscience.nl/oo.sh.txt

    ReplyDelete
  7. Yeah, I know, I'm not Bash master at all :P, indeed tanks for example, I didn't know about the :: operand.

    ReplyDelete
  8. I like the use of the Class_func call inside a variable name passing the object instance name as the first parameter.
    I found that I often need to loop over a list of objects. In this case, my variable contains the object name
    so I need an extra level of de-referencing with an extra eval.

    I want to be able to do this:
    for PtrPbj in AOBJ BOBJ COBJ ; do
    eval \$${PtrObj}_initialize
    done
    without the extra eval and \$

    eg. If my object is named AOBJ and I have a reference to it like this PtrAOBJ="AOBJ" then when I want to call
    AOBJ_func using PtrAOBJ, I need to do this:

    val \$${PtrAOBJ}_func "param_to_AOBJ_func"

    If instead of creating variables like
    export AOBJ_func="Class_func AOBJ"
    I create a function
    AOBJ_func() { Class_func AOBJ $@; }
    then I can do one less level of eval by calling it like this:
    ${PtrAOBJ}_func "param_to_func" # works because it expands to AOBJ_func which can be evaluated directly without another eval.

    Previously I was trying to re-parse the whole class function for every instance, but
    now with this simple 1 line function, I can get the same result.

    ReplyDelete
  9. GÓWNO, a nie programowanie obiektowe ...

    ReplyDelete
  10. This is not what bash is for. It's called python look it up...

    ReplyDelete
    Replies
    1. bash is smaller, faster, and language agnostic

      NO modules needed

      Delete
  11. Thanks a lot! :$
    I try to publish articles that are on my interest, and that's normally hard to find in other websites/blogs, or not very well explained. I'm not a regular writer, but i try every article to count.

    ReplyDelete
  12. Programming is combination of intelligent and creative work. Programmers can do anything with code. The entire Programming tutorials that you mention here on this blog are awesome. Beginners Heap also provides latest tutorials of Programming from beginning to advance level.
    Be with us to learn programming in new and creative way.

    ReplyDelete
  13. hi i like to dive into this stuff, as i am a bit familiar with bash. at your declare methods part however, i think i found a bug.

    # Declare methods. (5)
    for method in $(compgen -A function)
    do
    export ${method/#$base\_/$this\_}="${method} ${this}"
    done

    it will export all found functions (by compgen) and adds the current vector to it, you see it when you run env after you've run this script. i reckon you need a check if the $method contaisn the $base first, before exporting it
    e.g. if [[ $method == $base_* ]]; then export.....

    ReplyDelete
    Replies
    1. Yes, there is a bug, but it's at line 32:

      Change: for method in $(compgen -A function)
      To: for method in $(compgen -A function ${base}_)

      Delete
  14. Nice... staying basic bash-y, but thinking and structuring oo for state and behavior. The challenge though is still the creation of gazillions of globals... as long as bash does not get an extension for structured variables, there is nothing that can prevent that... Writing a 'cap' is a great stop gap solution... did it myself several times, for PL/I and even COBOL, including VM with garbage collection (back in end 80' / early 90').

    ReplyDelete
  15. I think I'd finally snap if I ever saw a coworker attempt to do something like this in a bash script...

    ReplyDelete
  16. Hi,
    This piece of code is great. I'm using it!
    How can I put it in a function? Here is the context, instead of doing $vector1_show, I want to put the object name in a string variable:

    function myfct (){
    vec=$1
    $\$vec_show
    }
    myfct "vector1"

    here $vec contains the name of the object
    In this situation it is stupid to use it via a function, but in my situation (more complicated) I have to do that...

    ReplyDelete
  17. just easy python, ruby node or whatever X(

    ReplyDelete
  18. i have grepper installed, so at the bottom of the code it shows div class="open_grepper_editor" title="Edit & Save To Grepper"/div but with tags XD XD :3

    ReplyDelete
  19. I was inspired by this blog and I used it to write oo_jq (see https://www.virtualtwigs.com/articles/oo_jq_article)

    ReplyDelete