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.") } }