The script can target all managed entries or a named switch group, creates a backup
before writing, requests elevation on Windows when needed, and flushes DNS after
changes so the new host mappings take effect immediately.
import argparse
import ctypes
import os
import re
import shutil
import subprocess
import sys
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable
HOSTS = Path(r"C:\Windows\System32\drivers\etc\hosts")
AUTO_TAG = "# auto"
AUTO_PREFIX = "# auto:"
IP_LIKE_RE = re.compile(r"^(?:\d{1,3}(?:\.\d{1,3}){3}|::1|localhost)\b", re.IGNORECASE)
ACTIVE_RE = re.compile(
r"^(?P<indent>\s*)(?P<entry>.*?\S)\s+#\s*auto(?:\s*:\s*(?P<switch>[^#]+?))?\s*$",
re.IGNORECASE,
)
DISABLED_RE = re.compile(
r"^(?P<indent>\s*)#\s*auto\s*:\s*(?P<rest>.+?)\s*$",
re.IGNORECASE,
)
@dataclass
class ManagedLine:
indent: str
entry: str
switch: str | None
active: bool
@property
def domains(self) -> list[str]:
base = self.entry.split("#", 1)[0].strip()
parts = base.split()
if len(parts) < 2:
return []
return parts[1:]
@property
def switch_name(self) -> str:
return (self.switch or "default").strip() or "default"
@dataclass
class Change:
action: str
switch: str
domains: list[str]
before: str
after: str
def is_windows() -> bool:
return os.name == "nt"
def is_admin() -> bool:
if not is_windows():
return True
try:
return bool(ctypes.windll.shell32.IsUserAnAdmin())
except Exception:
return False
def quote_windows_arg(arg: str) -> str:
if not arg:
return '""'
if not re.search(r'[\s"]', arg):
return arg
escaped = arg.replace('\\', '\\\\').replace('"', '\"')
return f'"{escaped}"'
def relaunch_as_admin() -> None:
script = str(Path(__file__).resolve())
params = " ".join([quote_windows_arg(script), *[quote_windows_arg(a) for a in sys.argv[1:]]])
try:
rc = ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, params, None, 1)
except Exception as exc:
print(f"❌ Could not request elevation: {exc}")
raise SystemExit(1)
if int(rc) <= 32:
print("❌ UAC elevation was denied or failed.")
raise SystemExit(1)
raise SystemExit(0)
def flush_dns() -> None:
try:
completed = subprocess.run(
["ipconfig", "/flushdns"],
check=True,
capture_output=True,
text=True,
)
output = (completed.stdout or "").strip()
print(f"✅ DNS cache flushed.{(' ' + output) if output else ''}")
except Exception as exc:
print(f"⚠️ Could not flush DNS: {exc}")
def make_backup(path: Path) -> Path:
backup = path.with_name(path.name + ".fixhosts.bak")
shutil.copy2(path, backup)
return backup
def looks_like_entry(text: str) -> bool:
token = text.strip().split(None, 1)[0] if text.strip() else ""
return bool(token and IP_LIKE_RE.match(token))
def parse_disabled_rest(rest: str) -> tuple[str | None, str] | None:
rest = rest.strip()
if not rest:
return None
if looks_like_entry(rest):
return None, rest
if ":" in rest:
maybe_switch, maybe_entry = rest.split(":", 1)
maybe_switch = maybe_switch.strip()
maybe_entry = maybe_entry.strip()
if maybe_switch and maybe_entry and looks_like_entry(maybe_entry):
return maybe_switch, maybe_entry
return None
def parse_managed_line(line: str) -> ManagedLine | None:
active_match = ACTIVE_RE.match(line)
if active_match:
entry = active_match.group("entry").strip()
switch = active_match.group("switch")
return ManagedLine(
indent=active_match.group("indent") or "",
entry=entry,
switch=switch.strip() if switch else None,
active=True,
)
disabled_match = DISABLED_RE.match(line)
if not disabled_match:
return None
parsed = parse_disabled_rest(disabled_match.group("rest"))
if not parsed:
return None
switch, entry = parsed
return ManagedLine(
indent=disabled_match.group("indent") or "",
entry=entry.strip(),
switch=switch.strip() if switch else None,
active=False,
)
def render_line(item: ManagedLine) -> str:
if item.active:
if item.switch:
return f"{item.indent}{item.entry} {AUTO_TAG}: {item.switch}"
return f"{item.indent}{item.entry} {AUTO_TAG}"
if item.switch:
return f"{item.indent}{AUTO_PREFIX} {item.switch}: {item.entry}"
return f"{item.indent}{AUTO_PREFIX} {item.entry}"
def toggle_line(item: ManagedLine) -> ManagedLine:
return ManagedLine(
indent=item.indent,
entry=item.entry,
switch=item.switch,
active=not item.active,
)
def read_hosts_lines(path: Path) -> list[str]:
return path.read_text(encoding="utf-8", errors="replace").splitlines()
def write_hosts_lines(path: Path, lines: Iterable[str]) -> None:
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
def print_summary(changes: list[Change], dry_run: bool) -> None:
if not changes:
print("ℹ️ No managed # auto lines were toggled.")
return
by_action: dict[str, dict[str, list[str]]] = {"ON": {}, "OFF": {}}
for change in changes:
bucket = by_action[change.action].setdefault(change.switch, [])
bucket.extend(change.domains or [change.before])
prefix = "🧪 Would turn" if dry_run else "✅ Turned"
for action in ("OFF", "ON"):
groups = by_action[action]
if not groups:
continue
print(f"{prefix} {action}:")
for switch, domains in groups.items():
unique_domains: list[str] = []
for domain in domains:
if domain not in unique_domains:
unique_domains.append(domain)
print(f" [{switch}]")
for domain in unique_domains:
print(f" - {domain}")
def toggle_hosts(path: Path, target_switch: str | None = None, dry_run: bool = False) -> int:
original_lines = read_hosts_lines(path)
new_lines: list[str] = []
changes: list[Change] = []
normalized_target = (target_switch or "").strip().lower()
for line in original_lines:
parsed = parse_managed_line(line)
if not parsed:
new_lines.append(line)
continue
switch_name = parsed.switch_name
if normalized_target and switch_name.lower() != normalized_target:
new_lines.append(line)
continue
toggled = toggle_line(parsed)
new_line = render_line(toggled)
new_lines.append(new_line)
changes.append(
Change(
action="ON" if toggled.active else "OFF",
switch=switch_name,
domains=parsed.domains,
before=line,
after=new_line,
)
)
if not changes:
print("ℹ️ Nothing matched.")
if normalized_target:
print(f" No managed entries were found for switch: {target_switch}")
else:
print(" No managed # auto entries were found.")
return 0
if dry_run:
print_summary(changes, dry_run=True)
return len(changes)
backup = make_backup(path)
write_hosts_lines(path, new_lines)
print(f"✅ Updated hosts file: {path}")
print(f"✅ Backup created: {backup}")
print_summary(changes, dry_run=False)
flush_dns()
return len(changes)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description=(
"Toggle managed Windows hosts entries between active lines like "
"'127.0.0.1 example.com # auto' and disabled lines like "
"'# auto: 127.0.0.1 example.com'."
)
)
parser.add_argument(
"--hosts",
default=str(HOSTS),
help="Path to the hosts file. Defaults to the Windows hosts file.",
)
parser.add_argument(
"--switch",
dest="switch_name",
help=(
"Only toggle entries in one named switch group. "
"Active form: '127.0.0.1 x.com # auto: social' | "
"Disabled form: '# auto: social: 127.0.0.1 x.com'"
),
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would change without writing the file.",
)
return parser
def main() -> int:
parser = build_parser()
args = parser.parse_args()
hosts_path = Path(args.hosts)
print("Fixing hosts.")
time.sleep(1)
if not hosts_path.exists():
print(f"❌ Hosts file not found: {hosts_path}")
return 1
if is_windows() and not args.dry_run and not is_admin():
print("🔒 Hosts file needs admin rights. Requesting elevation...")
relaunch_as_admin()
try:
toggle_hosts(
path=hosts_path,
target_switch=args.switch_name,
dry_run=args.dry_run,
)
return 0
except PermissionError:
print("❌ Permission denied while writing the hosts file.")
if is_windows() and not is_admin():
print(" Run this as Administrator or accept the UAC prompt.")
return 1
except Exception as exc:
print(f"❌ Error: {exc}")
return 1
print("Fixing hosts.")
time.sleep(2)
if __name__ == "__main__":
raise SystemExit(main())