Input Lines
one alternative per line
Regex
0 alternatives
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
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.