<div align="center">
  <img src="https://dotenv-fusion-2a1246.gitlab.io/assets/dotenv-fusion_logo.jpg" alt="dotenv-fusion logo" width="400"/>
</div>

# dotenv-fusion

An advanced `.env` loader for Python with variable expansion, imports, conditions, typing and validation.

Compatible with other dotenv loaders: all advanced directives start with `#@` and are seen as comments by standard tools.

## Installation

The software is currently in preview mode; only the UVX version from GitLab is available at this time.

```bash
# run directly with uvx from Gitlab (no install needed)
uvx git+https://gitlab.com/pytgaen-group/dotenv-fusion

# Install from PyPI (when published)
pip install dotenv-fusion

# Or with uv
uv pip install dotenv-fusion

# Or run directly with uvx (no install needed)
uvx dotenv-fusion load

# Or install from source
pip install .
```

## Usage

### Shell integration (primary usage)

```bash
# Bash / Zsh
eval "$(dotenv-fusion load)"
eval "$(dotenv-fusion load -f .env.production)"

# Fish
eval (dotenv-fusion load -o fish)

# Example workflow
eval "$(dotenv-fusion load -f .env.production)"
echo $DATABASE_URL
```

### Convenient aliases

Add to your `~/.bashrc`, `~/.zshrc`, or `~/.config/fish/config.fish`:

```bash
# Bash/Zsh
alias dof='eval "$(dotenv-fusion load)"'
alias dofo='eval "$(dotenv-fusion load --override)"'

# Fish
alias dof='eval (dotenv-fusion load -o fish)'
alias dofo='eval (dotenv-fusion load -o fish --override)'
```

Then simply run `dof` to load your `.env` file, or `dofo` to load with override.

## 🚀 Meet Fuse: Universal .env Compatibility

**The Problem**: Tools like VS Code, Docker, and most editors only understand standard `.env` format. They can't parse advanced directives like `#@import`, `#@if`, etc.

**The Solution**: dotenv-fusion introduces a **compilation system** that transforms your advanced `.env-fuse` files into standard `.env` format, making them compatible with **every tool** while keeping all the power of advanced features.

```bash
# 1. Write advanced config with all features
# .env-fuse
#@import configs/database-${ENV}.env
#@if ${ENV} == production
LOG_LEVEL=error
#@endif

# 2. Compile to standard .env (one command!)
dotenv-fusion fuse --var ENV=production

# 3. VS Code, Docker, and all tools work perfectly!
# .env is now a flat file with resolved values
```

### Why this is powerful

- ✨ **Universal Compatibility**: Your `.env` files work with VS Code, Docker, Kubernetes, and every tool  
- 🎯 **Keep Advanced Features**: Use imports, conditions, and variables in `.env-fuse`  
- 🏗️ **Build-Time Variables**: Compile different environments with `--var` flags  
- ⚡ **Zero Configuration**: No VS Code extensions, no Docker plugins needed  
- 🔄 **Like TypeScript for .env**: `.env-fuse` → compile → `.env` (just like `.ts` → `.js`)  

### Compilation Workflows

**Multi-Environment Builds** (CI/CD):

```bash
# Development
dotenv-fusion fuse --var ENV=dev -o .env.dev

# Staging
dotenv-fusion fuse --var ENV=staging -o .env.staging

# Production EU
dotenv-fusion fuse --var ENV=prod --var REGION=eu -o .env.prod-eu

# Production US
dotenv-fusion fuse --var ENV=prod --var REGION=us -o .env.prod-us
```

**Docker Integration**:

```dockerfile
# Compile during build
ARG BUILD_ENV=production
RUN dotenv-fusion fuse --var ENV=${BUILD_ENV}
# Now .env is available for all other tools!
```

**One-Command Development**:

```bash
# Compile and load in one shot
dotenv-fusion fuse --load --var ENV=dev
# Variables are in your environment, .env file created
```

**Dynamic Imports**:

```bash
# .env-fuse
#@import secrets/${TENANT_ID}/api-keys.env
#@import configs/features-${FEATURE_SET}.env

# Compile for different tenants/features
dotenv-fusion fuse --var TENANT_ID=customer-123 --var FEATURE_SET=premium
dotenv-fusion fuse --var TENANT_ID=customer-456 --var FEATURE_SET=basic
```

### Fuse Command Reference

```bash
# Basic compilation
dotenv-fusion fuse                              # .env-fuse → .env

# With compilation variables (build-time variables)
dotenv-fusion fuse --var ENV=prod               # Single variable
dotenv-fusion fuse --var ENV=prod --var REGION=eu  # Multiple variables

# Custom source/target
dotenv-fusion fuse -f .env-fuse.prod           # Custom source
dotenv-fusion fuse -o .env.production          # Custom target

# Compile and load
dotenv-fusion fuse --load                       # Compile then load
dotenv-fusion fuse --load --var ENV=prod       # With variables

# Output options
dotenv-fusion fuse --stdout                     # Print to stdout
dotenv-fusion fuse --strip-comments             # No header comments
dotenv-fusion fuse -v                           # Verbose mode

# Search parent directories
dotenv-fusion fuse --walk                       # Search 1 parent (default)
dotenv-fusion fuse --walk-up 3                  # Search up to 3 parents
```

### Auto-Fallback in Load

The `load` command automatically falls back to `.env-fuse` if `.env` doesn't exist:

```bash
# Smart fallback
eval "$(dotenv-fusion load)"
# 1. Looks for .env
# 2. If not found → loads .env-fuse (with compilation)
# 3. If not found → loads .env-fuse.local

# With compilation variables
eval "$(dotenv-fusion load --var ENV=prod --var REGION=eu)"

# Search parent directories (e.g., from a subdirectory)
eval "$(dotenv-fusion load --walk)"           # Search 1 parent (default)
eval "$(dotenv-fusion load --walk-up 3)"      # Search up to 3 parents
DOTENV_FUSION_WALK_DEPTH=5 dotenv-fusion load --walk  # Custom default depth
```

### File Naming Convention

Like `.env` files, `.env-fuse` follows the same patterns:

- `.env-fuse` - Base configuration (source file)
- `.env-fuse.local` - Local developer overrides (gitignored)
- `.env-fuse.production` - Production config
- `.env-fuse.development` - Development config

Compiled files remove the `-fuse` suffix:

- `.env-fuse` → `.env`
- `.env-fuse.production` → `.env.production`

### CLI commands

```bash
# Load and export variables for shell integration
dotenv-fusion load [options]

# List all variable names
dotenv-fusion list [-f FILE]

# Show variable documentation
dotenv-fusion docs [-f FILE]

# Generate template .env file
dotenv-fusion template [-f FILE] > .env.example

# Load command options:
  -f, --file FILE      File to load (default: .env)
  -o, --format FORMAT  Output format: bash, zsh, fish, json (default: bash)
  --override           Override existing environment variables
  --var, -V KEY=VALUE  Compilation variable (e.g., --var ENV=prod)
  --walk               Search parent directories if not found locally (default: 1 level, configure with DOTENV_FUSION_WALK_DEPTH)
  --walk-up N          Search up to N parent directories (implies --walk)
  -v, --verbose        Verbose mode: -v (logic and names), -vv (values with secrets masked)
  -d, --debug          Show resolved values
  -t, --typed         Show values with types
  -h, --help          Help

Examples:
  dotenv-fusion docs                    # Show variable documentation
  dotenv-fusion template > .env.example # Generate template
  dotenv-fusion load -f .env.local -d   # Debug mode
  dotenv-fusion load -o json            # Output as JSON
  dotenv-fusion load -v                 # Verbose: show logic and variable names
  dotenv-fusion load -vv                # Very verbose: show values (secrets masked)
```

### Python API (optional)

For Python projects that need programmatic access:

```python
from dotenv_fusion import load_dotenv, get_env

# Load .env file
values = load_dotenv()  # or load_dotenv(".env.production")

# Get a variable
port = get_env("PORT", "3000")

# Variables are also available via os.environ
import os
print(os.environ["APP_NAME"])
```

### Single-script usage

The core `dotenvfusion.py` can be used standalone without installation:

```bash
# Direct usage
python -m dotenv_fusion.dotenvfusion -f .env --debug

# Or copy the file for standalone usage
cp src/dotenv_fusion/dotenvfusion.py /usr/local/bin/
python /usr/local/bin/dotenvfusion.py --help
```

## Features

### Variable expansion

```bash
# Internal variables
BASE_URL=http://localhost
API_URL=${BASE_URL}/api/v1

# System variables
DATA_DIR=$HOME/data
WORK_DIR=$PWD/workspace

# Default value
CACHE_DIR=${CACHE_PATH:-/tmp/cache}
TIMEOUT=${REQUEST_TIMEOUT:-30}
```

### Quotes

```bash
# Double quotes: variable expansion + escape sequences
MESSAGE="Welcome to ${APP_NAME}\nVersion: ${VERSION}"

# Single quotes: literal value (no expansion)
PATTERN='${NOT_EXPANDED}'

# No quotes: expansion + inline comments
DEBUG=true  # This is a comment
```

### Escaping

```bash
# Literal dollar
PRICE="\$99.99"
REGEX='\$[0-9]+'

# Backslash
PATH="C:\\Users\\name"
```

## Directives

All directives start with `#@` to remain compatible with other loaders.

### File imports

```bash
# Simple import (error if file doesn't exist)
#@import database.env

# With prefix (HOST becomes DB_HOST)
#@import database.env prefix=DB_

# Optional import (ignored if file doesn't exist)
#@import local.env mode=ifexist

# Override (overwrites existing variables)
#@import overrides.env override=true

# Combined
#@import secrets.env prefix=SECRET_ mode=ifexist override=true

# Dynamic path
#@import configs/${ENV}.env
```

Options:

- `prefix=PREFIX_`: prefix added to imported variables
- `override=true`: overwrite already defined variables (default: first-wins)
- `mode=ifexist`: silently ignore if file doesn't exist

### Variable definitions

The `#@def` directive defines type, validation, required status, default value and documentation for a variable:

```bash
#@def PORT type=int default=3000 validate=^\d+$ doc="HTTP listening port"
#@def DEBUG type=bool default=false doc="Enable debug mode"
#@def API_KEY required=true doc="API key (required)"
#@def EMAIL validate=^[\w.-]+@[\w.-]+\.\w+$ doc="Contact email"
#@def TAGS type=list default=web,api doc="Comma-separated tags"
#@def CONFIG type=json doc="JSON configuration"

PORT=8080
API_KEY=secret-key
EMAIL=contact@example.com
```

Options:

- `type=`: int, float, bool, str, list, json (default: str)
- `required=true`: error if variable is not defined (default: false)
- `default=`: default value if not defined
- `validate=`: regex pattern to validate the value
- `secret=true`: mark variable as secret (masked in verbose mode)
- `lazy=true`: preserve `${VAR}` references in fuse output instead of resolving (default: false)
- `doc=`: variable description

Show documentation:

```bash
dotenv-fusion docs
# PORT (int) (default: 3000): HTTP listening port
# DEBUG (bool) (default: false): Enable debug mode
# API_KEY (str) [required]: API key (required)
```

### Verbose mode and secrets

Verbose mode outputs diagnostic information to stderr while loading:

```bash
# Level 1: Show logic and variable names
dotenv-fusion load -v

# Output (stderr):
# [LOAD] .env
# [VAR] APP_NAME
# [VAR] DATABASE_PASSWORD (secret)
# [VAR] API_KEY (secret)
# [SUCCESS] Loaded 3 variables from 1 file(s)

# Level 2: Show values with secrets masked
dotenv-fusion load -vv

# Output (stderr):
# [LOAD] .env
# [VAR] APP_NAME = MyApp
# [VAR] DATABASE_PASSWORD = ***REDACTED*** (secret)
# [VAR] API_KEY = ***REDACTED*** (secret)
# [SUCCESS] Loaded 3 variables from 1 file(s)
```

**Secret detection**: Variables are automatically marked as secrets if their name contains common patterns (password, secret, token, key, api_key, credential, etc.). You can also explicitly mark variables as secrets:

```bash
#@def MY_TOKEN type=str secret=true doc="Explicitly marked as secret"
MY_TOKEN=my-secret-value

#@def PUBLIC_VAR type=str doc="Not a secret"
PUBLIC_VAR=public-value
```

**Note**: Secrets are only masked in verbose output (stderr). The actual export commands and environment variables contain the real values for proper shell integration.

```bash
# Verbose output goes to stderr (visible but doesn't affect eval)
eval "$(dotenv-fusion load -vv)"

# Environment variables contain real values
echo $API_KEY  # Shows the actual secret value, not ***REDACTED***
```

Generate template:

```bash
dotenv-fusion template > .env.example
```

### Conditions

```bash
ENV=production

#@if ${ENV} == production
LOG_LEVEL=error
DEBUG=false
#@else
LOG_LEVEL=debug
DEBUG=true
#@endif

# Existence test
#@ifdef CI
CI_MODE=true
#@endif

#@ifndef LOCAL
REMOTE=true
#@endif
```

Supported operators: `==`, `!=`, `>`, `<`, `>=`, `<=`

### Processing modes

The `#@mode` directive changes how subsequent lines are processed:

```bash
# Enable override mode: later definitions override earlier ones
#@mode override=true
API_URL=https://prod.api.com  # This will override any previous API_URL

# Enable strict mode: error if variable is undefined
#@mode strict=true
VALUE=${MUST_EXIST}  # Error if MUST_EXIST doesn't exist

# Combine multiple modes on one line
#@mode override=true strict=true

# Disable modes
#@mode override=false strict=false
```

**Available modes:**

- `override=true/false` - When true, variable definitions override existing values (default: no, first-wins)
- `strict=true/false` - When true, undefined variables raise an error instead of becoming empty (default: no)

By default, an undefined variable becomes an empty string and first definition wins.

## Complete examples

### Web application

```bash
# .env
#@def APP_NAME required=true doc="Application name"
#@def ENV required=true validate=^(dev|staging|production)$ doc="Environment"
#@def PORT type=int default=3000 doc="HTTP port"
#@def DEBUG type=bool default=false doc="Debug mode"

APP_NAME=MyApp
ENV=production

#@import configs/base.env
#@import configs/${ENV}.env
#@import .env.local mode=ifexist

#@if ${ENV} == production
#@import secrets/prod.env prefix=SECRET_
#@else
#@import secrets/dev.env prefix=SECRET_
#@endif
```

### Microservices

```bash
# .env
#@import services/database.env prefix=DB_
#@import services/redis.env prefix=REDIS_
#@import services/rabbitmq.env prefix=MQ_

# Result:
# DB_HOST, DB_PORT, DB_USER, DB_PASS
# REDIS_HOST, REDIS_PORT
# MQ_HOST, MQ_PORT, MQ_VHOST
```

### CI/CD

```bash
# .env
#@ifdef CI
#@import ci/test.env override=true
#@endif

#@ifdef DOCKER
#@import docker/container.env
#@endif

#@if ${DEPLOY_TARGET} == aws
#@import deploy/aws.env
#@endif

#@if ${DEPLOY_TARGET} == gcp
#@import deploy/gcp.env
#@endif
```

## Behavior

### Resolution order

1. Compilation variables (`--var`) - highest priority
2. Variables defined in the current file and imported files (in order)
3. System environment variables
4. Default values (`${VAR:-default}`)

### Precedence (first-wins)

By default, the first definition wins:

```bash
# .env
PORT=3000
#@import other.env  # If other.env contains PORT=8080, it's ignored
```

Use `override=true` to change this behavior.

### Circular imports

Circular imports are detected and silently ignored.

## Security

### Shell escaping

When using `eval "$(dotenv-fusion load)"`, all values are properly escaped to prevent command injection:

- **`shlex.quote()`**: Uses Python's standard shell escaping function
- **Variable name validation**: Only alphanumeric + underscore names are exported
- **Command injection prevention**: Dangerous values like `$(rm -rf /)` are safely escaped

```bash
# Example: dangerous value is safely escaped
DANGEROUS=$(echo "pwned")  # Will be exported as '$(echo "pwned")' - NOT executed

# Safe to use with eval
eval "$(dotenv-fusion load)"
echo $DANGEROUS  # Shows: $(echo "pwned") - the literal string
```

### Best practices

- Always use `eval "$(dotenv-fusion load)"` pattern for shell integration
- Review `.env` files before loading untrusted sources
- Use `--debug` flag to inspect values before loading: `dotenv-fusion load --debug`

## Python API

```python
from dotenv_fusion import DotenvLoader, load_dotenv, get_env

# Simple function
values = load_dotenv(".env", override=False, apply_types=True)

# Class for more control
loader = DotenvLoader()
values = loader.load(".env")

# Access raw values (strings)
raw_values = loader.values

# Access typed values
typed_values = loader.typed_values

# Get documentation
docs = loader.get_docs()

# Generate template
template = loader.generate_template()
```

## License

LGPL-3.0-or-later
