{{breadcrumbs}}
← BACK TO HUB
HUB / lib_bejson_parse.sh

lib_bejson_parse.sh

Runtime
Bash
Category
Bash
Path
/storage/emulated/0/Projects/Management/Libraries/sh/lib_bejson_parse.sh
FILE // lib_bejson_parse.sh
#!/bin/bash
# # Library:     lib_bejson_parse.sh
# MFDB Version: 1.3.1
# Format_Creator: Elton Boehnen
# Status:      OFFICIAL - v1.3.1
# Date:        2026-05-06

#===============================================================================
# Library:     lib_bejson_parse.sh
# Jurisdiction: ["BASH", "CORE_COMMAND"]
# Status:      OFFICIAL — Core-Command/Lib (v1.1)
# Author:      Elton Boehnen
# Version:     1.1 (OFFICIAL)
# Date:        2026-04-23
# Description: BEJSON structured parser — extracts files from BEJSON 104 / 104a /
104db schemas. Sources lib_bejson_core.sh and lib_bejson_validator.sh.
Author:      Elton Boehnen
Version:     2.0.0 (OFFICIAL)
Date:        2026-04-16
Compatibility: Bash 4.0+, Termux/Android
Dependencies:  lib_bejson_core.sh, lib_bejson_validator.sh, jq, zip
Changelog v2.0.0:
[FIX] Integrated `sync` call after batch file extraction to ensure 
durability on flash media (Termux/Android).
#===============================================================================
#===============================================================================
# Library:     lib_bejson_parse.sh
# Description: BEJSON structured parser — extracts files from BEJSON 104 / 104a /
#              104db schemas. Sources lib_bejson_core.sh and lib_bejson_validator.sh.
# Compatibility: Bash 4.0+, Termux/Android
# Dependencies:  lib_bejson_core.sh, lib_bejson_validator.sh, jq, zip
#
# Changelog v2.0.0:
#   [FIX] Integrated `sync` call after batch file extraction to ensure 
#         durability on flash media (Termux/Android).

set -o pipefail
set -o nounset

# ------------------------------------------------------------------
# Source BEJSON ecosystem — core + validator
# ------------------------------------------------------------------
BEJSON_PARSE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

if [[ -f "$BEJSON_PARSE_DIR/lib_bejson_core.sh" ]]; then
    source "$BEJSON_PARSE_DIR/lib_bejson_core.sh"
elif [[ -f "$BEJSON_PARSE_DIR/lib-bejson_core.sh" ]]; then
    source "$BEJSON_PARSE_DIR/lib-bejson_core.sh"
else
    echo "ERROR: lib_bejson_core.sh not found in $BEJSON_PARSE_DIR" >&2
    return 1 2>/dev/null || exit 1
fi

# Default output directory (callers may override via argument)
BEJSON_PARSE_DEFAULT_OUT="${BEJSON_PARSE_DIR}/output"

# Globals populated by bejson_extract_data
BEJSON_PROJECT_NAME="My_Project"
BEJSON_FILES_NAMES=()
BEJSON_FILES_CONTENTS=()

# ------------------------------------------------------------------
# PARSER CORE — functions mirrored from lib_bejson_parse.py
# ------------------------------------------------------------------

#-------------------------------------------------------------------------------
# bejson_parse_json <text>
#   Strip any prose wrapper from <text> and print valid JSON to stdout.
#   Mirrors parse_json() in lib_bejson_parse.py.
#   Returns 0 on success, 1 on parse failure.
#-------------------------------------------------------------------------------
bejson_parse_json() {
    local text="$1"

    # Try to extract a JSON object using awk (handles multi-line)
    local clean
    clean=$(echo "$text" | awk '
        /\{/ { found=1 }
        found { buf = buf $0 "\n" }
        END   { print buf }
    ' | sed 's/^[^{]*//')   # trim anything before first {

    # Validate with jq
    if echo "$clean" | jq '.' > /dev/null 2>&1; then
        echo "$clean" | jq '.'
        return 0
    fi

    # Fallback: try raw text as-is
    if echo "$text" | jq '.' > /dev/null 2>&1; then
        echo "$text" | jq '.'
        return 0
    fi

    echo "ERROR: parse_json — could not parse JSON from input" >&2
    return 1
}

#-------------------------------------------------------------------------------
# bejson_extract_data <json_string>
#   Walk a BEJSON object and populate:
#     BEJSON_PROJECT_NAME      (string)
#     BEJSON_FILES_NAMES[]     (array)
#     BEJSON_FILES_CONTENTS[]  (array)
#   Mirrors extract_data() in lib_bejson_parse.py — same logic.
#   Returns 0 on success, 1 if no files found.
#-------------------------------------------------------------------------------
bejson_extract_data() {
    local json="$1"

    BEJSON_PROJECT_NAME="My_Project"
    BEJSON_FILES_NAMES=()
    BEJSON_FILES_CONTENTS=()

    # ---- Resolve project name from first matching key across all rows ----
    # Priority: zip_file_name / project_name → zip_file_name → container_name
    local proj_raw=""

    # Try zip_file_name first (104a)
    proj_raw=$(echo "$json" | jq -r '
        .Values[] |
        to_entries |
        . as $row |
        ( .[] | select(.key == (
            [ .[] | .key ] | index(
              (.[] | select(
                (.value | type == "object") and (.value.name // "" | ascii_downcase | gsub("[^a-z0-9]";"";"g")) == "zipfilename"
              ) // empty)
            )
          ))
        ) | .value
    ' 2>/dev/null | head -1)

    # Simpler approach: use jq to find field index for each candidate key, then read value
    _bejson_parse_get_project_name() {
        local j="$1"
        local result="My_Project"

        local field_count
        field_count=$(echo "$j" | jq '.Fields | length')

        # Build a map: normalised_name -> index
        local cands=("zipfilename" "projectname" "containername")

        for cand in "${cands[@]}"; do
            local idx
            idx=$(echo "$j" | jq --arg c "$cand" '
                .Fields | to_entries[] |
                select((.value.name | ascii_downcase | gsub("[^a-z0-9]";"")) == $c) |
                .key
            ' 2>/dev/null | head -1)

            if [[ -n "$idx" ]]; then
                local val
                val=$(echo "$j" | jq -r --argjson i "$idx" '
                    .Values[] |
                    if (. | length) > $i then .[$i] else null end |
                    select(. != null and . != "") |
                    tostring
                ' 2>/dev/null | head -1)

                if [[ -n "$val" && "$val" != "null" ]]; then
                    result="$val"
                    break
                fi
            fi
        done

        echo "$result"
    }

    BEJSON_PROJECT_NAME=$(_bejson_parse_get_project_name "$json")
    # Sanitise (strip chars forbidden in dir names)
    BEJSON_PROJECT_NAME=$(echo "$BEJSON_PROJECT_NAME" | sed 's/[<>:"\/\\|?*]/_/g')

    # ---- Extract file1..file50 name/content pairs from every row ----
    local field_count
    field_count=$(echo "$json" | jq '.Fields | length')

    for i in $(seq 1 50); do
        local name_key="file${i}name"
        local cont_key="file${i}content"

        # Find column indices
        local name_idx cont_idx
        name_idx=$(echo "$json" | jq --arg k "$name_key" '
            .Fields | to_entries[] |
            select((.value.name | ascii_downcase | gsub("[^a-z0-9]";"")) == $k) |
            .key
        ' 2>/dev/null | head -1)

        cont_idx=$(echo "$json" | jq --arg k "$cont_key" '
            .Fields | to_entries[] |
            select((.value.name | ascii_downcase | gsub("[^a-z0-9]";"")) == $k) |
            .key
        ' 2>/dev/null | head -1)

        [[ -z "$name_idx" || -z "$cont_idx" ]] && continue

        # Scan every row for this pair
        local row_count
        row_count=$(echo "$json" | jq '.Values | length')

        for r in $(seq 0 $(( row_count - 1 ))); do
            local fname fcont
            fname=$(echo "$json" | jq -r --argjson r "$r" --argjson ni "$name_idx" '
                .Values[$r][$ni] // empty | select(. != null and . != "")
            ' 2>/dev/null)
            fcont=$(echo "$json" | jq -r --argjson r "$r" --argjson ci "$cont_idx" '
                .Values[$r][$ci] // empty | select(. != null and . != "")
            ' 2>/dev/null)

            if [[ -n "$fname" && -n "$fcont" ]]; then
                BEJSON_FILES_NAMES+=("$fname")
                BEJSON_FILES_CONTENTS+=("$fcont")
            fi
        done
    done

    if [[ ${#BEJSON_FILES_NAMES[@]} -eq 0 ]]; then
        echo "ERROR: extract_data — no file entries found in schema" >&2
        return 1
    fi

    return 0
}

#-------------------------------------------------------------------------------
# bejson_save_files <proj> <out_dir> [overwrite]
#   Write files to disk, generate _REPORT.txt, and zip everything.
#   Uses global arrays BEJSON_FILES_NAMES[] and BEJSON_FILES_CONTENTS[].
#   Mirrors save_files() in lib_bejson_parse.py — same logic.
#
#   Arguments:
#     proj      - project name (used as folder/zip base name)
#     out_dir   - base output directory (default: ./output)
#     overwrite - "true" = merge/update mode; anything else = timestamped
#
#   Echoes result JSON: {"success":true/false,"message":"...","path":"...","file_count":N}
#   Returns 0 on success, 1 on failure.
#-------------------------------------------------------------------------------
bejson_save_files() {
    local proj="${1:-My_Project}"
    local base_dir="${2:-$BEJSON_PARSE_DEFAULT_OUT}"
    local overwrite="${3:-false}"

    # Ensure base dir exists
    if [[ ! -d "$base_dir" ]]; then
        mkdir -p "$base_dir" 2>/dev/null || {
            echo '{"success":false,"message":"Cannot create output dir: '"$base_dir"'"}'
            return 1
        }
    fi

    local target
    if [[ "$overwrite" == "true" ]]; then
        target="${base_dir}/${proj}"
        local bak_target="${base_dir}/${proj}_BACKUP"
        if [[ -d "$target" ]]; then
            [[ -d "$bak_target" ]] && rm -rf "$bak_target"
            cp -r "$target" "$bak_target" 2>/dev/null \
                || echo "WARNING: backup of $target failed" >&2
        fi
    else
        local ts
        ts=$(date +"%Y%m%d_%H%M%S")
        target="${base_dir}/${ts}_${proj}"
    fi

    mkdir -p "$target" 2>/dev/null || {
        echo '{"success":false,"message":"Cannot create target dir: '"$target"'"}'
        return 1
    }

    local file_count=${#BEJSON_FILES_NAMES[@]}

    # Write each file
    local i
    for i in $(seq 0 $(( file_count - 1 ))); do
        local fname="${BEJSON_FILES_NAMES[$i]}"
        local fcont="${BEJSON_FILES_CONTENTS[$i]}"
        local fpath="${target}/${fname}"
        local fdir
        fdir=$(dirname "$fpath")
        [[ ! -d "$fdir" ]] && mkdir -p "$fdir"
        printf '%s' "$fcont" > "$fpath"
    done
    
    # CRITICAL FIX: explicit disk sync after batch write
    if command -v sync &>/dev/null; then
        sync "$target" 2>/dev/null || sync 2>/dev/null || true
    fi

    # Build report
    local ts_now mode_str sep52 dash52 report_lines report_text
    ts_now=$(date +"%Y-%m-%d %H:%M:%S")
    if [[ "$overwrite" == "true" ]]; then
        mode_str="Merge/Update (overwrite)"
    else
        mode_str="Timestamped (new folder)"
    fi
    sep52=$(printf '=%.0s' {1..52})
    dash52=$(printf -- '-%.0s' {1..52})

    report_text="${sep52}
  STRUCTURED PARSER — BUILD REPORT
${sep52}
Project    : ${proj}
Generated  : ${ts_now}
Mode       : ${mode_str}
Output Dir : ${target}
Files      : ${file_count}
${dash52}
FILE LIST
${dash52}"

    for i in $(seq 0 $(( file_count - 1 ))); do
        local fname="${BEJSON_FILES_NAMES[$i]}"
        local fpath="${target}/${fname}"
        local size_b=0
        [[ -f "$fpath" ]] && size_b=$(wc -c < "$fpath" | tr -d ' ')
        local size_s
        if (( size_b >= 1024 )); then
            size_s="$(echo "scale=1; $size_b/1024" | bc) KB"
        else
            size_s="${size_b} B"
        fi
        local idx_padded
        idx_padded=$(printf '%02d' $(( i + 1 )))
        report_text="${report_text}
  [${idx_padded}] ${fname}  (${size_s})"
    done

    report_text="${report_text}
${dash52}
Zip        : ${proj}_update.zip
${sep52}
"

    # Write report
    printf '%s\n' "$report_text" > "${target}/_REPORT.txt"
    if command -v sync &>/dev/null; then
        sync "${target}/_REPORT.txt" 2>/dev/null || sync 2>/dev/null || true
    fi

    # Build zip
    local zip_path="${target}/${proj}_update.zip"
    local zip_ok=true

    if command -v zip &>/dev/null; then
        (
            cd "$target" || exit 1
            local files_to_zip=()
            for i in $(seq 0 $(( file_count - 1 ))); do
                files_to_zip+=("${BEJSON_FILES_NAMES[$i]}")
            done
            zip -r "${proj}_update.zip" "${files_to_zip[@]}" "_REPORT.txt" > /dev/null 2>&1
        ) || zip_ok=false
    else
        echo "WARNING: 'zip' not found — skipping zip step (pkg install zip)" >&2
        zip_ok=false
    fi

    local zip_note=""
    [[ "$zip_ok" == "false" ]] && zip_note=" (zip skipped)"

    local escaped_target
    escaped_target=$(echo "$target" | sed 's/"/\\"/g')

    echo "{\"success\":true,\"message\":\"Saved ${file_count} file(s)${zip_note}\",\"path\":\"${escaped_target}\",\"file_count\":${file_count}}"
    return 0
}