Tools: goini: Stop Writing Bash to Parse .ini Files (2026)

Tools: goini: Stop Writing Bash to Parse .ini Files (2026)

Installation

The Sample File

Validating With Exit Codes

Does a Section Have a Key?

Does a Key Have a Specific Value?

Are Multiple Sections All Present?

Does a Section Exist?

Reading With STDOUT

List All Sections

List Keys in a Section

List Key-Value Pairs in a Section

Writing to the File

Add a New Section

Add a Key to a Section

Modify an Existing Key

Real Pipeline Examples

Validating a Service Configuration Before Deploy

Bootstrapping a Fresh Config File

Promoting Config From Staging to Production

Extracting Values for Use in Other Scripts

GitHub Actions

GitLab CI

Complete Flag Reference

A Few Things Worth Knowing I built goenv because I was tired of writing fragile shell one-liners to work with .env files. The same problem existed with .ini files — and goini is the same answer applied there. If you've ever written something like this in a pipeline: ...you know exactly why this tool exists. That breaks the moment someone adds a comment, changes indentation, or reorders keys. It's archaeology, not automation. goini is a compiled CLI binary that lets you read, write, validate and export .ini files from your pipeline scripts. Every operation either succeeds at exit code 0 or fails at exit code 1. That makes it composable. You can gate deployments on it. You can use it in an if statement. It behaves like a Unix citizen. The source is at github.com/andreimerlescu/goini. Or grab the binary directly: Every example below operates against this sample.ini: Two sections. A handful of keys. Simple on purpose — the point is showing what goini can do with it, not the data. This is the core use case for pipeline work. You don't need stdout. You need a binary answer: does this thing exist or not, and did it match or not. When you need to pull data out of the file and feed it downstream, these are your commands. This is idempotency by design. goini won't silently create a duplicate. It fails loudly so your pipeline knows something unexpected happened. Again — it won't overwrite. If you need to change an existing value, that's what -modify-key is for. The distinction between -add-key and -modify-key is intentional and important. You can't accidentally overwrite with add. You can't accidentally create with modify. They're separate operations with separate failure modes. -add-key and -modify-key are intentionally separate. You can't accidentally overwrite an existing value with -add-key — it refuses. You can't accidentally create a new key with -modify-key — it refuses. They each have one job and one failure mode. That's the point. -add-section is idempotent-safe. Adding a section that already exists exits with code 1. In a pipeline where you need true idempotency, pipe it with || true. In a pipeline where you want to catch unexpected state, let it fail. -are-sections-present takes a comma-separated list. All sections must be present for exit code 0. One missing section fails the whole check. Output format flags work with read operations. -csv, -json, and -yaml apply to -sections, -list-keys, and -list-key-values. They don't apply to write operations — those just use the exit code. Every write operation reads the file, modifies the result in memory, and writes it back. There's no in-place line editing. This means the file that comes out is clean and predictable regardless of what formatting the original had. The source and releases are at github.com/andreimerlescu/goini. Apache 2.0. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Code Block

Copy

grep -A 5 "^\[default\]" config.ini | grep "^user" | cut -d= -f2 | tr -d ' ' grep -A 5 "^\[default\]" config.ini | grep "^user" | cut -d= -f2 | tr -d ' ' grep -A 5 "^\[default\]" config.ini | grep "^user" | cut -d= -f2 | tr -d ' ' go install github.com/andreimerlescu/goini@latest go install github.com/andreimerlescu/goini@latest go install github.com/andreimerlescu/goini@latest curl -sL https://github.com/andreimerlescu/goini/releases/download/v1.0.0/goini-linux-amd64 \ --output /tmp/goini chmod +x /tmp/goini sudo mv /tmp/goini /usr/local/bin/goini which goini curl -sL https://github.com/andreimerlescu/goini/releases/download/v1.0.0/goini-linux-amd64 \ --output /tmp/goini chmod +x /tmp/goini sudo mv /tmp/goini /usr/local/bin/goini which goini curl -sL https://github.com/andreimerlescu/goini/releases/download/v1.0.0/goini-linux-amd64 \ --output /tmp/goini chmod +x /tmp/goini sudo mv /tmp/goini /usr/local/bin/goini which goini [default] user = Yeshua key = 369 port = 1776 country = ISRAEL [extra] ssh_key = ~/.ssh/id_rsa ssh_key_pub = ~/.ssh/id_rsa.pub [default] user = Yeshua key = 369 port = 1776 country = ISRAEL [extra] ssh_key = ~/.ssh/id_rsa ssh_key_pub = ~/.ssh/id_rsa.pub [default] user = Yeshua key = 369 port = 1776 country = ISRAEL [extra] ssh_key = ~/.ssh/id_rsa ssh_key_pub = ~/.ssh/id_rsa.pub goini -ini sample.ini -section default -has-section-key user echo $? # 0 — key 'user' exists in 'default' goini -ini sample.ini -section default -has-section-key non_existent_key echo $? # 1 — key doesn't exist goini -ini sample.ini -section default -has-section-key user echo $? # 0 — key 'user' exists in 'default' goini -ini sample.ini -section default -has-section-key non_existent_key echo $? # 1 — key doesn't exist goini -ini sample.ini -section default -has-section-key user echo $? # 0 — key 'user' exists in 'default' goini -ini sample.ini -section default -has-section-key non_existent_key echo $? # 1 — key doesn't exist goini -ini sample.ini -section default -key user -has-section-key-value Yeshua echo $? # 0 goini -ini sample.ini -section default -key user -has-section-key-value No echo $? # 1 goini -ini sample.ini -section default -key user -has-section-key-value Yeshua echo $? # 0 goini -ini sample.ini -section default -key user -has-section-key-value No echo $? # 1 goini -ini sample.ini -section default -key user -has-section-key-value Yeshua echo $? # 0 goini -ini sample.ini -section default -key user -has-section-key-value No echo $? # 1 goini -ini sample.ini -are-sections-present default,extra echo $? # 0 — both sections exist goini -ini sample.ini -are-sections-present default,non_existent_section echo $? # 1 — one is missing goini -ini sample.ini -are-sections-present default,extra echo $? # 0 — both sections exist goini -ini sample.ini -are-sections-present default,non_existent_section echo $? # 1 — one is missing goini -ini sample.ini -are-sections-present default,extra echo $? # 0 — both sections exist goini -ini sample.ini -are-sections-present default,non_existent_section echo $? # 1 — one is missing goini -ini sample.ini -has-section default echo $? # 0 goini -ini sample.ini -has-section ghost echo $? # 1 goini -ini sample.ini -has-section default echo $? # 0 goini -ini sample.ini -has-section ghost echo $? # 1 goini -ini sample.ini -has-section default echo $? # 0 goini -ini sample.ini -has-section ghost echo $? # 1 goini -ini sample.ini -sections # default # extra goini -ini sample.ini -sections -csv # default,extra goini -ini sample.ini -sections -json # [ # "default", # "extra" # ] goini -ini sample.ini -sections -yaml # - default # - extra goini -ini sample.ini -sections # default # extra goini -ini sample.ini -sections -csv # default,extra goini -ini sample.ini -sections -json # [ # "default", # "extra" # ] goini -ini sample.ini -sections -yaml # - default # - extra goini -ini sample.ini -sections # default # extra goini -ini sample.ini -sections -csv # default,extra goini -ini sample.ini -sections -json # [ # "default", # "extra" # ] goini -ini sample.ini -sections -yaml # - default # - extra goini -ini sample.ini -section default -list-keys # user # key # port # country goini -ini sample.ini -section default -list-keys -json # [ # "user", # "key", # "port", # "country" # ] goini -ini sample.ini -section default -list-keys # user # key # port # country goini -ini sample.ini -section default -list-keys -json # [ # "user", # "key", # "port", # "country" # ] goini -ini sample.ini -section default -list-keys # user # key # port # country goini -ini sample.ini -section default -list-keys -json # [ # "user", # "key", # "port", # "country" # ] goini -ini sample.ini -section default -list-key-values # user = Yeshua # key = 369 # port = 1776 # country = ISRAEL goini -ini sample.ini -section default -list-key-values -json # { # "country": "ISRAEL", # "key": "369", # "port": "1776", # "user": "Yeshua" # } goini -ini sample.ini -section default -list-key-values # user = Yeshua # key = 369 # port = 1776 # country = ISRAEL goini -ini sample.ini -section default -list-key-values -json # { # "country": "ISRAEL", # "key": "369", # "port": "1776", # "user": "Yeshua" # } goini -ini sample.ini -section default -list-key-values # user = Yeshua # key = 369 # port = 1776 # country = ISRAEL goini -ini sample.ini -section default -list-key-values -json # { # "country": "ISRAEL", # "key": "369", # "port": "1776", # "user": "Yeshua" # } goini -ini sample.ini -add-section new_section echo $? # 0 — section added # Try to add the same section again goini -ini sample.ini -add-section new_section echo $? # 1 — already exists, refused goini -ini sample.ini -add-section new_section echo $? # 0 — section added # Try to add the same section again goini -ini sample.ini -add-section new_section echo $? # 1 — already exists, refused goini -ini sample.ini -add-section new_section echo $? # 0 — section added # Try to add the same section again goini -ini sample.ini -add-section new_section echo $? # 1 — already exists, refused goini -ini sample.ini -section default -key new_setting -value 123 -add-key echo $? # 0 — new_setting=123 added to [default] # Try to add a key that already exists goini -ini sample.ini -section default -key user -value something -add-key echo $? # 1 — key already exists, refused goini -ini sample.ini -section default -key new_setting -value 123 -add-key echo $? # 0 — new_setting=123 added to [default] # Try to add a key that already exists goini -ini sample.ini -section default -key user -value something -add-key echo $? # 1 — key already exists, refused goini -ini sample.ini -section default -key new_setting -value 123 -add-key echo $? # 0 — new_setting=123 added to [default] # Try to add a key that already exists goini -ini sample.ini -section default -key user -value something -add-key echo $? # 1 — key already exists, refused goini -ini sample.ini -section default -key user -value NewUserValue -modify-key echo $? # 0 — user updated to NewUserValue in [default] # Try to modify a key that doesn't exist goini -ini sample.ini -section default -key non_existent_key -value some_value -modify-key echo $? # 1 — key doesn't exist, refused goini -ini sample.ini -section default -key user -value NewUserValue -modify-key echo $? # 0 — user updated to NewUserValue in [default] # Try to modify a key that doesn't exist goini -ini sample.ini -section default -key non_existent_key -value some_value -modify-key echo $? # 1 — key doesn't exist, refused goini -ini sample.ini -section default -key user -value NewUserValue -modify-key echo $? # 0 — user updated to NewUserValue in [default] # Try to modify a key that doesn't exist goini -ini sample.ini -section default -key non_existent_key -value some_value -modify-key echo $? # 1 — key doesn't exist, refused #!/bin/bash set -euo pipefail CONFIG="infra/service.ini" echo "==> Validating ${CONFIG}..." # Required sections must exist goini -ini "${CONFIG}" -are-sections-present database,cache,api \ || { echo "ERROR: missing required sections in ${CONFIG}"; exit 1; } # Required keys must exist in each section goini -ini "${CONFIG}" -section database -has-section-key host \ || { echo "ERROR: database.host is required"; exit 1; } goini -ini "${CONFIG}" -section database -has-section-key port \ || { echo "ERROR: database.port is required"; exit 1; } goini -ini "${CONFIG}" -section api -has-section-key base_url \ || { echo "ERROR: api.base_url is required"; exit 1; } # Assert environment is correct before touching production goini -ini "${CONFIG}" -section api -key environment -has-section-key-value staging \ || { echo "ERROR: api.environment must be 'staging'"; exit 1; } echo "==> Validation passed." #!/bin/bash set -euo pipefail CONFIG="infra/service.ini" echo "==> Validating ${CONFIG}..." # Required sections must exist goini -ini "${CONFIG}" -are-sections-present database,cache,api \ || { echo "ERROR: missing required sections in ${CONFIG}"; exit 1; } # Required keys must exist in each section goini -ini "${CONFIG}" -section database -has-section-key host \ || { echo "ERROR: database.host is required"; exit 1; } goini -ini "${CONFIG}" -section database -has-section-key port \ || { echo "ERROR: database.port is required"; exit 1; } goini -ini "${CONFIG}" -section api -has-section-key base_url \ || { echo "ERROR: api.base_url is required"; exit 1; } # Assert environment is correct before touching production goini -ini "${CONFIG}" -section api -key environment -has-section-key-value staging \ || { echo "ERROR: api.environment must be 'staging'"; exit 1; } echo "==> Validation passed." #!/bin/bash set -euo pipefail CONFIG="infra/service.ini" echo "==> Validating ${CONFIG}..." # Required sections must exist goini -ini "${CONFIG}" -are-sections-present database,cache,api \ || { echo "ERROR: missing required sections in ${CONFIG}"; exit 1; } # Required keys must exist in each section goini -ini "${CONFIG}" -section database -has-section-key host \ || { echo "ERROR: database.host is required"; exit 1; } goini -ini "${CONFIG}" -section database -has-section-key port \ || { echo "ERROR: database.port is required"; exit 1; } goini -ini "${CONFIG}" -section api -has-section-key base_url \ || { echo "ERROR: api.base_url is required"; exit 1; } # Assert environment is correct before touching production goini -ini "${CONFIG}" -section api -key environment -has-section-key-value staging \ || { echo "ERROR: api.environment must be 'staging'"; exit 1; } echo "==> Validation passed." #!/bin/bash set -euo pipefail CONFIG="deploy.ini" # Add sections — each will fail if already present, which is fine # because set -euo pipefail would catch it; use || true if idempotency is needed goini -ini "${CONFIG}" -add-section app || true goini -ini "${CONFIG}" -add-section database || true goini -ini "${CONFIG}" -add-section cache || true # Populate app section goini -ini "${CONFIG}" -section app -key name -value "payments" -add-key || true goini -ini "${CONFIG}" -section app -key version -value "$(cat VERSION)" -add-key || true goini -ini "${CONFIG}" -section app -key env -value "staging" -add-key || true # Populate database section goini -ini "${CONFIG}" -section database -key host -value "db.internal" -add-key || true goini -ini "${CONFIG}" -section database -key port -value "5432" -add-key || true goini -ini "${CONFIG}" -section database -key name -value "payments_db" -add-key || true # Verify it all landed goini -ini "${CONFIG}" -section app -list-key-values goini -ini "${CONFIG}" -section database -list-key-values #!/bin/bash set -euo pipefail CONFIG="deploy.ini" # Add sections — each will fail if already present, which is fine # because set -euo pipefail would catch it; use || true if idempotency is needed goini -ini "${CONFIG}" -add-section app || true goini -ini "${CONFIG}" -add-section database || true goini -ini "${CONFIG}" -add-section cache || true # Populate app section goini -ini "${CONFIG}" -section app -key name -value "payments" -add-key || true goini -ini "${CONFIG}" -section app -key version -value "$(cat VERSION)" -add-key || true goini -ini "${CONFIG}" -section app -key env -value "staging" -add-key || true # Populate database section goini -ini "${CONFIG}" -section database -key host -value "db.internal" -add-key || true goini -ini "${CONFIG}" -section database -key port -value "5432" -add-key || true goini -ini "${CONFIG}" -section database -key name -value "payments_db" -add-key || true # Verify it all landed goini -ini "${CONFIG}" -section app -list-key-values goini -ini "${CONFIG}" -section database -list-key-values #!/bin/bash set -euo pipefail CONFIG="deploy.ini" # Add sections — each will fail if already present, which is fine # because set -euo pipefail would catch it; use || true if idempotency is needed goini -ini "${CONFIG}" -add-section app || true goini -ini "${CONFIG}" -add-section database || true goini -ini "${CONFIG}" -add-section cache || true # Populate app section goini -ini "${CONFIG}" -section app -key name -value "payments" -add-key || true goini -ini "${CONFIG}" -section app -key version -value "$(cat VERSION)" -add-key || true goini -ini "${CONFIG}" -section app -key env -value "staging" -add-key || true # Populate database section goini -ini "${CONFIG}" -section database -key host -value "db.internal" -add-key || true goini -ini "${CONFIG}" -section database -key port -value "5432" -add-key || true goini -ini "${CONFIG}" -section database -key name -value "payments_db" -add-key || true # Verify it all landed goini -ini "${CONFIG}" -section app -list-key-values goini -ini "${CONFIG}" -section database -list-key-values #!/bin/bash set -euo pipefail STAGING="config.staging.ini" PROD="config.production.ini" echo "==> Promoting ${STAGING} → ${PROD}..." # Verify staging has what we expect before promoting goini -ini "${STAGING}" -section app -key env -has-section-key-value staging \ || { echo "ERROR: source must be staging"; exit 1; } # Flip the environment value in production goini -ini "${PROD}" -section app -key env -value production -modify-key \ || { echo "ERROR: failed to set env=production"; exit 1; } # Confirm goini -ini "${PROD}" -section app -key env -has-section-key-value production \ && echo "==> Promotion complete." \ || { echo "ERROR: production env value not confirmed"; exit 1; } #!/bin/bash set -euo pipefail STAGING="config.staging.ini" PROD="config.production.ini" echo "==> Promoting ${STAGING} → ${PROD}..." # Verify staging has what we expect before promoting goini -ini "${STAGING}" -section app -key env -has-section-key-value staging \ || { echo "ERROR: source must be staging"; exit 1; } # Flip the environment value in production goini -ini "${PROD}" -section app -key env -value production -modify-key \ || { echo "ERROR: failed to set env=production"; exit 1; } # Confirm goini -ini "${PROD}" -section app -key env -has-section-key-value production \ && echo "==> Promotion complete." \ || { echo "ERROR: production env value not confirmed"; exit 1; } #!/bin/bash set -euo pipefail STAGING="config.staging.ini" PROD="config.production.ini" echo "==> Promoting ${STAGING} → ${PROD}..." # Verify staging has what we expect before promoting goini -ini "${STAGING}" -section app -key env -has-section-key-value staging \ || { echo "ERROR: source must be staging"; exit 1; } # Flip the environment value in production goini -ini "${PROD}" -section app -key env -value production -modify-key \ || { echo "ERROR: failed to set env=production"; exit 1; } # Confirm goini -ini "${PROD}" -section app -key env -has-section-key-value production \ && echo "==> Promotion complete." \ || { echo "ERROR: production env value not confirmed"; exit 1; } #!/bin/bash set -euo pipefail CONFIG="service.ini" # Pull values out and assign to shell variables DB_HOST=$(goini -ini "${CONFIG}" -section database -list-key-values -json \ | python3 -c "import sys,json; print(json.load(sys.stdin)['host'])") DB_PORT=$(goini -ini "${CONFIG}" -section database -list-key-values -json \ | python3 -c "import sys,json; print(json.load(sys.stdin)['port'])") echo "Connecting to ${DB_HOST}:${DB_PORT}" #!/bin/bash set -euo pipefail CONFIG="service.ini" # Pull values out and assign to shell variables DB_HOST=$(goini -ini "${CONFIG}" -section database -list-key-values -json \ | python3 -c "import sys,json; print(json.load(sys.stdin)['host'])") DB_PORT=$(goini -ini "${CONFIG}" -section database -list-key-values -json \ | python3 -c "import sys,json; print(json.load(sys.stdin)['port'])") echo "Connecting to ${DB_HOST}:${DB_PORT}" #!/bin/bash set -euo pipefail CONFIG="service.ini" # Pull values out and assign to shell variables DB_HOST=$(goini -ini "${CONFIG}" -section database -list-key-values -json \ | python3 -c "import sys,json; print(json.load(sys.stdin)['host'])") DB_PORT=$(goini -ini "${CONFIG}" -section database -list-key-values -json \ | python3 -c "import sys,json; print(json.load(sys.stdin)['port'])") echo "Connecting to ${DB_HOST}:${DB_PORT}" name: Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install goini run: | curl -sL https://github.com/andreimerlescu/goini/releases/download/v1.0.0/goini-linux-amd64 \ --output /usr/local/bin/goini chmod +x /usr/local/bin/goini - name: Validate config run: | goini -ini config/service.ini -are-sections-present app,database,cache || exit 1 goini -ini config/service.ini -section app -has-section-key version || exit 1 goini -ini config/service.ini -section app -key env -has-section-key-value staging || exit 1 - name: Deploy run: ./scripts/deploy.sh name: Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install goini run: | curl -sL https://github.com/andreimerlescu/goini/releases/download/v1.0.0/goini-linux-amd64 \ --output /usr/local/bin/goini chmod +x /usr/local/bin/goini - name: Validate config run: | goini -ini config/service.ini -are-sections-present app,database,cache || exit 1 goini -ini config/service.ini -section app -has-section-key version || exit 1 goini -ini config/service.ini -section app -key env -has-section-key-value staging || exit 1 - name: Deploy run: ./scripts/deploy.sh name: Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install goini run: | curl -sL https://github.com/andreimerlescu/goini/releases/download/v1.0.0/goini-linux-amd64 \ --output /usr/local/bin/goini chmod +x /usr/local/bin/goini - name: Validate config run: | goini -ini config/service.ini -are-sections-present app,database,cache || exit 1 goini -ini config/service.ini -section app -has-section-key version || exit 1 goini -ini config/service.ini -section app -key env -has-section-key-value staging || exit 1 - name: Deploy run: ./scripts/deploy.sh stages: - validate - deploy validate_config: stage: validate image: golang:1.22 before_script: - curl -sL https://github.com/andreimerlescu/goini/releases/download/v1.0.0/goini-linux-amd64 --output /usr/local/bin/goini - chmod +x /usr/local/bin/goini script: - goini -ini config/service.ini -are-sections-present app,database || exit 1 - goini -ini config/service.ini -section database -has-section-key host || exit 1 - goini -ini config/service.ini -section app -key env -has-section-key-value staging || exit 1 stages: - validate - deploy validate_config: stage: validate image: golang:1.22 before_script: - curl -sL https://github.com/andreimerlescu/goini/releases/download/v1.0.0/goini-linux-amd64 --output /usr/local/bin/goini - chmod +x /usr/local/bin/goini script: - goini -ini config/service.ini -are-sections-present app,database || exit 1 - goini -ini config/service.ini -section database -has-section-key host || exit 1 - goini -ini config/service.ini -section app -key env -has-section-key-value staging || exit 1 stages: - validate - deploy validate_config: stage: validate image: golang:1.22 before_script: - curl -sL https://github.com/andreimerlescu/goini/releases/download/v1.0.0/goini-linux-amd64 --output /usr/local/bin/goini - chmod +x /usr/local/bin/goini script: - goini -ini config/service.ini -are-sections-present app,database || exit 1 - goini -ini config/service.ini -section database -has-section-key host || exit 1 - goini -ini config/service.ini -section app -key env -has-section-key-value staging || exit 1