Metadata-Version: 2.4
Name: dotenv-fusion
Version: 0.7.0
Summary: Advanced .env loader with imports, conditionals, typing, validation, and compilation to standard format
Keywords: dotenv,environment-variables,configuration,env-loader,variable-expansion,imports,conditionals,typing,validation,shell-integration,fuse,compilation
Author: Gaëtan Montury
Author-email: Gaëtan Montury <10528250-pytgaen@users.noreply.gitlab.com>
License-Expression: LGPL-3.0-or-later
License-File: LICENSE.txt
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Systems Administration
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Operating System :: OS Independent
Classifier: Environment :: Console
Classifier: Typing :: Typed
Requires-Dist: pytest>=9 ; extra == 'dev'
Requires-Dist: pytest-cov>=5 ; extra == 'dev'
Requires-Dist: vermin>=1.8.0 ; extra == 'dev'
Maintainer: Gaëtan Montury
Maintainer-email: Gaëtan Montury <10528250-pytgaen@users.noreply.gitlab.com>
Requires-Python: >=3.10
Project-URL: Homepage, https://gitlab.com/pytgaen-group/dotenv-fusion
Project-URL: Documentation, https://dotenv-fusion-988a9a.gitlab.io
Project-URL: Repository, https://gitlab.com/pytgaen-group/dotenv-fusion
Project-URL: Issues, https://gitlab.com/pytgaen-group/dotenv-fusion/-/issues
Project-URL: Changelog, https://gitlab.com/pytgaen-group/dotenv-fusion/-/blob/main/CHANGELOG.md
Provides-Extra: dev
Description-Content-Type: text/markdown

<div align="center">
  <img src="https://gitlab.com/pytgaen-group/dotenv-fusion/-/raw/main/docs/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.

Advanced directives start with `#@` and remain comments for dotenv loaders that
accept comment lines. Use `fuse` to flatten directives, selecting the dialect
required by the consuming tool.

Read the [documentation](https://dotenv-fusion-988a9a.gitlab.io/) or use the
curated [llms.txt](https://dotenv-fusion-988a9a.gitlab.io/llms.txt) index for AI
tools.

## Installation

The package is available from PyPI and can also be run directly from GitLab during development.

```bash
# Install from PyPI
pip install dotenv-fusion

# Or add it to an uv project
uv add dotenv-fusion

# Or install the CLI in an isolated uv tool environment
uv tool install dotenv-fusion

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

# Or run directly from GitLab
uvx git+https://gitlab.com/pytgaen-group/dotenv-fusion

# 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)

# PowerShell
dotenv-fusion load -o powershell | Invoke-Expression

# Validate without exporting
dotenv-fusion check
dotenv-fusion check -f .env-fuse --var ENV=prod

# Run an application without printing shell exports
dotenv-fusion exec -- application --serve

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

### Shared schema contracts

Keep values in `.env-fuse` while sharing one explicit contract:

```dotenv
# .env-fuse
#@schema application.env-schema
ENV=development
PORT=8080
```

```text
# application.env-schema
ENV type=enum values=development,staging,production required=true
PORT type=int min=1 max=65535 default=8080
API_URL type=str format=url schemes=https required=true
```

Schemas use the same options as `#@def`, can include other schemas with
`#@schema`, and are loaded by `check`, `load`, `exec`, `list`, `docs`,
`template`, and `fuse`. See [Define a Shared Schema](docs/schema.md) for imports,
duplicates, confinement, monorepo examples, and migration.

### Age-backed secrets

Keep several secrets in one age-encrypted dotenv bundle and select only the
keys used by the application:

```dotenv
# .env-fuse
# Optional: defaults to ./secrets relative to this file
DOTENV_FUSION_AGE_STORE=./secrets

#@def DB_USERNAME source="age://store/database.env.age#USERNAME"
#@def DB_PASSWORD required=true source="age://store/database.env.age#PASSWORD"
DATABASE_URL=postgresql://${DB_USERNAME}:${DB_PASSWORD}@database/app
```

```bash
# Optional for recipient-encrypted files; repeat --age-identity for several identities
export DOTENV_FUSION_AGE_IDENTITY=/home/user/.config/age/keys.txt

dotenv-fusion check
dotenv-fusion check --no-resolve-sources  # Static validation without age access
dotenv-fusion exec -- application --serve
eval "$(dotenv-fusion load)"              # Explicit plaintext shell exports
```

Relative store paths are resolved from the main `.env-fuse`, not the shell's
working directory. `DOTENV_FUSION_AGE_STORE` from the parent environment has
priority over the value in the file; absolute paths are also supported.

The plaintext of `database.env.age` is a normal dotenv bundle such as
`USERNAME=application` and `PASSWORD=...`. It is decrypted once per invocation,
even when several selectors use it. `load` and `exec` override existing
environment variables by default. Use `--no-override` to preserve them and
avoid contacting age for values already present.

`list`, `docs`, and `template` inspect source declarations without contacting
age. `check` resolves sources by default; use `check --no-resolve-sources` for
syntax and dependency validation on runners without store access. `load`
resolves sources and emits real shell exports, so `exec` is preferred whenever
the application can be launched directly.

`fuse` resolves sources and writes their plaintext values to the requested output:

```bash
dotenv-fusion fuse -o .env.production
dotenv-fusion fuse -o .env.production --diff
```

`fuse --stdout` and `fuse --load` also emit resolved plaintext values, like
`load`. New output files use mode `0600` on POSIX; diffs of sourced configurations
redact every assignment value because an old artifact's secret provenance may
no longer be available.

Diffs always mask existing assignment values, even after removing the last
source declaration. New public values remain visible in source-free configurations.

#### Migrate an existing `.env`

Select secrets by exact name or case-sensitive glob. Quote globs so the shell
does not expand them:

```bash
dotenv-fusion migrate secret \
  --from .env \
  --to .env-fuse \
  --secret '*_TOKEN' \
  --secret '*_PWD' \
  --public 'PUBLIC_*' \
  --age-output secrets/application.env.age \
  --dry-run

dotenv-fusion migrate secret \
  --from .env \
  --to .env-fuse \
  --secret '*_TOKEN' \
  --secret '*_PWD' \
  --public 'PUBLIC_*' \
  --age-output secrets/application.env.age \
  --age-recipient 'age1...'
```

The original `.env` is retained. Secret values are passed directly to `age`
through stdin, without a plaintext temporary file, and neither values nor age
output are printed. New outputs use mode `0600` on POSIX; existing outputs are
refused unless `--force` is explicit.

#### Create or edit an encrypted bundle

With `DOTENV_FUSION_AGE_IDENTITY` configured:

```bash
dotenv-fusion secret edit secrets/application.env.age
```

The command opens an existing bundle or a new empty document in your editor,
validates the dotenv content, then encrypts it. In the normal single-key case,
the public recipient is derived from your identity automatically. Existing
bundles receive a unique encrypted backup before atomic replacement.
See [Create & Edit Secrets](docs/secret-edit.md) for
editor selection, multiple recipients, and plaintext temporary-file handling.

### Convenient aliases

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

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

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

# PowerShell
function dof { dotenv-fusion load -o powershell | Invoke-Expression }
function dofn { dotenv-fusion load -o powershell --no-override | Invoke-Expression }
```

Then run `dof` to load with override (the default), or `dofn` to preserve
variables that already exist in the shell.

## 🚀 Meet Fuse: Portable .env Compilation

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

**The Solution**: dotenv-fusion compiles advanced `.env-fuse` files into flat
assignments. The default `dotenv` dialect keeps the existing quoted output for
dotenv-aware consumers. Use `--dialect docker` for `docker run --env-file`,
whose parser preserves quotes instead of removing them.

```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. The selected consumer reads a flat file with resolved values
```

### Why this is powerful

- ✨ **Explicit compatibility**: Use the default dialect for dotenv-aware tools
    and `--dialect docker` for `docker run --env-file`
- 🎯 **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 CLI Integration**:

```bash
dotenv-fusion fuse --var ENV=production --dialect docker -o .env.docker
docker run --env-file .env.docker my-image
```

The Docker dialect writes raw values because `docker run --env-file` performs
no quote removal or escape processing. It refuses values containing newlines or
NUL bytes, which that line-oriented format cannot represent.

**One-Command Development**:

```bash
# Compile, write .env, and emit shell exports for eval
eval "$(dotenv-fusion fuse --load --var ENV=dev)"
# Variables are in your shell, .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
dotenv-fusion fuse --dialect docker -o .env.docker  # docker run --env-file

# Compile and emit shell exports
eval "$(dotenv-fusion fuse --load)"             # Compile then export via eval
eval "$(dotenv-fusion fuse --load --var ENV=prod)"  # With variables

# Output and review options
dotenv-fusion fuse --stdout                     # Print to stdout
dotenv-fusion fuse --dry-run                    # Preview without writing
dotenv-fusion fuse --diff                       # Show redacted diff; exits 1 on drift
dotenv-fusion fuse -o .env.production  # Explicit secret artifact
dotenv-fusion fuse --strip-comments             # No header comments
dotenv-fusion fuse --restrict-imports            # Confine imports to the source directory
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
```

### Automatic Input Selection in Load

The `load` command selects the first existing input in its load order:

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

# Promote .env while retaining the other fallbacks
eval "$(dotenv-fusion load --load-order=env)"
eval "$(DOTENV_FUSION_LOAD_ORDER=env dotenv-fusion load)"

# 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
```

`--load-order` and `DOTENV_FUSION_LOAD_ORDER` accept a partial comma-separated
list of `fuse`, `fuse-local`, and `env`. Listed inputs are promoted; omitted
inputs retain their default relative order. For example, `env` expands to
`env,fuse,fuse-local`. An explicit `-f/--file` bypasses automatic ordering.

`load` overrides existing environment variables by default. Use
`--no-override`, or set `DOTENV_FUSION_EXISTING_ENV=no-override`, to preserve
them. Explicit `--override` or `--no-override` takes precedence over the
environment setting.

### 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
# Resolve and export variables for shell integration
dotenv-fusion load [options]

# Validate without exporting or writing output
dotenv-fusion check [options]

# Resolve sources and run a command directly
dotenv-fusion exec [options] -- COMMAND [ARG...]

# Migrate selected variables from .env into an age bundle
dotenv-fusion migrate secret [options]

# Move inline #@def declarations into a new schema
dotenv-fusion migrate schema -f .env-fuse -o application.env-schema [--dry-run]

# Create or edit an encrypted dotenv bundle
dotenv-fusion secret edit PATH [options]

# List all variable names
dotenv-fusion list [options]

# Show variable documentation
dotenv-fusion docs [options]

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

# Compile .env-fuse to standard .env
dotenv-fusion fuse [options]

# Load command options:
  -f, --file FILE      File to load (default: auto-detect by load order)
  -o, --format FORMAT  Output format: bash, zsh, fish, powershell, json (default: bash)
  --load-order ORDER   Promote auto-detected inputs: fuse, fuse-local, env
  --override           Override existing environment variables (default)
  --no-override        Preserve existing environment variables
  --var, -V KEY=VALUE  Compilation variable (e.g., --var ENV=prod)
  --restrict-imports   Confine imports to the source file's parent directory
  --age-identity PATH  Age identity file (repeatable)
  --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 with secrets masked
  -t, --typed          Show values with types and secrets masked
  -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 for
regular dotenv features:

```bash
# Direct usage
python src/dotenv_fusion/dotenvfusion.py load -f .env --debug

# Or install the standalone core with curl
DOTENV_FUSION_REF=main  # Use a release tag for a reproducible installation
install -d ~/.local/bin
curl -fsSLo ~/.local/bin/dotenv-fusion \
  "https://gitlab.com/pytgaen-group/dotenv-fusion/-/raw/${DOTENV_FUSION_REF}/src/dotenv_fusion/dotenvfusion.py"
chmod +x ~/.local/bin/dotenv-fusion
```

pip, uv, and uvx installs include the official age backend. For a standalone
installation, add it as an optional sidecar:

```bash
DOTENV_FUSION_REF=main  # Use the same ref as the standalone core
install -d ~/.local/lib/dotenv-fusion/backends
curl -fsSLo ~/.local/lib/dotenv-fusion/backends/age.py \
  "https://gitlab.com/pytgaen-group/dotenv-fusion/-/raw/${DOTENV_FUSION_REF}/src/dotenv_fusion/backends/age.py"
```

An executable installed as `<prefix>/bin/dotenv-fusion` discovers sidecars in
`<prefix>/lib/dotenv-fusion/backends/`. An absolute
`DOTENV_FUSION_BACKEND_PATH` overrides that location. URI schemes remain
allowlisted by the core; `.env-fuse` cannot select an arbitrary Python module.
Always download the standalone core and its sidecars from the same ref. For a
stable installation, replace `main` with a release tag.

## 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 the contract for a variable inline. The same
grammar is available in an external [schema](docs/schema.md):

```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"
#@def ENV type=enum values=development,staging,production
#@def API_URL type=str format=url schemes=https

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

Options:

- `type=`: int, float, bool, str, list, json, enum (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/false`: explicitly mark or unmark a secret; when omitted, the
    variable name is used for auto-detection
- `lazy=true`: preserve `${VAR}` references in fuse output instead of resolving (default: false)
- `doc=`: variable description
- `values=`: comma-separated members required by `type=enum`
- `min=`, `max=`: inclusive bounds for int and float
- `nonempty=`, `min_length=`, `max_length=`: constraints for str and list
- `format=url`, `schemes=`: local URL syntax and allowed-scheme validation

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)
```

With `--no-override -v`, shell export commands also report variables skipped
because the parent environment wins, followed by an `[EXPORT]` count. These
diagnostics go to stderr and never alter the stdout stream consumed by `eval`.

**Secret detection**: Variables are automatically marked as secrets if their name contains common patterns (password, secret, token, key, api_key, credential, etc.). Use `secret=true` to force masking or `secret=false` to override a false positive:

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

#@def PUBLIC_KEY type=str secret=false doc="Explicitly public despite its name"
PUBLIC_KEY=public-value
```

**Note**: Secrets and values derived from them are masked in verbose, `--debug`,
and `--typed` diagnostic output. The actual export commands and environment
variables contain the real values for proper shell integration. Diagnostic
commands do not provide an option to reveal secrets.

```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
```

Secret values and values derived from them are emitted as empty placeholders;
public values and defaults remain available in the generated template.

### 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
1. Variables defined in the current file and imported files (in order)
1. System environment variables
1. 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:

- **POSIX shell escaping**: Uses Python's standard `shlex.quote()` function
- **PowerShell escaping**: Uses single-quoted literals with embedded quotes escaped
- **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
- Treat `.env` and `.env-fuse` files as trusted configuration: they can expand
    process environment variables and import readable files.
- Use `--restrict-imports` to confine resolved imports, including symlinks, to
    the main source file's parent directory. External imports remain allowed by
    default for backward compatibility.
- On POSIX, `fuse` creates new output files with mode `0600` and preserves the
    permissions of existing output files.
- Use `--debug` to inspect resolved non-secret values before loading; secrets and
    their derived values remain masked: `dotenv-fusion load --debug`

## Python API

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

# Simple function; auto-detects .env-fuse, .env-fuse.local, then .env
values = load_dotenv(override=False, apply_types=True)

# Resolve dynamic imports and optionally search one parent directory
values = load_dotenv(compile_vars={"ENV": "prod"}, walk=1)

# 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()
```

The names listed in `dotenv_fusion.__all__` form the supported public API.
Other module attributes remain implementation details. Shipping `py.typed`
describes the annotations of that public surface; it does not promote internal
helpers into the API.

## License

LGPL-3.0-or-later
