Parsing INI Files in Pure Bash: Without External Tools
AI generated
$_
#!/
Bash · Configuration · Parsing · Built-ins
Parsing INI Files in Pure Bash
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.

16 min read Associative arrays · Bash 4+ read · regex · quoting

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.

11. FAQ: INI Parsing in Bash: The Essentials at a Glance

1From which Bash version do associative arrays work?
Associative arrays with declare -A are available from Bash 4.0 onward. macOS still ships Bash 3.2 by default, which is why a Homebrew-installed Bash 5.x must be used explicitly there.
2Why use while IFS= read -r instead of 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. Without both options, some values get parsed incorrectly.
3How do I recognize a section line with Bash regex?
With a pattern like ^\[([^]]+)\]$ matched against [[ $line =~ ... ]]. The section name then ends up in BASH_REMATCH[1].
4How do I store two-level INI data in Bash?
The most practical approach is a single associative array with a composite key like section.key, because Bash has no native nested arrays.
5How do I strip quotes around a value?
With parameter expansion like value="${value%\"}" and value="${value#\"}", which remove a trailing or leading quote character respectively, if present.
6Can a self-built parser handle nested sections?
Not practically. Nested structures require a real tokenizer or parser, such as Python's configparser or converting to YAML followed by yq processing.
7How do I handle comments at the end of a value line?
A simple approach cuts the line at the first # or ;, which is enough for most configuration files, but incorrectly also cuts comment characters inside quoted values.
8When is Python's configparser worth it over a custom Bash parser?
As soon as value interpolation, strict type checking, or more complex quoting rules are needed. configparser covers the full INI language and can be used from Bash with a one-line python3 -c call.
9Is a self-built INI parser safe against malformed input files?
Only to a degree. A simple parser usually ignores unexpected lines silently instead of raising an error. Production systems should additionally validate that expected required keys are present after parsing.
10How do I reliably test a self-built INI parser?
With a set of fixed test files covering typical edge cases like quotes, empty values, comments, and missing sections, combined with a simple test runner like BATS that checks the parsed values against expected output.