Skip to content

Instantly share code, notes, and snippets.

@HeGanjie
Created November 7, 2025 08:11
Show Gist options
  • Select an option

  • Save HeGanjie/7f23911ef37f97dd833653a709bb48a5 to your computer and use it in GitHub Desktop.

Select an option

Save HeGanjie/7f23911ef37f97dd833653a709bb48a5 to your computer and use it in GitHub Desktop.
Python script to help me merge .env files
import argparse
import os
from typing import Dict, List, Tuple
# 从 python-dotenv 库导入核心函数
from dotenv import dotenv_values
def compare_configs(
base_config: Dict[str, str],
prod_config: Dict[str, str]
) -> Tuple[List[str], List[str], List[Tuple[str, str, str]]]:
"""
比较两个从 .env 文件加载的配置字典。
返回一个元组,包含:
- missing_keys: 在 base_config 中存在,但在 prod_config 中缺失的键。
- added_keys: 在 prod_config 中存在,但在 base_config 中不存在的键。
- modified_keys: 在两个配置中都存在但值不同的键 (key, base_val, prod_val)。
"""
base_keys = set(base_config.keys())
prod_keys = set(prod_config.keys())
# 1. 找出缺失的配置
missing_keys = sorted(list(base_keys - prod_keys))
# 2. 找出新增的配置
added_keys = sorted(list(prod_keys - base_keys))
# 3. 找出修改过的配置
common_keys = base_keys & prod_keys
modified_keys = []
for key in sorted(list(common_keys)):
# 注意:dotenv_values 返回的值可能为 None,需要处理
base_val = base_config.get(key)
prod_val = prod_config.get(key)
if base_val != prod_val:
modified_keys.append((key, str(base_val), str(prod_val)))
return missing_keys, added_keys, modified_keys
def main():
"""主函数,处理命令行参数和打印结果。"""
parser = argparse.ArgumentParser(
description="比较两个 .env 文件,找出差异。依赖 python-dotenv 库。",
formatter_class=argparse.RawTextHelpFormatter
)
parser.add_argument(
'base_file',
default='.env.example',
nargs='?',
help="基础/模板 .env 文件路径 (默认为: .env.example)"
)
parser.add_argument(
'prod_file',
default='.env.prod',
nargs='?',
help="生产/当前 .env 文件路径 (默认为: .env.prod)"
)
args = parser.parse_args()
# 检查文件是否存在
if not os.path.exists(args.base_file):
print(f"❌ 错误: 模板文件 '{args.base_file}' 不存在。")
return
if not os.path.exists(args.prod_file):
print(f"⚠️ 警告: 生产文件 '{args.prod_file}' 不存在,将视为空配置。")
print(f"--- 正在比较 '{args.base_file}' (模板) 和 '{args.prod_file}' (生产) ---\n")
# 使用 dotenv_values 读取文件内容到字典
# 它会自动处理注释、空行和各种值的格式
base_config = dotenv_values(args.base_file)
prod_config = dotenv_values(args.prod_file)
missing, added, modified = compare_configs(base_config, prod_config)
has_diff = False
if missing:
has_diff = True
print(f"❌ 1. 在 '{args.prod_file}' 中缺失的配置 ({len(missing)} 项):")
print(" (这些配置存在于模板文件中,但在你的生产配置中没有找到)")
for key in missing:
print(f" - {key}")
print("-" * 20)
if added:
has_diff = True
print(f"✨ 2. 在 '{args.prod_file}' 中新增的配置 ({len(added)} 项):")
print(" (这些是你的生产配置中独有的,模板文件中没有)")
for key in added:
print(f" + {key}")
print("-" * 20)
if modified:
has_diff = True
print(f"🔄 3. 在 '{args.prod_file}' 中修改过的配置 ({len(modified)} 项):")
print(" (这些配置在两个文件中都存在,但值不相同)")
for key, base_val, prod_val in modified:
print(f" - {key}:")
print(f" 模板值: {base_val}")
print(f" 生产值: {prod_val}")
print("-" * 20)
if not has_diff:
print(f"✅ 完美! '{args.prod_file}' 包含了 '{args.base_file}' 的所有配置且值相同 (新增项不计入)。")
if __name__ == "__main__":
main()
import argparse
import os
from typing import Dict, List
from dotenv import dotenv_values
def get_ordered_keys(file_path: str) -> List[str]:
"""
解析 .env 文件,返回一个保留了原始顺序的 key 列表。
"""
keys = []
try:
with open(file_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
if '=' in line:
key = line.split('=', 1)[0].strip()
if key: # 确保 key 不是空的
keys.append(key)
except FileNotFoundError:
pass # 如果文件不存在,返回空列表
return keys
def format_value(value: str) -> str:
"""
根据值的内容决定是否需要加引号,使其更安全。
"""
value_str = str(value)
# 如果值中包含空格、#、=,或者本身就是用引号包裹的,则强制使用双引号
if (' ' in value_str or '#' in value_str or '=' in value_str) and \
not (value_str.startswith('"') and value_str.endswith('"')):
return f'"{value_str}"'
return value_str
def main():
parser = argparse.ArgumentParser(
description="智能合并 .env 文件:将 .env.prod 的值和新增项合并到 .env.example 的结构中。",
formatter_class=argparse.RawTextHelpFormatter
)
parser.add_argument('base_file', default='.env.example', nargs='?', help="基础/模板 .env 文件路径")
parser.add_argument('prod_file', default='.env.prod', nargs='?', help="生产/当前 .env 文件路径")
parser.add_argument('-o', '--output', dest='output_file', default=None, help="输出合并后的内容到指定文件")
args = parser.parse_args()
if not os.path.exists(args.base_file):
print(f"❌ 错误: 模板文件 '{args.base_file}' 不存在。")
return
# --- 1. 数据准备 ---
base_config = dotenv_values(args.base_file)
prod_config = dotenv_values(args.prod_file) if os.path.exists(args.prod_file) else {}
base_keys_set = set(base_config.keys())
prod_ordered_keys = get_ordered_keys(args.prod_file)
merged_lines = []
processed_keys = set() # 用于跟踪所有已处理的 key
# --- 2. 主逻辑:遍历模板文件 ---
with open(args.base_file, 'r', encoding='utf-8') as f:
for line in f:
original_line = line.strip()
# 保留注释和空行
if not original_line or original_line.startswith('#'):
merged_lines.append(original_line)
continue
if '=' not in original_line:
merged_lines.append(original_line)
continue
key = original_line.split('=', 1)[0].strip()
# 如果该行在模板中是无效的(例如被注释掉),则跳过
if key not in base_keys_set:
merged_lines.append(original_line)
continue
# A. 处理当前模板中的 key
if key in prod_config:
# 如果生产配置中存在,使用生产配置的值
new_line = f'{key}={format_value(prod_config[key])}'
merged_lines.append(new_line)
else:
# 生产配置中不存在,保留模板原样
merged_lines.append(original_line)
processed_keys.add(key)
# B. 智能插入:查找并插入紧随其后的新增 key
try:
# 找到当前 key 在生产配置顺序中的位置
current_key_index = prod_ordered_keys.index(key)
# 向后查找,直到遇到下一个也存在于模板中的 key
for next_key in prod_ordered_keys[current_key_index + 1:]:
if next_key in base_keys_set:
# 找到了下一个“锚点”,停止查找
break
else:
# 这是一个新增的 key,插入它
if next_key not in processed_keys:
new_line = f'{next_key}={format_value(prod_config[next_key])}'
merged_lines.append(new_line)
processed_keys.add(next_key)
except (ValueError, IndexError):
# 如果当前 key 不在生产配置中,或已是最后一个,则忽略
pass
# --- 3. 收尾:处理剩余未处理的 key ---
# 这些通常是 .prod 文件开头的新增项,或其上下文在 .example 中完全不存在
remaining_keys = [
key for key in prod_ordered_keys
if key not in processed_keys and key in prod_config
]
if remaining_keys:
merged_lines.append('')
merged_lines.append('# --- Remaining Added Settings ---')
for key in remaining_keys:
new_line = f'{key}={format_value(prod_config[key])}'
merged_lines.append(new_line)
# --- 4. 输出结果 ---
final_output = "\n".join(merged_lines)
if args.output_file:
with open(args.output_file, 'w', encoding='utf-8') as f:
f.write(final_output)
print(f"✅ 合并成功!结果已保存到文件: {args.output_file}")
else:
print(f"--- 智能合并预览 (基于 '{args.base_file}' 结构) ---")
print(final_output)
print("--- 预览结束 ---")
print("\n💡 提示: 使用 -o <文件名> (例如: -o .env.merged) 可以将结果保存到文件。")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment