You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

912 lines
26 KiB

#!/bin/bash
# ==============================================================================
# Bash INI Parser Library
# ==============================================================================
# A lightweight library for manipulating INI configuration files in Bash scripts
#
# Author: Leandro Ferreira (https://leandrosf.com)
# Version: 0.0.1
# License: BSD
# GitHub: https://github.com/lsferreira42
# ==============================================================================
# Configuration
# These variables can be overridden by setting environment variables with the same name
# For example: export INI_DEBUG=1 before sourcing this library
INI_DEBUG=${INI_DEBUG:-0} # Set to 1 to enable debug messages
INI_STRICT=${INI_STRICT:-0} # Set to 1 for strict validation of section/key names
INI_ALLOW_EMPTY_VALUES=${INI_ALLOW_EMPTY_VALUES:-1} # Set to 1 to allow empty values
INI_ALLOW_SPACES_IN_NAMES=${INI_ALLOW_SPACES_IN_NAMES:-1} # Set to 1 to allow spaces in section/key names
# ==============================================================================
# Utility Functions
# ==============================================================================
# Print debug messages
function ini_debug() {
if [ "${INI_DEBUG}" -eq 1 ]; then
echo "[DEBUG] $1" >&2
fi
}
# Print error messages
function ini_error() {
echo "[ERROR] $1" >&2
}
# Validate section name
function ini_validate_section_name() {
local section="$1"
if [ -z "$section" ]; then
ini_error "Section name cannot be empty"
return 1
fi
if [ "${INI_STRICT}" -eq 1 ]; then
# Check for illegal characters in section name
if [[ "$section" =~ [\[\]\=] ]]; then
ini_error "Section name contains illegal characters: $section"
return 1
fi
fi
if [ "${INI_ALLOW_SPACES_IN_NAMES}" -eq 0 ] && [[ "$section" =~ [[:space:]] ]]; then
ini_error "Section name contains spaces: $section"
return 1
fi
return 0
}
# Validate key name
function ini_validate_key_name() {
local key="$1"
if [ -z "$key" ]; then
ini_error "Key name cannot be empty"
return 1
fi
if [ "${INI_STRICT}" -eq 1 ]; then
# Check for illegal characters in key name
if [[ "$key" =~ [\[\]\=] ]]; then
ini_error "Key name contains illegal characters: $key"
return 1
fi
fi
if [ "${INI_ALLOW_SPACES_IN_NAMES}" -eq 0 ] && [[ "$key" =~ [[:space:]] ]]; then
ini_error "Key name contains spaces: $key"
return 1
fi
return 0
}
# Create a secure temporary file
function ini_create_temp_file() {
mktemp "${TMPDIR:-/tmp}/ini_XXXXXXXXXX"
}
# Trim whitespace from start and end of a string
function ini_trim() {
local var="$*"
# Remove leading whitespace
var="${var#"${var%%[![:space:]]*}"}"
# Remove trailing whitespace
var="${var%"${var##*[![:space:]]}"}"
echo "$var"
}
# Escape special characters in a string for regex matching
function ini_escape_for_regex() {
echo "$1" | sed -e 's/[]\/()$*.^|[]/\\&/g'
}
# ==============================================================================
# File Operations
# ==============================================================================
function ini_check_file() {
local file="$1"
# Check if file parameter is provided
if [ -z "$file" ]; then
ini_error "File path is required"
return 1
fi
# Check if file exists
if [ ! -f "$file" ]; then
ini_debug "File does not exist, attempting to create: $file"
# Create directory if it doesn't exist
local dir
dir=$(dirname "$file")
if [ ! -d "$dir" ]; then
mkdir -p "$dir" 2>/dev/null || {
ini_error "Could not create directory: $dir"
return 1
}
fi
# Create the file
if ! touch "$file" 2>/dev/null; then
ini_error "Could not create file: $file"
return 1
fi
ini_debug "File created successfully: $file"
fi
# Check if file is writable
if [ ! -w "$file" ]; then
ini_error "File is not writable: $file"
return 1
fi
return 0
}
# ==============================================================================
# Core Functions
# ==============================================================================
function ini_read() {
local file="$1"
local section="$2"
local key="$3"
# Validate parameters
if [ -z "$file" ] || [ -z "$section" ] || [ -z "$key" ]; then
ini_error "ini_read: Missing required parameters"
return 1
fi
# Validate section and key names only if strict mode is enabled
if [ "${INI_STRICT}" -eq 1 ]; then
ini_validate_section_name "$section" || return 1
ini_validate_key_name "$key" || return 1
fi
# Check if file exists
if [ ! -f "$file" ]; then
ini_error "File not found: $file"
return 1
fi
# Escape section and key for regex pattern
local escaped_section
escaped_section=$(ini_escape_for_regex "$section")
local escaped_key
escaped_key=$(ini_escape_for_regex "$key")
local section_pattern="^\[$escaped_section\]"
local in_section=0
ini_debug "Reading key '$key' from section '$section' in file: $file"
while IFS= read -r line; do
# Skip comments and empty lines
if [[ -z "$line" || "$line" =~ ^[[:space:]]*[#\;] ]]; then
continue
fi
# Check for section
if [[ "$line" =~ $section_pattern ]]; then
in_section=1
ini_debug "Found section: $section"
continue
fi
# Check if we've moved to a different section
if [[ $in_section -eq 1 && "$line" =~ ^\[[^]]+\] ]]; then
ini_debug "Reached end of section without finding key"
return 1
fi
# Check for key in the current section
if [[ $in_section -eq 1 ]]; then
local key_pattern="^[[:space:]]*${escaped_key}[[:space:]]*="
if [[ "$line" =~ $key_pattern ]]; then
local value="${line#*=}"
# Trim whitespace
value=$(ini_trim "$value")
# Check for quoted values
if [[ "$value" =~ ^\"(.*)\"$ ]]; then
# Remove the quotes
value="${BASH_REMATCH[1]}"
# Handle escaped quotes within the value
value="${value//\\\"/\"}"
fi
ini_debug "Found value: $value"
echo "$value"
return 0
fi
fi
done < "$file"
ini_debug "Key not found: $key in section: $section"
return 1
}
function ini_list_sections() {
local file="$1"
# Validate parameters
if [ -z "$file" ]; then
ini_error "ini_list_sections: Missing file parameter"
return 1
fi
# Check if file exists
if [ ! -f "$file" ]; then
ini_error "File not found: $file"
return 1
fi
ini_debug "Listing sections in file: $file"
# Extract section names
grep -o '^\[[^]]*\]' "$file" 2>/dev/null | sed 's/^\[\(.*\)\]$/\1/'
return 0
}
function ini_list_keys() {
local file="$1"
local section="$2"
# Validate parameters
if [ -z "$file" ] || [ -z "$section" ]; then
ini_error "ini_list_keys: Missing required parameters"
return 1
fi
# Validate section name only if strict mode is enabled
if [ "${INI_STRICT}" -eq 1 ]; then
ini_validate_section_name "$section" || return 1
fi
# Check if file exists
if [ ! -f "$file" ]; then
ini_error "File not found: $file"
return 1
fi
# Escape section for regex pattern
local escaped_section
escaped_section=$(ini_escape_for_regex "$section")
local section_pattern="^\[$escaped_section\]"
local in_section=0
ini_debug "Listing keys in section '$section' in file: $file"
while IFS= read -r line; do
# Skip comments and empty lines
if [[ -z "$line" || "$line" =~ ^[[:space:]]*[#\;] ]]; then
continue
fi
# Check for section
if [[ "$line" =~ $section_pattern ]]; then
in_section=1
ini_debug "Found section: $section"
continue
fi
# Check if we've moved to a different section
if [[ $in_section -eq 1 && "$line" =~ ^\[[^]]+\] ]]; then
break
fi
# Extract key name from current section
if [[ $in_section -eq 1 && "$line" =~ ^[[:space:]]*[^=]+= ]]; then
local key="${line%%=*}"
key=$(ini_trim "$key")
ini_debug "Found key: $key"
echo "$key"
fi
done < "$file"
return 0
}
function ini_section_exists() {
local file="$1"
local section="$2"
# Validate parameters
if [ -z "$file" ] || [ -z "$section" ]; then
ini_error "ini_section_exists: Missing required parameters"
return 1
fi
# Validate section name only if strict mode is enabled
if [ "${INI_STRICT}" -eq 1 ]; then
ini_validate_section_name "$section" || return 1
fi
# Check if file exists
if [ ! -f "$file" ]; then
ini_debug "File not found: $file"
return 1
fi
# Escape section for regex pattern
local escaped_section
escaped_section=$(ini_escape_for_regex "$section")
ini_debug "Checking if section '$section' exists in file: $file"
# Check if section exists
grep -q "^\[$escaped_section\]" "$file"
local result=$?
if [ $result -eq 0 ]; then
ini_debug "Section found: $section"
else
ini_debug "Section not found: $section"
fi
return $result
}
function ini_add_section() {
local file="$1"
local section="$2"
# Validate parameters
if [ -z "$file" ] || [ -z "$section" ]; then
ini_error "ini_add_section: Missing required parameters"
return 1
fi
# Validate section name only if strict mode is enabled
if [ "${INI_STRICT}" -eq 1 ]; then
ini_validate_section_name "$section" || return 1
fi
# Check and create file if needed
ini_check_file "$file" || return 1
# Check if section already exists
if ini_section_exists "$file" "$section"; then
ini_debug "Section already exists: $section"
return 0
fi
ini_debug "Adding section '$section' to file: $file"
# Add a newline if file is not empty
if [ -s "$file" ]; then
echo "" >> "$file"
fi
# Add the section
echo "[$section]" >> "$file"
return 0
}
function ini_write() {
local file="$1"
local section="$2"
local key="$3"
local value="$4"
# Validate parameters
if [ -z "$file" ] || [ -z "$section" ] || [ -z "$key" ]; then
ini_error "ini_write: Missing required parameters"
return 1
fi
# Validate section and key names only if strict mode is enabled
if [ "${INI_STRICT}" -eq 1 ]; then
ini_validate_section_name "$section" || return 1
ini_validate_key_name "$key" || return 1
fi
# Check for empty value if not allowed
if [ -z "$value" ] && [ "${INI_ALLOW_EMPTY_VALUES}" -eq 0 ]; then
ini_error "Empty values are not allowed"
return 1
fi
# Check and create file if needed
ini_check_file "$file" || return 1
# Create section if it doesn't exist
ini_add_section "$file" "$section" || return 1
# Escape section and key for regex pattern
local escaped_section
escaped_section=$(ini_escape_for_regex "$section")
local escaped_key
escaped_key=$(ini_escape_for_regex "$key")
local section_pattern="^\[$escaped_section\]"
local key_pattern="^[[:space:]]*${escaped_key}[[:space:]]*="
local in_section=0
local found_key=0
local temp_file
temp_file=$(ini_create_temp_file)
ini_debug "Writing key '$key' with value '$value' to section '$section' in file: $file"
# Special handling for values with quotes or special characters
if [ "${INI_STRICT}" -eq 1 ] && [[ "$value" =~ [[:space:]\"\'\`\&\|\<\>\;\$] ]]; then
value="\"${value//\"/\\\"}\""
ini_debug "Value contains special characters, quoting: $value"
fi
# Process the file line by line
while IFS= read -r line || [ -n "$line" ]; do
# Check for section
if [[ "$line" =~ $section_pattern ]]; then
in_section=1
echo "$line" >> "$temp_file"
continue
fi
# Check if we've moved to a different section
if [[ $in_section -eq 1 && "$line" =~ ^\[[^]]+\] ]]; then
# Add the key-value pair if we haven't found it yet
if [ $found_key -eq 0 ]; then
echo "$key=$value" >> "$temp_file"
found_key=1
fi
in_section=0
fi
# Update the key if it exists in the current section
if [[ $in_section -eq 1 && "$line" =~ $key_pattern ]]; then
echo "$key=$value" >> "$temp_file"
found_key=1
continue
fi
# Write the line to the temp file
echo "$line" >> "$temp_file"
done < "$file"
# Add the key-value pair if we're still in the section and haven't found it
if [ $in_section -eq 1 ] && [ $found_key -eq 0 ]; then
echo "$key=$value" >> "$temp_file"
fi
# Use atomic operation to replace the original file
mv "$temp_file" "$file"
ini_debug "Successfully wrote key '$key' with value '$value' to section '$section'"
return 0
}
function ini_remove_section() {
local file="$1"
local section="$2"
# Validate parameters
if [ -z "$file" ] || [ -z "$section" ]; then
ini_error "ini_remove_section: Missing required parameters"
return 1
fi
# Validate section name only if strict mode is enabled
if [ "${INI_STRICT}" -eq 1 ]; then
ini_validate_section_name "$section" || return 1
fi
# Check if file exists
if [ ! -f "$file" ]; then
ini_error "File not found: $file"
return 1
fi
# Escape section for regex pattern
local escaped_section
escaped_section=$(ini_escape_for_regex "$section")
local section_pattern="^\[$escaped_section\]"
local in_section=0
local temp_file
temp_file=$(ini_create_temp_file)
ini_debug "Removing section '$section' from file: $file"
# Process the file line by line
while IFS= read -r line; do
# Check for section
if [[ "$line" =~ $section_pattern ]]; then
in_section=1
continue
fi
# Check if we've moved to a different section
if [[ $in_section -eq 1 && "$line" =~ ^\[[^]]+\] ]]; then
in_section=0
fi
# Write the line to the temp file if not in the section to be removed
if [ $in_section -eq 0 ]; then
echo "$line" >> "$temp_file"
fi
done < "$file"
# Use atomic operation to replace the original file
mv "$temp_file" "$file"
ini_debug "Successfully removed section '$section'"
return 0
}
function ini_remove_key() {
local file="$1"
local section="$2"
local key="$3"
# Validate parameters
if [ -z "$file" ] || [ -z "$section" ] || [ -z "$key" ]; then
ini_error "ini_remove_key: Missing required parameters"
return 1
fi
# Validate section and key names only if strict mode is enabled
if [ "${INI_STRICT}" -eq 1 ]; then
ini_validate_section_name "$section" || return 1
ini_validate_key_name "$key" || return 1
fi
# Check if file exists
if [ ! -f "$file" ]; then
ini_error "File not found: $file"
return 1
fi
# Escape section and key for regex pattern
local escaped_section
escaped_section=$(ini_escape_for_regex "$section")
local escaped_key
escaped_key=$(ini_escape_for_regex "$key")
local section_pattern="^\[$escaped_section\]"
local key_pattern="^[[:space:]]*${escaped_key}[[:space:]]*="
local in_section=0
local temp_file
temp_file=$(ini_create_temp_file)
ini_debug "Removing key '$key' from section '$section' in file: $file"
# Process the file line by line
while IFS= read -r line; do
# Check for section
if [[ "$line" =~ $section_pattern ]]; then
in_section=1
echo "$line" >> "$temp_file"
continue
fi
# Check if we've moved to a different section
if [[ $in_section -eq 1 && "$line" =~ ^\[[^]]+\] ]]; then
in_section=0
fi
# Skip the key to be removed
if [[ $in_section -eq 1 && "$line" =~ $key_pattern ]]; then
continue
fi
# Write the line to the temp file
echo "$line" >> "$temp_file"
done < "$file"
# Use atomic operation to replace the original file
mv "$temp_file" "$file"
ini_debug "Successfully removed key '$key' from section '$section'"
return 0
}
# ==============================================================================
# Extended Functions
# ==============================================================================
function ini_get_or_default() {
local file="$1"
local section="$2"
local key="$3"
local default_value="$4"
# Validate parameters
if [ -z "$file" ] || [ -z "$section" ] || [ -z "$key" ]; then
ini_error "ini_get_or_default: Missing required parameters"
return 1
fi
# Try to read the value
local value
value=$(ini_read "$file" "$section" "$key" 2>/dev/null)
local result=$?
# Return the value or default
if [ $result -eq 0 ]; then
echo "$value"
else
echo "$default_value"
fi
return 0
}
function ini_import() {
local source_file="$1"
local target_file="$2"
local import_sections=("${@:3}")
# Validate parameters
if [ -z "$source_file" ] || [ -z "$target_file" ]; then
ini_error "ini_import: Missing required parameters"
return 1
fi
# Check if source file exists
if [ ! -f "$source_file" ]; then
ini_error "Source file not found: $source_file"
return 1
fi
# Check and create target file if needed
ini_check_file "$target_file" || return 1
ini_debug "Importing from '$source_file' to '$target_file'"
# Get sections from source file
local sections
sections=$(ini_list_sections "$source_file")
# Loop through sections
for section in $sections; do
# Skip if specific sections are provided and this one is not in the list
if [ ${#import_sections[@]} -gt 0 ] && ! [[ ${import_sections[*]} =~ $section ]]; then
ini_debug "Skipping section: $section"
continue
fi
ini_debug "Importing section: $section"
# Add the section to the target file
ini_add_section "$target_file" "$section"
# Get keys in this section
local keys
keys=$(ini_list_keys "$source_file" "$section")
# Loop through keys
for key in $keys; do
# Read the value and write it to the target file
local value
value=$(ini_read "$source_file" "$section" "$key")
ini_write "$target_file" "$section" "$key" "$value"
done
done
ini_debug "Import completed successfully"
return 0
}
function ini_to_env() {
local file="$1"
local prefix="$2"
local section="$3"
# Validate parameters
if [ -z "$file" ]; then
ini_error "ini_to_env: Missing file parameter"
return 1
fi
# Check if file exists
if [ ! -f "$file" ]; then
ini_error "File not found: $file"
return 1
fi
ini_debug "Exporting INI values to environment variables with prefix: $prefix"
# If section is specified, only export keys from that section
if [ -n "$section" ]; then
if [ "${INI_STRICT}" -eq 1 ]; then
ini_validate_section_name "$section" || return 1
fi
local keys
keys=$(ini_list_keys "$file" "$section")
for key in $keys; do
local value
value=$(ini_read "$file" "$section" "$key")
# Export the variable with the given prefix
if [ -n "$prefix" ]; then
export "${prefix}_${section}_${key}=${value}"
else
export "${section}_${key}=${value}"
fi
done
else
# Export keys from all sections
local sections
sections=$(ini_list_sections "$file")
for section in $sections; do
local keys
keys=$(ini_list_keys "$file" "$section")
for key in $keys; do
local value
value=$(ini_read "$file" "$section" "$key")
# Export the variable with the given prefix
if [ -n "$prefix" ]; then
export "${prefix}_${section}_${key}=${value}"
else
export "${section}_${key}=${value}"
fi
done
done
fi
ini_debug "Environment variables set successfully"
return 0
}
function ini_key_exists() {
local file="$1"
local section="$2"
local key="$3"
# Validate parameters
if [ -z "$file" ] || [ -z "$section" ] || [ -z "$key" ]; then
ini_error "ini_key_exists: Missing required parameters"
return 1
fi
# Validate section and key names only if strict mode is enabled
if [ "${INI_STRICT}" -eq 1 ]; then
ini_validate_section_name "$section" || return 1
ini_validate_key_name "$key" || return 1
fi
# Check if file exists
if [ ! -f "$file" ]; then
ini_debug "File not found: $file"
return 1
fi
# First check if section exists
if ! ini_section_exists "$file" "$section"; then
ini_debug "Section not found: $section"
return 1
fi
# Check if key exists by trying to read it
if ini_read "$file" "$section" "$key" >/dev/null 2>&1; then
ini_debug "Key found: $key in section: $section"
return 0
else
ini_debug "Key not found: $key in section: $section"
return 1
fi
}
# ==============================================================================
# Array Functions
# ==============================================================================
function ini_read_array() {
local file="$1"
local section="$2"
local key="$3"
# Validate parameters
if [ -z "$file" ] || [ -z "$section" ] || [ -z "$key" ]; then
ini_error "ini_read_array: Missing required parameters"
return 1
fi
# Read the value
local value
value=$(ini_read "$file" "$section" "$key") || return 1
# Split the array by commas
# We need to handle quoted values properly
local -a result=()
local in_quotes=0
local current_item=""
for (( i=0; i<${#value}; i++ )); do
local char="${value:$i:1}"
# Handle quotes
if [ "$char" = '"' ]; then
# shellcheck disable=SC1003
# Check if the quote is escaped
if [ $i -gt 0 ] && [ "${value:$((i-1)):1}" = "\\" ]; then
# It's an escaped quote, keep it
current_item="${current_item:0:-1}$char"
else
# Toggle quote state
in_quotes=$((1 - in_quotes))
fi
# Handle comma separator
elif [ "$char" = ',' ] && [ $in_quotes -eq 0 ]; then
# End of an item
result+=("$(ini_trim "$current_item")")
current_item=""
else
# Add character to current item
current_item="$current_item$char"
fi
done
# Add the last item
if [ -n "$current_item" ] || [ ${#result[@]} -gt 0 ]; then
result+=("$(ini_trim "$current_item")")
fi
# Output the array items, one per line
for item in "${result[@]}"; do
echo "$item"
done
return 0
}
function ini_write_array() {
local file="$1"
local section="$2"
local key="$3"
shift 3
local -a array_values=("$@")
# Validate parameters
if [ -z "$file" ] || [ -z "$section" ] || [ -z "$key" ]; then
ini_error "ini_write_array: Missing required parameters"
return 1
fi
# Process array values and handle quoting
local array_string=""
local first=1
for value in "${array_values[@]}"; do
# Add comma separator if not the first item
if [ $first -eq 0 ]; then
array_string="$array_string,"
else
first=0
fi
# Quote values with spaces or special characters
if [[ "$value" =~ [[:space:],\"] ]]; then
# Escape quotes
value="${value//\"/\\\"}"
array_string="$array_string\"$value\""
else
array_string="$array_string$value"
fi
done
# Write the array string to the ini file
ini_write "$file" "$section" "$key" "$array_string"
return $?
}
# Load additional modules if defined
if [ -n "${INI_MODULES_DIR:-}" ] && [ -d "${INI_MODULES_DIR}" ]; then
for module in "${INI_MODULES_DIR}"/*.sh; do
if [ -f "$module" ] && [ -r "$module" ]; then
# shellcheck disable=SC1090,SC1091
source "$module"
fi
done
fi