Blog

  • Remove duplicates

    Original 0 lines
    Result 0 unique
    Cleanup rules
    Applied top to bottom
  • Extract URLs from text

    Input text Paste any text containing URLs
    Extracted URLs (editable) 0 found
    Open in groups of URLs
    Buttons are generated after extraction or after editing the output. Each button opens one group (e.g., 1 = URLs 1–5, 2 = URLs 6–10). The last group opens the remaining URLs.

  • Compare two lists

    Block A
    Result view shows cleaned lines. Click to edit original text.
    Block B
    Result view shows cleaned lines. Click to edit original text.
    Cleanup rules Applied top → bottom; removes matches before comparing.

  • HTML Title Ruler in Pixels

  • Bash script to check redirects

    urlhops() {
      # Minimal deps: bash, curl, awk, sed, tr, head
      # Input: paste URLs separated by space/tab/comma/newline, finish with EMPTY LINE.
      #
      # TLS behavior:
      # - Default: INSECURE (curl -k).
      # - Secure mode: URLHOPS_SECURE=1 urlhops  (no -k).
    
      local max_hops=30
      local timeout=20
      local title_bytes=250000
    
      # Default insecure; secure mode disables -k
      local CURL_TLS="-k"
      [ "${URLHOPS_SECURE:-0}" = "1" ] && CURL_TLS=""
    
      # Colors (disable if not TTY or NO_COLOR set)
      local use_color=1
      [[ ! -t 1 ]] && use_color=0
      [[ -n "${NO_COLOR:-}" ]] && use_color=0
    
      local RST='' BLD='' FG_LGT='' BG_GRN='' BG_RED='' BG_BLU='' BG_MAG='' BG_GRY=''
      if [ "$use_color" -eq 1 ]; then
        RST=$'\033[0m'; BLD=$'\033[1m'
        FG_LGT=$'\033[97m'
        BG_GRN=$'\033[42m'; BG_RED=$'\033[41m'; BG_BLU=$'\033[44m'; BG_MAG=$'\033[45m'; BG_GRY=$'\033[100m'
      fi
    
      # ASCII box drawing (Cygwin-safe)
      local TL='+' TR='+' BL='+' BR='+' H='-' V='|'
    
      _strip_ansi() { LC_ALL=C sed -r 's/\x1B\[[0-9;]*[mK]//g'; }
      _vislen() { printf '%s' "$1" | _strip_ansi | LC_ALL=C awk '{print length($0)}'; }
    
      _badge() {
        local code="$1" bg
        case "$code" in
          2??) bg="$BG_GRN" ;;
          3??) bg="$BG_BLU" ;;
          4??) bg="$BG_RED" ;;
          5??) bg="$BG_MAG" ;;
          *)   bg="$BG_GRY" ;;
        esac
        printf '%s%s%s%s[ %3s ]%s' "$BLD" "$bg" "$FG_LGT" "$BLD" "$code" "$RST"
      }
    
      _normalize_url() {
        local u="$1"
        if [[ "$u" =~ ^https?:// ]]; then printf '%s' "$u"
        else printf 'http://%s' "$u"
        fi
      }
    
      _abs_url() {
        # Best-effort Location resolution without python
        local base="$1" loc="$2"
        [ -z "$loc" ] && { printf '%s' ""; return 0; }
        [[ "$loc" =~ ^https?:// ]] && { printf '%s' "$loc"; return 0; }
    
        local origin path dir
        origin="$(printf '%s' "$base" | LC_ALL=C sed -nE 's#^(https?://[^/]+).*#\1#p')"
        [ -z "$origin" ] && { printf '%s' "$loc"; return 0; }
    
        if [[ "$loc" == /* ]]; then
          printf '%s%s' "$origin" "$loc"
          return 0
        fi
    
        path="$(printf '%s' "$base" | LC_ALL=C sed -nE 's#^https?://[^/]+(/.*)?#\1#p')"
        [ -z "$path" ] && path="/"
        dir="$(printf '%s' "$path" | LC_ALL=C sed -E 's#[^/]*$##')"
        [ -z "$dir" ] && dir="/"
    
        printf '%s%s%s' "$origin" "$dir" "$loc"
      }
    
      _status_and_location() {
        # Outputs: "<code>\n<location>\n"
        local url="$1" headers code location
        headers="$(
          curl $CURL_TLS -sS -D - -o /dev/null \
            --max-time "$timeout" \
            --connect-timeout "$timeout" \
            -H 'User-Agent: urlhops/visual-1.2' \
            "$url" 2>/dev/null
        )"
        if [ -z "$headers" ]; then
          printf '000\n\n'
          return 0
        fi
    
        code="$(printf '%s' "$headers" | LC_ALL=C awk 'NR==1 {print $2}')"
        [ -z "$code" ] && code="000"
        location="$(printf '%s' "$headers" | LC_ALL=C awk 'BEGIN{IGNORECASE=1} /^Location:[[:space:]]*/ {sub(/^Location:[[:space:]]*/,""); gsub("\r",""); print; exit}')"
        printf '%s\n%s\n' "$code" "$location"
      }
    
      _fetch_title() {
        # Safe title extraction; returns empty if not found.
        local url="$1" html title
        html="$(
          curl $CURL_TLS -sS -L --compressed \
            --max-time "$timeout" \
            --connect-timeout "$timeout" \
            -H 'User-Agent: urlhops/title-1.2' \
            "$url" 2>/dev/null | head -c "$title_bytes"
        )"
        [ -z "$html" ] && { printf '%s' ""; return 0; }
    
        # Extract first <title>...</title>, case-insensitive, collapse whitespace.
        title="$(printf '%s' "$html" \
          | tr '\n' ' ' \
          | LC_ALL=C sed -nE 's/.*<[Tt][Ii][Tt][Ll][Ee][^>]*>([^<]{0,500})<[/][Tt][Ii][Tt][Ll][Ee][^>]*>.*/\1/p' \
          | head -n 1 \
          | LC_ALL=C sed -E 's/[[:space:]]+/ /g; s/^[[:space:]]+//; s/[[:space:]]+$//')"
    
        printf '%s' "$title"
      }
    
      _box_top() { printf '%s%*s%s\n' "$TL" "$1" '' "$TR" | tr ' ' "$H"; }
      _box_bot() { printf '%s%*s%s\n' "$BL" "$1" '' "$BR" | tr ' ' "$H"; }
      _box_row() {
        local w="$1" text="$2"
        local pad=$((w - $(_vislen "$text"))); (( pad < 0 )) && pad=0
        printf '%s %s%*s %s\n' "$V" "$text" "$pad" "" "$V"
      }
    
      # Visual separation before prompt
      echo
      echo
      echo "Paste URLs (space/tab/comma/newline). Finish with an EMPTY LINE:"
    
      # Read until blank line
      local urls=() line
      while IFS= read -r line; do
        [ -z "$line" ] && break
        line="${line//$'\t'/ }"
        line="${line//,/ }"
        # shellcheck disable=SC2206
        local parts=($line)
        local p
        for p in "${parts[@]}"; do
          [ -n "$p" ] && urls+=("$p")
        done
      done
    
      if [ ${#urls[@]} -eq 0 ]; then
        echo "No URLs detected."
        return 1
      fi
    
      local u
      for u in "${urls[@]}"; do
        local start="$(_normalize_url "$u")"
    
        # First hop (initial URL, always bold)
        local out code loc
        out="$(_status_and_location "$start")"
        code="$(printf '%s' "$out" | LC_ALL=C sed -n '1p')"
        loc="$(printf '%s' "$out" | LC_ALL=C sed -n '2p')"
    
        local lines=()
        lines+=("$(_badge "$code") ${BLD}${start}${RST}")
    
        local cur="$start"
        local hops=0
        while [ $hops -lt $max_hops ]; do
          hops=$((hops+1))
    
          if [[ "$code" =~ ^30[12378]$ ]] && [ -n "$loc" ]; then
            lines+=("  -> $loc")
            local next
            next="$(_abs_url "$cur" "$loc")"
            next="$(_normalize_url "$next")"
            [ "$next" = "$cur" ] && break
            cur="$next"
    
            out="$(_status_and_location "$cur")"
            code="$(printf '%s' "$out" | LC_ALL=C sed -n '1p')"
            loc="$(printf '%s' "$out" | LC_ALL=C sed -n '2p')"
            lines+=("$(_badge "$code") $cur")
            continue
          fi
          break
        done
    
        local title="$(_fetch_title "$cur")"
        [ -z "$title" ] && title="<empty>"
        lines+=("Title: $title")
    
        local w=0 s len
        for s in "${lines[@]}"; do
          len="$(_vislen "$s")"
          (( len > w )) && w=$len
        done
        w=$((w + 2))
    
        _box_top "$w"
        for s in "${lines[@]}"; do _box_row "$w" "$s"; done
        _box_bot "$w"
        echo
      done
    }
    
    urlhops