Language Features

gobash introduces several programming language constructs via bash functions and wrapping existing binaries (e.g., jq). It does not modify bash itself.

As a result of the gobash design, you can introduce it step by step, as you do not need to modify any of the existing code you have.

Basic Terminology

A package corresponds to a single directory and a module corresponds to a single .sh file.

Thus, we define a program as a set of packages with one or more modules each.

Reserved Words

gobash is a set of functions and a few global variables.

There are several keywords, which means a set of functions that a user should avoid redefining. Below is the current list of keywords; to get the list of all functions in gobash you can run ./gobash func sys_functions (or use grep over the repository).

make_ # allocates an object (and introduces a struct at the same time)
amake_ # allocates an object (which is an instance of an anonymous struct)

gobash also uses several (readonly) global variables that a user should be aware of:

EC # error code returned from library functions if something goes wrong
NULL # null value to be used to set non-primitive fields
TRUE # true boolean value
FALSE # false boolean value

BOOL # boolean type used in some APIs
INT # int type used in some APIs
FLOAT # float type used in some APIs
STRING # string type used in some APIs

Finally, gobash uses the file descriptor 3 in some functions.

Structs

Similar to structs/records in other programming languages, you can create complex data types with gobash. Unlike in other languages, in gobash you only need to implement a constructor. The name of the constructor is automatically the name of the struct as well.

function Person() {
        make_ $FUNCNAME \
              "name" "$1" \
              "age" "$2"
}

You can think of the Person function as both defining a struct and providing a constructor (although it is more the latter). Note that the constructor function can perform any other work, e.g., check the validity of arguments.

The first argument to make_ provides the name of the struct. While one can play with generating these names (or replacing with some other structs), in the most common scenarios, the first argument we set to be the name of the constructor (i.e., $FUNCNAME).

Objects

Once a struct is defined, it can be used to create objects and set/get the fields.

p=$(Person "Jessy" 10)
$p age 20 # set the field value
$p age # get the field value

Note

When an object is passed to a function as an argument, it has to be quoted.

function person_print() {
        local -r p="${1}"
        $p to_string
}

person_print "$p" # valid
# person_print $p # not valid

Anonymous Structs

It is sometimes convenient to create an anonymous struct to carry several values to a function or group some relevant data within a single function. Anonymous structs are a good choice in that case.

In the example below, we create an anonymous struct that keeps values for a 2D point.

#!/bin/bash
. gobash/gobash

function print() {
        local -r p="${1}"
        $p to_string
}

function make_and_print() {
        # the line below creates an instance of anonymous struct
        local -r p=$(amake_ "x" 3 "y" 5)
        print "$p"
}

make_and_print
# Output
# {
#   "x": "3",
#   "y": "5"
# }

Methods

Adding a method to a struct is done by implementing a function that is prefixed by the struct name followed by _ followed by the method name. For example, if we have a struct Str, we can add a method compute by writing a function Str_compute.

The first argument of each method is the object on which the method is called (this is similar to this and self in other programming languages).

In the example below, we introduce a new struct (Circle) and add a method that computes its total area.

function Circle() {
        [ $# -ne 1 ] && return $EC
        local -r r="${1}"

        make_ $FUNCNAME \
            "r" "${r}"
}

function Circle_area() {
        local -r c="${1}"

        echo "$MATH_PI * $($c r) * $($c r)" | bc
}

Invoking a method is similar to other programming languages. Below, we create an instance of a Circle and compute the total area.

c=$(Circle 20)
$c area

To String

gobash provides a default to_string method for each struct. The default implementation outputs the object in the json format. One can decide to override the default behavior by implementing to_string method for a specific struct.

The example below shows the default to_string output for the Person struct (introduced earlier in this document) and then implements a more specific to_string method.

In the snippet below, we construct one object and use the default to_string method.

p=$(Person "Jessy" 10)
$p to_string

As a result we get output in the json format, which can be convenient for further processing (e.g., using jq).

{
    "name": "Jessy",
    "age": "10"
}

In the snippet below, we implement a to_string method for the Person struct. Specifically, we output a simple string that prints only the name of the person.

function Person_to_string() {
        local -r p="${1}"
        echo "I am $($p name)."
}
p=$(Person "Jessy" 10)
$p to_string
# Output
# I am Jessy.

Return Value

This section is about returning data from a function to its caller. (The next section talks about error handling and the return statement.)

gobash uses primarily three approaches to return data from a function.

First, simple functions use echo to return desired value or an object. In the next code snippet, we return a point that has values of coordinates double of the given point.

#!/bin/bash
. gobash/gobash

function Point() {
        make_ $FUNCNAME \
              "x" "$1" \
              "y" "$2"
}

function point_double() {
        local -r p="${1}"

        local x=$(( $($p x) * $($p x) ))
        local y=$(( $($p y) * $($p y) ))
        local -r d=$(Point ${x} ${y})

        # Return newly created point. Do not forget that you need quotes.
        echo "$d"
}

p=$(Point 3 4)
d=$(point_double "$p")
$d to_string

Second, we use “out” argument that is populated inside the function body. In the next code snippet, we write the same function as above, but this time we pass the object, which we set inside the body.

#!/bin/bash
. gobash/gobash

function Point() {
        make_ $FUNCNAME \
              "x" "$1" \
              "y" "$2"
}

function point_double() {
        # We prefer to have out arguments first.
        local -r d="${1}"
        local -r p="${2}"

        local x=$(( $($p x) * $($p x) ))
        local y=$(( $($p y) * $($p y) ))
        $d x ${x}
        $d y ${y}
}

p=$(Point 3 4)
d=$(Point 0 0)
point_double "$d" "$p"
$d to_string

Third, we use an instance of the Result struct as an out argument, which can carry a value (val). Basically, this is a specialized form of the second case.

#!/bin/bash
. gobash/gobash

function Point() {
        make_ $FUNCNAME \
              "x" "$1" \
              "y" "$2"
}

function point_double() {
        local -r res="${1}"
        local -r p="${2}"

        local x=$(( $($p x) * $($p x) ))
        local y=$(( $($p y) * $($p y) ))
        local -r d=$(Point ${x} ${y})
        $res val "$d"
}

p=$(Point 3 4)
res=$(Result)
point_double "$res" "$p"
$($res val) to_string

Error Handling

Each function should return exit code: zero if the execution went well and non-zero if there was an issue detected. (While return $? in many cases at the end of a function is not needed, we sometimes use them explicitly.) gobash uses return $EC for indicating an issue inside functions from the library. Any function invocation should ideally check for errors. An example below shows a basic case of checking argument types and returning and error in case of an incorrect type.

function Point() {
        local -r x="${1}"
        local -r y="${2}"

        ! is_int "${x}" && return $EC
        ! is_int "${y}" && return $EC

        make_ $FUNCNAME \
              "x" "${x}" \
              "y" "${y}"
}

Now a caller can check for an error.

p=$(Point 3 4) || echo "error"

One exception to the rule above (when it comes to zero/non-zero values) is that functions that return boolean values return $TRUE (0) and $FALSE (1). Check the function in bool.sh.

Sometimes a more descriptive error message is more appropriate. In those cases, we use context arguments. A context argument is used to store and carry information about errors and stacktrace (at the time of an error). Each function in gobash accepts context (ctx) as the first argument; if one is not given, then the global context is used to store errors and stacktraces. In the example below, we use the global context. (We provide more examples with context in the examples directory.)

#!/bin/bash
. gobash/gobash

function Point() {
        make_ "$FUNCNAME" \
              "x" "$1" \
              "y" "$2"
}

function point_double() {
        local -r p="${1}"

        is_null $($p x) && ctx_w "'x' is incorrect" && return $EC
        is_null $($p y) && ctx_w "'y' is incorrect" && return $EC

        local x=$(( $($p x) * $($p x) ))
        local y=$(( $($p y) * $($p y) ))
        local -r d=$(Point ${x} ${y})
        $res val "$d"
}

ctx_clear # clear the global context (as it is stored across runs)
p=$(Point 3 10)
$p x $NULL
point_double "$p" || \
        { ctx_show; ctx_stack; } # show the error and stacktrace.