blob: 3522ee1331526d198a3804b8b4308e26666af30f [file] [log] [blame] [edit]
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-2.0
#
# XML Manipulation Library using xmlstarlet
#
# Usage:
# source path/to/xml_util.sh
#
# # Scenario 1: Create a new file
# xml_util::init "/path/to/new.xml" "bisect"
#
# # Scenario 2: Load an existing file
# xml_util::load "/path/to/existing.xml"
#
# --- Include Guard---
if [[ -n "${__XML_UTIL_SOURCED__:-}" ]]; then
return 0
fi
readonly __XML_UTIL_SOURCED__=1
# --- Dependencies ---
if [[ -z "${__COMMON_LIB_SOURCED__:-}" ]]; then
_XML_UTIL_SCRIPT_PATH="$(realpath "${BASH_SOURCE[0]}")"
_XML_UTIL_SCRIPT_DIR="$(dirname "${_XML_UTIL_SCRIPT_PATH}")"
_COMMON_LIB_PATH="${_XML_UTIL_SCRIPT_DIR}/../common_lib.sh"
if [[ ! -f "$_COMMON_LIB_PATH" ]]; then
echo "FATAL ERROR (xml_util): Cannot find required library '$_COMMON_LIB_PATH'" >&2
return 1
fi
if ! source "$_COMMON_LIB_PATH"; then
echo "FATAL ERROR (xml_util): Failed to source library '$_COMMON_LIB_PATH'" >&2
return 1
fi
fi
if ! command -v xmlstarlet &> /dev/null; then
log_error "Required command 'xmlstarlet' not found."
return 1
fi
# --- Internal State ---
_XML_UTIL_DEFAULT_FILE=""
# --- Public Functions ---
function xml_util::init() {
local file_path="$1"
local root_tag="${2:-root}" # Default to "root" if not specified
if [[ -z "$file_path" ]]; then
log_error "No file path provided."
return 1
fi
# Check if file already exists -> Error out
if [[ -f "$file_path" ]]; then
log_error "File already exists: $file_path. Use xml_util::load to use an existing file."
return 1
fi
local dir_name
dir_name=$(dirname "$file_path")
if [[ ! -d "$dir_name" ]]; then
log_info "Creating directory $dir_name..."
mkdir -p "$dir_name"
fi
log_info "Creating new XML file at $file_path with root tag <$root_tag/>..."
echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?><${root_tag}/>" > "$file_path"
# Validate the newly created file
if ! xmlstarlet val -q "$file_path"; then
log_error "Internal Error. Failed to create a valid XML file at '$file_path'."
return 1
fi
_XML_UTIL_DEFAULT_FILE="$file_path"
log_info "XML library context set to new file: ${file_path}"
}
function xml_util::load() {
local file_path="$1"
_xml_util::check_file "$file_path" || return 1
# Validate XML format
if ! xmlstarlet val -q "$file_path"; then
log_error "Validation failed. '$file_path' is not a valid XML file."
return 1
fi
_XML_UTIL_DEFAULT_FILE="$file_path"
log_info "XML library context loaded from: ${file_path}"
}
function _xml_util::check_file() {
local file="$1"
if [[ -z "$file" ]]; then
log_error "XML operation failed: No XML file specified (and no default set via xml_util::load/xml_util::init)."
return 1
fi
if [[ ! -f "$file" ]]; then
log_error "XML operation failed: File not found: $file"
return 1
fi
return 0
}
function xml_util::read_value() {
local xpath="$1"
local file="${2:-$_XML_UTIL_DEFAULT_FILE}"
_xml_util::check_file "$file" || return 1
xmlstarlet sel -t -v "$xpath" "$file" 2>/dev/null
}
function xml_util::read_values_to_array() {
local xpath="$1"
local -n _dest_array_ref="$2"
local file="${3:-$_XML_UTIL_DEFAULT_FILE}"
_xml_util::check_file "$file" || return 1
mapfile -t _dest_array_ref < <(xmlstarlet sel -t -v "$xpath" -n "$file" 2>/dev/null)
}
function xml_util::read_attributes_to_array() {
local xpath="$1"
local -n _dest_array_ref="$2"
local file="${3:-$_XML_UTIL_DEFAULT_FILE}"
if [[ "$xpath" != *"@"* ]]; then
log_error "Invalid XPath '$xpath'. Attribute XPath must contain '@'."
return 1
fi
_xml_util::check_file "$file" || return 1
mapfile -t _dest_array_ref < <(xmlstarlet sel -t -v "$xpath" -n "$file" 2>/dev/null)
}
function xml_util::update_xml_node() {
local xpath_expr="$1"
local value="$2"
local file="${3:-$_XML_UTIL_DEFAULT_FILE}"
_xml_util::check_file "$file" || return 1
log_info "Updating XML node in $(basename "$file"): $xpath_expr -> $value"
xmlstarlet ed -L -u "$xpath_expr" -v "$value" "$file"
}
function xml_util::update_xml_attribute() {
local xpath_expr="$1"
local attr_name="$2"
local value="$3"
local file="${4:-$_XML_UTIL_DEFAULT_FILE}"
_xml_util::check_file "$file" || return 1
log_info "Updating XML attribute in $(basename "$file"): $xpath_expr @$attr_name -> $value"
# Delete the attribute first to avoid errors if it doesn't exist, then insert it.
xmlstarlet ed -L \
-d "${xpath_expr}/@${attr_name}" \
-i "$xpath_expr" -t "attr" -n "$attr_name" -v "$value" \
"$file"
}
# --- Construction Helpers (Command Builders) ---
# These functions append arguments to a command array for efficient batch editing.
# They do not perform file I/O directly.
function xml_util::add_node() {
local -n _cmd_array_ref="$1"
local parent_xpath="$2"
local element_name="$3"
_cmd_array_ref+=(-s "$parent_xpath" -t elem -n "$element_name")
}
function xml_util::add_attribute() {
local -n _cmd_array_ref="$1"
local parent_xpath="$2"
local attr_name="$3"
local attr_value="$4"
_cmd_array_ref+=(-i "$parent_xpath" -t attr -n "$attr_name" -v "$attr_value")
}
function xml_util::add_element() {
local -n _cmd_array_ref="$1"
local parent_xpath="$2"
local element_name="$3"
local element_value="$4"
_cmd_array_ref+=(-s "$parent_xpath" -t elem -n "$element_name" -v "$element_value")
}
# NOTE: use '__cmd_array_ref' to avoid "circular reference" if the caller passes a variable '_cmd_array_ref'.
function xml_util::add_element_with_attr() {
local -n __cmd_array_ref="$1"
local parent_xpath="$2"
local el_name="$3"
local el_val="$4"
local attr_name="$5"
local attr_val="$6"
# Add the element
xml_util::add_element __cmd_array_ref "$parent_xpath" "$el_name" "$el_val"
# Add the attribute to the newly created element (using last() to target it)
xml_util::add_attribute __cmd_array_ref "${parent_xpath}/${el_name}[last()]" "$attr_name" "$attr_val"
}