BEJSON 104 Bash Library
This is a comprehensive Bash library for creating, validating, and interacting with BEJSON 104 files. It provides a rich set of functions for file creation, record management, data retrieval, and advanced queries, all from the command line using `jq`.
Full Library Source
#!/bin/bash
################################################################################
### BEJSON 104 BASH LIBRARY ###
### ###
### A comprehensive bash library for working with BEJSON version 104 files ###
### Created for handling structured data interchange in bash scripts ###
### ###
### Author: BEJSON 104 Library Team ###
### Version: 1.0.0 ###
### License: MIT ###
################################################################################
# Global variables for library state
declare -g BEJSON_LOADED_FILE=""
declare -g BEJSON_LOADED_CONTENT=""
declare -g BEJSON_IS_VALID=0
################################################################################
### UTILITY FUNCTIONS ###
################################################################################
### FUNCTION: bejson_check_dependencies ###
# Description: Checks if required dependencies (jq) are installed
# Parameters: None
# Returns: 0 if dependencies are met, 1 otherwise
bejson_check_dependencies() {
if ! command -v jq &> /dev/null; then
echo "Error: jq is required but not installed. Please install jq to use this library." >&2
return 1
fi
return 0
}
### FUNCTION: bejson_error ###
# Description: Prints an error message to stderr
# Parameters: $1 - Error message
# Returns: None
bejson_error() {
echo "BEJSON Error: $1" >&2
}
### FUNCTION: bejson_success ###
# Description: Prints a success message to stdout
# Parameters: $1 - Success message
# Returns: None
bejson_success() {
echo "BEJSON Success: $1"
}
################################################################################
### FILE CREATION FUNCTIONS ###
################################################################################
### FUNCTION: bejson_create_file ###
# Description: Creates a new BEJSON 104 file with the specified structure
# Parameters:
# $1 - Output file path
# $2 - Records type (e.g., "Book", "User", etc.)
# $3 - Fields JSON array (e.g., '[{"name":"ID","type":"integer"},{"name":"Name","type":"string"}]')
# $4 - (Optional) Format creator name (defaults to "Elton Boehnen")
# $5 - (Optional) Parent hierarchy
# Returns: 0 on success, 1 on failure
# Example: bejson_create_file "data.json" "Book" '[{"name":"ISBN","type":"string"},{"name":"Title","type":"string"}]'
bejson_create_file() {
local output_file="$1"
local records_type="$2"
local fields="$3"
local format_creator="${4:-Elton Boehnen}"
local parent_hierarchy="$5"
# Validate parameters
if [[ -z "$output_file" || -z "$records_type" || -z "$fields" ]]; then
bejson_error "Missing required parameters for bejson_create_file"
echo "Usage: bejson_create_file [format_creator] [parent_hierarchy]"
return 1
fi
# Validate fields JSON
if ! echo "$fields" | jq empty 2>/dev/null; then
bejson_error "Invalid JSON format for fields parameter"
return 1
fi
# Build the BEJSON structure
local bejson_content
if [[ -n "$parent_hierarchy" ]]; then
bejson_content=$(jq -n --arg format "BEJSON" --arg version "104" --arg creator "$format_creator" --arg parent "$parent_hierarchy" --arg rectype "$records_type" --argjson fields "$fields" '{
"Format": $format,
"Format_Version": $version,
"Format_Creator": $creator,
"Parent_Hierarchy": $parent,
"Records_Type": [$rectype],
"Fields": $fields,
"Values": []
}')
else
bejson_content=$(jq -n --arg format "BEJSON" --arg version "104" --arg creator "$format_creator" --arg rectype "$records_type" --argjson fields "$fields" '{
"Format": $format,
"Format_Version": $version,
"Format_Creator": $creator,
"Records_Type": [$rectype],
"Fields": $fields,
"Values": []
}')
fi
# Write to file
echo "$bejson_content" > "$output_file"
if [[ $? -eq 0 ]]; then
bejson_success "BEJSON 104 file created: $output_file"
return 0
else
bejson_error "Failed to write BEJSON file: $output_file"
return 1
fi
}
### FUNCTION: bejson_add_record ###
# Description: Adds a record (row of values) to an existing BEJSON 104 file
# Parameters:
# $1 - BEJSON file path
# $2 - Values JSON array (e.g., '["978-0321765723", "The C++ Programming Language", 2013]')
# Returns: 0 on success, 1 on failure
# Example: bejson_add_record "data.json" '["978-0321765723", "The C++ Programming Language", 2013]'
bejson_add_record() {
local file_path="$1"
local values="$2"
# Validate parameters
if [[ -z "$file_path" || -z "$values" ]]; then
bejson_error "Missing required parameters for bejson_add_record"
echo "Usage: bejson_add_record "
return 1
fi
# Check if file exists
if [[ ! -f "$file_path" ]]; then
bejson_error "File not found: $file_path"
return 1
fi
# Validate values JSON
if ! echo "$values" | jq empty 2>/dev/null; then
bejson_error "Invalid JSON format for values parameter"
return 1
fi
# Add the record to the Values array
local updated_content
updated_content=$(jq --argjson newval "$values" '.Values += [$newval]' "$file_path")
if [[ $? -eq 0 ]]; then
echo "$updated_content" > "$file_path"
bejson_success "Record added to $file_path"
return 0
else
bejson_error "Failed to add record to $file_path"
return 1
fi
}
################################################################################
### FILE LOADING FUNCTIONS ###
################################################################################
### FUNCTION: bejson_load_file ###
# Description: Loads a BEJSON 104 file into memory for processing
# Parameters: $1 - File path to load
# Returns: 0 on success, 1 on failure
# Side Effects: Sets BEJSON_LOADED_FILE, BEJSON_LOADED_CONTENT, BEJSON_IS_VALID
# Example: bejson_load_file "data.json"
bejson_load_file() {
local file_path="$1"
# Validate parameter
if [[ -z "$file_path" ]]; then
bejson_error "Missing file path parameter for bejson_load_file"
echo "Usage: bejson_load_file "
return 1
fi
# Check if file exists
if [[ ! -f "$file_path" ]]; then
bejson_error "File not found: $file_path"
return 1
fi
# Check if file is readable
if [[ ! -r "$file_path" ]]; then
bejson_error "File is not readable: $file_path"
return 1
fi
# Load file content
BEJSON_LOADED_CONTENT=$(cat "$file_path")
# Validate JSON format
if ! echo "$BEJSON_LOADED_CONTENT" | jq empty 2>/dev/null; then
bejson_error "File is not valid JSON: $file_path"
BEJSON_LOADED_FILE=""
BEJSON_LOADED_CONTENT=""
BEJSON_IS_VALID=0
return 1
fi
BEJSON_LOADED_FILE="$file_path"
BEJSON_IS_VALID=1
bejson_success "File loaded: $file_path"
return 0
}
### FUNCTION: bejson_unload_file ###
# Description: Clears the currently loaded BEJSON file from memory
# Parameters: None
# Returns: 0 always
# Example: bejson_unload_file
bejson_unload_file() {
BEJSON_LOADED_FILE=""
BEJSON_LOADED_CONTENT=""
BEJSON_IS_VALID=0
bejson_success "File unloaded from memory"
return 0
}
### FUNCTION: bejson_get_loaded_file ###
# Description: Returns the path of the currently loaded file
# Parameters: None
# Returns: Prints the file path, or empty string if no file is loaded
# Example: current_file=$(bejson_get_loaded_file)
bejson_get_loaded_file() {
echo "$BEJSON_LOADED_FILE"
}
################################################################################
### VALIDATION FUNCTIONS ###
################################################################################
### FUNCTION: bejson_check_format_version ###
# Description: Checks if a file or loaded content has the correct BEJSON 104 format version
# Parameters: $1 - (Optional) File path. If not provided, uses loaded content
# Returns: 0 if version is "104", 1 otherwise
# Example: bejson_check_format_version "data.json"
bejson_check_format_version() {
local file_path="$1"
local content
if [[ -n "$file_path" ]]; then
# Check specific file
if [[ ! -f "$file_path" ]]; then
bejson_error "File not found: $file_path"
return 1
fi
content=$(cat "$file_path")
else
# Use loaded content
if [[ -z "$BEJSON_LOADED_CONTENT" ]]; then
bejson_error "No file loaded. Use bejson_load_file first or provide a file path"
return 1
fi
content="$BEJSON_LOADED_CONTENT"
fi
local format_version
format_version=$(echo "$content" | jq -r '.Format_Version // empty')
if [[ "$format_version" == "104" ]]; then
bejson_success "Format version is 104"
return 0
else
bejson_error "Format version is not 104 (found: $format_version)"
return 1
fi
}
### FUNCTION: bejson_validate_file ###
# Description: Validates a BEJSON 104 file against the specification
# Parameters: $1 - (Optional) File path. If not provided, uses loaded content
# Returns: 0 if valid, 1 if invalid
# Example: bejson_validate_file "data.json"
bejson_validate_file() {
local file_path="$1"
local content
if [[ -n "$file_path" ]]; then
# Validate specific file
if [[ ! -f "$file_path" ]]; then
bejson_error "File not found: $file_path"
return 1
fi
content=$(cat "$file_path")
else
# Use loaded content
if [[ -z "$BEJSON_LOADED_CONTENT" ]]; then
bejson_error "No file loaded. Use bejson_load_file first or provide a file path"
return 1
fi
content="$BEJSON_LOADED_CONTENT"
fi
# Validate JSON format
if ! echo "$content" | jq empty 2>/dev/null; then
bejson_error "Invalid JSON format"
return 1
fi
# Check mandatory fields
local format=$(echo "$content" | jq -r '.Format // empty')
local format_version=$(echo "$content" | jq -r '.Format_Version // empty')
local records_type=$(echo "$content" | jq -r '.Records_Type // empty')
local fields=$(echo "$content" | jq -r '.Fields // empty')
local values=$(echo "$content" | jq -r '.Values // empty')
# Validate Format
if [[ "$format" != "BEJSON" ]]; then
bejson_error "Invalid or missing Format field (must be 'BEJSON')"
return 1
fi
# Validate Format_Version
if [[ "$format_version" != "104" ]]; then
bejson_error "Invalid or missing Format_Version field (must be '104')"
return 1
fi
# Validate Records_Type (must be array with exactly one element)
local records_type_count=$(echo "$content" | jq '.Records_Type | length')
if [[ "$records_type_count" != "1" ]]; then
bejson_error "Records_Type must contain exactly one element (found: $records_type_count)"
return 1
fi
# Validate Fields (must be array)
if [[ "$fields" == "null" ]] || [[ -z "$fields" ]]; then
bejson_error "Missing Fields array"
return 1
fi
local fields_is_array=$(echo "$content" | jq -r '.Fields | type')
if [[ "$fields_is_array" != "array" ]]; then
bejson_error "Fields must be an array"
return 1
fi
# Validate each field has name and type
local fields_count=$(echo "$content" | jq '.Fields | length')
for ((i=0; i [file_path]"
return 1
fi
if [[ -n "$file_path" ]]; then
if [[ ! -f "$file_path" ]]; then
bejson_error "File not found: $file_path"
return 1
fi
content=$(cat "$file_path")
else
if [[ -z "$BEJSON_LOADED_CONTENT" ]]; then
bejson_error "No file loaded. Use bejson_load_file first or provide a file path"
return 1
fi
content="$BEJSON_LOADED_CONTENT"
fi
# Check if index is valid
local total_records=$(echo "$content" | jq '.Values | length')
if [[ "$index" -ge "$total_records" ]] || [[ "$index" -lt 0 ]]; then
bejson_error "Invalid record index: $index (total records: $total_records)"
return 1
fi
echo "$content" | jq -c ".Values[$index]"
}
### FUNCTION: bejson_get_multiple_records ###
# Description: Retrieves multiple records by index range (0-based, inclusive)
# Parameters:
# $1 - Start index (0-based)
# $2 - End index (0-based, inclusive)
# $3 - (Optional) File path. If not provided, uses loaded content
# Returns: Prints the records as JSON array
# Example: records=$(bejson_get_multiple_records 0 2 "data.json")
bejson_get_multiple_records() {
local start_index="$1"
local end_index="$2"
local file_path="$3"
local content
if [[ -z "$start_index" ]] || [[ -z "$end_index" ]]; then
bejson_error "Missing index parameters"
echo "Usage: bejson_get_multiple_records [file_path]"
return 1
fi
if [[ -n "$file_path" ]]; then
if [[ ! -f "$file_path" ]]; then
bejson_error "File not found: $file_path"
return 1
fi
content=$(cat "$file_path")
else
if [[ -z "$BEJSON_LOADED_CONTENT" ]]; then
bejson_error "No file loaded. Use bejson_load_file first or provide a file path"
return 1
fi
content="$BEJSON_LOADED_CONTENT"
fi
# Validate indices
local total_records=$(echo "$content" | jq '.Values | length')
if [[ "$start_index" -lt 0 ]] || [[ "$end_index" -ge "$total_records" ]] || [[ "$start_index" -gt "$end_index" ]]; then
bejson_error "Invalid index range: $start_index to $end_index (total records: $total_records)"
return 1
fi
echo "$content" | jq -c ".Values[$start_index:$((end_index + 1))]"
}
### FUNCTION: bejson_get_total_records ###
# Description: Returns the total number of records in the Values array
# Parameters: $1 - (Optional) File path. If not provided, uses loaded content
# Returns: Prints the count as integer
# Example: count=$(bejson_get_total_records "data.json")
bejson_get_total_records() {
local file_path="$1"
local content
if [[ -n "$file_path" ]]; then
if [[ ! -f "$file_path" ]]; then
bejson_error "File not found: $file_path"
return 1
fi
content=$(cat "$file_path")
else
if [[ -z "$BEJSON_LOADED_CONTENT" ]]; then
bejson_error "No file loaded. Use bejson_load_file first or provide a file path"
return 1
fi
content="$BEJSON_LOADED_CONTENT"
fi
echo "$content" | jq '.Values | length'
}
### FUNCTION: bejson_get_record_value_by_field ###
# Description: Retrieves a specific field value from a specific record
# Parameters:
# $1 - Record index (0-based)
# $2 - Field name
# $3 - (Optional) File path. If not provided, uses loaded content
# Returns: Prints the value
# Example: value=$(bejson_get_record_value_by_field 0 "Title" "data.json")
bejson_get_record_value_by_field() {
local record_index="$1"
local field_name="$2"
local file_path="$3"
local content
if [[ -z "$record_index" ]] || [[ -z "$field_name" ]]; then
bejson_error "Missing required parameters"
echo "Usage: bejson_get_record_value_by_field [file_path]"
return 1
fi
if [[ -n "$file_path" ]]; then
if [[ ! -f "$file_path" ]]; then
bejson_error "File not found: $file_path"
return 1
fi
content=$(cat "$file_path")
else
if [[ -z "$BEJSON_LOADED_CONTENT" ]]; then
bejson_error "No file loaded. Use bejson_load_file first or provide a file path"
return 1
fi
content="$BEJSON_LOADED_CONTENT"
fi
# Find field index
local field_index=$(echo "$content" | jq --arg fname "$field_name" '[.Fields[].name] | index($fname)')
if [[ "$field_index" == "null" ]]; then
bejson_error "Field not found: $field_name"
return 1
fi
# Validate record index
local total_records=$(echo "$content" | jq '.Values | length')
if [[ "$record_index" -ge "$total_records" ]] || [[ "$record_index" -lt 0 ]]; then
bejson_error "Invalid record index: $record_index (total records: $total_records)"
return 1
fi
# Get the value
echo "$content" | jq -r ".Values[$record_index][$field_index]"
}
### FUNCTION: bejson_get_multiple_record_values_by_field ###
# Description: Retrieves a specific field value from multiple records
# Parameters:
# $1 - Field name
# $2 - (Optional) File path. If not provided, uses loaded content
# Returns: Prints the values as JSON array
# Example: values=$(bejson_get_multiple_record_values_by_field "Title" "data.json")
bejson_get_multiple_record_values_by_field() {
local field_name="$1"
local file_path="$2"
local content
if [[ -z "$field_name" ]]; then
bejson_error "Missing field name parameter"
echo "Usage: bejson_get_multiple_record_values_by_field [file_path]"
return 1
fi
if [[ -n "$file_path" ]]; then
if [[ ! -f "$file_path" ]]; then
bejson_error "File not found: $file_path"
return 1
fi
content=$(cat "$file_path")
else
if [[ -z "$BEJSON_LOADED_CONTENT" ]]; then
bejson_error "No file loaded. Use bejson_load_file first or provide a file path"
return 1
fi
content="$BEJSON_LOADED_CONTENT"
fi
# Find field index
local field_index=$(echo "$content" | jq --arg fname "$field_name" '[.Fields[].name] | index($fname)')
if [[ "$field_index" == "null" ]]; then
bejson_error "Field not found: $field_name"
return 1
fi
# Get all values for this field
echo "$content" | jq -c "[.Values[][$field_index]]"
}
### FUNCTION: bejson_get_records_type ###
# Description: Retrieves the Records_Type from a BEJSON file
# Parameters: $1 - (Optional) File path. If not provided, uses loaded content
# Returns: Prints the record type as string
# Example: rec_type=$(bejson_get_records_type "data.json")
bejson_get_records_type() {
local file_path="$1"
local content
if [[ -n "$file_path" ]]; then
if [[ ! -f "$file_path" ]]; then
bejson_error "File not found: $file_path"
return 1
fi
content=$(cat "$file_path")
else
if [[ -z "$BEJSON_LOADED_CONTENT" ]]; then
bejson_error "No file loaded. Use bejson_load_file first or provide a file path"
return 1
fi
content="$BEJSON_LOADED_CONTENT"
fi
echo "$content" | jq -r '.Records_Type[0]'
}
################################################################################
### ADVANCED QUERY FUNCTIONS ###
################################################################################
### FUNCTION: bejson_filter_records_by_field ###
# Description: Filters records where a specific field matches a value
# Parameters:
# $1 - Field name
# $2 - Value to match
# $3 - (Optional) File path. If not provided, uses loaded content
# Returns: Prints matching records as JSON array
# Example: matches=$(bejson_filter_records_by_field "Author" "Robert C. Martin" "data.json")
bejson_filter_records_by_field() {
local field_name="$1"
local match_value="$2"
local file_path="$3"
local content
if [[ -z "$field_name" ]] || [[ -z "$match_value" ]]; then
bejson_error "Missing required parameters"
echo "Usage: bejson_filter_records_by_field [file_path]"
return 1
fi
if [[ -n "$file_path" ]]; then
if [[ ! -f "$file_path" ]]; then
bejson_error "File not found: $file_path"
return 1
fi
content=$(cat "$file_path")
else
if [[ -z "$BEJSON_LOADED_CONTENT" ]]; then
bejson_error "No file loaded. Use bejson_load_file first or provide a file path"
return 1
fi
content="$BEJSON_LOADED_CONTENT"
fi
# Find field index
local field_index=$(echo "$content" | jq --arg fname "$field_name" '[.Fields[].name] | index($fname)')
if [[ "$field_index" == "null" ]]; then
bejson_error "Field not found: $field_name"
return 1
fi
# Get field type
local field_type=$(echo "$content" | jq -r ".Fields[$field_index].type")
# Filter records based on type
if [[ "$field_type" == "integer" ]] || [[ "$field_type" == "float" ]]; then
# Numeric comparison
echo "$content" | jq -c --argjson val "$match_value" --argjson idx "$field_index" '[.Values[] | select(.[$idx] == $val)]'
elif [[ "$field_type" == "boolean" ]]; then
# Boolean comparison
local bool_val="false"
if [[ "$match_value" == "true" ]] || [[ "$match_value" == "1" ]]; then
bool_val="true"
fi
echo "$content" | jq -c --argjson val "$bool_val" --argjson idx "$field_index" '[.Values[] | select(.[$idx] == $val)]'
else
# String comparison
echo "$content" | jq -c --arg val "$match_value" --argjson idx "$field_index" '[.Values[] | select(.[$idx] == $val)]'
fi
}
### FUNCTION: bejson_print_table ###
# Description: Prints records in a human-readable table format
# Parameters: $1 - (Optional) File path. If not provided, uses loaded content
# Returns: Prints formatted table to stdout
# Example: bejson_print_table "data.json"
bejson_print_table() {
local file_path="$1"
local content
if [[ -n "$file_path" ]]; then
if [[ ! -f "$file_path" ]]; then
bejson_error "File not found: $file_path"
return 1
fi
content=$(cat "$file_path")
else
if [[ -z "$BEJSON_LOADED_CONTENT" ]]; then
bejson_error "No file loaded. Use bejson_load_file first or provide a file path"
return 1
fi
content="$BEJSON_LOADED_CONTENT"
fi
# Get field names
local field_names=$(echo "$content" | jq -r '.Fields[].name' | tr '
' ' ')
echo -e "$field_names"
echo "----------------------------------------"
# Print each record
local total_records=$(echo "$content" | jq '.Values | length')
for ((i=0; i/dev/null; then
bejson_error "Content is not valid JSON."
return 1
fi
# Assign to the specified variable in the caller's scope
eval "$_output_var='$_temp_content'"
return 0
}
# Internal helper to write updated content back to file or global variable.
# Parameters: $1 - file_path (optional). If empty, updates BEJSON_LOADED_CONTENT.
# $2 - new_content (JSON string)
# Returns: 0 on success, 1 on failure.
_bejson_write_content_back() {
local _file_path="$1"
local _new_content="$2"
if [[ -n "$_file_path" ]]; then
echo "$_new_content" > "$_file_path"
if [[ $? -ne 0 ]]; then
bejson_error "Failed to write updated content to $_file_path"
return 1
fi
# If the file was the loaded one, update global content as well
if [[ "$BEJSON_LOADED_FILE" == "$_file_path" ]]; then
BEJSON_LOADED_CONTENT="$_new_content"
fi
else
BEJSON_LOADED_CONTENT="$_new_content"
fi
return 0
}
################################################################################
### DATA MODIFICATION FUNCTIONS ###
################################################################################
### FUNCTION: bejson_set_record ###
# Description: Replaces an entire record (row of values) at a given index.
# Parameters:
# $1 - Record index (0-based)
# $2 - New values JSON array (e.g., '["new_isbn", "New Title", 2024]')
# $3 - (Optional) File path. If not provided, uses loaded content.
# Returns: 0 on success, 1 on failure.
# Example: bejson_set_record 0 '["978-1234567890", "Updated Book", 2024]' "data.json"
bejson_set_record() {
local record_index="$1"
local new_values_json="$2"
local file_path="$3"
local content
if [[ -z "$record_index" || -z "$new_values_json" ]]; then
bejson_error "Missing required parameters for bejson_set_record"
echo "Usage: bejson_set_record [file_path]"
return 1
fi
if ! _bejson_get_content_or_load "$file_path" content; then return 1; fi
# Validate new_values_json
if ! echo "$new_values_json" | jq empty 2>/dev/null; then
bejson_error "Invalid JSON format for new_values_json parameter"
return 1
fi
local total_records=$(echo "$content" | jq '.Values | length')
if [[ "$record_index" -ge "$total_records" ]] || [[ "$record_index" -lt 0 ]]; then
bejson_error "Invalid record index: $record_index (total records: $total_records)"
return 1
fi
local fields_count=$(echo "$content" | jq '.Fields | length')
local new_values_count=$(echo "$new_values_json" | jq 'length')
if [[ "$new_values_count" -ne "$fields_count" ]]; then
bejson_error "New values array has $new_values_count elements, but $fields_count fields are defined."
return 1
fi
local updated_content
updated_content=$(echo "$content" | jq --argjson newval "$new_values_json" --argjson idx "$record_index" '.Values[$idx] = $newval')
if [[ $? -eq 0 ]]; then
if ! _bejson_write_content_back "$file_path" "$updated_content"; then return 1; fi
bejson_success "Record $record_index updated."
return 0
else
bejson_error "Failed to set record $record_index."
return 1
fi
}
### FUNCTION: bejson_set_value ###
# Description: Sets a single value within a specific record at a specific field.
# Parameters:
# $1 - Record index (0-based)
# $2 - Field name
# $3 - New value (will be converted to JSON type by jq)
# $4 - (Optional) File path. If not provided, uses loaded content.
# Returns: 0 on success, 1 on failure.
# Example: bejson_set_value 0 "Title" "The Updated C++ Language" "data.json"
bejson_set_value() {
local record_index="$1"
local field_name="$2"
local new_value="$3"
local file_path="$4"
local content
if [[ -z "$record_index" || -z "$field_name" || -z "$new_value" ]]; then
bejson_error "Missing required parameters for bejson_set_value"
echo "Usage: bejson_set_value [file_path]"
return 1
fi
if ! _bejson_get_content_or_load "$file_path" content; then return 1; fi
local field_index=$(echo "$content" | jq --arg fname "$field_name" '[.Fields[].name] | index($fname)')
if [[ "$field_index" == "null" ]]; then
bejson_error "Field not found: $field_name"
return 1
fi
local total_records=$(echo "$content" | jq '.Values | length')
if [[ "$record_index" -ge "$total_records" ]] || [[ "$record_index" -lt 0 ]]; then
bejson_error "Invalid record index: $record_index (total records: $total_records)"
return 1
fi
# Attempt to parse new_value as JSON, if it fails, treat as string
local new_value_json
if echo "$new_value" | jq empty 2>/dev/null; then
new_value_json="$new_value"
else
new_value_json=$(jq -n --arg val "$new_value" '$val') # Quote as string
fi
local updated_content
updated_content=$(echo "$content" | jq --argjson newval "$new_value_json" --argjson ridx "$record_index" --argjson fidx "$field_index" '.Values[$ridx][$fidx] = $newval')
if [[ $? -eq 0 ]]; then
if ! _bejson_write_content_back "$file_path" "$updated_content"; then return 1; fi
bejson_success "Value for record $record_index, field '$field_name' updated."
return 0
else
bejson_error "Failed to set value for record $record_index, field '$field_name'."
return 1
fi
}
### FUNCTION: bejson_set_field_property ###
# Description: Modifies a property (e.g., 'name' or 'type') of a field definition.
# Parameters:
# $1 - Field name (current name)
# $2 - Property name to change (e.g., "name", "type")
# $3 - New property value
# $4 - (Optional) File path. If not provided, uses loaded content.
# Returns: 0 on success, 1 on failure.
# Example: bejson_set_field_property "OldID" "name" "NewID" "data.json"
bejson_set_field_property() {
local field_name="$1"
local property_name="$2"
local new_property_value="$3"
local file_path="$4"
local content
if [[ -z "$field_name" || -z "$property_name" || -z "$new_property_value" ]]; then
bejson_error "Missing required parameters for bejson_set_field_property"
echo "Usage: bejson_set_field_property [file_path]"
return 1
fi
if ! _bejson_get_content_or_load "$file_path" content; then return 1; fi
local field_index=$(echo "$content" | jq --arg fname "$field_name" '[.Fields[].name] | index($fname)')
if [[ "$field_index" == "null" ]]; then
bejson_error "Field definition not found for name: $field_name"
return 1
fi
# Validate property_name
case "$property_name" in
"name"|"type")
;;
*)
bejson_error "Invalid property name: $property_name. Only 'name' or 'type' can be modified."
return 1
;;
esac
# Attempt to parse new_property_value as JSON, if it fails, treat as string
local new_prop_val_json
if echo "$new_property_value" | jq empty 2>/dev/null; then
new_prop_val_json="$new_property_value"
else
new_prop_val_json=$(jq -n --arg val "$new_property_value" '$val') # Quote as string
fi
local updated_content
updated_content=$(echo "$content" | jq --argjson fidx "$field_index" --arg prop "$property_name" --argjson newval "$new_prop_val_json" '.Fields[$fidx][$prop] = $newval')
if [[ $? -eq 0 ]]; then
if ! _bejson_write_content_back "$file_path" "$updated_content"; then return 1; fi
bejson_success "Field '$field_name' property '$property_name' updated to '$new_property_value'."
return 0
else
bejson_error "Failed to update field '$field_name' property '$property_name'."
return 1
fi
}
### FUNCTION: bejson_set_multiple_field_properties ###
# Description: Modifies a specific property (e.g., 'type') for multiple field definitions.
# Parameters:
# $1 - JSON array of field names to update (e.g., '["ID", "Title"]')
# $2 - Property name to change (e.g., "type")
# $3 - New property value
# $4 - (Optional) File path. If not provided, uses loaded content.
# Returns: 0 on success, 1 on failure.
# Example: bejson_set_multiple_field_properties '["ID", "ISBN"]' "type" "string" "data.json"
bejson_set_multiple_field_properties() {
local field_names_json="$1"
local property_name="$2"
local new_property_value="$3"
local file_path="$4"
local content
if [[ -z "$field_names_json" || -z "$property_name" || -z "$new_property_value" ]]; then
bejson_error "Missing required parameters for bejson_set_multiple_field_properties"
echo "Usage: bejson_set_multiple_field_properties [file_path]"
return 1
fi
if ! _bejson_get_content_or_load "$file_path" content; then return 1; fi
if ! echo "$field_names_json" | jq empty 2>/dev/null; then
bejson_error "Invalid JSON format for field_names_json parameter"
return 1
fi
# Validate property_name
case "$property_name" in
"name"|"type")
;;
*)
bejson_error "Invalid property name: $property_name. Only 'name' or 'type' can be modified."
return 1
;;
esac
local updated_content="$content"
local success_count=0
local failure_count=0
# Attempt to parse new_property_value as JSON, if it fails, treat as string
local new_prop_val_json
if echo "$new_property_value" | jq empty 2>/dev/null; then
new_prop_val_json="$new_property_value"
else
new_prop_val_json=$(jq -n --arg val "$new_property_value" '$val') # Quote as string
fi
# Construct a single jq command for efficiency
local jq_command="."
# Read field names into a bash array
local field_names_array=()
while IFS=$'
' read -r line; do
field_names_array+=("$line")
done < <(echo "$field_names_json" | jq -r '.[]')
for field_name in "${field_names_array[@]}"; do
local field_index=$(echo "$updated_content" | jq --arg fname "$field_name" '[.Fields[].name] | index($fname)')
if [[ "$field_index" == "null" ]]; then
bejson_error "Field definition not found for name: $field_name. Skipping."
failure_count=$((failure_count + 1))
continue
fi
jq_command+=" | .Fields[$field_index].$property_name = $new_prop_val_json"
success_count=$((success_count + 1))
done
if [[ "$success_count" -gt 0 ]]; then
updated_content=$(echo "$content" | jq "$jq_command")
if [[ $? -eq 0 ]]; then
if ! _bejson_write_content_back "$file_path" "$updated_content"; then return 1; fi
bejson_success "$success_count field properties updated. $failure_count fields skipped."
return 0
else
bejson_error "Failed to update multiple field properties."
return 1
fi
else
bejson_error "No valid fields found to update properties."
return 1
fi
}
### FUNCTION: bejson_set_multiple_records ###
# Description: Replaces a contiguous range of records with new records.
# Parameters:
# $1 - Start index (0-based)
# $2 - End index (0-based, inclusive)
# $3 - New values JSON array of arrays (e.g., '[["val1", "val2"], ["val3", "val4"]]')
# $4 - (Optional) File path. If not provided, uses loaded content.
# Returns: 0 on success, 1 on failure.
# Example: bejson_set_multiple_records 0 1 '[["new_isbn_A", "Title A"], ["new_isbn_B", "Title B"]]' "data.json"
bejson_set_multiple_records() {
local start_index="$1"
local end_index="$2"
local new_values_json_array_of_arrays="$3"
local file_path="$4"
local content
if [[ -z "$start_index" || -z "$end_index" || -z "$new_values_json_array_of_arrays" ]]; then
bejson_error "Missing required parameters for bejson_set_multiple_records"
echo "Usage: bejson_set_multiple_records [file_path]"
return 1
fi
if ! _bejson_get_content_or_load "$file_path" content; then return 1; fi
# Validate new_values_json_array_of_arrays
if ! echo "$new_values_json_array_of_arrays" | jq empty 2>/dev/null; then
bejson_error "Invalid JSON format for new_values_json_array_of_arrays parameter"
return 1
fi
local total_records=$(echo "$content" | jq '.Values | length')
if [[ "$start_index" -lt 0 ]] || [[ "$end_index" -ge "$total_records" ]] || [[ "$start_index" -gt "$end_index" ]]; then
bejson_error "Invalid index range: $start_index to $end_index (total records: $total_records)"
return 1
fi
local fields_count=$(echo "$content" | jq '.Fields | length')
local new_records_count=$(echo "$new_values_json_array_of_arrays" | jq 'length')
if [[ "$((end_index - start_index + 1))" -ne "$new_records_count" ]]; then
bejson_error "Number of new records ($new_records_count) does not match the range size ($((end_index - start_index + 1)))."
return 1
fi
local i
for ((i=0; i [file_path]"
return 1
fi
if ! _bejson_get_content_or_load "$file_path" content; then return 1; fi
if ! echo "$record_indices_json" | jq empty 2>/dev/null; then
bejson_error "Invalid JSON format for record_indices_json parameter"
return 1
fi
local field_index=$(echo "$content" | jq --arg fname "$field_name" '[.Fields[].name] | index($fname)')
if [[ "$field_index" == "null" ]]; then
bejson_error "Field not found: $field_name"
return 1
fi
local total_records=$(echo "$content" | jq '.Values | length')
local updated_content="$content"
local success_count=0
local failure_count=0
# Attempt to parse new_value as JSON, if it fails, treat as string
local new_value_json
if echo "$new_value" | jq empty 2>/dev/null; then
new_value_json="$new_value"
else
new_value_json=$(jq -n --arg val "$new_value" '$val') # Quote as string
fi
local jq_command="."
local record_indices_array=()
while IFS=$'
' read -r line; do
record_indices_array+=("$line")
done < <(echo "$record_indices_json" | jq -r '.[]')
for ridx in "${record_indices_array[@]}"; do
if [[ "$ridx" -ge "$total_records" ]] || [[ "$ridx" -lt 0 ]]; then
bejson_error "Invalid record index: $ridx. Skipping."
failure_count=$((failure_count + 1))
continue
fi
jq_command+=" | .Values[$ridx][$field_index] = $new_value_json"
success_count=$((success_count + 1))
done
if [[ "$success_count" -gt 0 ]]; then
updated_content=$(echo "$content" | jq "$jq_command")
if [[ $? -eq 0 ]]; then
if ! _bejson_write_content_back "$file_path" "$updated_content"; then return 1; fi
bejson_success "$success_count values for field '$field_name' updated across multiple records. $failure_count records skipped."
return 0
else
bejson_error "Failed to update multiple values for field '$field_name'."
return 1
fi
else
bejson_error "No valid records found to update values for field '$field_name'."
return 1
fi
}
### FUNCTION: bejson_delete_record ###
# Description: Deletes a record at a specified index.
# Parameters:
# $1 - Record index (0-based)
# $2 - (Optional) File path. If not provided, uses loaded content.
# Returns: 0 on success, 1 on failure.
# Example: bejson_delete_record 1 "data.json"
bejson_delete_record() {
local record_index="$1"
local file_path="$2"
local content
if [[ -z "$record_index" ]]; then
bejson_error "Missing record index parameter for bejson_delete_record"
echo "Usage: bejson_delete_record [file_path]"
return 1
fi
if ! _bejson_get_content_or_load "$file_path" content; then return 1; fi
local total_records=$(echo "$content" | jq '.Values | length')
if [[ "$record_index" -ge "$total_records" ]] || [[ "$record_index" -lt 0 ]]; then
bejson_error "Invalid record index: $record_index (total records: $total_records)"
return 1
fi
local updated_content
updated_content=$(echo "$content" | jq --argjson idx "$record_index" 'del(.Values[$idx])')
if [[ $? -eq 0 ]]; then
if ! _bejson_write_content_back "$file_path" "$updated_content"; then return 1; fi
bejson_success "Record $record_index deleted."
return 0
else
bejson_error "Failed to delete record $record_index."
return 1
fi
}
################################################################################
### DATA SELECTION FUNCTIONS ###
################################################################################
# Global variable for selected records persistence
declare -g BEJSON_SELECTED_PERSIST_FILE=""
### FUNCTION: bejson_select ###
# Description: Selects records based on a jq filter expression. The filter is applied
# to each item in the '.Values' array.
# Parameters:
# $1 - jq filter expression (e.g., 'select(.[1] == "Some Title")' or 'select(.[0] > 100)')
# $2 - (Optional) File path. If not provided, uses loaded content.
# Returns: Prints the matching records as a JSON array. 0 on success, 1 on failure.
# Example: bejson_select 'select(.[1] == "The C++ Programming Language")' "data.json"
bejson_select() {
local jq_filter_expression="$1"
local file_path="$2"
local content
if [[ -z "$jq_filter_expression" ]]; then
bejson_error "Missing jq filter expression parameter for bejson_select"
echo "Usage: bejson_select [file_path]"
return 1
fi
if ! _bejson_get_content_or_load "$file_path" content; then return 1; fi
local result
# Apply the filter to each element of the .Values array and collect results
result=$(echo "$content" | jq -c "[.Values[] | $jq_filter_expression]")
if [[ $? -eq 0 ]]; then
echo "$result"
return 0
else
bejson_error "jq filter failed. Check expression: $jq_filter_expression"
return 1
fi
}
### FUNCTION: bejson_select_persist ###
# Description: Selects records using a jq filter and stores them in a temporary BEJSON file.
# The temporary file will contain a valid BEJSON 104 structure with the selected records.
# Parameters:
# $1 - jq filter expression (e.g., 'select(.[2] > 2020)')
# $2 - (Optional) File path to the source BEJSON file. If not provided, uses loaded content.
# $3 - (Optional) Output temporary file path. If not provided, a unique temp file is generated.
# Returns: Prints the path to the temporary BEJSON file. 0 on success, 1 on failure.
# Side Effects: Sets BEJSON_SELECTED_PERSIST_FILE global variable.
# Example: temp_file=$(bejson_select_persist 'select(.[1] == "The C++ Programming Language")' "data.json")
bejson_select_persist() {
local jq_filter_expression="$1"
local source_file_path="$2"
local output_temp_file="${3:-$(mktemp /tmp/bejson_selected_XXXXXX.json)}"
local content
if [[ -z "$jq_filter_expression" ]]; then
bejson_error "Missing jq filter expression parameter for bejson_select_persist"
echo "Usage: bejson_select_persist [source_file_path] [output_temp_file]"
return 1
fi
if ! _bejson_get_content_or_load "$source_file_path" content; then return 1; fi
local selected_values
selected_values=$(echo "$content" | jq -c "[.Values[] | $jq_filter_expression]")
if [[ $? -ne 0 ]]; then
bejson_error "jq filter failed during selection for persistence. Check expression: $jq_filter_expression"
return 1
fi
# Get original metadata
local format_data=$(echo "$content" | jq -c '{
"Format": .Format,
"Format_Version": .Format_Version,
"Format_Creator": .Format_Creator,
"Parent_Hierarchy": .Parent_Hierarchy // null,
"Records_Type": .Records_Type,
"Fields": .Fields
}')
# Construct the new BEJSON content with selected values
local new_bejson_content
new_bejson_content=$(echo "$format_data" | jq --argjson selected_vals "$selected_values" '.Values = $selected_vals')
if [[ $? -ne 0 ]]; then
bejson_error "Failed to construct new BEJSON content for persistence."
return 1
fi
echo "$new_bejson_content" > "$output_temp_file"
if [[ $? -eq 0 ]]; then
BEJSON_SELECTED_PERSIST_FILE="$output_temp_file"
bejson_success "Selected records persisted to temporary file: $output_temp_file"
echo "$output_temp_file"
return 0
else
bejson_error "Failed to write selected records to temporary file: $output_temp_file"
return 1
fi
}
### FUNCTION: bejson_remove_selected_persist ###
# Description: Deletes the temporary file created by bejson_select_persist.
# Parameters:
# $1 - (Optional) Path to the temporary file. If not provided, uses the globally stored path.
# Returns: 0 on success, 1 on failure.
# Example: bejson_remove_selected_persist
# Example: bejson_remove_selected_persist "/tmp/my_temp_file.json"
bejson_remove_selected_persist() {
local temp_file_path="${1:-$BEJSON_SELECTED_PERSIST_FILE}"
if [[ -z "$temp_file_path" ]]; then
bejson_error "No temporary file path provided or globally stored."
return 1
fi
if [[ -f "$temp_file_path" ]]; then
rm "$temp_file_path"
if [[ $? -eq 0 ]]; then
if [[ "$temp_file_path" == "$BEJSON_SELECTED_PERSIST_FILE" ]]; then
BEJSON_SELECTED_PERSIST_FILE="" # Clear global if it was the one deleted
fi
bejson_success "Temporary file removed: $temp_file_path"
return 0
else
bejson_error "Failed to remove temporary file: $temp_file_path"
return 1
fi
else
bejson_error "Temporary file not found: $temp_file_path"
return 1
fi
}
################################################################################
### LIBRARY INITIALIZATION ###
################################################################################
# Check dependencies on library load
if ! bejson_check_dependencies; then
bejson_error "Library initialization failed due to missing dependencies"
return 1 2>/dev/null || exit 1
fi
# Print library loaded message
echo "BEJSON 104 Library v1.0.0 loaded successfully"
echo "Use 'bejson_' prefix to access library functions"
echo "Example: bejson_create_file, bejson_load_file, bejson_validate_file"