144 lines
4.2 KiB
Go
144 lines
4.2 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"flag"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func extractAnchorText(line string) (string, bool) {
|
|
// Busca el texto entre > y < del primer <a ...>...</a>
|
|
gt := strings.Index(line, ">")
|
|
if gt == -1 {
|
|
return "", false
|
|
}
|
|
lt := strings.Index(line[gt+1:], "<")
|
|
if lt == -1 {
|
|
return "", false
|
|
}
|
|
text := line[gt+1 : gt+1+lt]
|
|
text = strings.TrimSpace(text)
|
|
if text == "" {
|
|
return "", false
|
|
}
|
|
return text, true
|
|
}
|
|
|
|
func main() {
|
|
// Flags
|
|
maxResults := flag.Int("max", 30, "Número máximo de resultados a mostrar")
|
|
prefix := flag.Bool("prefix", false, "Buscar por prefijo en lugar de subcadena")
|
|
timeout := flag.Duration("timeout", 20*time.Second, "Timeout de la petición HTTP (e.g., 10s, 2m)")
|
|
noColor := flag.Bool("nocolor", false, "Desactivar iconos/colores")
|
|
flag.Parse()
|
|
|
|
if flag.NArg() < 1 {
|
|
fmt.Println("Uso: pip_busca [opciones] <cadena_busqueda>")
|
|
fmt.Println("Opciones:")
|
|
fmt.Println(" -max N Limitar el número de resultados (por defecto 30)")
|
|
fmt.Println(" -prefix Coincidencia por prefijo (más rápida si buscas por inicio)")
|
|
fmt.Println(" -timeout DUR Timeout de HTTP (por defecto 20s)")
|
|
fmt.Println(" -nocolor Desactiva iconos/colores en salida")
|
|
os.Exit(1)
|
|
}
|
|
|
|
query := strings.ToLower(flag.Arg(0))
|
|
url := "https://pypi.org/simple/"
|
|
|
|
client := &http.Client{Timeout: *timeout}
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "❌ Error creando petición: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
req.Header.Set("User-Agent", "pip_busca-go/1.0 (+https://pypi.org)")
|
|
req.Header.Set("Accept", "text/html")
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "❌ Error conectando con PyPI: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
fmt.Fprintf(os.Stderr, "❌ Respuesta HTTP no OK: %s\n", resp.Status)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if !*noColor {
|
|
fmt.Printf("🔍 Buscando: %s\n\n", query)
|
|
} else {
|
|
fmt.Printf("Buscando: %s\n\n", query)
|
|
}
|
|
|
|
scanner := bufio.NewScanner(resp.Body)
|
|
// Aumenta el buffer por si alguna línea es grande (aunque en /simple suele ser pequeño)
|
|
const maxLine = 1024 * 1024 // 1 MiB
|
|
buf := make([]byte, 64*1024)
|
|
scanner.Buffer(buf, maxLine)
|
|
|
|
encontrados := 0
|
|
queryStarted := false // para modo -prefix: romper cuando superemos el bloque
|
|
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
// Normalizamos a minúsculas para buscar
|
|
lower := strings.ToLower(line)
|
|
|
|
// Solo nos interesan líneas con anclas
|
|
if !strings.Contains(lower, "<a ") || !strings.Contains(lower, "</a>") {
|
|
continue
|
|
}
|
|
|
|
name, ok := extractAnchorText(line)
|
|
if !ok {
|
|
continue
|
|
}
|
|
nameLower := strings.ToLower(name)
|
|
|
|
match := false
|
|
if *prefix {
|
|
// Coincidencia por prefijo
|
|
if strings.HasPrefix(nameLower, query) {
|
|
match = true
|
|
queryStarted = true
|
|
} else if queryStarted {
|
|
// /simple/ está ordenado; si ya pasamos el bloque del prefijo, podemos cortar.
|
|
// Cuando el nombre actual ya no empieza por el prefijo y ya habíamos empezado
|
|
// a encontrar coincidencias, significa que hemos superado la zona.
|
|
break
|
|
}
|
|
} else {
|
|
// Coincidencia por subcadena
|
|
if strings.Contains(nameLower, query) {
|
|
match = true
|
|
}
|
|
}
|
|
|
|
if match {
|
|
if !*noColor {
|
|
fmt.Printf("📦 %s\n", name)
|
|
} else {
|
|
fmt.Println(name)
|
|
}
|
|
encontrados++
|
|
if encontrados >= *maxResults {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "⚠️ Aviso: error leyendo la respuesta: %v\n", err)
|
|
}
|
|
|
|
if encontrados == 0 {
|
|
fmt.Println("❌ No se encontraron paquetes.")
|
|
}
|
|
}
|