Motivation

Unmarshaling strings representing highly structured data in Bash in a reliable and easy to use way has historically been impossible

Bash only allows for simple/flat mapping via an associative array

{
    "atom": "Hydrogen"
}
assert [ "${OBJECT[atom]}" = 'Hydrogen' ]

But, when it comes to even the slightest of more complicated structures, Bash cannot cope: associative arrays do not nest inside one another

{
    "stars": {
        "cool": "Wolf 359"
    }
}

Note only that, but indexed arrays cannot nest inside associative arrays

{
    "nearby": [
        "Alpha Centauri A",
        "Alpha Centauri B",
        "Proxima Centauri",
        "Barnard's Star",
        "Luhman 16"
    ]
}

Solution

bash-object solves this problem, in the most general case. The repository contains functions for storing and retreiving strings, indexed arrays, and associative arrays in a heterogenous hierarchy

Let's take a look at the most basic case

{
    "xray": {
        "yankee": "zulu"
    }
}

In Bash, it will be stored in memory eventually using declarations similar to the following

declare -A __bash_object_unique_global_variable_xray=([yankee]='zulu')
declare -A OBJECT=([xray]=$'\x1C\x1Dtype=object;&__bash_object_unique_global_variable_xray')

You can retrieve the data with

bobject get-object --value 'OBJECT' 'xray'
assert [ "${REPLY[yankee]}" = zulu ]

The implementation hinges on Bash's declare -n. When using get-object, this is what would happen behind the scenes at the lowest level

local current_object_name='__bash_object_unique_global_variable_xray'
local -n current_object="$current_object_name"

declare -gA REPLY=()
local key=
for key in "${!current_object[@]}"; do
    REPLY["$key"]="${current_object[$key]}"
done

Another implementation detail is how a new variable is created in the global scope. This can occur when you are setting an array (indexed array) or object (associative array) at some place in the object hierarchy. In the previous example, __bash_object_unique_global_variable_xray is the new variable created in the global scope; in practice, the name looks a lot different, as seen in the example below

local global_object_name=
printf -v global_object_name '%q' "__bash_object_${root_object_name}_${root_object_query}_${RANDOM}_${RANDOM}"

if ! declare -gA "$global_object_name"; then
    bash_object.util.die 'ERROR_INTERNAL' "Could not declare variable '$global_object_name'"
    return
fi
local -n global_object="$global_object_name"
global_object=()

The %q probably isn't needed (it was originally there because the implementation previously used eval), but it's still there as of this writing