Reading sections and key-value pairs without external tools
Plenty of legacy tools and simple configuration files still use the INI format today: sections in square brackets, key-value pairs underneath. Bash ships with enough built-ins to read such a format reliably, as long as comments, whitespace, and quoting pitfalls are planned for from the start instead of patched in afterward.
Table of Contents
- 1. Why INI files still show up in Bash scripts
- 2. The structure of an INI file and the real parsing challenges
- 3. Line-by-line parsing with while read and regex
- 4. Mapping sections onto associative arrays
- 5. Handling comments, blank lines, and whitespace robustly
- 6. Values with quotes and special characters: the real quoting trap
- 7. The limits of a self-built parser compared to real INI parsers
- 8. When an external tool is worth it after all
- 9. A complete example and a comparison of the approaches
- 10. Summary
- 11. FAQ
1. Why INI files still show up in Bash scripts
The INI format is anything but modern, yet it still shows up in legacy tools, PHP configurations, system-wide services, and hand-written internal tools, because it is extremely easy for humans to read and write. Where YAML or JSON demand strict structure with indentation or braces, an INI file gets by with a flat list of sections and simple key=value lines, without users needing to worry about escaping or nesting.
When a Bash script needs to read such a configuration file, for instance to pull database credentials out of a my.cnf-like file or to configure an internal deployment tool, an external dependency like Python or a dedicated INI parsing tool is not always available or wanted, especially in slim container images or on tightly managed servers. A self-built parser using pure Bash built-ins closes that gap, as long as the requirements stay manageable.
2. The structure of an INI file and the real parsing challenges
A typical INI file consists of optional sections in square brackets like [database], followed by lines of the form key = value, plus comment lines usually starting with ; or #, and blank lines for structure. Values before the first section traditionally belong to an implicit, unnamed default section.
The real difficulty in parsing does not lie in this basic skeleton but in the details: values may or may not be quoted, whitespace around the equals sign is sometimes present and sometimes not, comments can appear at the end of a line after a value, and some INI dialects even allow comment characters inside values as long as they are quoted. A robust parser has to make deliberate decisions about these cases instead of silently getting them wrong.
3. Line-by-line parsing with while read and regex
The starting point for a Bash INI parser is a while read loop that reads the file line by line, combined with Bash's built-in regex matching via [[ $line =~ regex ]]. A section line can be recognized with a simple pattern like ^\[([^]]+)\]$, a key-value line with ^([^=]+)=(.*)$, with the matches retrieved afterward through the BASH_REMATCH array.
It matters to write the loop as while IFS= read -r line instead of a plain while read line: IFS= prevents leading and trailing whitespace of the line from being swallowed, and -r prevents backslashes in values from being wrongly interpreted as escape characters. Both details are frequently left out in tutorials but quickly lead to incorrectly parsed values on real configuration files.
#!/usr/bin/env bash
set -euo pipefail
parse_ini_naive() {
local file="$1" section=""
while IFS= read -r line; do
# Strip comments and surrounding whitespace first
line="${line%%[#;]*}"
line="$(echo "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
[[ -z "$line" ]] && continue
if [[ "$line" =~ ^\[([^]]+)\]$ ]]; then
section="${BASH_REMATCH[1]}"
elif [[ "$line" =~ ^([^=]+)=(.*)$ ]]; then
local key="${BASH_REMATCH[1]%% }" value="${BASH_REMATCH[2]}"
echo "[$section] $key = $value"
fi
done < "$file"
}
4. Mapping sections onto associative arrays
Plain text output is fine for a debugging script, but a configuration parser typically needs to make values programmatically retrievable. From Bash 4 onward, associative arrays declared with declare -A are the tool for that. The common trick for mapping a two-level structure (section plus key) into a single, flat associative array is a composite key like "$section.$key".
This structure is less elegant than a native nested array, which Bash does not have, but it works reliably and is easy to search, for instance with ${!ini_data[@]} to find every key of a given section via a prefix filter. For most configuration files with a flat section hierarchy, this approach is entirely sufficient and considerably more readable than simulating nested arrays through workarounds.
#!/usr/bin/env bash
set -euo pipefail
declare -A ini_data
parse_ini() {
local file="$1" section=""
while IFS= read -r line; do
line="${line%%[#;]*}"
line="$(echo "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
[[ -z "$line" ]] && continue
if [[ "$line" =~ ^\[([^]]+)\]$ ]]; then
section="${BASH_REMATCH[1]}"
elif [[ "$line" =~ ^([^=]+)=(.*)$ ]]; then
local key="${BASH_REMATCH[1]}" value="${BASH_REMATCH[2]}"
key="$(echo "$key" | sed -e 's/[[:space:]]*$//')"
ini_data["${section}.${key}"]="$value"
fi
done < "$file"
}
parse_ini "app.ini"
echo "${ini_data[database.host]}"
echo "${ini_data[database.port]}"
5. Handling comments, blank lines, and whitespace robustly
A common mistake in simple parsers is blindly stripping comment characters with a text cut, without checking whether the character sits inside a quoted value. The expression line="${line%%[#;]*}" from the earlier examples is deliberately kept simple and also cuts when a # sits in the middle of a value, which is good enough for most practical configuration files but not one hundred percent correct.
Blank lines and lines consisting only of whitespace should be consistently skipped after trimming, instead of wandering into the result array as empty keys. It is equally important to strip whitespace both before the key and around the equals sign, because INI files vary widely in formatting in practice, depending on which tool last wrote them or which human last edited them by hand.
6. Values with quotes and special characters: the real quoting trap
Many INI files optionally wrap values in double quotes, usually to preserve leading or trailing whitespace or to protect a # inside the value itself from comment detection. A naive parser that does not explicitly strip these quotes returns values like "secretpassword" instead of secretpassword, quotes included as part of the string, which quietly breaks downstream comparisons and uses of the value.
A small helper function that removes leading and trailing quotes with a parameter expansion like value="${value%\"}"; value="${value#\"}" solves the most common case reliably. Escaped quotes inside a value, meaning a quote character actually meant to be part of the content, are exactly the point where a self-built Bash parser reaches its limits, because handling that correctly needs a real tokenizer instead of a simple regex.
#!/usr/bin/env bash
set -euo pipefail
strip_quotes() {
local value="$1"
# Remove one matching pair of surrounding double quotes, if present
sed -e 's/^"//' -e 's/"$//' <<< "$value"
}
strip_quotes '"hello world"' # -> hello world
strip_quotes 'no-quotes-here' # -> no-quotes-here
7. The limits of a self-built parser compared to real INI parsers
As handy as a self-built Bash parser is for simple configuration files, it hits clear limits. Nested sections, which some INI dialects simulate through dot notation in the section name, multi-line values with continuation lines, or value interpolation, where one line refers to a previously set value, cannot be represented with a simple while-read loop, or only with considerable extra effort.
A self-built parser also lacks any type checking: everything is read as a string, and distinguishing between number, boolean, and string is left entirely to the caller, including the usual Bash quirk that true and false as values are just strings and trigger no native boolean behavior. Anyone with such requirements should weigh them deliberately against the simplicity of the self-built approach, instead of painstakingly rebuilding them in Bash afterward.
8. When an external tool is worth it after all
As soon as a configuration file needs nested structures, arrays as values, or strict schema validation, switching to a real parser is usually the better investment. Python's built-in configparser module covers practically the entire INI language including interpolation and can be used from a Bash script with a one-line python3 -c call, without rewriting the rest of the script in Python.
For projects that already have yq or jq in the toolbox, it is also worth converting the INI file to JSON or YAML once and continuing from there with the already available, well-tested tools, instead of reimplementing quoting and escaping rules in Bash regex. The rule of thumb: a self-built parser is worth it for small, well-known, flat configuration files; anything beyond that deserves a real parsing tool.
9. A complete example and a comparison of the approaches
The table below summarizes when a self-built Bash parser is enough and when reaching for an external tool pays off, depending on the configuration file's complexity and the robustness requirements.
| Requirement | Custom Bash parser | python3 configparser | yq/jq after conversion |
|---|---|---|---|
| Flat sections, simple values | Well suited | Works, more overhead | Works, more overhead |
| Quotes inside values | Manual via parameter expansion | Natively supported | Natively supported after conversion |
| Value interpolation | Not practical | Natively supported | Not applicable |
| Nested structures | Not practical | Limited | Very good, after conversion |
| No external dependency | Yes | No, needs Python | No, needs yq/jq |
Mironsoft
Shell automation, DevOps tooling and deployment infrastructure
Shell scripts that hold up in production?
We review existing Bash scripts, spot fragile patterns and replace them with robust Bash patterns: complete error handling, logging and safe parallelization for your deployment stack.
Code Review
ShellCheck analysis and manual review for critical Bash pattern violations.
Refactoring
Retrofitting error handling, logging and safe file operations.
CI Integration
Wiring ShellCheck and BATS into pipelines and building regression tests.
10. Summary
INI Parsing in Bash: The Essentials at a Glance
Basic structure
while IFS= read -r line combined with Bash regex matching via [[ $line =~ ... ]] reliably recognizes sections and key-value lines.
Storage
Associative arrays with a composite key section.key represent a two-level INI structure inside a single flat Bash array.
Quoting
Quotes around values must be explicitly stripped with parameter expansion, otherwise they end up unnoticed in the parsed value.
Limits
Nesting, interpolation, and type checking are not practical in pure Bash; beyond that point, configparser or yq are worth it.