Created
March 17, 2025 09:29
-
-
Save tnwei/45c8238cd947166df65a5825a220541e to your computer and use it in GitHub Desktop.
envcheck prototype
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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