Skip to content

Instantly share code, notes, and snippets.

@tnwei
Created March 17, 2025 09:29
Show Gist options
  • Save tnwei/45c8238cd947166df65a5825a220541e to your computer and use it in GitHub Desktop.
Save tnwei/45c8238cd947166df65a5825a220541e to your computer and use it in GitHub Desktop.
envcheck prototype
#!/usr/bin/env python3
# Put this in ~/.local/bin and chmod +x
"""
envcheck: A utility to manage and sync .env files with their example templates.
"""
import os
import sys
import re
from collections import OrderedDict
from typing import Dict, List, Optional, Tuple, OrderedDict as OrderedDictType
def parse_env_file(filename: str) -> OrderedDictType[str, str]:
"""Parse an env file into an OrderedDict.
Args:
filename: Path to the env file
Returns:
OrderedDict containing key-value pairs from the env file
"""
if not os.path.exists(filename):
return OrderedDict()
env_vars: OrderedDictType[str, str] = OrderedDict()
with open(filename, "r") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
match = re.match(r"^([A-Za-z0-9_]+)=(.*?)$", line)
if match:
key, value = match.groups()
env_vars[key] = value
return env_vars
def get_differences(
example_vars: Dict[str, str], env_vars: Dict[str, str]
) -> List[str]:
"""Get keys present in example but missing in env.
Args:
example_vars: Variables from the example file
env_vars: Variables from the env file
Returns:
List of keys that are in example but missing in env
"""
return [key for key in example_vars if key not in env_vars]
def find_example_file(
env_file: str, default_example: str = ".env.example"
) -> Optional[str]:
"""Find the corresponding example file for an env file.
Args:
env_file: Path to the env file
default_example: Default example filename to use
Returns:
Path to the example file or None if not found
"""
# First try with .example suffix
example_file = env_file + ".example"
if os.path.exists(example_file):
return example_file
# Then try replacing the extension
base_name = env_file.rsplit(".", 1)[0] if "." in env_file else env_file
example_file = f"{base_name}.example"
if os.path.exists(example_file):
return example_file
# Try the default .env.example in the same directory
dir_path = os.path.dirname(env_file)
example_file = (
os.path.join(dir_path, default_example) if dir_path else default_example
)
if os.path.exists(example_file):
return example_file
# If in a subdirectory, try the root default example file
if dir_path and os.path.exists(default_example):
return default_example
return None
def list_env_files(
default_example: str = ".env.example",
) -> Tuple[List[str], List[str]]:
"""List all env files in current directory and subdirectories.
Args:
default_example: Default example filename to identify
Returns:
Tuple of (env_files, example_files)
"""
env_files: List[str] = []
example_files: List[str] = []
for root, _, files in os.walk("."):
for file in files:
file_path = os.path.join(root, file)
if file_path.startswith("./"):
file_path = file_path[2:] # Remove leading ./
if ".env" in file:
if (
file.endswith(".example")
or ".example." in file
or file == default_example
):
example_files.append(file_path)
else:
env_files.append(file_path)
return env_files, example_files
def update_env_file(env_file: str, example_file: str) -> int:
"""Update env file with missing keys from example.
Args:
env_file: Path to the env file to update
example_file: Path to the example file to use as template
Returns:
0 on success, non-zero on error
"""
env_vars = parse_env_file(env_file)
example_vars = parse_env_file(example_file)
missing_keys = get_differences(example_vars, env_vars)
if not missing_keys:
print(f"✓ {env_file} is in sync with {example_file}")
return 0
# Create directory if it doesn't exist
dir_path = os.path.dirname(env_file)
if dir_path:
os.makedirs(dir_path, exist_ok=True)
# Create or update env file
exists = os.path.exists(env_file)
with open(env_file, "a" if exists else "w") as f:
if exists:
f.write(f"\n# Added by envcheck from {example_file}\n")
for key in missing_keys:
f.write(f"{key}={example_vars[key]}\n")
print(f"✓ Added {len(missing_keys)} missing keys to {env_file}")
for key in missing_keys:
print(f" + {key}")
return 0
def create_env_file(env_file: str, example_file: str) -> int:
"""Create a new env file based on example.
Args:
env_file: Path to the env file to create
example_file: Path to the example file to use as template
Returns:
0 on success, non-zero on error
"""
if os.path.exists(env_file):
print(f"✗ Error: {env_file} already exists. Use 'update' instead.")
return 1
example_vars = parse_env_file(example_file)
# Create directory if it doesn't exist
dir_path = os.path.dirname(env_file)
if dir_path:
os.makedirs(dir_path, exist_ok=True)
with open(env_file, "w") as f:
for key, value in example_vars.items():
f.write(f"{key}={value}\n")
print(f"✓ Created {env_file} with {len(example_vars)} keys from {example_file}")
return 0
def show_help() -> None:
"""Show help information."""
print("Usage:")
print(
" envcheck - List env files and differences"
)
print(
" envcheck create <env_file> [example_file] - Create env file from example"
)
print(
" envcheck update <env_file> [example_file] - Update env file with missing keys"
)
print("")
print("Examples:")
print(" envcheck create .env")
print(" envcheck create prod/.env.staging")
print(" envcheck create prod/.env.staging prod/.env.example")
print(" envcheck update .env")
print(" envcheck update prod/.env.staging")
print(" envcheck update prod/.env.staging prod/.env.example")
def cmd_list(default_example: str = ".env.example") -> int:
"""Run the list command.
Args:
default_example: Default example filename to use
Returns:
0 on success, non-zero on error
"""
env_files, example_files = list_env_files(default_example)
if not example_files:
print(f"✗ No example files (like {default_example}) found.")
return 1
print(f"Found {len(example_files)} example files and {len(env_files)} env files.\n")
for example_file in example_files:
example_vars = parse_env_file(example_file)
# Determine the corresponding env file
base_name = example_file
if base_name.endswith(".example"):
base_name = base_name[:-8] # Remove '.example'
elif ".example." in base_name:
base_name = base_name.replace(".example.", ".")
elif base_name == default_example:
base_name = ".env"
env_file = base_name
if os.path.exists(env_file):
env_vars = parse_env_file(env_file)
missing_keys = get_differences(example_vars, env_vars)
if missing_keys:
print(
f"⚠ {env_file} is missing {len(missing_keys)} keys from {example_file}:"
)
for key in missing_keys:
print(f" - {key}")
else:
print(f"✓ {env_file} is in sync with {example_file}")
else:
print(f"⚠ {env_file} doesn't exist (template available: {example_file})")
print()
return 0
def cmd_create(args: List[str], default_example: str = ".env.example") -> int:
"""Run the create command.
Args:
args: Command line arguments
default_example: Default example filename to use
Returns:
0 on success, non-zero on error
"""
if len(args) < 1:
print("✗ Error: Missing target .env file path.")
print("Usage: envcheck create <env_file> [example_file]")
return 1
env_file = args[0]
if len(args) >= 2:
# Use the provided example file
example_file = args[1]
if not os.path.exists(example_file):
print(f"✗ Error: Example file {example_file} not found.")
return 1
else:
# Try to find a matching example file
example_file = find_example_file(env_file, default_example)
if not example_file:
print(f"✗ Error: Could not find a matching example file for {env_file}.")
print(
f"Please specify the example file, or create a {default_example} file:"
)
print(f"envcheck create <env_file> <example_file>")
return 1
return create_env_file(env_file, example_file)
def cmd_update(args: List[str], default_example: str = ".env.example") -> int:
"""Run the update command.
Args:
args: Command line arguments
default_example: Default example filename to use
Returns:
0 on success, non-zero on error
"""
if len(args) < 1:
print("✗ Error: Missing target .env file path.")
print("Usage: envcheck update <env_file> [example_file]")
return 1
env_file = args[0]
if not os.path.exists(env_file):
print(f"✗ Error: {env_file} not found. Use 'create' instead.")
return 1
if len(args) >= 2:
# Use the provided example file
example_file = args[1]
if not os.path.exists(example_file):
print(f"✗ Error: Example file {example_file} not found.")
return 1
else:
# Try to find a matching example file
example_file = find_example_file(env_file, default_example)
if not example_file:
print(f"✗ Error: Could not find a matching example file for {env_file}.")
print(
f"Please specify the example file, or create a {default_example} file:"
)
print(f"envcheck update <env_file> <example_file>")
return 1
return update_env_file(env_file, example_file)
def main() -> int:
"""Main CLI entry point.
Returns:
Exit code (0 for success, non-zero for error)
"""
# Default example file name
DEFAULT_EXAMPLE = ".env.example"
if len(sys.argv) == 1:
# List mode
return cmd_list(DEFAULT_EXAMPLE)
elif sys.argv[1] == "create":
return cmd_create(sys.argv[2:], DEFAULT_EXAMPLE)
elif sys.argv[1] == "update":
return cmd_update(sys.argv[2:], DEFAULT_EXAMPLE)
else:
show_help()
return 1
if __name__ == "__main__":
sys.exit(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment