Parsing INI Files with Pure Bash
Published: June 24, 2020
While working on my static site generator squib, I decided to transition the configuration file to a .ini
. The rational being that I needed a simple format that could easily be parsed with minimal effort. Squib is written 100% in Bash, so I wanted a solution that didn't require additional dependencies.
Prior to this I'd source a shell script that exclusively contained variables. It worked relatively well until I needed variables with multi-line values. It became messy as I resorted to using a combination of heredocs and cat
.
The solution below uses only Bash builtins. It's rough around the edges and doesn't fully comply with the INI spec, but it does exactly what I need it to. If you have any feedback please comment in the gist.
The Code
Copyright 2020 Eli Gladman
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
parse_ini() {
# Usage: parse_ini "path/to/file"
# parse_ini "path/to/file" false
# By default variables will be evaluated. Disable this by passing in "false"
local regex_contains_variable="^([a-zA-Z0-9_]{1,})([[:space:]]\=[[:space:]])(.*)$"
local regex_contains_wrapped_quotes="^([\"\'])(.*)([\"\'])$"
local regex_contains_inside_quotes="^([\"])(.*)([\"])(.*)([\"])$"
local multi_line=""
while IFS='' read -r line || [[ -n "$line" ]]; do
# Strip trailing comments from line
if [[ "$line" == *[[:space:]]\#* ]]; then
line="${line%%\#*}"
fi
case "$line" in
\#*|\;*)
# Ignore commented line
continue
;;
\[*|*\])
# There is no explicit "end of section" delimiter; sections end at
# the next section declaration
section="${line##\[}" # Strip left bracket
section="${section%%\]}_" # Strip right bracket
continue
;;
*\\)
multi_line+="${line%%\\}\n"
;;
*)
if [[ -n "$multi_line" ]]; then
multi_line+="$line"
line="$multi_line"
multi_line=""
fi
;;
esac
if [[ ! "$line" =~ $regex_contains_variable ]] || [[ -n "$multi_line" ]]; then
continue
fi
local name val
val="${line/*\= }" # Everything after the equals sign
name="${section}${line/$val}" # Invert match. This way we know we've captured everything
name="${name%% *}" # Remove trailing whitespace and equals sign
# Wrap 'val' in quotes if they're missing
if [[ ! "$val" =~ $regex_contains_wrapped_quotes ]]; then
val="\"$val\""
fi
if [[ "$val" =~ $regex_contains_inside_quotes ]]; then
local tmp
tmp="${val##\"}" # Strip left quote
tmp="${tmp%%\"}" # Strip right quote
tmp="${tmp//\"/\\\"}" # Escape all quotes
val="\"$tmp\"" # Add back the wrapped quotes
unset tmp
fi
local declaration
declaration="$(printf '%b\n' "$name=$val")"
[[ "$2" == "false" ]] || eval "$declaration"
printf '%s\n' "$declaration"
done < "$1"
}
Sample INI
# A comment
; Another comment
foo = Hello World
; First Section
[Apples]
key = "value"
integer = 1234
real = 3.14
string1 = "Case 1"
string2 = 'Case 2'
; Second Section
[Oranges]
key = new value
integer = 1234
real = 3.14
pangram = The quick\
brown fox\
jumps over the lazy dog
string2 = 'Case 2'
Results
The following variables will be set:
foo="Hello World"
Apples_key="value"
Apples_integer="1234"
Apples_real="3.14"
Apples_string1="Case 1"
Apples_string2='Case 2'
Oranges_key="new value"
Oranges_integer="1234"
Oranges_real="3.14"
Oranges_string1="Case 1"
Oranges_pangram="The quick\nbrown fox\njumps over the lazy dog"
Oranges_string3="Case 3"
Tags: bash