-
Hahn Axel (hahn) authoredHahn Axel (hahn) authored
ini.class.sh 15.60 KiB
#!/bin/bash
# ======================================================================
#
# READ INI FILE with Bash
# https://axel-hahn.de/blog/2018/06/08/bash-ini-dateien-parsen-lesen/
#
# Author: Axel hahn
# License: GNU GPL 3.0
# Source: https://github.com/axelhahn/bash_iniparser
# Docs: https://www.axel-hahn.de/docs/bash_iniparser/
#
# ----------------------------------------------------------------------
# TODO
# - ini.validate:
# - handle non existing validation rules
# - define all vars as local
# - detect invalid lines (all except sections, values, comments, empty lines)
# - ini.export:
# - detect unsafe entries $() and backticks
# - ini.value:
# - detect comment after value
# ----------------------------------------------------------------------
# 2024-02-04 v0.1 Initial version
# 2024-02-08 v0.2 add ini.varexport; improve replacements of quotes
# 2024-02-10 v0.3 handle spaces and tabs around vars and values
# 2024-02-12 v0.4 rename local varables
# 2024-02-20 v0.5 handle special chars in keys; add ini.dump + ini.help
# 2024-02-21 v0.6 harden ini.value for keys with special chars; fix fetching last value
# 2024-03-17 v0.7 add ini.validate
# 2024-03-17 v0.8 errors are written to STDERR; update help; use [[:space:]] in regex; update help
# 2024-03-18 v0.9 validator improvements (but ini validate is not rock solid yet)
# ======================================================================
INI_FILE=
INI_SECTION=
# ----------------------------------------------------------------------
# SETTER
# ----------------------------------------------------------------------
# Set the INI file - and optional section - for short calls.
# param string filename
# param string optional: section
function ini.set(){
INI_FILE=
INI_SECTION=
if [ ! -f "$1" ]; then
echo "ERROR: file does not exist: $1" >&2
exit 1
fi
INI_FILE="$1"
test -n "$2" && ini.setsection "$2"
}
# Set the INI section for short calls.
# param string section
function ini.setsection(){
if [ -z "$INI_FILE" ]; then
echo "ERROR: ini file needs to be set first. Use ini.set <INIFILE> [<SECTION>]." >&2
exit 1
fi
if [ -n "$1" ]; then
if ini.sections "$INI_FILE" | grep "^${1}$" >/dev/null; then
INI_SECTION=$1
else
echo "ERROR: Section [$1] does not exist in [$INI_FILE]." >&2
exit 1
fi
fi
}
# ----------------------------------------------------------------------
# GETTER
# ----------------------------------------------------------------------
# Get all sections
# param1 - name of the ini file
function ini.sections(){
local myinifile=${1:-$INI_FILE}
grep "^\[" "$myinifile" | sed 's,^\[,,' | sed 's,\].*,,'
}
# Get all content inside a section
# param1 - name of the ini file
# param2 - name of the section in ini file
function ini.section(){
local myinifile=${1:-$INI_FILE}
local myinisection=${2:-$INI_SECTION}
sed -e "0,/^\[${myinisection}\]/ d" -e '/^\[/,$ d' "$myinifile" \
| grep -v "^[#;]" \
| sed -e "s/^[ \t]*//g" -e "s/[ \t]*=[ \t]*/=/g"
}
# Get all keys inside a section
# param1 - name of the ini file
# param2 - name of the section in ini file
function ini.keys(){
local myinifile=${1:-$INI_FILE}
local myinisection=${2:-$INI_SECTION}
ini.section "${myinifile}" "${myinisection}" \
| grep "^[\ \t]*[^=]" \
| cut -f 1 -d "=" \
| sort -u
}
# Get a value of a variable in a given section
# param1 - name of the ini file
# param2 - name of the section in ini file
# param3 - name of the variable to read
function ini.value(){
if [ -n "$2" ] && [ -z "$3" ]; then
ini.value "$INI_FILE" "$1" "$2"
elif [ -z "$2" ]; then
ini.value "$INI_FILE" "$INI_SECTION" "$1"
else
local myinifile=$1
local myinisection=$2
local myvarname=$3
local out
regex="${myvarname//[^a-zA-Z0-9:()]/.}"
out=$(ini.section "${myinifile}" "${myinisection}" \
| sed -e "s,^[[:space:]]*,,g" -e "s,[[:space:]]*=,=,g" \
| grep -F "${myvarname}=" \
| grep "^${regex}=" \
| cut -f 2- -d "=" \
| sed -e 's,^[[:space:]]*,,' -e 's,[[:space:]]*$,,'
)
grep "\[\]$" <<< "$myvarname" >/dev/null || out="$( echo "$out" | tail -1 )"
# delete quote chars on start and end
grep '^".*"$' <<< "$out" >/dev/null && out=$(echo "$out" | sed -e's,^"\(.*\)"$,\1,g')
grep "^'.*'$" <<< "$out" >/dev/null && out=$(echo "$out" | sed -e"s,^'\(.*\)'$,\1,g")
echo "$out"
fi
}
# dump the ini file for visuall check of the parsing functions
# param string filename
ini.dump() {
local myinifile=${1:-$INI_FILE}
echo -en "\e[1;33m"
echo "+----------------------------------------"
echo "|"
echo "| $myinifile"
echo "|"
echo -e "+----------------------------------------\e[0m"
echo -e "\e[34m"
sed "s,^, ,g" "${myinifile}"
echo -e "\e[0m"
echo " Parsed data:"
echo
ini.sections "$myinifile" | while read -r myinisection; do
if ! ini.keys "$myinifile" "$myinisection" | grep -q "."; then
echo -e " ----- section \e[35m[$myinisection]\e[0m"
else
echo -e " --+-- section \e[35m[$myinisection]\e[0m"
echo " |"
ini.keys "$myinifile" "$myinisection" | while read -r mykey; do
value="$(ini.value "$myinifile" "$myinisection" "$mykey")"
# printf " %-15s => %s\n" "$mykey" "$value"
printf " \`---- %-20s => " "$mykey"
echo -e "\e[1;36m$value\e[0m"
done
fi
echo
done
echo
}
function ini.help(){
# local _self
# if _is_sourced; then
# _self="ini."
# else
# _self="$( basename "$0" ) "
# fi
cat <<EOH
INI.CLASS.SH
A bash implementation to read ini files.
Author: Axel hahn
License: GNU GPL 3.0
Source: https://github.com/axelhahn/bash_iniparser
Docs: https://www.axel-hahn.de/docs/bash_iniparser/
Usage:
(1)
source the file ini.class.sh
(2)
ini.help
to show this help with all available functions.
BASIC ACCESS:
ini.value <INIFILE> <SECTION> <KEY>
Get a avlue of a variable in a given section.
Tho shorten ini.value with 3 parameters:
ini.set <INIFILE> [<SECTION>]
or
ini.set <INIFILE>
ini.setsection <SECTION>
This sets the ini file and/ or section as default.
Afterwards you can use:
ini.value <KEY>
and
ini.value <SECTION> <KEY>
OTHER GETTERS:
ini.sections <INIFILE>
Get all sections in the ini file.
The <INIFILE> is not needed if ini.set <INIFILE> was used before.
ini.keys <INIFILE> <SECTION>
Get all keys in the given section.
The <INIFILE> is not needed if ini.set <INIFILE> was used before.
The <SECTION> is not needed if ini.setsection <SECTION> was used
before.
ini.dump <INIFILE>
Get a pretty overview of the ini file.
You get a colored view of the content and a parsed view of the
sections and keys + values.
VALIDATION:
ini.validate <INIFILE> <VALIDATIONINI> <FLAG>
Validate your ini file with the rules of a given validation ini file.
The ini for validation contains rules for
* writing sections and keys
* sections and keys thet must exist or can exist
* describe values of keys to ensure to get vald data (eg a regex)
see https://www.axel-hahn.de/docs/bash_iniparser/Validation.html
The <FLAG> is optional. By default it is 0 and shows error information
only on STDOUT. Set it to 1 to see more output about the validation
process.
EOH
}
# Create bash code to export all variables as hash.
# Example: eval "$( ini.varexport "cfg_" "$inifile" )"
#
# param string prefix for the variables
# param string ini file to read
function ini.varexport(){
local myprefix="$1"
local myinifile="$2"
local var=
for myinisection in $(ini.sections "$myinifile"); do
var="${myprefix}${myinisection}"
echo "declare -A ${var}; "
echo "export ${var}; "
for mykey in $(ini.keys "$myinifile" "$myinisection"); do
value="$(ini.value "$myinifile" "$myinisection" "$mykey")"
echo ${var}[$mykey]="\"$value\""
done
done
}
# validate the ini file
# param string path of ini file to validate
# param string path of ini file with validation rules
# param bool optional: show more output; default: 0
function ini.validate(){
function _vd(){
test "$bShowAll" -ne "0" && echo "$*"
}
local myinifile="$1"
local myvalidationfile="$2"
local bShowAll="${3:-0}"
local ERROR="\e[1;31mERROR\e[0m"
local iErr; typeset -i iErr=0
# TODO: make all used vars local
_vd "START: Validate ini '${myinifile}'"
_vd " with '${myvalidationfile}'"
if [ ! -f "${myinifile}" ]; then
echo -e "$ERROR: Ini file in first param '${myinifile}' does not exist." >&2
return 1
fi
if [ ! -f "${myvalidationfile}" ]; then
echo -e "$ERROR: Validation file in 2nd param '${myvalidationfile}' does not exist." >&2
return 1
fi
# declare all needed variables in case that those sections are not defined
# in vilidation ini
declare -A validate_style
declare -A validate_sections
declare -A validate_varsMust
declare -A validate_varsCan
eval "$( ini.varexport "validate_" "$myvalidationfile" )"
if [ -z "${validate_style[*]}${validate_sections[*]}${validate_varsMust[*]}${validate_varsCan[*]}" ]; then
echo -e "$ERROR: Validation file in 2nd param doesn't seem to be a validation.ini." >&2
echo " Hint: Maybe it is no validation file (yet) or you flipped the parameters." >&2
return 1
fi
# ----- Check if all MUST sections are present
if [ -n "${validate_sections['must']}" ]; then
_vd "--- Sections that MUST exist:"
for section in $( tr "," " " <<< "${validate_sections['must']}");
do
if ini.sections "$myinifile" | grep -q "^$section$" ; then
_vd "OK: Section is present [$section]."
else
echo -e "$ERROR: Section [$section] is not present." >&2
iErr+=1
fi
done
fi
# ----- Loop over sections
_vd "--- Validate section names"
for section in $( ini.sections "$myinifile" )
do
# ----- Check if our section name has the allowed syntax
if [ -n "${validate_style['section']}" ]; then
if ! grep -qE "${validate_style['section']}" <<< "$section" ; then
echo -e "$ERROR: Section [$section] violates style rule '${validate_style['section']}'" >&2
iErr+=1
else
_vd "OK: Section name [$section] matches style rule '${validate_style['section']}'"
fi
fi
# ----- Check if our sections are in MUST or CAN
if ! grep -Fq ",${section}," <<< ",${validate_sections['must']},${validate_sections['must']},"; then
echo -e "$ERROR: Unknown section name: [$section] - ist is not listed as MUST nor CAN." >&2
iErr+=1
else
_vd "OK: section [$section] is valid"
_vd " Check keys of section [$section]"
# ----- Check MUST keys in the current section
for myKeyEntry in "${!validate_varsMust[@]}"; do
if ! grep -q "^${section}\." <<< "${myKeyEntry}"; then
continue
fi
mustkey="$( echo "$myKeyEntry" | cut -f2 -d '.')"
# TODO
keyregex="$( echo "$mustkey" | sed -e's,\[,\\[,g' )"
if ini.keys "$myinifile" "$section" | grep -q "^${keyregex}$"; then
_vd " OK: [$section] -> $mustkey is a MUST"
else
echo -e " $ERROR: [$section] -> $mustkey is a MUST key but was not found im section [$section]." >&2
iErr+=1
fi
done
# ----- Check if our keys are MUST or CAN keys
for mykey in $( ini.keys "$myinifile" "$section"); do
if [ -n "${validate_style['key']}" ]; then
if ! grep -qE "${validate_style['key']}" <<< "$mykey" ; then
echo -e "$ERROR: Key [$section] -> $mykey violates style rule '${validate_style['key']}'" >&2
iErr+=1
else
_vd " OK: [$section] -> $mykey matches style rule '${validate_style['key']}'"
fi
fi
keyregex="$( echo "${mykey}" | sed -e's,\[,\\[,g' | sed -e's,\],\\],g' )"
local mustKeys
mustKeys="$( echo "${!validate_varsMust[@]}" | tr ' ' ',')"
local canKeys
canKeys="$( echo "${!validate_varsCan[@]}" | tr ' ' ',')"
if ! grep -Fq ",${section}.$mykey," <<< ",${canKeys},${mustKeys},"; then
echo -e " $ERROR: [$section] -> $mykey is invalid." >&2
iErr+=1
else
local valKey
valKey="${section}.${mykey}"
if [ -n "${validate_varsCan[$valKey]}" ] && [ -n "${validate_varsMust[$valKey]}" ]; then
echo -e " $ERROR: '$valKey' is defined twice - in varsMust and varsCan as well. Check validation file '$myvalidationfile'." >&2
fi
sValidate="${validate_varsCan[${section}.${mykey}]}"
if [ -z "$sValidate" ]; then
sValidate="${validate_varsMust[${section}.${mykey}]}"
fi
if [ -z "$sValidate" ]; then
_vd " OK: [$section] -> $mykey exists (no check)"
else
local checkType
local checkValue
local value
checkType=$( cut -f 1 -d ':' <<< "$sValidate" )
checkValue=$( cut -f 2- -d ':' <<< "$sValidate" )
value="$(ini.value "$myinifile" "$section" "$mykey")"
local regex
case $checkType in
'INTEGER') regex="^[1-9][0-9]*$" ;;
'ONEOF') regex="^(${checkValue//,/|})$" ;;
'REGEX') regex="$checkValue" ;;
*)
echo -e " $ERROR: ceck type '$checkType' is not supported." >&2
esac
if [ -n "$regex" ]; then
if ! grep -Eq "${regex}" <<< "$value" ; then
echo -e " $ERROR: [$section] -> $mykey is valid but value '$value' does NOT match '$regex'" >&2
else
_vd " OK: [$section] -> $mykey is valid and value matches '$regex'"
fi
fi
fi
fi
done
fi
done
if [ $iErr -gt 0 ]; then
echo "RESULT: Errors were found for $myinifile" >&2
else
_vd "RESULT: OK, Ini file $myinifile looks fine."
fi
return $iErr
}
# ----------------------------------------------------------------------