How a lightweight Go tool and companion package eliminated the fragile bash gymnastics we'd been writing for years.
The Problem With .env Files in Pipelines
Every team has the same archaeology story. Someone wrote a deployment script three years ago that does something like this:
grep "^DB_HOST=" .env | cut -d= -f2
Then someone else needed to handle quoted values, so it became:
grep "^DB_HOST=" .env | cut -d= -f2 | tr -d '"'
Then a third person needed to check whether a variable existed before deploying, and now you have forty lines of bash doing something that should take one. The file accumulates. Nobody touches it. It works until it doesn't.
goenv by Andrei Merlescu solves this with two things: a compiled CLI binary you can drop into any pipeline, and a typed Go package for applications that need to read env vars with real type safety and validation. This article walks through both — from initial setup through production pipeline automation — using real scenarios drawn from actual DevOps workflows.
What goenv Actually Is
Before diving in, it helps to understand the two distinct components:
The goenv CLI is a binary that lets you create, read, modify, validate, and export .env files into other formats (JSON, YAML, TOML, INI, XML). It is designed to be composed in shell scripts and CI pipelines.
The env package (github.com/andreimerlescu/goenv/env) is a Go library for reading typed environment variables from the process environment — booleans, integers, durations, lists, maps — with fallbacks, validation helpers, and configurable parsing behavior.
They share a common philosophy: be explicit, be composable, fail loudly when something is wrong.
Installation
CLI Tool
go install github.com/andreimerlescu/goenv@latest
In a CI environment where you cannot rely on go install, build and cache the binary:
GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o bin/goenv-linux-amd64 .
The repository ships pre-built binaries for darwin-amd64, darwin-arm64, linux-amd64, and windows in its bin/ directory, which you can copy directly into your pipeline image.
Go Package (env only, no CLI required)
go get -u github.com/andreimerlescu/goenv/env
Scenario 1: Bootstrapping a Fresh Service Environment
Your team is deploying a new microservice. The deployment script needs to create the .env file from scratch, populate it with runtime values, and validate it before the service starts. Previously this was done with a heredoc and sed. Now:
#!/bin/bash
set -euo pipefail
SERVICE_DIR="/opt/services/payments"
ENV_FILE="${SERVICE_DIR}/service.env"
# Create the file if it doesn't exist yet
goenv -file "${ENV_FILE}" -init -write
# Populate with values derived at deploy time
goenv -file "${ENV_FILE}" -write -add -env SERVICE_NAME -value "payments"
goenv -file "${ENV_FILE}" -write -add -env SERVICE_PORT -value "8443"
goenv -file "${ENV_FILE}" -write -add -env DEPLOY_SHA -value "$(git rev-parse --short HEAD)"
goenv -file "${ENV_FILE}" -write -add -env DEPLOY_DATE -value "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
goenv -file "${ENV_FILE}" -write -add -env DATA_FOLDER -value "$(pwd)/data"
goenv -file "${ENV_FILE}" -write -add -env LOG_LEVEL -value "info"
# Confirm everything landed
goenv -file "${ENV_FILE}" -print
# Validate required keys exist before handing off to the service
goenv -file "${ENV_FILE}" -has -env SERVICE_NAME || { echo "SERVICE_NAME missing"; exit 1; }
goenv -file "${ENV_FILE}" -has -env DEPLOY_SHA || { echo "DEPLOY_SHA missing"; exit 1; }
echo "Environment bootstrapped successfully."
Notice the key discipline here: -add only adds a key if it does not already exist. If you run this script twice, it does not overwrite DEPLOY_SHA with a different commit hash on the second run. The existing value is preserved. This is intentional and important for idempotent pipelines.
Scenario 2: Validation Gate Before Deployment
Your CD pipeline has a validation stage that runs before any deployment. It needs to confirm that a staging .env file contains the expected values — not just that the keys exist, but that they hold the right content. The -is flag handles this:
#!/bin/bash
set -euo pipefail
ENV_FILE=".env.staging"
echo "==> Validating staging environment..."
# Assert APP_ENV equals "staging"
if ! goenv -file "${ENV_FILE}" -is -env APP_ENV -value staging; then
echo "ERROR: APP_ENV must be 'staging' in ${ENV_FILE}"
exit 1
fi
# Assert DEBUG is not enabled in staging
if goenv -file "${ENV_FILE}" -is -env DEBUG -value true; then
echo "ERROR: DEBUG must not be 'true' in staging"
exit 1
fi
# Assert a required key exists
if ! goenv -file "${ENV_FILE}" -has -env DATABASE_URL; then
echo "ERROR: DATABASE_URL is required"
exit 1
fi
# Assert a key that should have been removed is actually gone
if ! goenv -file "${ENV_FILE}" -not -has -env LEGACY_API_KEY; then
echo "ERROR: LEGACY_API_KEY should have been removed from ${ENV_FILE}"
exit 1
fi
# Assert DB_PORT is not pointing at a dev-only value
if goenv -file "${ENV_FILE}" -is -env DB_PORT -value 5433; then
echo "ERROR: DB_PORT 5433 is the dev port — use 5432 in staging"
exit 1
fi
echo "==> Validation passed."
This pattern makes validation failures explicit and human-readable. Each check carries its own error message. The pipeline log tells you exactly what failed and why, rather than leaving you to diff files manually.
Scenario 3: Multi-Format Export for Infrastructure Tools
Your deployment touches three different systems: a Terraform workspace that reads JSON, an Ansible playbook that reads INI, and a Helm values override that reads YAML. You maintain one .env file and derive the others:
#!/bin/bash
set -euo pipefail
ENV_FILE="infra/deploy.env"
echo "==> Exporting ${ENV_FILE} to all formats..."
# Generate all formats in one command
goenv -file "${ENV_FILE}" -mkall -write
# Resulting files:
# infra/deploy.env.json ← Terraform input
# infra/deploy.env.yaml ← Helm values
# infra/deploy.env.ini ← Ansible inventory vars
# infra/deploy.env.toml ← any TOML-native tooling
# infra/deploy.env.xml ← any XML-native tooling
ls -lh infra/deploy.env*
# Verify the JSON is valid before passing it to Terraform
cat infra/deploy.env.json | python3 -m json.tool > /dev/null \
&& echo "JSON valid" \
|| { echo "JSON invalid"; exit 1; }
echo "==> Export complete."
If you only need one format for a specific pipeline step, export just that one:
# Jenkins pipeline step: export for Ansible only
goenv -file deploy.env -ini -write
# GitLab CI step: export for Helm only
goenv -file deploy.env -yaml -write
You cannot combine format flags in a single call — goenv exits with an error if you try to use -json and -yaml together. This is by design: one command, one output format, one file. Use -mkall when you want all of them.
Scenario 4: Cleaning Up After a Pipeline Run
Generated format files are build artifacts. They should not accumulate between runs. The -cleanall flag removes all derived format files while leaving the source .env intact:
#!/bin/bash
# cleanup.sh — run as a post-pipeline hook
ENV_FILE="deploy.env"
echo "==> Cleaning derived format files..."
goenv -file "${ENV_FILE}" -cleanall -write
# Only deploy.env remains; .json .yaml .toml .ini .xml are removed
ls -la *.env*
echo "==> Clean complete."
You can also prevent deletion entirely via environment variable, which is useful if a downstream job still needs the files when the cleanup hook fires:
export AM_GO_ENV_NEVER_DELETE=true
goenv -file deploy.env -cleanall -write
# → no files deleted; flag is silently respected
Scenario 5: Production File Protection
Production .env files warrant special treatment. goenv has a built-in protection mechanism: any file whose path contains production is treated as protected by default. Interacting with it requires explicitly passing -prod:
# This exits with an error — no -prod flag
goenv -file .env.production -write -add -env SECRET_KEY -value "new-secret"
# This works — -prod flag grants access
goenv -prod -file .env.production -write -add -env SECRET_KEY -value "new-secret"
For pipelines where you want to enforce this at the environment level and never allow any script to write production files regardless of flags:
export GOENV_NEVER_WRITE_PRODUCTION=true
# Even with -prod, writes are blocked
goenv -prod -file .env.production -write -add -env SECRET_KEY -value "x"
# → exits with error
This is particularly useful in CI environments where you want a hard guarantee that no job — regardless of what flags it passes — can mutate a production config.
Scenario 6: n8n Automation Deployment
This is a direct example from the goenv README. You are deploying an n8n instance and need to build its environment file from scratch with runtime values:
#!/bin/bash
set -euo pipefail
ENV_FILE="n8n.env"
DOMAIN="gh.dev"
SUBDOMAIN="n8n"
goenv -init -write -file "${ENV_FILE}"
goenv -write -file "${ENV_FILE}" -add -env DATA_FOLDER -value "$(pwd)"
goenv -write -file "${ENV_FILE}" -add -env DOMAIN -value "${DOMAIN}"
goenv -write -file "${ENV_FILE}" -add -env SUBDOMAIN -value "${SUBDOMAIN}"
goenv -write -file "${ENV_FILE}" -add -env SSL_EMAIL -value "webmaster@${SUBDOMAIN}.${DOMAIN}"
# Inspect before deploying
goenv -file "${ENV_FILE}" -print
# Validate the SSL email was formed correctly
goenv -file "${ENV_FILE}" -is -env SSL_EMAIL -value "webmaster@n8n.gh.dev" \
&& echo "SSL_EMAIL correct" \
|| { echo "SSL_EMAIL malformed"; exit 1; }
# Check GENERIC_TIMEZONE — expected to be absent until set
if goenv -file "${ENV_FILE}" -not -has -env GENERIC_TIMEZONE; then
echo "WARNING: GENERIC_TIMEZONE not set — n8n will default to UTC"
fi
# Export for review as TOML and XML
goenv -file "${ENV_FILE}" -toml
goenv -file "${ENV_FILE}" -xml
Scenario 7: Environment Promotion (Dev → Staging → Prod)
You have three environment files. When promoting a release, you need to carry certain values forward, strip others, and enforce that the promoted file is valid before it is used:
#!/bin/bash
set -euo pipefail
SOURCE=".env.development"
TARGET=".env.staging"
echo "==> Promoting ${SOURCE} to ${TARGET}..."
# Read values from dev and write into staging
# (goenv -add will not overwrite if key already exists in staging)
for KEY in APP_NAME APP_VERSION DEPLOY_SHA LOG_LEVEL; do
VALUE=$(goenv -file "${SOURCE}" -is -env "${KEY}" -value "" || true)
goenv -file "${TARGET}" -write -add -env "${KEY}" -value "${VALUE}"
done
# Scrub dev-only keys from the staging file
goenv -file "${TARGET}" -write -rm -env LOCAL_DB_PATH
goenv -file "${TARGET}" -write -rm -env DEV_MOCK_PAYMENTS
goenv -file "${TARGET}" -write -rm -env SKIP_AUTH
# Confirm dev-only keys are absent
goenv -file "${TARGET}" -not -has -env LOCAL_DB_PATH || { echo "LOCAL_DB_PATH still present"; exit 1; }
goenv -file "${TARGET}" -not -has -env DEV_MOCK_PAYMENTS || { echo "DEV_MOCK_PAYMENTS still present"; exit 1; }
# Assert staging-specific values are correct
goenv -file "${TARGET}" -is -env APP_ENV -value staging || { echo "APP_ENV must be staging"; exit 1; }
echo "==> Promotion complete."
goenv -file "${TARGET}" -print
Scenario 8: GitHub Actions / GitLab CI Integration
GitHub Actions
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Install goenv
run: go install github.com/andreimerlescu/goenv@latest
- name: Validate environment file
run: |
goenv -file .env.staging -has -env DATABASE_URL || exit 1
goenv -file .env.staging -has -env SERVICE_PORT || exit 1
goenv -file .env.staging -not -has -env DEBUG_KEY || exit 1
goenv -file .env.staging -is -env APP_ENV -value staging || exit 1
- name: Export to JSON for Terraform
run: |
goenv -file .env.staging -json -write
cat .env.staging.json
- name: Deploy
run: ./scripts/deploy.sh
- name: Clean up artifacts
if: always()
run: goenv -file .env.staging -cleanall -write
GitLab CI
stages:
- validate
- export
- deploy
- cleanup
validate_env:
stage: validate
image: golang:1.22
script:
- go install github.com/andreimerlescu/goenv@latest
- goenv -file .env.production -has -env DB_HOST || exit 1
- goenv -file .env.production -has -env REDIS_URL || exit 1
- goenv -file .env.production -not -has -env DEV_FLAG || exit 1
- goenv -file .env.production -is -env APP_ENV -value production || exit 1
export_formats:
stage: export
image: golang:1.22
script:
- go install github.com/andreimerlescu/goenv@latest
- goenv -file .env.production -mkall -write
artifacts:
paths:
- .env.production.json
- .env.production.yaml
cleanup:
stage: cleanup
image: golang:1.22
when: always
script:
- go install github.com/andreimerlescu/goenv@latest
- goenv -file .env.production -cleanall -write
Scenario 9: Using the env Package in a Go Service
The CLI handles files. The env package handles your running process. Here is a realistic service configuration pattern:
package main
import (
"fmt"
"log"
"net/http"
"time"
"github.com/andreimerlescu/goenv/env"
)
type Config struct {
Host string
Port int
Debug bool
Timeout time.Duration
MaxConns int
AllowedOrigins []string
DBCredentials map[string]string
}
func LoadConfig() Config {
// MustExist exits (or panics) if these are absent —
// use in init() to catch misconfiguration at startup
env.MustExist("DB_HOST")
env.MustExist("DB_PASS")
return Config{
Host: env.String("HOST", "0.0.0.0"),
Port: env.Int("PORT", 8080),
Debug: env.Bool("DEBUG", false),
Timeout: env.Duration("REQUEST_TIMEOUT", 30*time.Second),
MaxConns: env.Int("DB_MAX_CONNS", 10),
// ALLOWED_ORIGINS="https://a.com,https://b.com"
AllowedOrigins: env.List("ALLOWED_ORIGINS", env.ZeroList),
// DB_CREDENTIALS="host=localhost,port=5432,name=mydb"
DBCredentials: env.Map("DB_CREDENTIALS", env.ZeroMap),
}
}
func main() {
cfg := LoadConfig()
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
log.Printf("starting on %s (debug=%v timeout=%s)", addr, cfg.Debug, cfg.Timeout)
http.ListenAndServe(addr, nil)
}
Scenario 10: Typed Validation in Application Startup
Beyond simply reading values, the env package can assert numeric constraints and list membership at startup — before the application accepts any traffic:
package main
import (
"fmt"
"os"
"github.com/andreimerlescu/goenv/env"
)
func validateEnvironment() {
errors := []string{}
// Port must be in user range
if !env.IntInRange("PORT", 8080, 1024, 49151) {
errors = append(errors, "PORT must be between 1024 and 49151")
}
// Max connections must not exceed a hard limit
if !env.IntLessThan("DB_MAX_CONNS", 10, 101) {
errors = append(errors, "DB_MAX_CONNS must be less than 101")
}
// Must have at least one allowed origin
if !env.ListIsLength("ALLOWED_ORIGINS", env.ZeroList, 0) {
if env.ListLength("ALLOWED_ORIGINS", env.ZeroList) == 0 {
errors = append(errors, "ALLOWED_ORIGINS must contain at least one entry")
}
}
// Feature map must contain required keys
if !env.MapHasKeys("FEATURE_FLAGS", env.ZeroMap, "auth", "payments", "notifications") {
errors = append(errors, "FEATURE_FLAGS must contain auth, payments, and notifications keys")
}
// DEBUG must be off in production
if env.IsTrue("DEBUG") && env.String("APP_ENV", "") == "production" {
errors = append(errors, "DEBUG must not be true in production")
}
// All gating flags must be false before going live
if !env.AreFalse("MAINTENANCE_MODE", "READ_ONLY", "LOCKED") {
errors = append(errors, "MAINTENANCE_MODE, READ_ONLY, and LOCKED must all be false")
}
if len(errors) > 0 {
fmt.Fprintln(os.Stderr, "Environment validation failed:")
for _, e := range errors {
fmt.Fprintf(os.Stderr, " - %s\n", e)
}
os.Exit(1)
}
}
func main() {
validateEnvironment()
fmt.Println("Environment OK — starting service")
}
Scenario 11: Custom Separators for Non-Standard Formats
Some systems export environment variables with non-comma separators. The env package lets you change list and map parsing at runtime, either in code or via environment variables:
package main
import (
"fmt"
"github.com/andreimerlescu/goenv/env"
)
func main() {
// Your upstream system exports pipe-separated lists
// SERVERS="web1|web2|web3"
env.ListSeparator = "|"
servers := env.List("SERVERS", env.ZeroList)
for i, s := range servers {
fmt.Printf("server[%d] = %s\n", i, s)
}
// Your config map uses pipe separation and tilde as key~value delimiter
// ROUTES="home~/,admin~/admin,api~/v1"
env.MapSeparator = "|"
env.MapItemSeparator = "~"
env.MapSplitN = 2
routes := env.Map("ROUTES", env.ZeroMap)
for path, target := range routes {
fmt.Printf("route %s -> %s\n", path, target)
}
}
Or set them entirely from the environment without touching code — useful when the same binary needs to adapt to different upstream formats across environments:
export AM_GO_ENV_LIST_SEPARATOR="|"
export AM_GO_ENV_MAP_SEPARATOR="|"
export AM_GO_ENV_MAP_ITEM_SEPARATOR="~"
export AM_GO_ENV_MAP_SPLIT_N=2
Scenario 12: Set, Unset, WasSet, WasUnset in Application Logic
Sometimes your application needs to mutate its own environment — for instance, deriving a value at startup and making it available to child processes or subpackages:
package main
import (
"fmt"
"path/filepath"
"github.com/andreimerlescu/goenv/env"
)
func main() {
u := env.User()
// Derive and set a path based on the current user's home directory
cacheDir := filepath.Join(u.HomeDir, ".cache", "myapp")
if !env.WasSet("CACHE_DIR", cacheDir) {
panic("failed to configure CACHE_DIR")
}
// Set with error handling
if err := env.Set("RUNTIME_USER", u.Username); err != nil {
panic(err)
}
// Remove a value that should not be visible to subprocesses
if !env.WasUnset("CI_JOB_TOKEN") {
fmt.Println("warning: could not unset CI_JOB_TOKEN")
}
// Verify cleanup
if env.Exists("CI_JOB_TOKEN") {
panic("CI_JOB_TOKEN still visible — aborting")
}
fmt.Println("CACHE_DIR:", env.String("CACHE_DIR", ""))
fmt.Println("RUNTIME_USER:", env.String("RUNTIME_USER", ""))
}
Scenario 13: Controlling Package Behavior for Different Environments
The env package ships with a Magic() function that reads AM_GO_ENV_* environment variables at startup and applies them automatically. This runs by default via the init() function. You can disable it and configure manually:
package main
import (
"log"
"github.com/andreimerlescu/goenv/env"
)
func init() {
// Disable automatic AM_GO_ENV_* discovery
env.UseMagic = false
// Configure explicitly for this service's requirements
env.AllowPanic = false // never panic — return fallbacks instead
env.PrintErrors = true // write parse errors to stderr
env.UseLogger = true // use structured INFO/ERR logger
env.EnableVerboseLogging = false // no debug noise in production
env.PanicNoUser = false // return default user on error
env.ListSeparator = ","
env.MapSeparator = ","
env.MapItemSeparator = "="
env.MapSplitN = 2
// Use base-10 int64 with 64-bit size (the defaults, made explicit)
env.Int64Base = 10
env.Int64BitSize = 64
log.Println("env package configured")
}
func main() {
port := env.Int("PORT", 8080)
log.Printf("port: %d", port)
}
When you want the opposite — maximum noise for debugging in a local dev environment — set these in your shell before running:
export AM_GO_ENV_ALWAYS_ALLOW_PANIC=true
export AM_GO_ENV_ALWAYS_PRINT_ERRORS=true
export AM_GO_ENV_ALWAYS_USE_LOGGER=true
export AM_GO_ENV_ENABLE_VERBOSE_LOGGING=true
go run main.go
Every call to env.String(), env.Int(), env.Bool() etc. will log to stdout when it falls back to a default value, giving you full visibility into what the process is and is not finding in its environment.
Complete CLI Flag Reference
| Flag | Type | Description |
|---|---|---|
-file |
string | Path to the .env file to operate on |
-env |
string | The environment variable name to query or write |
-value |
string | The value to pair with -env
|
-init |
bool | Create the file if it does not exist (empty) |
-write |
bool | Permit writes to the file |
-add |
bool | Add -env=-value if key is absent |
-rm |
bool | Remove the key matching -env
|
-has |
bool | Assert that -env exists in the file |
-is |
bool | Assert that -env equals -value
|
-not |
bool | Negate -has or -is
|
-print |
bool | Print all key=value pairs to stdout |
-json |
bool | Output as JSON |
-yaml |
bool | Output as YAML |
-toml |
bool | Output as TOML |
-ini |
bool | Output as INI |
-xml |
bool | Output as XML |
-mkall |
bool | Write all five format files at once |
-cleanall |
bool | Delete all derived format files |
-prod |
bool | Allow interaction with .env.production files |
-vv |
bool | Verbose output |
-v |
bool | Print version |
Complete env Package Function Reference
Types
env.String("KEY", "fallback")
env.Int("KEY", 0)
env.Int64("KEY", int64(0))
env.Float32("KEY", float32(0.0))
env.Float64("KEY", float64(0.0))
env.Bool("KEY", false)
env.Duration("KEY", 5*time.Second)
env.UnitDuration("KEY", 10, time.Second) // KEY=10 → 10s
env.List("KEY", env.ZeroList) // "a,b,c" → []string
env.Map("KEY", env.ZeroMap) // "k=v,k2=v2" → map[string]string
Existence and Truthiness
env.Exists("KEY") // bool
env.MustExist("KEY") // exits/panics if absent
env.IsTrue("KEY") // true if value is "true"/"1"/"t"
env.IsFalse("KEY") // true if value is "false"/"0"/"f" or unset
env.AreTrue("KEY1", "KEY2", "KEY3") // true if all are true
env.AreFalse("KEY1", "KEY2", "KEY3") // true if all are false/unset
Mutation
env.Set("KEY", "value") // error
env.Unset("KEY") // error
env.WasSet("KEY", "value") // bool — sets and verifies
env.WasUnset("KEY") // bool — unsets and confirms
Numeric Validation
env.IntLessThan("KEY", fallback, max)
env.IntGreaterThan("KEY", fallback, min)
env.IntInRange("KEY", fallback, min, max)
env.Int64LessThan("KEY", fallback, max)
env.Int64GreaterThan("KEY", fallback, min)
env.Int64InRange("KEY", fallback, min, max)
Collection Validation
env.ListLength("KEY", fallback)
env.ListIsLength("KEY", fallback, wantLength)
env.ListContains("KEY", fallback, "needle")
env.MapHasKey("KEY", fallback, "key")
env.MapHasKeys("KEY", fallback, "k1", "k2", "k3")
System
env.User() // *user.User — current OS user with safe fallback
Key Behavioral Details Worth Knowing
-add is idempotent. It only writes if the key is absent. If you run your bootstrap script twice, the second run does not clobber the first. This is the behavior you want in any pipeline that might retry.
-rm rewrites the file. It reads all lines, filters out the matching key, and writes the result back. It does not do in-place line editing.
Format flags are mutually exclusive. Combining -json and -yaml in one call exits with an error. Use -mkall when you need all formats.
-cleanall without -write is a dry run. It will print what it would delete but will not actually remove anything.
The env package's Magic() runs at import time. If you import the package, AM_GO_ENV_* variables in your shell are picked up automatically before main() runs. Set env.UseMagic = false in your own init() if you want explicit control.
MustExist respects AllowPanic. By default it calls os.Exit(1). Set AM_GO_ENV_ALWAYS_ALLOW_PANIC=true (or env.AllowPanic = true) to get a panic() instead — useful in test environments where you want the stack trace.
Closing Thoughts
The fragile bash patterns that accumulate around .env files are a symptom of tooling that was never designed for structured config management. goenv treats .env files as first-class data: readable, writable, queryable, exportable, and validatable through a consistent interface that composes cleanly into any pipeline.
The CLI gives shell scripts and CI jobs a stable contract for interacting with env files. The env package gives Go applications a typed, validated, zero-dependency way to read their own environment. Together they cover the full lifecycle of an environment variable from the file it is defined in to the running process that uses it.
The source is at github.com/andreimerlescu/goenv. The env sub-package is independently installable at github.com/andreimerlescu/goenv/env and carries an Apache 2.0 license.
Top comments (0)