Skip to content

Instantly share code, notes, and snippets.

@MattJDavidson
Last active January 16, 2025 08:09
Show Gist options
  • Select an option

  • Save MattJDavidson/1d4c7edb43a5304e685721efb9300233 to your computer and use it in GitHub Desktop.

Select an option

Save MattJDavidson/1d4c7edb43a5304e685721efb9300233 to your computer and use it in GitHub Desktop.
lazy_hosts_blocker
#!/bin/bash
CONFIG_DIR="$HOME/.config/blocklists"
HOSTS_FILE="/etc/hosts"
DEFAULT_IP="0.0.0.0"
ACTION="block"
LIST_NAME=""
DRYRUN=false
CHANGED_DOMAINS=()
show_help() {
echo "Usage: $0 [--unblock|-U] [--dryrun] <list_name>"
echo "Options:"
echo " --unblock, -U Unblock domains in the specified list in ~/.config/blocklists."
echo " --dryrun Show what would be done without making changes."
echo " --help Show this help message."
}
exit_with_error() {
echo "Error: $1" >&2
exit "$2"
}
ensure_root() {
if [ "$EUID" -ne 0 ]; then
exit_with_error "Please run as root (sudo)." 1
fi
}
parse_arguments() {
for arg in "$@"; do
case $arg in
--unblock|-U)
ACTION="unblock"
;;
--dryrun)
DRYRUN=true
;;
--help)
show_help
exit 0
;;
*)
if [ -z "$LIST_NAME" ]; then
LIST_NAME="$arg"
else
exit_with_error "Unexpected argument: $arg" 2
fi
;;
esac
done
if [ -z "$LIST_NAME" ]; then
exit_with_error "No list name provided. Use --help for usage instructions." 3
fi
}
validate_blocklist() {
LIST_FILE="$CONFIG_DIR/$LIST_NAME"
if [ ! -f "$LIST_FILE" ]; then
exit_with_error "The blocklist '$LIST_NAME' does not exist at $LIST_FILE. Available lists are in $CONFIG_DIR." 4
fi
}
block_domain() {
local domain="$1"
local subdomain="www.$domain"
# Detect which sed style is supported
# On macOS, `sed -i '' ...` is required.
# On Linux (GNU sed), `sed -i ...` suffices.
if sed --version 2>/dev/null | grep -q "GNU"; then
SED_INPLACE=(-i)
else
SED_INPLACE=(-i '')
fi
for entry in "$domain" "$subdomain"; do
# If the domain is already a sub-subdomain (e.g., old.reddit.com),
# skip adding "www." version.
if [[ "$domain" == *.*.* ]]; then
[[ "$entry" == "$subdomain" ]] && continue
fi
# Check if the line exists (commented or uncommented)
if grep -Eq "^[#[:space:]]*0\.0\.0\.0[[:space:]]+$entry" "$HOSTS_FILE"; then
if [ "$DRYRUN" = false ]; then
# 1) Remove all leading # and whitespace
sed "${SED_INPLACE[@]}" -E \
"/^[#[:space:]]*0\.0\.0\.0[[:space:]]+$entry/ s/^[#[:space:]]+//" \
"$HOSTS_FILE"
# 2) Normalize spacing (replace runs of whitespace with a single tab)
sed "${SED_INPLACE[@]}" -E \
"/^0\.0\.0\.0[[:space:]]+$entry/ s/[[:space:]]+/\t/" \
"$HOSTS_FILE"
fi
CHANGED_DOMAINS+=("Uncommented $entry")
else
# If domain not found in the file, append it
if [ "$DRYRUN" = false ]; then
echo -e "$DEFAULT_IP\t$entry" >> "$HOSTS_FILE"
fi
CHANGED_DOMAINS+=("Blocked $entry")
fi
done
}
unblock_domain() {
local domain="$1"
local subdomain="www.$domain"
for entry in "$domain" "$subdomain"; do
if [[ "$domain" == *.*.* ]]; then
[[ "$entry" == "$subdomain" ]] && continue
fi
if grep -Eq "^[#[:space:]]*.*$entry" "$HOSTS_FILE"; then
if [ "$DRYRUN" = false ]; then
sed -i -E "/^[#[:space:]]*.*$entry/ s/^#*[[:space:]]*/# /" "$HOSTS_FILE"
fi
CHANGED_DOMAINS+=("Commented out $entry")
fi
done
}
process_list() {
while IFS= read -r domain || [ -n "$domain" ]; do
[[ -z "$domain" || "$domain" == \#* ]] && continue
if [[ "$ACTION" == "block" ]]; then
block_domain "$domain"
elif [[ "$ACTION" == "unblock" ]]; then
unblock_domain "$domain"
fi
done < "$LIST_FILE"
}
display_summary() {
if [ "${#CHANGED_DOMAINS[@]}" -gt 0 ]; then
echo "Changes made:"
for change in "${CHANGED_DOMAINS[@]}"; do
echo "- $change"
done
else
echo "No changes were made."
fi
if [ "$DRYRUN" = true ]; then
echo "This was a dry run. No changes were made to $HOSTS_FILE."
fi
}
main() {
parse_arguments "$@"
validate_blocklist
ensure_root
process_list
display_summary
echo "Operation completed successfully for the '$LIST_NAME' blocklist."
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment