Blog

  • 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
    
  • Portadas del día

    Una selección de las portadas de los periódicos de hoy. Son de Kiosko.net y las pongo aquí para verlas juntas. Si alguna está en blanco es porque no se ha publicado aun.