Sign in Join

Live chat for Free Pascal, Lazarus, Delphi and every Pascal programmer.

Catalog · How the catalog works
Anything · Lazarus or Delphi
Random projectRandom project

dotenv-fp

A feature-rich dotenv library for Free Pascal: load environment variables from .env files with type-safe getters, validation, interpolation, and zero memory management.

View on GitHub

Member rating

No ratings yet.

Sign in to rate this project.

README

🌿 dotenv-fp

Load environment variables from .env files in Free Pascal — the easy way!

A feature-rich dotenv library for Free Pascal 3.2.2+ inspired by python-dotenv. Perfect for managing configuration in your Pascal applications without hardcoding sensitive data.

✨ Features

Feature Description
📁 Load .env files Read configuration from .env files
🔗 Variable interpolation Use ${VAR} or $VAR syntax to reference other variables
📝 Multi-line values Support for values spanning multiple lines
🎯 Quoted values Single quotes, double quotes, or unquoted
💬 Comments Use # for comments (line or inline)
🐚 Shell compatible Supports export prefix for shell compatibility
🔒 Type-safe getters GetInt(), GetBool(), GetFloat(), GetArray()
⚡ Default values Fallback values when keys are missing
✅ Validation Check for required variables before running
📚 Multiple files Load .env, .env.local, .env.production, etc.
🌍 Environment-aware Auto-load .env.{environment} files (v1.1.0+)
💾 Save to file Generate .env files programmatically (v1.1.0+)
📋 Generate examples Create .env.example for version control (v1.1.0+)
💬 Interactive prompts GetOrPrompt() for first-run setup (v1.1.0+)
🚨 Strict loading Actionable file, line, key, and reason diagnostics (v1.2.0+)
🧭 Typed schema validation Report all missing and invalid values together (v1.2.0+)
🛡️ Safe serialization Atomic, escaped, load/save round trips (v1.2.0+)
🔨 Reproducible examples Build every canonical example into examples-bin/ (v1.2.1+)
🏷️ Key prefixing Add prefixes like APP_ to all loaded keys
🧹 Zero memory leaks Uses advanced records — no manual Free calls!
📦 Zero dependencies Only standard FPC units

🚀 Quick Start

From the repository root, create your local configuration:

bash
# Linux/macOS
cp .env.example .env
powershell
# Windows PowerShell
Copy-Item .env.example .env

Build all canonical Lazarus examples and run the newcomer example:

bash
bash ./build-examples.sh
./examples-bin/hello_dotenv
powershell
./build-examples.ps1
.\examples-bin\hello_dotenv.exe

Expected output:

text
Hello from My First Pascal App!
Port: 3000
Debug: TRUE

For your own project, copy src/DotEnv.pas beside your program or add src to its unit search path, then add DotEnv to the uses clause. The complete starter is in examples/hello-dotenv, and examples/README.md defines the complete learning path. Files under examples/ are the source of truth for runnable examples.

📖 .env File Format

bash
# 💬 Comments start with #
# Empty lines are ignored

# Simple key-value pairs
DATABASE_URL=postgresql://localhost/mydb
PORT=3000
DEBUG=true

# 🎯 Quoted values (preserves spaces and special chars)
SECRET_KEY="my-secret-key"
MESSAGE='Hello, World!'
GREETING="Welcome to the app!"

# 🔗 Variable interpolation
BASE_URL=https://api.example.com
API_ENDPOINT=${BASE_URL}/v1/users
FULL_URL=$BASE_URL/health

# 📝 Multi-line values (use quotes)
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA...
...more lines...
-----END RSA PRIVATE KEY-----"

# 🐚 Shell-compatible export syntax
export SHELL_VAR=works_in_bash_too

# 📋 Arrays (comma-separated, parsed with GetArray)
ALLOWED_HOSTS=localhost,127.0.0.1,example.com
FEATURES=auth,logging,cache

📚 API Reference

Creating and Loading

pascal
var
  Env: TDotEnv;
  Options: TDotEnvOptions;
begin
  // 🟢 Simple usage
  Env := TDotEnv.Create;
  Env.Load;                    // Load .env
  Env.Load('.env.local');      // Load specific file
  Env.LoadRequired('.env');    // Strict: raises actionable errors
  
  // 🟡 With options
  Options := TDotEnvOptions.Default;
  Options.Override := True;    // Override existing env vars
  Options.Verbose := True;     // Print debug info with likely secrets redacted
  Options.Prefix := 'APP_';    // Prefix all keys with APP_
  
  Env := TDotEnv.CreateWithOptions(Options);
  Env.Load;
  
  // 🔵 Load multiple files (later files override earlier ones)
  Env.LoadMultiple(['.env', '.env.local', '.env.development']);

  // 🟣 Load from string (great for testing!)
  Env.LoadFromString('KEY=value' + LineEnding + 'OTHER=test');

  // 🌍 NEW in v1.1.0: Environment-aware loading
  Env.LoadForEnvironment('production');   // Loads .env + .env.production
  Env.LoadForEnvironment();               // Auto-detects from APP_ENV or NODE_ENV
end;

Getting Values

pascal
// 📝 Strings
Env.Get('KEY');                    // Returns '' if missing
Env.Get('KEY', 'default');         // Returns 'default' if missing
Env.GetRequired('KEY');            // Raises exception if missing

// 🔢 Integers  
Env.GetInt('PORT');                // Returns 0 if missing/invalid
Env.GetInt('PORT', 3000);          // Returns 3000 if missing/invalid
Env.GetIntRequired('PORT');        // Raises exception if missing/invalid

// ✅ Booleans (recognizes: true/false, yes/no, 1/0, on/off)
Env.GetBool('DEBUG');              // Returns False if missing
Env.GetBool('DEBUG', True);        // Returns True if missing
Env.GetBoolRequired('DEBUG');      // Raises exception if missing/invalid

// 🔬 Floats
Env.GetFloat('RATE');              // Returns 0.0 if missing/invalid
Env.GetFloat('RATE', 0.5);         // Returns 0.5 if missing/invalid
Env.GetFloatRequired('RATE');      // Raises exception if missing/invalid

// 📋 Arrays (splits comma-separated values)
Hosts := Env.GetArray('ALLOWED_HOSTS');        // Split by comma
Tags := Env.GetArray('TAGS', ';');             // Split by semicolon

// 💬 NEW in v1.1.0: Interactive prompts (for setup scripts)
DbUrl := Env.GetOrPrompt('DATABASE_URL',
                        'Enter database URL',
                        'postgres://localhost/mydb');
// Prompts user if DATABASE_URL is missing, uses existing value if present

Validation

pascal
var
  Missing: TStringDynArray;
  I: Integer;
begin
  // ✅ Check if all required keys exist
  if Env.Validate(['DATABASE_URL', 'SECRET_KEY', 'PORT']) then
    WriteLn('All required configuration present!')
  else
  begin
    // 📋 Get list of missing keys
    Missing := Env.GetMissing(['DATABASE_URL', 'SECRET_KEY', 'PORT']);
    WriteLn('Missing configuration:');
    for I := 0 to High(Missing) do
      WriteLn('  - ', Missing[I]);
    Halt(1);
  end;
end;

For application startup, prefer aggregate typed validation so users see every configuration problem in one run:

pascal
Env.ValidateSchemaRequired([
  TDotEnvSchemaItem.Create('DATABASE_URL'),
  TDotEnvSchemaItem.Create('PORT', dvkInteger),
  TDotEnvSchemaItem.Create('DEBUG', dvkBoolean)
]);

Utilities

pascal
// 🔍 Check if key exists
if Env.Has('OPTIONAL_FEATURE') then
  EnableFeature;

// 📋 Get all keys and values
Keys := Env.Keys;           // TStringDynArray of all key names
Values := Env.Values;       // TStringDynArray of all values  
Pairs := Env.AsArray;       // TDotEnvPairArray with Key/Value records

// 🔢 Count loaded variables
WriteLn('Loaded ', Env.Count, ' environment variables');

// 🐛 Debug output with likely secrets replaced by [REDACTED]
WriteLn(Env.ToRedactedString);

// 📁 See which files were loaded
for I := 0 to High(Env.LoadedFiles) do
  WriteLn('Loaded: ', Env.LoadedFiles[I]);

File Operations

pascal
// 💾 Save environment variables to a file
Env := TDotEnv.Create;
Env.SetToEnv('DATABASE_URL', 'postgres://localhost/mydb');
Env.SetToEnv('PORT', '3000');
Env.SetToEnv('DEBUG', 'true');
Env.Save('.env');  // Safely escapes values and atomically replaces the file

// 📋 Generate .env.example for version control
Env.Load('.env');
Env.GenerateExample('.env', '.env.example');
// Creates .env.example with keys but empty values

// 🌍 Environment-aware loading pattern
Env := TDotEnv.Create;
Env.LoadForEnvironment('development');   // Loads .env + .env.development
Env.LoadForEnvironment('production');    // Loads .env + .env.production
Env.LoadForEnvironment();                // Auto-detects from APP_ENV/NODE_ENV

// 💬 Interactive setup script example
Env := TDotEnv.Create;
Env.Load('.env');  // Try to load existing config
DbUrl := Env.GetOrPrompt('DATABASE_URL',
                        'Enter database URL',
                        'postgres://localhost/mydb');
Port := Env.GetOrPrompt('PORT', 'Enter port', '3000');
Env.Save('.env');  // Save the configuration
WriteLn('Configuration saved!');

Global Helpers (Simple API)

For quick scripts or simple applications:

pascal
uses DotEnv;

begin
  DotEnvLoad;                              // Load .env
  DotEnvLoad('.env.local');                // Load specific file
  
  WriteLn('Database configured: ',
          DotEnvGet('DATABASE_URL') <> ''); // Report presence, not the URL
  WriteLn(DotEnvGet('PORT', '3000'));      // Get with default
  
  DotEnvSet('RUNTIME_VAR', 'value');       // Set at runtime
end.

🔗 Variable Interpolation

Reference other variables using ${VAR} or $VAR syntax:

bash
# Define base values
APP_NAME=MyAwesomeApp
APP_VERSION=1.0.0
BASE_URL=https://api.example.com

# Reference them in other values
APP_TITLE=${APP_NAME} v${APP_VERSION}
API_USERS=${BASE_URL}/users
API_HEALTH=$BASE_URL/health

# Also works with system environment variables!
HOME_CONFIG=${HOME}/.myapp/config

Resolution order:

  1. Variables defined earlier in the same .env file
  2. System environment variables

⚙️ Options

Option Type Default Description
Override Boolean False Override existing system environment variables
Interpolate Boolean True Enable ${VAR} variable interpolation
Verbose Boolean False Print debug info with likely secrets redacted
Prefix String '' Add prefix to all loaded key names
pascal
var
  Options: TDotEnvOptions;
begin
  Options := TDotEnvOptions.Default;  // Start with defaults
  Options.Override := True;
  Options.Prefix := 'MYAPP_';
  
  Env := TDotEnv.CreateWithOptions(Options);
  Env.Load;
  
  // KEY=value in .env becomes accessible as MYAPP_KEY
  WriteLn(Env.Get('MYAPP_KEY'));
end;

⚠️ Error Handling

pascal
uses DotEnv;

var
  Env: TDotEnv;
begin
  Env := TDotEnv.Create;
  Env.Load;
  
  // 🔴 Handle missing required keys
  try
    Env.GetRequired('MISSING_KEY');
  except
    on E: EDotEnvMissingKey do
      WriteLn('Configuration error: ', E.Message);
  end;
  
  // 🔴 Handle invalid type conversions
  try
    Env.GetIntRequired('NOT_A_NUMBER');
  except
    on E: EDotEnvParseError do
      WriteLn('Parse error: ', E.Message);
  end;
end.

🆚 Comparison with Python dotenv

dotenv-fp is inspired by python-dotenv and provides equivalent core functionality, plus additional features suited for statically-typed Pascal development:

Feature python-dotenv dotenv-fp
Load .env file ✅ ✅
Variable interpolation ✅ ✅
Multi-line values ✅ ✅
Quoted values ✅ ✅
Export prefix ✅ ✅
Comments ✅ ✅
Override mode ✅ ✅
Type-safe getters — ✅
Built-in validation — ✅
Array parsing — ✅
Key prefixing — ✅
Automatic memory management N/A ✅

🧪 Running Tests

The library includes a comprehensive test suite using FPCUnit:

bash
cd tests
fpc -B "-Fu../src" "-FU." TestRunner.pas
./TestRunner -a --format=plain

On Windows, use TestRunner.exe -a --format=plain for the final command.

Expected output:

pascal
Time:00.200 N:121 E:0 F:0 I:0
  TTestDotEnv Time:00.200 N:121 E:0 F:0 I:0
    00.000  Test01_BasicParsing_SimpleKeyValue
    00.000  Test02_BasicParsing_TrimmedValue
    ...
Number of run tests: 121
Number of errors:    0
Number of failures:  0

📁 Project Structure

pascal
dotenv-fp/
????????? src/
???   ????????? DotEnv.pas          # Main library unit
????????? tests/
???   ????????? TestRunner.pas      # FPCUnit test runner
???   ????????? DotEnv.Test.pas     # Test cases
????????? examples/
???   ????????? hello-dotenv/       # Five-minute newcomer example
???   ????????? basic/              # Basic API tour
???   ????????? advanced/           # Advanced features example
???   ????????? environment-aware/  # Self-contained environment layering
???   ????????? interactive-setup/  # Prompt, save, and template workflow
????????? examples-bin/           # Generated example binaries (gitignored)
????????? docs/                   # Documentation
????????? build-examples.ps1      # Windows example build entrypoint
????????? build-examples.sh       # Linux/macOS example build entrypoint
????????? .env.example            # Example .env file
????????? CHANGELOG.md
????????? README.md

💡 Tips for New Free Pascal Developers

  1. Advanced Records: This library uses advanced records ({$modeswitch advancedrecords}), which means you don't need to call .Free — memory is managed automatically!
  2. Mode ObjFPC: Make sure your program uses {$mode objfpc}{$H+}{$J-} for compatibility.
  3. String Handling: The {$H+} switch enables long strings (AnsiString) by default, which this library requires.
  4. File Paths: Use forward slashes / or the PathDelim constant for cross-platform compatibility.

🤝 Contributing

Contributions are welcome! Feel free to:

  • 🐛 Report bugs
  • 💡 Suggest features
  • 🔧 Submit pull requests

📄 License

MIT License — See LICENSE file for details.

🙏 Acknowledgments

  • Inspired by python-dotenv
  • Built for the awesome Free Pascal community

Happy coding! 🚀