#!/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 } # ----------------------------------------------------------------------