#!/usr/bin/env bash # # check_redirects.sh - Follow and report HTTP redirect chains (max 10 hops). # # Run straight from a gist (it will ask for the URLs): # bash <(curl -fsSL https://gist.githubusercontent.com/.../raw/check_redirects.sh) # curl -fsSL https://gist.githubusercontent.com/.../raw/check_redirects.sh | bash # # Arguments/flags still work if you want to skip the prompts: # bash <(curl -fsSL URL) https://a.com https://b.com # bash <(curl -fsSL URL) -f urls.txt set -uo pipefail MAX_REDIRECTS=10 TIMEOUT=10 CSV=0 URL_FILE="" USER_AGENT="Mozilla/5.0 (redirect-checker)" if [[ -t 1 ]]; then RED=$'\e[31m'; GREEN=$'\e[32m'; YELLOW=$'\e[33m'; CYAN=$'\e[36m'; BOLD=$'\e[1m'; RESET=$'\e[0m' else RED=""; GREEN=""; YELLOW=""; CYAN=""; BOLD=""; RESET="" fi usage() { cat <<'EOF' Usage: check_redirects.sh [options] [url ...] (no URLs given = interactive prompts) Options: -f FILE Read URLs from FILE (one per line, # comments and blank lines ignored) -m N Max redirects to follow (default: 10) -t SECS Timeout per request in seconds (default: 10) -c Output CSV (url,final_url,final_status,hops,result) -h Show help EOF exit "${1:-0}" } while getopts ":f:m:t:ch" opt; do case "$opt" in f) URL_FILE="$OPTARG" ;; m) MAX_REDIRECTS="$OPTARG" ;; t) TIMEOUT="$OPTARG" ;; c) CSV=1 ;; h) usage 0 ;; :) echo "Option -$OPTARG requires an argument." >&2; usage 1 ;; \?) echo "Unknown option: -$OPTARG" >&2; usage 1 ;; esac done shift $((OPTIND - 1)) command -v curl >/dev/null 2>&1 || { echo "Error: curl is required." >&2; exit 1; } # ---- Collect URLs ---- URLS=() if [[ -n "$URL_FILE" ]]; then [[ -r "$URL_FILE" ]] || { echo "Error: cannot read $URL_FILE" >&2; exit 1; } while IFS= read -r line || [[ -n "$line" ]]; do URLS+=("$line"); done < "$URL_FILE" fi URLS+=("$@") # ---- Interactive prompts (read from the terminal, so it works with curl | bash) ---- TTY="${TTY_DEVICE:-/dev/tty}" if [[ ${#URLS[@]} -eq 0 ]]; then if ! { exec 3<"$TTY"; } 2>/dev/null; then echo "Error: no URLs given and no terminal available to ask for them." >&2 usage 1 fi echo "${BOLD}Redirect checker${RESET} (max $MAX_REDIRECTS redirects per URL)" echo " 1) Check a single URL" echo " 2) Paste a list of URLs" echo " 3) Load URLs from a file" read -r -u 3 -p "Choose [1-3]: " choice case "$choice" in 1) read -r -u 3 -p "URL: " line URLS+=("$line") ;; 2) echo "Paste URLs, one per line. Press Enter on an empty line when done:" while IFS= read -r -u 3 line; do line="${line//$'\r'/}" [[ -z "${line// /}" ]] && break URLS+=("$line") done ;; 3) read -r -u 3 -p "File path: " URL_FILE URL_FILE="${URL_FILE/#\~/$HOME}" [[ -r "$URL_FILE" ]] || { echo "Error: cannot read $URL_FILE" >&2; exit 1; } while IFS= read -r line || [[ -n "$line" ]]; do URLS+=("$line"); done < "$URL_FILE" ;; *) echo "Invalid choice." >&2; exit 1 ;; esac read -r -u 3 -p "Save results as CSV instead of on-screen output? [y/N]: " ans if [[ "$ans" =~ ^[Yy] ]]; then read -r -u 3 -p "CSV file name [redirects.csv]: " csvfile csvfile="${csvfile:-redirects.csv}" CSV=1 echo "Writing results to $csvfile ..." exec >"$csvfile" fi exec 3<&- fi [[ ${#URLS[@]} -eq 0 ]] && { echo "No URLs provided." >&2; exit 1; } csv_escape() { local s="${1//\"/\"\"}"; printf '"%s"' "$s"; } # ---- Check a single URL ---- check_url() { local start="$1" current="$1" hops=0 result="" code="" next="" declare -A seen=() [[ "$current" =~ ^https?:// ]] || current="http://$current" [[ $CSV -eq 0 ]] && printf '\n%s%s%s\n' "$BOLD" "$start" "$RESET" while :; do local out out=$(curl -s -o /dev/null --max-time "$TIMEOUT" -A "$USER_AGENT" \ -w '%{http_code} %{redirect_url}' "$current" 2>/dev/null) code="${out%% *}" next="${out#* }"; [[ "$next" == "$out" ]] && next="" if [[ -z "$code" || "$code" == "000" ]]; then result="ERROR (connection failed/timeout)" [[ $CSV -eq 0 ]] && printf ' [%2d] %sERR%s %s\n' "$hops" "$RED" "$RESET" "$current" break fi local colour="$GREEN" [[ "$code" =~ ^3 ]] && colour="$YELLOW" [[ "$code" =~ ^[45] ]] && colour="$RED" [[ $CSV -eq 0 ]] && printf ' [%2d] %s%s%s %s\n' "$hops" "$colour" "$code" "$RESET" "$current" seen["$current"]=1 # Not a redirect (or redirect without Location) -> done if [[ ! "$code" =~ ^3 || -z "$next" ]]; then if [[ "$code" =~ ^2 ]]; then result="OK" elif [[ "$code" =~ ^3 ]]; then result="REDIRECT WITHOUT LOCATION" else result="HTTP $code"; fi break fi if [[ -n "${seen[$next]:-}" ]]; then result="LOOP DETECTED" [[ $CSV -eq 0 ]] && printf ' %s↻ loops back to %s%s\n' "$RED" "$next" "$RESET" current="$next"; hops=$((hops + 1)) break fi hops=$((hops + 1)) current="$next" if (( hops > MAX_REDIRECTS )); then result="TOO MANY REDIRECTS (>$MAX_REDIRECTS)" break fi done if [[ $CSV -eq 1 ]]; then printf '%s,%s,%s,%s,%s\n' "$(csv_escape "$start")" "$(csv_escape "$current")" \ "$code" "$hops" "$(csv_escape "$result")" else local rc="$GREEN"; [[ "$result" != "OK" ]] && rc="$RED" printf ' %s→ %s%s | hops: %d | final: %s\n' "$rc" "$result" "$RESET" "$hops" "$current" fi [[ "$result" == "OK" ]] } # ---- Main ---- [[ $CSV -eq 1 ]] && echo "url,final_url,final_status,hops,result" total=0; failed=0 for raw in "${URLS[@]}"; do url="$(echo "$raw" | tr -d '\r' | xargs)" # trim whitespace / CRLF [[ -z "$url" || "$url" == \#* ]] && continue total=$((total + 1)) check_url "$url" || failed=$((failed + 1)) done if [[ $CSV -eq 0 ]]; then printf '\n%sChecked %d URL(s): %d OK, %d with issues%s\n' "$CYAN" "$total" "$((total - failed))" "$failed" "$RESET" fi (( failed == 0 ))