Created
March 27, 2023 09:45
-
-
Save pansapiens/6687d0f3d02fe8b3a656e53ba00dbf91 to your computer and use it in GitHub Desktop.
Split ~/.ssh/config file into separate per-host files
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 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