Created
November 7, 2025 08:11
-
-
Save HeGanjie/7f23911ef37f97dd833653a709bb48a5 to your computer and use it in GitHub Desktop.
Python script to help me merge .env files
This file contains hidden or 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
| 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() |
This file contains hidden or 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
| 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