Skip to content

Instantly share code, notes, and snippets.

@pansapiens
Created March 27, 2023 09:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pansapiens/6687d0f3d02fe8b3a656e53ba00dbf91 to your computer and use it in GitHub Desktop.
Save pansapiens/6687d0f3d02fe8b3a656e53ba00dbf91 to your computer and use it in GitHub Desktop.
Split ~/.ssh/config file into separate per-host files
#!/usr/bin/env python
import os
import shutil
import re
import sys
import difflib
import argparse
def parse_ssh_config(config_path):
with open(config_path, "r") as f:
config_lines = f.readlines()
hosts = {}
host_lines = []
host_name = None
for line in config_lines:
if re.match(r"^Host\b", line):
if host_lines and host_name is not None:
hosts[host_name] = host_lines
host_name = re.search(r"^Host\s+(.+)", line).group(1)
host_lines = []
host_lines.append(line)
if host_lines and host_name is not None:
hosts[host_name] = host_lines
return hosts
def backup_ssh_config(ssh_config_path, ssh_config_backup_base):
backup_index = 0
ssh_config_backup_path = ssh_config_backup_base
while os.path.exists(ssh_config_backup_path):
backup_index += 1
ssh_config_backup_path = f"{ssh_config_backup_base}.{backup_index}"
shutil.copy2(ssh_config_path, ssh_config_backup_path)
return ssh_config_backup_path
def split_ssh_config(force=False):
ssh_config_path = os.path.expanduser("~/.ssh/config")
ssh_config_backup_base = os.path.expanduser("~/.ssh/config.backup")
ssh_config_d_path = os.path.expanduser("~/.ssh/config.d")
# Make a backup of the original config file
backup_ssh_config(ssh_config_path, ssh_config_backup_base)
# Create the config.d directory if it doesn't exist
if not os.path.exists(ssh_config_d_path):
os.makedirs(ssh_config_d_path)
# Parse the config file and get host configurations
hosts = parse_ssh_config(ssh_config_path)
# Write each host configuration to a separate file in config.d directory
for host_name, host_lines in hosts.items():
# Replace any non-filename friendly characters
safe_host_name = re.sub(r"[^\w\-_]", "_", host_name)
host_config_file = os.path.join(ssh_config_d_path, f"{safe_host_name}.conf")
if os.path.exists(host_config_file):
with open(host_config_file, "r") as f:
existing_lines = f.readlines()
if existing_lines != host_lines:
if force:
sys.stderr.write(f"Warning: Overwriting {host_config_file}\n")
else:
sys.stderr.write(
f"Error: {host_config_file} already exists and has different content.\n"
)
diff = difflib.unified_diff(
existing_lines,
host_lines,
fromfile=host_config_file,
tofile=f"{safe_host_name}_new.conf",
)
sys.stderr.writelines(diff)
sys.stderr.write("\n")
continue
with open(host_config_file, "w") as f:
f.writelines(host_lines)
# Replace the original config file with an include directive and a comment
with open(ssh_config_path, "w") as f:
f.write(
"# This file is generated by ssh_config_split - see ~/.ssh/config.d for host configurations.\n"
)
f.write(f"Include {ssh_config_d_path}/*\n")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Splits your ~/.ssh/config file into separate per-host files in ~/.ssh/config.d/"
)
parser.add_argument(
"--force",
action="store_true",
help="Force overwriting existing host config files in ~/.ssh/config.d/",
)
args = parser.parse_args()
split_ssh_config(force=args.force)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment