package main
import (
"bufio"
"flag"
"fmt"
"net/http"
"os"
"strings"
"time"
)
func extractAnchorText(line string) (string, bool) {
// Busca el texto entre > y < del primer ...
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] ")
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, "") {
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.")
}
}