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:
grep -A 5 "^\[default\]" config.ini | grep "^user" | cut -d= -f2 | tr -d ' '
...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.
Installation
go install github.com/andreimerlescu/goini@latest
Or grab the binary directly:
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
The Sample File
Every example below operates against this sample.ini:
[default]
user = Yeshua
key = 369
port = 1776
country = ISRAEL
[extra]
ssh_key = ~/.ssh/id_rsa
ssh_key_pub = ~/.ssh/id_rsa.pub
Two sections. A handful of keys. Simple on purpose — the point is showing what goini can do with it, not the data.
Validating With Exit Codes
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.
Does a Section Have a Key?
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
Does a Key Have a Specific Value?
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
Are Multiple Sections All Present?
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
Does a Section Exist?
goini -ini sample.ini -has-section default
echo $? # 0
goini -ini sample.ini -has-section ghost
echo $? # 1
Reading With STDOUT
When you need to pull data out of the file and feed it downstream, these are your commands.
List All Sections
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
List Keys in a Section
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"
# ]
List Key-Value Pairs in a Section
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"
# }
Writing to the File
Add a New Section
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
This is idempotency by design. goini won't silently create a duplicate. It fails loudly so your pipeline knows something unexpected happened.
Add a Key to a Section
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
Again — it won't overwrite. If you need to change an existing value, that's what -modify-key is for.
Modify an Existing Key
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
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.
Real Pipeline Examples
Validating a Service Configuration Before Deploy
#!/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."
Bootstrapping a Fresh Config File
#!/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
Promoting Config From Staging to Production
#!/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; }
Extracting Values for Use in Other Scripts
#!/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}"
GitHub Actions
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
GitLab CI
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
Complete Flag Reference
| Flag | Type | Description |
|---|---|---|
-ini |
string |
Required. Path to the .ini file |
-section |
string | Section name for scoped operations |
-sections |
bool | List all sections in the file |
-add-section |
string | Add a new section. Fails if already present |
-has-section |
string | Exit 0 if section exists, 1 if not |
-has-section-key |
string | Exit 0 if key exists in -section
|
-has-section-key-value |
string | Exit 0 if key in -section equals this value. Requires -key
|
-are-sections-present |
string | Comma-separated list. Exit 0 if all present |
-key |
string | Key name for value operations |
-value |
string | Value for write or comparison operations |
-add-key |
bool | Add -key=-value to -section. Fails if key exists |
-modify-key |
bool | Update existing -key in -section. Fails if key absent |
-list-keys |
bool | Print all keys in -section to stdout |
-list-key-values |
bool | Print all key=value pairs in -section to stdout |
-csv |
bool | Output as CSV |
-json |
bool | Output as JSON |
-yaml |
bool | Output as YAML |
A Few Things Worth Knowing
-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.
Top comments (0)