bash-ini-parser/parse_ini
2019-07-16 21:38:27 +01:00

335 lines
13 KiB
Bash
Executable file

#!/bin/bash
# http://en.wikipedia.org/wiki/INI_file:
# * Provides a good explanation of the ini format - use this for docs *
# * INI's have 'sections' and 'properties'. Properties have key = value format *
#
# Case insensitivity: Add a case insensitive option (converts everything to lowercase)
# Comments: Allow ; and # for comments. Must be on their own line
# Blank lines: Allow blank lines always
# Duplicate names: Duplicate property values overwrite previous values.
# Provide an option to abort/error is duplicate is found?
# Add option to merge duplicates separated by octal byte (\036 ??)
# Duplicate sections are merged. Option to error if dup.
# Escape chars: Handled by bash directly. Allow \ to continue a line.
# Global properties: Support. Add to a GLOBAL section?
# Hierarchy: No hierarchy support. Each section is own section.
# Name/value delim: Use = by default. Allow : via option?
# Quoted values: Allow values to be within " and ' to keep literal formatting.
# Whitespace: Whitespace around section labels and []s is removed.
# Whitespace within section labels is kept / translated.
# Whitespace around property names is removed.
# Whitespace within property names is kept as is (spaces squashed - option to override).
# Property values have whitespace between = and data removed.
# Property values are kept as is (no squashing)
# Ordering: GLOBAL section must be at the top, sections continue until next section or EOF.
# http://www.regular-expressions.info/posixbrackets.html
# http://ajdiaz.wordpress.com/2008/02/09/bash-ini-parser/
# https://github.com/rudimeier/bash_ini_parser/blob/ff9d46a5503bf41b3344af85447e28cbaf95350e/read_ini.sh
# http://tldp.org/LDP/abs/html/
# Specs:
# [section] Can be upper/lower/mixed case (set by options)
# Can only include: '-+_. [:alnum:]'
# Any single or consecutive occurance of '-+_. ' are converted to a *single* _
# eg: [foo -+_. bar] becomes [foo_bar] ??
# Any leading/trailing spaces/tabs between the []s and name will be removed.
# Notes:
# * To make env vars available to subsequent programs, use -x|--export.
parser_getopts() {
while [ ! -z "$1" ]; do
case "$1" in
-h|-help|--help)
parser_help
return 0
;;
-v|-version|--version)
parser_version
return 0
;;
--)
# Stop option processing.
break
;;
-*|--*)
echo "${0##*/}: invalid option: $1"
return 1
;;
esac
done
}
parser_help() {
#........1.........2.........3.........4.........5.........6.........7.........8
cat <<-EOF
Usage: ${0##*/} [options] <inifile>
Parse an ini file into environment variables which can be used natively in Bash.
Options:
-e <varname>, --envvar=<varname>
The prefix of the environment variables set by the parser.
The default is 'INI'.
# -p <prefix>, --prefix=<prefix> Set the prefix to all environment variables set by the parser. A single
# underscore '_' is automatically added to the end.
# Default: INI
-d <char(s)>, --envdelim=<char(s)>
The character(s) to use as a deliminator between the environment variable
and the section name. This is used when creating the environment
variables which hold options belonging to a particular section of the ini
file. Only letters, numbers and underscores (_) may be used. To use no
deliminator at all, use -d '' or --envdelim=''.
The default deliminator is a single underscore '_' ???
-i, --implied-boolean
Options usually require a value (after the =) in order to be set.
With this option, any key without a value contained in the ini file
if assumed to be a boolean 'true' and set accordingly. Likewise, any key
preceeded with 'no_' (eg: no_foo) will set the value of 'foo' to boolean 'false'.
Cannot be used with --no-boolean.
-c, --case-sensitive
Be case sensitive with section names and properties.
Section names and property names will be used as is - no translation.
-d, --delim
The deliminator between the key and value. Must be a single character. Default =
-g, --global-name <name>
INI files can contain an optional implied "global" section - where there
are property names/values before any [section] header. This option
specified what section name the implied "global" section should be given
in the environment variables which are set. The default is 'global'.
-l, --lowercase
Usually, environment variables are converted to all uppercase before being set.
This option forces all environment variables to be converted to lowercase instead.
Note: This only effects the environment variable set with -e, and the section names
read from the ini file. Options are ??????????????????????????????????????
-x, --export
Export environment variables.
--no-boolean
Don't parse 'yes', 'true', 'on', 'no', 'false', 'off' into the corresponding boolean
values, and set the options strictly as is. Incompatible with -i.
--no-squash
Do not squash multiple consecutive occurances of punctuation characters
into a single _ when parsing section names and options. With this option
'foo.-_bar' would become 'foo___bar' rather than 'foo_bar'.
--no-duplicates
If a duplicate section name or option name is found, report error and stop.
Usually sections with the same name will have their options merged, and
duplicate option values will overwrite previous ones.
# -c, --check-only Only validate the ini file, don't parse it into the environment
--check
Check/validate the INI file by running it through the parser. Testing the
ini file will report any problems or syntax errors in the file, but will
not set up the environment variables as would happen in normal parsing.
Any parse errors are reported to stderr. When combined with the --debug
option, every detail of the parsing process is reported to stderr.
--debug
Show full details of the ini file parsing process. Detail is written to
stderr. Unless --test is used with this option, the parser will still
set up the environment as would happen normally,
-h, --help
Show (this) help.
-v, --version
Show version and copyright information.
# -b, --booleans Allow 'yes', 'true', 'on', 'no', 'false', 'off' to be used as values
# and interpited as boolean values. 'yes', 'true', 'on' set option value to "1".
# 'no', 'false', 'off' set option value to "0".
# -?, --???? Interprite the presense of an option name without any value as a boolean
# 'true', and no_<option> as a boolean 'false', setting the option value
# to 1 or 0 accordingly. eg: 'foo' in the ini file would set option foo = 1
# and 'no_foo' would set foo = 0.
# ???? Implies -b ????
# --check Parse the file, report any problems, but don't output the code.
# --debug Show all details of the parsing process to stderr. If --check is used, no code is outputted.
EOF
}
parser_version() {
#........1.........2.........3.........4.........5.........6.........7.........8
cat <<-EOF
Bash INI file parser v0.1.0.
Copyright (C) 2019 Darren 'Tadgy' Austin <darren (at) afterdark.org.uk>.
Licensed under the terms of the GNU General Public Licence version 3.
This program comes with ABSOLUTELY NO WARRANTY. For details and a full copy of
the license terms, see: <http://gnu.org/licenses/gpl.html>. This is free
software - you can modify and redistribute it under the terms of the GPL v3.
EOF
}
parse_ini() {
# Bash v4.1+ is required.
if [[ -z "${BASH_VERSINFO[0]}" ]] || ((BASH_VERSINFO[0] < 4)); then
echo "${0##*/}: minimum of bash v4 required" >&2
return 1
fi
# Set defaults.
local ACCEPTABLE_CHARS="[:alnum:]_.+-" # Characters allowed in [section] names and keys. Must be valid regex bracket exp.
local CONVERT_CHARS=".+-" # Characters in [section] names or keys that are converted to underscore.
local KEYVALUE_DELIM="=" # Delimintator between key and value.
local VARIABLE_PREFIX="INI" # Prefix for all variables.
local VARIABLE_DELIM="_" # Deliminator between prefix and section.
# Parse options.
# parser_getopts "$@" || return 1
# Make sure we have an INI file after all the options are removed.
if (($# == 0)) || (($# > 1)) || [[ -z "$1" ]]; then
echo "Usage: ${0##*/} [options] <INI file>" >&2
echo "Try: ${0##*/} --help" >&2
return 1
else
local INIFILE="$1"
fi
if [[ "$INIFILE" == "-" ]]; then
INIFD="1"
else
# File accessability checks
if [ ! -e "$INIFILE" ]; then
echo "${0##*/}: no such file: $INIFILE" >&2
return 1
elif [ ! -f "$INIFILE" ]; then
echo "${0##*/}: not a regular file: $INIFILE" >&2
return 1
elif [ ! -r "$INIFILE" ]; then
echo "${0##*/}: permission denied: $INIFILE" >&2
return 1
fi
# Open the ini file for reading
if ! exec {INIFD}<"$INIFILE"; then
echo "${0##*/}: failed to open INI file" >&2
return 1
fi
fi
# Extglob is required.
shopt -s extglob
# Variables
local LINE LINENUMBER=0 REPLY
# Parse the INI file.
while :; do
unset LINE
while :; do
# Read a line of input from the file descriptor.
# The 'read' will do the job of removing leading whitespace from the line.
read -r -u "$INIFD" REPLY || break 2
((LINENUMBER++))
# Handle line continuations.
if [[ "${REPLY: -1:1}" == "\\" ]]; then
LINE+="${REPLY:0:-1}"
continue
else
LINE+="$REPLY"
break
fi
done
# Ignore the line if it's a comment.
[[ "$LINE" =~ ^[[:blank:]]*([#;].*)*$ ]] && continue
printf "<%s>\n" "$LINE"
# Process the line.
if [[ "${LINE:0:1}" == "[" ]]; then
if [[ "${LINE: -1:1}" != "]" ]]; then
echo "${0##*/}: line $LINENUMBER: unmatched [ in section definition - ignoring section" >&2
IGNORE_SECTION=1
continue
elif [[ "$LINE" =~ [^[:blank:][]$ACCEPTABLE_CHARS] ]]; then
echo "${0##*/}: line $LINENUMBER: invalid characters in section definition - ignoring section" >&2
IGNORE_SECTION=1
continue
else
# Strip the []s.
LINE="${LINE/#[/}"
LINE="${LINE/%]/}"
printf "<%s>\n" "$LINE"
continue
# FIXME: To convert single/consecutive punct_class into a single _ :
PARSER_READLINE="${PARSER_READLINE//+([$PARSER_PUNCT_CLASS])/_}"
# FIXME: To convert ALL occurances of punct_class into _ :
# PARSER_READLINE="${PARSER_READLINE//@([$PARSER_PUNCT_CLASS])/_}"
# FIXME: To convert single/consecutive punct_class into a single _ except for multiple _s already in line
# PARSER_READLINE="${PARSER_READLINE//+([${PARSER_PUNCT_CLASS/_//}])/_}"
# FIXME: To convert section name to uppercase:
PARSER_READLINE="${PARSER_READLINE^^}"
# FIXME: To convert section name to lowercase:
# PARSER_READLINE="${PARSER_READLINE,,}"
# Declare the associative array.
# FIXME: If doing validation only, don't declare here.
PARSER_CURSEC="$PARSER_READLINE"
declare -g -A $PARSER_ENV_PREFIX$PARSER_READLINE
IGNORE_SECTION=0
fi
fi
# if $IGNORE_SECTION != 0; then continue
# Remove trailing whitespace from key part.
LINE="${LINE/+([[:blank:]])$KEYVALUE_DELIM/$KEYVALUE_DELIM}"
# Remove leading whitespace from value part.
LINE="${LINE/$KEYVALUE_DELIM+([[:blank:]])/$KEYVALUE_DELIM}"
# Extract the key and the value
KEY="${LINE%%$KEYVALUE_DELIM*}"
VALUE="${LINE#*$KEYVALUE_DELIM}"
# If the value starts with a " or ' it must end with same.
if [[ "${VALUE:0:1}" =~ [\"\'] ]]; then
if [[ "${VALUE:0:1}" != "${VALUE: -1:1}" ]]; then
echo "${0##*/}: unmatched quotes on line $LINENUMBER - ignoring line"
continue
fi
fi
printf "<%s = %s>\n" "$KEY" "$VALUE"
exit
# if first non-whitespace char after the first = is " or ', check the last non-whitespace char on the line.
# if that character is a matching " or ', skip to normal processing.
# if that character doesn't match the opening " or ', go to continued line processing
# else (no opening " or ') check the last non-whitespace char on the line; if its a \ (line continuation marker)
# go to continued line processing
# fi
# Continued line processing
# Notes: If within a " or ' block, keep whitespace as entered - don't strip from begining of line.
# If here from a continueation marker, remove leading whitespace.
# Will need a flag to show if we're looking for an ending " or '
# Normal processing:
# Escape chars?
# Close file descriptor for ini file
# clean up the environment
# IFS=$INI_SAVED_IFS
# Remove any variables begining INI_
done
}
parse_ini "$@"
exit $?