Blog

  • Group values and calculate total by match

    Input
    One line per item. Numeric extraction is controlled by the regex below. Group rules on the right sum matching lines.
    Numeric extraction
    The first capturing group must contain the numeric text. Example: .*\(([\d.,]+)\).*
    By default both are ignored. Example with ignored comma: 3445,53 → 344553. Example with decimal comma: 3445,53 → 3445.53.
    Calculation by regex
    Matching is case-insensitive and uses the original text exactly as written.

  • Convert to list

    Original 0 items
    Result 0 items
    Separator
    Single input
  • Generate matching Regex

    Input Lines one alternative per line
    Regex 0 alternatives
    Substitutions (word alternatives)
    If a line contains the left word, the generator also considers versions where that word is replaced by any right-side alternative. Add multiple alternatives per word. You can add and remove rows.

  • Combine values

    Value Regex Capture group #1 must contain the value chunk (e.g. “(20)” or “(10 A, 10 B)”).

  • Aggregate groups of values

    Input text Groups by blank lines; headings (no value) split sections
    Output 0 sections
    Value-extraction regex
    First capturing group is numeric. Headings are preserved; groups are sorted within each section.

  • 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/Powershell script to check redirects

    function urlhops {
      $MaxHops    = 30
      $TimeoutSec = 20
      $TitleBytes = 250000
    
      $SecureTls = ($env:URLHOPS_SECURE -eq "1")
      $NoColor   = -not [string]::IsNullOrEmpty($env:NO_COLOR)
    
      # Colors (best-effort)
      $Esc = [char]27
      $UseColor = -not $NoColor
      $RST=""; $BLD=""; $FG_LGT=""; $BG_GRN=""; $BG_RED=""; $BG_BLU=""; $BG_MAG=""; $BG_GRY=""
      if ($UseColor) {
        $RST    = "$Esc[0m"; $BLD="$Esc[1m"; $FG_LGT="$Esc[97m"
        $BG_GRN="$Esc[42m"; $BG_RED="$Esc[41m"; $BG_BLU="$Esc[44m"; $BG_MAG="$Esc[45m"; $BG_GRY="$Esc[100m"
      }
    
      function Strip-Ansi([string]$s) { if ($null -eq $s) { "" } else { [regex]::Replace($s, "`e\[[0-9;]*[mK]", "") } }
      function VisLen([string]$s) { (Strip-Ansi $s).Length }
    
      function Badge([string]$code) {
        $bg = $BG_GRY
        if ($code -match '^\d{3}$') {
          switch -regex ($code) {
            '^2\d\d$' { $bg = $BG_GRN; break }
            '^3\d\d$' { $bg = $BG_BLU; break }
            '^4\d\d$' { $bg = $BG_RED; break }
            '^5\d\d$' { $bg = $BG_MAG; break }
            default   { $bg = $BG_GRY; break }
          }
        }
        if (-not $UseColor) { return "[ $code ]" }
        return ("{0}{1}{2}{0}[ {3,3} ]{4}" -f $BLD, $bg, $FG_LGT, $code, $RST)
      }
    
      function Normalize-Url([string]$u) {
        if ($null -eq $u) { $u = "" }
        $u = $u.Trim()
        if ($u -match '^(?i)https?://') { return $u }
        if ([string]::IsNullOrEmpty($u)) { return $u }
        "http://$u"
      }
    
      function Abs-Url([string]$base, [string]$loc) {
        if ([string]::IsNullOrEmpty($loc)) { return "" }
        if ($loc -match '^(?i)https?://') { return $loc }
        try {
          $baseUri = New-Object System.Uri($base)
          (New-Object System.Uri($baseUri, $loc)).AbsoluteUri
        } catch { $loc }
      }
    
      function Get-CurlExe {
        $cmd = Get-Command curl.exe -ErrorAction SilentlyContinue
        if (-not $cmd) { throw "No encuentro curl.exe en PATH." }
        $cmd.Source
      }
      $CurlExe = Get-CurlExe
    
      function Get-StatusAndLocation([string]$url) {
        # Captura headers incluso si curl los manda por stderr
        $args = @(
          "-sS",
          "--max-time", "$TimeoutSec",
          "--connect-timeout", "$TimeoutSec",
          "-H", "User-Agent: urlhops/visual-1.2",
          "-o", "NUL",
          "-D", "-",
          "-I",
          $url
        )
        if (-not $SecureTls) { $args = @("-k") + $args }
    
        try {
          $raw = & $CurlExe @args 2>&1
          if (-not $raw) { return @{ Code="000"; Location="" } }
    
          # Normaliza a texto único y separa en líneas
          $text  = ($raw | Out-String)
          $lines = $text -split "`r?`n"
    
          # Encuentra el ÚLTIMO status HTTP del output (evita "200 Connection established", etc.)
          $statusIdx = @()
          for ($i=0; $i -lt $lines.Count; $i++) {
            if ($lines[$i] -match '^HTTP/\S+\s+(\d{3})') { $statusIdx += $i }
          }
          if ($statusIdx.Count -eq 0) { return @{ Code="000"; Location="" } }
    
          $last = $statusIdx[-1]
          $code = $matches[1]
    
          # Location: busca desde ese bloque hacia abajo hasta línea vacía
          $loc = ""
          for ($j=$last+1; $j -lt $lines.Count; $j++) {
            $ln = $lines[$j]
            if ([string]::IsNullOrWhiteSpace($ln)) { break }
            if ($ln -match '^(?i)Location:\s*(.+)\s*$') { $loc = $matches[1].Trim(); break }
          }
    
          return @{ Code=$code; Location=$loc }
        } catch {
          return @{ Code="000"; Location="" }
        }
      }
    
      function Fetch-Title([string]$url) {
        $args = @(
          "-sS", "-L", "--compressed",
          "--max-time", "$TimeoutSec",
          "--connect-timeout", "$TimeoutSec",
          "-H", "User-Agent: urlhops/title-1.2",
          $url
        )
        if (-not $SecureTls) { $args = @("-k") + $args }
    
        try {
          $raw = & $CurlExe @args 2>&1
          if (-not $raw) { return "" }
          $html = ($raw | Out-String)
          if ($html.Length -gt $TitleBytes) { $html = $html.Substring(0, $TitleBytes) }
          $one = $html -replace "`r"," " -replace "`n"," "
          $m = [regex]::Match($one, '(?is)<title[^>]*>\s*(.{0,500}?)\s*</title>')
          if ($m.Success) { return ([regex]::Replace($m.Groups[1].Value, '\s+', ' ').Trim()) }
          ""
        } catch { "" }
      }
    
      # ASCII box
      $TL='+'; $TR='+'; $BL='+'; $BR='+'; $H='-'; $V='|'
      function Box-Top([int]$w) { $TL + ($H * $w) + $TR }
      function Box-Bot([int]$w) { $BL + ($H * $w) + $BR }
      function Box-Row([int]$w, [string]$text) {
        $pad = $w - (VisLen $text); if ($pad -lt 0) { $pad = 0 }
        $V + " " + $text + (" " * $pad) + " " + $V
      }
    
      while ($true) {
        Write-Host ""
        Write-Host ""
        Write-Host "Paste URLs (space/tab/comma/newline). Finish with an EMPTY LINE:"
    
        $urls = New-Object System.Collections.Generic.List[string]
        while ($true) {
          $line = Read-Host
          if ([string]::IsNullOrEmpty($line)) { break }
          $line = $line -replace "`t"," " -replace ","," "
          $parts = $line.Split(" ", [System.StringSplitOptions]::RemoveEmptyEntries)
          foreach ($p in $parts) { if ($p) { [void]$urls.Add($p) } }
        }
    
        if ($urls.Count -eq 0) { Write-Host "No URLs detected."; continue }
    
        foreach ($u in $urls) {
          $start = Normalize-Url $u
    
          $r = Get-StatusAndLocation $start
          $code = $r.Code
          $loc  = $r.Location
    
          $lines = New-Object System.Collections.Generic.List[string]
          $lines.Add( ("{0} {1}{2}{3}" -f (Badge $code), $BLD, $start, $RST) )
    
          $cur = $start
          $hops = 0
          while ($hops -lt $MaxHops) {
            $hops++
            if ($code -match '^30(1|2|3|7|8)$' -and -not [string]::IsNullOrEmpty($loc)) {
              $lines.Add("  -> $loc")
              $next = Normalize-Url (Abs-Url $cur $loc)
              if ($next -eq $cur) { break }
              $cur = $next
    
              $r = Get-StatusAndLocation $cur
              $code = $r.Code
              $loc  = $r.Location
              $lines.Add( ("{0} {1}" -f (Badge $code), $cur) )
              continue
            }
            break
          }
    
          $title = Fetch-Title $cur
          if ([string]::IsNullOrEmpty($title)) { $title = "<empty>" }
          $lines.Add("Title: $title")
    
          $w = 0
          foreach ($s in $lines) { $w = [Math]::Max($w, (VisLen $s)) }
          $w += 2
    
          Write-Host (Box-Top $w)
          foreach ($s in $lines) { Write-Host (Box-Row $w $s) }
          Write-Host (Box-Bot $w)
          Write-Host ""
        }
      }
    }
    
    urlhops
    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"
      }
    
      # BUCLE PRINCIPAL: repetir prompt -> procesar -> volver a preguntar
      while true; do
        # 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."
          continue
        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
      done
    }
    
    urlhops