estentor

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 8, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

README

estentor

Biblioteca en Go para construir consolas interactivas: prompt, registro de comandos, ayuda que se escribe sola, historial con sugerencia en gris, edición de línea, tablas alineadas, banner de bienvenida e indicador de carga.

Una sola dependencia, y del equipo de Go: golang.org/x/term, para el modo crudo de la terminal.

idas> help

  clear (cls)          limpia la pantalla
  equipos              muestra una tabla de ejemplo
  exit (quit, salir)   sale de la consola
  help (?)             lista los comandos disponibles

Guías de uso

Este README es la visión de conjunto. Para el detalle de cada pieza:

Instalar

go get github.com/MarcosGiojiho/estentor

Necesita Go 1.26 o posterior. El paquete se llama estentor; el tipo principal, Consola.

Uso mínimo

package main

import "github.com/MarcosGiojiho/estentor"

func main() {
    estentor.Nueva("miapp").Arrancar()
}

Eso ya te da un prompt miapp> con clear, help y exit funcionando.

Para que un main pueda salir con código distinto de cero cuando la entrada falla, ArrancarCon(ctx) devuelve error:

if err := c.ArrancarCon(context.Background()); err != nil {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
}

Devuelve nil en las salidas limpias: ErrSalir, Ctrl-D, fin de la entrada o contexto cancelado.

Probar el ejemplo

cd estentor
go run ./cmd/ejemplo

Desde la raíz del proyecto. Los banners salen de cmd/ejemplo/banners/, y con -banners <carpeta> se le puede dar otra. Si no la encuentra, avisa por stderr y arranca sin banner.

Trae equipos, equipo <ip> —que completa las IPs con el tabulador y entra en su contexto—, escanear para ver una tarea de fondo, eco, y registrar <fichero> para volcar la sesión.


Consola

c := estentor.Nueva("idas")
c.Arrancar()

Nueva deja la consola leyendo de la entrada estándar y escribiendo en la salida estándar. Arrancar muestra un banner y ejecuta lo que se escriba, hasta que un comando devuelve ErrSalir o se acaba la entrada (Ctrl-D).

Campos que se pueden tocar:

Campo Qué es
Nombre lo que sale en el prompt
Entrada de dónde lee. Por defecto os.Stdin
Salida dónde escribe. Por defecto os.Stdout
Banners textos de bienvenida, se muestra uno al azar
Variables valores que se sustituyen en los textos
Prompt función que compone el prompt. Por defecto nombre>
Color ColorAuto, ColorSiempre o ColorNunca
Pegado PegadoAuto o PegadoNunca: si lo pegado es texto o teclas
NoRecordar qué líneas no entran en el historial
MaxLinea lo más larga que puede ser una línea. Por defecto 1 MB

Entrada y Salida son io.Reader e io.Writer, así que se pueden cambiar por lo que sea. Es lo que permite probar la consola sin teclear nada:

c.Entrada = strings.NewReader("help\nexit\n")
c.Salida = &bytes.Buffer{}

Si lo que pones guarda lo escrito en un buffer —un bufio.Writer—, la consola lo vacía sola antes de quedarse esperando a que teclees y al cerrarse. Sin eso el prompt no aparecía hasta que el buffer se llenara, y parecía colgada.

Comandos

c.Registrar(estentor.Comando{
    Nombre:    "eco",
    Sinonimos: []string{"echo"},
    Ayuda:     "repite lo que se le pase",
    Ejecutar: func(ctx context.Context, args []string) error {
        if len(args) == 0 {
            return errors.New("falta el texto")
        }
        fmt.Fprintln(c.Salida, strings.Join(args, " "))
        return nil
    },
})
  • Con una terminal de verdad hay historial, edición de línea y sugerencia en gris sacada del historial, que la flecha derecha acepta. Y tabulador para completar el nombre del comando: si casan varios avanza hasta lo común, y al segundo tabulador seguido te lista cuáles eran, sin tocar tu línea.
  • El tabulador completa también los argumentos, si el comando dice cuáles con Completar. Lo que se teclea largo no es escanear, son sus argumentos.
  • help <comando> enseña su ficha, sacada de Uso y Ayuda. Y Args comprueba cuántos argumentos hacen falta antes de ejecutar, con estentor.Exactamente(n) o estentor.AlMenos(n), para no volver a escribir esa comprobación en cada comando.
  • help se construye solo. Cualquier comando registrado aparece en la ayuda con su descripción, ordenado y alineado. No hay ninguna lista que mantener.
  • Los sinónimos funcionan igual que el nombre, pero en la ayuda salen junto a él en vez de como entradas aparte.
  • El error no lleva el nombre del comando. La consola le pone delante eco: , así que si lo renombras el mensaje se corrige solo.
  • Registrar un nombre que ya existe lo sustituye, así que se pueden reemplazar help o exit por versiones propias.
Salir

Un comando termina la consola devolviendo estentor.ErrSalir:

Ejecutar: func([]string) error {
    return estentor.ErrSalir
},

No es un fallo: es la forma de decir «hemos acabado», igual que io.EOF señala el final de una lectura sin ser un error. La consola lo distingue con errors.Is, así que se puede envolver con %w sin romper nada.

De fábrica
Comando Sinónimos Qué hace
clear cls limpia la pantalla y el historial de desplazamiento
help ? lista los comandos
set lista, enseña o guarda una variable
unset quita una variable
back atras sale del contexto actual
jobs trabajos lista las tareas de fondo
kill cancelar pide a una tarea que pare
exit quit, salir termina

Tablas

Tabla no depende de la consola: se construye aparte y se puede usar en cualquier programa.

t := estentor.NuevaTabla("IP", "Referencia", "Estado")
t.Fila("10.20.0.41", "6ES7 516-3AN02-0AB0", "responde")
t.Fila("10.20.0.88", "AS228T-A", "responde")

fmt.Println(t)     // en cualquier programa
c.Tabla(t)         // o en la salida de la consola
  IP          Referencia           Estado
  ----------  -------------------  --------
  10.20.0.41  6ES7 516-3AN02-0AB0  responde
  10.20.0.88  AS228T-A             responde
  • Cumple fmt.Stringer, así que fmt.Println, %v y %s la pintan solos. También hay Escribir(w io.Writer).
  • Fila devuelve la tabla, así que se pueden encadenar llamadas.
  • Los anchos se miden en columnas de terminal, no en bytes ni en caracteres: los acentos (también en NFD), la ñ y el CJK de ancho doble no descuadran las columnas.
  • Los anchos se recalculan al pintar, así que una tabla se puede modificar y volver a imprimir sin que se descuadre.
  • Una fila con menos celdas que cabeceras se completa con huecos; las celdas que sobran se ignoran.
  • Sangria son los espacios de la izquierda. Por defecto dos.

Y se puede modificar, buscar dentro y ordenar:

t.Celda(0, 2, "sin identificar")   // cambia una celda; crece si no existe
t.CambiarFila(1, "10.20.0.88", "AS228T-A", "responde")
t.InsertarFila(0, "10.20.0.1", "?", "?")
t.BorrarFila(2)                     // false si esa fila no existía
t.Limpiar()                         // quita las filas, deja las cabeceras

valor, existe := t.Obtener(0, 2)    // nunca revienta
i := t.Buscar(0, "10.20.0.88")      // índice, o -1 si no está
i = t.BuscarSi(func(fila []string) bool {
    return strings.HasPrefix(fila[0], "10.20.")
})
sub := t.Filtrar(func(fila []string) bool {
    return fila[2] == "responde"    // tabla nueva, no toca la original
})
t.Ordenar(0)                        // estable

Celda y CambiarFila hacen crecer la tabla si la posición no existe, así que se puede rellenar por coordenadas sin preparar nada. Y a diferencia de indexar t.Filas a mano, nunca revientan.

Detalle completo en docs/tablas.md.

Color

fmt.Fprintln(c.Salida, c.Pintar("no se pudo", estentor.Rojo))
fmt.Fprintln(c.Salida, c.Pintar("listo", estentor.Verde, estentor.Negrita))

En automático se pinta solo con terminal de verdad, y respeta NO_COLOR y TERM=dumb. Los errores ya salen en rojo de fábrica.

Y en hexadecimal, degradando solo a la paleta de 256 si la terminal no admite 24 bits:

c.Pintar("critico", estentor.Hex("#ff5555"))

Las tablas se colorean por reglas, y los códigos no descuadran las columnas: el cálculo de anchos los descuenta.

t.ColorearCabeceras(estentor.Negrita)
t.ColorearColumnaEntera(0, estentor.Cian)
t.Colorear("responde", estentor.Verde)
t.Colorear("sin identificar", estentor.Hex("#ff8800"))

Detalle en docs/tablas.md.

Indicador de carga

parar := c.Cargando("Cargando {nombre} para {usuario}")
cargar()          // el trabajo real de arranque
parar()

Pinta [|] Cargando idas para marcos girando, y al llamar a parar() borra la línea entera.

Si la salida no es una terminal —redirigida a un fichero o a otro programa— no gira: escribe el texto una vez y ya está, para no llenar el log de basura.

Banners

banners, err := estentor.BannersDeCarpeta("banners")
if err != nil {
    fmt.Fprintf(os.Stderr, "aviso: %v\n", err)   // avisa...
}
c.Banners = banners                              // ...y arranca con los que haya

Lee los .txt de una carpeta y al arrancar muestra uno al azar. Para añadir uno propio basta con dejar un fichero ahí: no hay que tocar código ni recompilar.

Devuelve los que sí pudo leer y el error de los que no, a la vez. Un .txt sin permisos no se lleva por delante a los buenos: te dice cuál falló y sigues con el resto. Por eso el ejemplo avisa y continúa en vez de rendirse.

También se pueden poner a mano: c.Banners = []string{"..."}.

Variables

En los banners y en los textos de carga, lo que vaya entre llaves se sustituye:

Bienvenido a {nombre}, {usuario}
conectado desde {host}

De fábrica vienen {nombre} (el de la consola), {usuario} y {host}. Se pueden añadir más:

c.Variables["version"] = "0.1"

Una variable que no existe se deja tal cual, así que si escribes {usuraio} lo verás en pantalla en vez de un hueco vacío y el error salta a la vista.

Y se pueden poner desde el prompt con set, que escribe en el mismo sitio. Lo que tecleas se expande antes de ejecutarse, así que sirven en cualquier comando:

idas> set objetivo 10.20.0.41
idas 10.20.0.41> eco el objetivo es {objetivo}
el objetivo es 10.20.0.41

La sustitución ocurre después de partir la línea, así que un valor con espacios sigue siendo un solo argumento.

Para escribir la marca tal cual, una barra invertida delante:

idas> conectar \{objetivo}
conectar {objetivo}

Doblar la llave no escapa, y es a propósito: {{7*7}} es una carga de prueba contra plantillas de servidor y tiene que llegar al comando tal y como la tecleaste.

Contexto en pila

set se aplica al nivel donde estás, y lo que pones dentro se va al salir:

idas> set puerto 502
idas> equipo 10.20.0.41
idas 10.20.0.41> set puerto 102     ← tapa al de fuera, no lo pisa
idas 10.20.0.41> back
idas> set puerto
puerto = 502                        ← intacto

Es lo que arregla el problema de msfconsole, donde el almacén es global y lo que pones para un módulo se filtra al siguiente. set a secas dice de qué nivel sale cada valor.

Detalle en docs/consola.md.

También está c.Expandir(texto) para sustituirlas en cualquier otro sitio.

Ctrl-C

Hace dos cosas distintas según dónde estés:

  • Escribiendo una línea: la descarta y vuelve a pedir. No cierra la consola.
  • Con un comando en marcha: le cancela el contexto que ya recibe.
Ejecutar: func(ctx context.Context, args []string) error {
    for _, ip := range rango {
        if err := ctx.Err(); err != nil {
            return err          // Ctrl-C llega por aquí
        }
        escanear(ip)
    }
    return nil
}

Cancelar es pedir, no obligar: un comando que no mire su contexto seguirá hasta el final. Por eso el segundo Ctrl-C vuelve a ser el de siempre y mata el proceso — si atrapáramos también ese, un comando que ignora su contexto dejaría la consola sin forma de salir.

Por debajo, mientras corre un comando la terminal sale del modo crudo. Es lo que hace que Ctrl-C vuelva a generar una señal: en modo crudo no genera ninguna, es el byte 3, y mientras el comando corre no hay nadie leyéndolo. De paso, lo que teclees mientras tanto lo guarda la propia terminal y aparece en el prompt siguiente, en vez de perderse.

Sin terminal —una tubería, un fichero, un test— nada de esto se activa, y SIGINT sigue matando el proceso, que es lo que espera cualquiera que meta esto en un script.

Buscar en el historial

Ctrl-R busca hacia atrás por subcadena. Otro Ctrl-R salta a la anterior que también case, Intro la ejecuta y Ctrl-C cancela la búsqueda dejando la línea como estaba.

t> (buscar)'motivo': set motivo acordado con produccion

El prompt cambia mientras buscas, y dice sin resultado cuando no encuentra nada en vez de quedarse callado enseñando algo viejo.

Moverse por palabras

Ctrl-W borra la palabra de atrás, Alt-B / Ctrl-← y Alt-F / Ctrl-→ mueven, y Alt-D borra la de delante. Entre «un carácter» y «toda la línea» no había nada, y con un comando de 120 caracteres eso son 40 pulsaciones de flecha.

Una palabra es un trozo entre espacios, y nada más. Es una decisión: bash corta además por puntuación en Alt-B, y aquí 10.20.0.0/24 serían cinco palabras. Predecible gana a cómodo.

Historial entre sesiones

El historial vive en memoria, y la biblioteca no decide dónde se guarda:

if f, err := os.Open(ruta); err == nil {
    _ = c.CargarHistorial(f)
    f.Close()
}

c.Arrancar()

if f, err := os.OpenFile(ruta, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600); err == nil {
    _ = c.GuardarHistorial(f)
    f.Close()
}

Sin esto, cada arranque empieza de cero — y no solo pierdes la flecha arriba: la sugerencia en gris mira el historial antes que los nombres de comando, así que sin persistencia solo sabe media cosa.

El 0600 no es decorativo. Esto escribe a disco lo que tecleas, y en una consola de auditoría eso son objetivos y rangos de red de un cliente. Para dejar cosas fuera:

c.NoRecordar = func(linea string) bool {
    return strings.Contains(linea, "--clave")
}

Se filtra al entrar, no al guardar, así que lo filtrado tampoco sale por la flecha arriba ni por la sugerencia.

Pegar texto

Lo que pegas entra como texto, no como teclas: los saltos de línea no ejecutan nada, el tabulador no completa y un Ctrl-C dentro no interrumpe. Pegas una lista de IPs, la revisas, y entonces pulsas Intro.

Como la línea de edición es una sola, los saltos pegados se quedan en espacios. Es lo único que puede verse ahí.

Se apaga con c.Pegado = estentor.PegadoNunca, y entonces pegar vuelve a ser teclear muy rápido. Está por una lección ajena: GNU readline lo trae activado desde la 8.1, pero CPython y FRR tuvieron que desactivarlo porque rompía a quien pegaba una configuración contando con que los saltos la fueran ejecutando.

Volcar la sesión a un fichero

parar := c.Registrando(f)   // desde un comando, con la consola en marcha
defer parar()

Guarda las preguntas y las respuestas: las líneas tecleadas con su prompt, la salida de los comandos y los avisos de las tareas de fondo, sin códigos de color. Con 0600, que es una sesión real contra sistemas de alguien.

Tareas de fondo

c.Lanzar("escaneo", func(ctx context.Context) error { ... })

jobs lista y kill N pide parar. Lo que costaba no eran las goroutines: una tarea que escribe mientras tecleas aparta tu línea, escribe encima y la repinta, con el cursor donde estaba.

Cancelar es pedir, no obligar: una tarea que no mire su contexto seguirá hasta el final.

Lo que no hace

Para que no lo descubras tecleando. Nada de esto está previsto hoy, y algunas cosas son decisiones y otras deudas:

Copiar la Consola. Trabaja siempre con el puntero que devuelve Nueva. Copiarla por valor comparte el registro de comandos con la original y deja los comandos de fábrica escribiendo en la salida de la primera. go vet lo avisa.

Cancelar no interrumpe una lectura en curso. ArrancarCon(ctx) cierra la consola desde fuera, pero si la cancelación llega mientras se está esperando a que alguien teclee, no se nota hasta que teclee.

Redimensionar la ventana a mitad de línea. El ancho de la terminal se vuelve a preguntar al empezar cada línea, no dentro de una: si estiras la ventana con un comando largo a medio escribir, se descuadra hasta que pulses Intro.

Estructura

estentor/
├── consola.go              la consola, los comandos, el registro y los banners
├── editor.go               el editor de línea propio
├── teclas.go               decodificador de teclas y secuencias de escape
├── tabla.go                tablas, independientes de la consola
├── color.go                estilos, hexadecimales y detección de terminal
├── contexto.go             la pila de niveles
├── trabajos.go             las tareas de fondo
├── *_test.go               al lado del código: Go lo exige
├── docs/                   las guías de uso
└── cmd/
    ├── ejemplo/            aplicación de ejemplo, con sus banners
    └── analizar/           el analizador del proyecto

El código de la biblioteca vive en la raíz del módulo, que es la convención de Go para un repositorio que es una biblioteca: se importa con la ruta a secas, sin un /pkg ni un /internal por el medio.

Comprobar el código

Todo de golpe:

go run ./cmd/analizar

Encadena formato, go vet, golangci-lint con 19 analizadores y los tests con detector de carreras y cobertura.

Dos cosas a propósito:

  • Está en Go y no en un script. Funciona igual en Linux, macOS y Windows, y no pide instalar nada: si tienes el proyecto, tienes Go.
  • Ejecuta siempre todos los pasos, aunque alguno falle, y resume al final. Un análisis que se para en el primer problema te hace dar una vuelta por cada uno.

Si golangci-lint no está instalado, lo dice y sigue, y el resumen distingue SIN COMPROBAR de FALLA. No dar por bueno lo que no se ha mirado.

Con -cobertura abre además el informe línea a línea en el navegador:

go run ./cmd/analizar -cobertura

Suelto:

gofmt -l .           # ficheros mal formateados. Vacío = bien
go build ./...       # ¿compila todo?
go vet ./...         # fallos que compilan pero están mal
go test -race ./...  # los tests, buscando carreras de datos
go doc github.com/MarcosGiojiho/estentor   # la documentación del código

golangci-lint se instala con brew install golangci-lint. La configuración está en .golangci.yml, y cada exclusión lleva escrito su motivo, porque una exclusión sin justificar es como se oxida un linter hasta que nadie lo mira.

Cubre lo que buscarías en una herramienta tipo SonarQube —duplicación con dupl, complejidad con gocognit y gocyclo, seguridad con gosec— sin servidor ni contenedor. Lo que no da es historial entre commits ni puerta de calidad, y eso empieza a hacer falta con varias personas o un CI, no aquí.

Licencia

Apache License 2.0. Ver LICENSE.

Documentation

Overview

Package estentor construye consolas interactivas.

Index

Constants

View Source
const MaxCrecimiento = 1 << 20

MaxCrecimiento es hasta dónde pueden estirar la tabla Celda y CambiarFila. Un índice mayor no hace nada: a esa escala viene de un error de cálculo, y reservar la memoria se llevaría el proceso por delante.

View Source
const MaxLineaPorDefecto = 1 << 20

MaxLineaPorDefecto es el límite de longitud de línea que pone Nueva: 1 MB. El de bufio.Scanner son 64 KB, que se pasan pegando un texto mediano.

View Source
const NivelGlobal = "global"

NivelGlobal es cómo se llama el nivel de fuera, el que siempre existe.

Variables

View Source
var ErrInterrumpido = errors.New("interrumpido")

ErrInterrumpido lo devuelve la lectura cuando se pulsa Ctrl-C.

Existe porque golang.org/x/term colapsa Ctrl-C y Ctrl-D en el mismo io.EOF, y no son lo mismo: uno cancela lo que estás escribiendo y el otro cierra la consola.

View Source
var ErrSalir = errors.New("salir de la consola")

ErrSalir lo devuelve un comando para que la consola termine. No es un fallo: es la forma de decir "hemos acabado", igual que io.EOF.

Functions

func AlMenos

func AlMenos(n int) func([]string) error

AlMenos exige ese número de argumentos o más.

func BannersDeCarpeta

func BannersDeCarpeta(ruta string) ([]string, error)

BannersDeCarpeta lee los ficheros .txt de una carpeta y devuelve su contenido, para usarlo como Consola.Banners. No entra en subcarpetas.

Devuelve los que sí se pudieron leer Y el error de los que no, diciendo cuál falló de cada uno. Las dos cosas a la vez, porque quien llama es quien decide: su argumento de venta es "deja un fichero ahí y ya", y antes un solo .txt sin permisos se llevaba por delante a todos los buenos.

banners, err := estentor.BannersDeCarpeta("banners")
if err != nil {
	fmt.Fprintln(os.Stderr, err)   // avisa...
}
c.Banners = banners                // ...y arranca con los que haya

Que la carpeta entera no se pueda leer sigue siendo un fallo completo: ahí no hay nada que devolver a medias.

func Exactamente

func Exactamente(n int) func([]string) error

Exactamente exige ese número de argumentos, ni uno más ni uno menos.

Types

type Comando

type Comando struct {
	Nombre string

	// Sinonimos son nombres alternativos. Funcionan igual que Nombre, pero en
	// la ayuda salen junto a él en vez de como entradas aparte.
	Sinonimos []string

	Ayuda string

	// Uso es la firma del comando, para que "help <nombre>" pueda decir en qué
	// orden van los argumentos. Una línea de Ayuda no da para eso.
	//
	//	Uso: "escanear <rango> [--perfil ics]"
	//
	// Es documentación escrita a mano, así que se queda obsoleta sin que nada
	// avise. Una firma mentirosa es peor que no tener ninguna: si cambias los
	// argumentos, cambia esto.
	Uso string

	// Args comprueba los argumentos antes de ejecutar. Si es nil no se
	// comprueba nada, que es como se comportaba todo antes de que existiera.
	//
	//	Args: estentor.AlMenos(1)
	//
	// La línea se para aquí a propósito: una función y dos ayudas. Nada de
	// tipos ni de banderas, que es donde esto dejaría de ser una biblioteca
	// pequeña.
	Args func(args []string) error

	// Completar ofrece los valores posibles para el argumento que se está
	// tecleando. Si es nil, el tabulador no hace nada sobre los argumentos de
	// este comando, que es como se comportaba todo antes.
	//
	// args son los argumentos ya cerrados que hay delante, así que se puede
	// decidir por posición; prefijo es lo que se lleva escrito del que toca.
	//
	//	Completar: func(args []string, prefijo string) []string {
	//		if len(args) > 0 {
	//			return puertos(prefijo)
	//		}
	//		return protocolos(prefijo)
	//	}
	//
	// Devuelve los candidatos sin filtrar por prefijo si no quieres: la consola
	// no los filtra, así que **filtrarlos es cosa tuya**. Se hace así porque hay
	// listas que no se filtran por prefijo —rutas, rangos— y decidirlo por ti
	// sería quitarte esa opción.
	Completar func(args []string, prefijo string) []string

	// Ejecutar recibe el contexto de la consola, que se cancela si se pulsa
	// Ctrl-C mientras el comando corre.
	Ejecutar func(ctx context.Context, args []string) error
}

Comando es algo que la consola sabe hacer. Se añaden con Registrar y aparecen solos en la ayuda.

type Consola

type Consola struct {
	Nombre  string
	Entrada io.Reader
	Salida  io.Writer

	// Banners son textos de bienvenida. Al arrancar se muestra uno al azar.
	// Si está vacío no se muestra nada.
	Banners []string

	// Variables se sustituyen en banners y textos de carga cuando aparecen
	// entre llaves. Nueva rellena "usuario" y "host"; "nombre" siempre sale
	// del campo Nombre. Se pueden añadir las que se quieran.
	Variables map[string]string

	// Prompt compone lo que se pinta antes de cada línea. Si es nil se usa
	// "nombre> ". Recibe la consola para poder reflejar su estado.
	Prompt func(c *Consola) string

	// Color decide cuándo se pinta con color. Por defecto, automático.
	Color ModoColor

	// Pegado decide si lo que se pega entra como texto o como teclas. Por
	// defecto, como texto.
	Pegado ModoPegado

	// MaxLinea es lo más larga que puede ser una línea, en bytes. Al pasarse,
	// la consola avisa y termina: el lector de la biblioteca estándar queda
	// inservible tras ese error y no se puede seguir leyendo de él.
	MaxLinea int

	// NoRecordar decide qué líneas no entran en el historial. Si es nil entran
	// todas.
	//
	// Existe porque el historial se puede guardar en disco, y en una consola de
	// auditoría lo que se teclea son objetivos, rangos de red de un cliente y a
	// veces credenciales. La consola no adivina cuáles: lo dice quien la usa.
	//
	//	c.NoRecordar = func(linea string) bool {
	//		return strings.Contains(linea, "--clave")
	//	}
	NoRecordar func(linea string) bool
	// contains filtered or unexported fields
}

Consola es una consola interactiva. Se crea con Nueva y se pone en marcha con Arrancar.

func Nueva

func Nueva(nombre string) *Consola

Nueva devuelve una consola que lee de la entrada estándar y escribe en la salida estándar.

func (*Consola) Arrancar

func (c *Consola) Arrancar()

Arrancar es ArrancarCon con un contexto vacío.

No devuelve nada a propósito, para que el ejemplo mínimo siga siendo una línea: estentor.Nueva("miapp").Arrancar(). Quien necesite saber cómo terminó usa ArrancarCon.

func (*Consola) ArrancarCon

func (c *Consola) ArrancarCon(ctx context.Context) error

ArrancarCon muestra un banner y va ejecutando lo que se escriba, hasta que un comando devuelve ErrSalir, se acaba la entrada, o se cancela el contexto.

Devuelve nil cuando la consola terminó como debía —con ErrSalir, con Ctrl-D, al acabarse la entrada o al cancelarse el contexto— y el error de lectura si lo hubo. Sirve para que un main termine con código distinto de cero:

if err := c.ArrancarCon(ctx); err != nil {
	os.Exit(1)
}

La cancelación surte efecto entre líneas: si llega mientras se está esperando a que alguien teclee, no se nota hasta que teclee.

func (*Consola) Cancelar

func (c *Consola) Cancelar(id int) bool

Cancelar le pide a una tarea que pare. Devuelve false si ese número no existe o si ya había acabado.

Pedir no es obligar: una tarea que no mire su contexto seguirá hasta el final. Eso no lo puede arreglar la consola.

func (*Consola) Cargando

func (c *Consola) Cargando(texto string) func()

Cargando escribe el texto con un indicador giratorio delante y devuelve la función que lo para y limpia la línea.

parar := c.Cargando("Cargando módulos")
defer parar()

func (*Consola) CargarHistorial

func (c *Consola) CargarHistorial(r io.Reader) error

CargarHistorial mete en el historial lo que haya en r, una línea por línea, de lo más viejo a lo más nuevo. Hay que llamarla antes de arrancar.

Se salta las líneas en blanco y las repetidas seguidas, y se queda con las últimas si vienen más de las que caben, para que un fichero que ha ido creciendo no arrastre basura de hace meses.

**La biblioteca no decide dónde vive el fichero**, solo lee de un io.Reader. Eso es de quien la usa, que es quien sabe si va en el home, junto al proyecto o en ningún sitio. Y quien tiene que ponerle los permisos: en una consola de auditoría el historial guarda objetivos y rangos de red de un cliente, así que 0600 no es una recomendación.

f, err := os.Open(ruta)
if err == nil {
	defer f.Close()
	_ = c.CargarHistorial(f)
}

func (*Consola) Claves

func (c *Consola) Claves() []string

Claves devuelve los nombres de las variables que se ven desde donde estás, en orden alfabético. Una tapada por un nivel más cercano sale una sola vez.

func (*Consola) Contexto

func (c *Consola) Contexto() []string

Contexto devuelve los nombres de los niveles, de fuera hacia dentro, sin contar el global.

func (*Consola) Entrar

func (c *Consola) Entrar(nombre string)

Entrar mete un nivel nuevo en la pila. Lo que se ponga con Poner a partir de ahora vive ahí, y desaparece al Volver.

c.Entrar("10.20.0.41")
c.Poner("puerto", "102")   // solo existe dentro de ese nivel

El nivel hereda lo de fuera: una variable que no esté aquí se busca en el nivel de arriba, y así hasta el global.

func (*Consola) Expandir

func (c *Consola) Expandir(texto string) string

Expandir sustituye las variables entre llaves por su valor. Las que no existan se dejan tal cual, para que un error de escritura se vea.

Una llave con una barra invertida delante es literal: \{objetivo} da {objetivo} tal cual, exista la variable o no. Es el único sitio donde la barra significa algo aquí; una barra suelta se deja como está, para no estropear rutas de Windows ni dibujos de banner.

Doblar la llave NO escapa: {{7*7}} llega entero al comando. Es a propósito, porque eso es una carga de prueba contra plantillas de servidor, y mutilarla en silencio sería el peor fallo posible en una consola de auditoría.

Recorre el texto una sola vez y no vuelve a mirar lo ya sustituido. Con la versión anterior, que encadenaba reemplazos recorriendo el mapa, el valor de una variable podía contener la marca de otra y el resultado dependía del orden de iteración: el mismo banner salía distinto entre arranques.

func (*Consola) GuardarHistorial

func (c *Consola) GuardarHistorial(w io.Writer) error

GuardarHistorial escribe el historial en w, una línea por línea, de lo más viejo a lo más nuevo. Lo que NoRecordar dejó fuera no está aquí, porque nunca llegó a entrar.

Llámala al cerrar, cuando ArrancarCon ya ha vuelto: es entonces cuando el historial está completo.

f, err := os.OpenFile(ruta, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
if err == nil {
	defer f.Close()
	_ = c.GuardarHistorial(f)
}

func (*Consola) HayColor

func (c *Consola) HayColor() bool

HayColor dice si ahora mismo se pintaría con color.

En automático manda el entorno antes que la terminal, porque quien pone NO_COLOR o TERM=dumb lo hace justo para que no le pinten nada.

func (*Consola) Historial

func (c *Consola) Historial() []string

Historial devuelve lo tecleado, de lo más viejo a lo más nuevo. Es una copia: tocarla no cambia el historial de la consola.

func (*Consola) Lanzar

func (c *Consola) Lanzar(nombre string, tarea func(ctx context.Context) error) int

Lanzar arranca una tarea de fondo y devuelve su número.

La tarea recibe un contexto que se cancela al pararla con Cancelar, y también cuando se cierra la consola. Escribir en c.Salida desde dentro es seguro: la consola pone un candado, y en modo crudo aparta la línea que se esté tecleando y la vuelve a pintar.

Lánzalas desde un comando, o sea con la consola ya en marcha. Arrancar envuelve c.Salida al empezar, así que una tarea lanzada antes podría estar leyendo ese campo justo mientras se cambia.

func (*Consola) OlvidarTrabajos

func (c *Consola) OlvidarTrabajos() int

OlvidarTrabajos quita de la lista los que ya acabaron.

func (*Consola) Pintar

func (c *Consola) Pintar(texto string, estilos ...Estilo) string

Pintar envuelve el texto con esos estilos, o lo devuelve tal cual si no toca pintar.

c.Pintar("no se pudo", estentor.Rojo)
c.Pintar("listo", estentor.Verde, estentor.Negrita)

func (*Consola) Poner

func (c *Consola) Poner(clave, valor string)

Poner guarda una variable. Es lo que hace el comando "set", y lo que hay que usar desde un comando o desde una tarea de fondo: escribir en el campo Variables mientras la consola corre no lleva candado.

La clave "nombre" se guarda en el campo Nombre, para que cambiarla se note en el prompt.

func (*Consola) Procedencia

func (c *Consola) Procedencia(clave string) (valor, deNivel string, existe bool)

Procedencia dice cuánto vale una variable y de qué nivel sale. Es lo que distingue "lo puse yo aquí" de "lo heredé de fuera".

func (*Consola) Quitar

func (c *Consola) Quitar(clave string) bool

Quitar borra una variable y dice si existía. La clave "nombre" no se puede quitar: siempre hay un nombre, aunque esté vacío.

func (*Consola) Registrando

func (c *Consola) Registrando(w io.Writer) func()

Registrando duplica en w todo lo que pase por la consola: lo que se teclea y lo que se contesta, incluidos los avisos de las tareas de fondo. Devuelve la función que lo para, que se puede llamar las veces que haga falta.

f, err := os.OpenFile(ruta, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o600)
if err == nil {
	parar := c.Registrando(f)
	defer func() { parar(); f.Close() }()
}

**Llámala con la consola en marcha**, o sea desde un comando, igual que las tareas de fondo: es el mismo contrato de siempre (los campos se configuran antes de arrancar, lo que corre se lanza desde dentro). Antes de arrancar no hay por dónde derivar y no hace nada.

Va sin códigos de color: un fichero con secuencias de escape dentro no se puede leer.

Lo que se registra es una sesión real contra sistemas de alguien, así que el fichero tiene el mismo problema de custodia que el historial. El 0600 del ejemplo no es decorativo.

func (*Consola) Registrar

func (c *Consola) Registrar(cmd Comando)

Registrar añade un comando con sus sinónimos.

Si ya había un comando con ese nombre lo sustituye entero, sinónimos incluidos: un help propio sin "?" deja de responder a "?". El comando es dueño de sus nombres, así que reemplazarlo se los lleva todos.

func (*Consola) Tabla

func (c *Consola) Tabla(t *Tabla)

Tabla pinta una tabla en la salida de la consola. La tabla se construye aparte con NuevaTabla, así que sirve igual fuera de la consola.

func (*Consola) Trabajos

func (c *Consola) Trabajos() []Trabajo

Trabajos devuelve una copia del estado de todas las tareas.

func (*Consola) Valor

func (c *Consola) Valor(clave string) (string, bool)

Valor devuelve una variable. El segundo valor dice si existía.

func (*Consola) Volver

func (c *Consola) Volver() bool

Volver saca el nivel actual y tira sus variables. Devuelve false si ya estabas en el global, que no se puede sacar.

func (*Consola) VolverATodo

func (c *Consola) VolverATodo()

VolverATodo sale de todos los niveles de golpe.

type EstadoTrabajo

type EstadoTrabajo int

EstadoTrabajo es en qué anda un trabajo de fondo.

const (
	// Corriendo es que sigue trabajando.
	Corriendo EstadoTrabajo = iota
	// Terminado es que acabó sin error.
	Terminado
	// Fallado es que acabó devolviendo un error.
	Fallado
	// Cancelado es que se le pidió parar.
	Cancelado
)

func (EstadoTrabajo) String

func (e EstadoTrabajo) String() string

type Estilo

type Estilo string

Estilo es un código de color o de énfasis. Se combinan pasando varios.

const (
	Rojo     Estilo = "\033[31m"
	Verde    Estilo = "\033[32m"
	Amarillo Estilo = "\033[33m"
	Azul     Estilo = "\033[34m"
	Magenta  Estilo = "\033[35m"
	Cian     Estilo = "\033[36m"
	Gris     Estilo = "\033[90m"

	Negrita Estilo = "\033[1m"
	Tenue   Estilo = "\033[2m"
	Cursiva Estilo = "\033[3m"
)

Los estilos disponibles. Se combinan pasando varios a Pintar.

func FondoHex

func FondoHex(codigo string) Estilo

FondoHex es como Hex pero para el fondo de la celda.

func Hex

func Hex(codigo string) Estilo

Hex devuelve el estilo de un color de texto dado en hexadecimal. Acepta "#ff8800", "ff8800" y la forma corta "#f80". Si no se entiende, devuelve un estilo vacío, que no pinta nada.

t.Colorear("critico", estentor.Hex("#ff5555"))

type ModoColor

type ModoColor int

ModoColor decide cuándo se pinta con color.

const (
	// ColorAuto pinta si la salida es una terminal y el entorno no dice lo
	// contrario. Es el valor por defecto.
	ColorAuto ModoColor = iota

	// ColorSiempre pinta pase lo que pase. Sirve para volcar a un fichero que
	// luego se va a mirar con `less -R`.
	ColorSiempre

	// ColorNunca no pinta nunca.
	ColorNunca
)

type ModoPegado

type ModoPegado int

ModoPegado decide si se le pide a la terminal que avise de lo que se pega.

const (
	// PegadoAuto pide el aviso. Lo pegado entra como texto: ni los saltos de
	// línea ejecutan, ni el tabulador completa, ni un Ctrl-C interrumpe. Es el
	// valor por defecto.
	PegadoAuto ModoPegado = iota

	// PegadoNunca no lo pide, y pegar vuelve a ser teclear muy rápido: cada
	// salto de línea ejecuta lo que lleves escrito.
	//
	// Existe por una lección ajena. GNU readline trae el pegado entre corchetes
	// activado desde la 8.1, pero CPython y FRR tuvieron que desactivarlo
	// porque rompía a quien pegaba una configuración entera contando con que
	// los saltos la fueran ejecutando. Si alguien depende de eso, aquí lo
	// apaga en vez de quedarse sin salida.
	PegadoNunca
)

type Tabla

type Tabla struct {
	Cabeceras []string
	Filas     [][]string

	// Sangria son los espacios de la izquierda. Por defecto dos. Un valor
	// negativo cuenta como cero.
	Sangria int

	// Color decide si se aplican las reglas al pintar. Consola.Tabla lo pone
	// según la terminal; usando la tabla suelta hay que ponerlo a mano.
	//
	// Las reglas se guardan igual aunque esté apagado, así que una tabla se
	// puede preparar con colores y volcarse en limpio a un fichero.
	Color bool
	// contains filtered or unexported fields
}

Tabla son unas cabeceras y sus filas, que se pintan con las columnas alineadas. No depende de la consola: se puede usar en cualquier programa.

t := estentor.NuevaTabla("IP", "Estado")
t.Fila("10.20.0.41", "responde")
fmt.Println(t)

Índices

Una sola regla para todos los métodos que reciben un índice:

  • Negativo: no hace nada. Los de consulta dicen que no lo encontraron (false, o -1). Ninguno revienta y ninguno hace algo distinto de lo pedido.
  • Pasado del final: los que modifican estiran la tabla hasta llegar (Celda, CambiarFila) o añaden al final (InsertarFila); los de consulta dicen que no lo encontraron.

El tope de estirado es MaxCrecimiento.

func NuevaTabla

func NuevaTabla(cabeceras ...string) *Tabla

NuevaTabla crea una tabla con esas cabeceras.

func (*Tabla) BorrarFila

func (t *Tabla) BorrarFila(i int) bool

BorrarFila quita una fila. Devuelve false si esa fila no existía.

func (*Tabla) Buscar

func (t *Tabla) Buscar(columna int, valor string) int

Buscar devuelve el índice de la primera fila cuya columna valga eso, o -1 si no hay ninguna. El -1 es el mismo convenio que usa strings.Index.

func (*Tabla) BuscarSi

func (t *Tabla) BuscarSi(cumple func(fila []string) bool) int

BuscarSi devuelve el índice de la primera fila que cumpla la condición, o -1.

i := t.BuscarSi(func(fila []string) bool {
    return strings.HasPrefix(fila[0], "10.20.")
})

func (*Tabla) CambiarFila

func (t *Tabla) CambiarFila(i int, celdas ...string) *Tabla

CambiarFila sustituye una fila entera. Si no existe, la tabla crece hasta llegar. Los índices negativos y los que pasen de MaxCrecimiento se ignoran.

func (*Tabla) Celda

func (t *Tabla) Celda(fila, columna int, valor string) *Tabla

Celda cambia una celda. Si la posición no existe, la tabla crece hasta llegar: así se puede rellenar por coordenadas sin preparar nada antes. Los índices negativos y los que pasen de MaxCrecimiento se ignoran.

func (*Tabla) Colorear

func (t *Tabla) Colorear(valor string, estilos ...Estilo) *Tabla

Colorear pinta con esos estilos las celdas cuyo valor sea exactamente ese, en cualquier columna.

t.Colorear("responde", estentor.Verde)
t.Colorear("sin identificar", estentor.Rojo, estentor.Negrita)

func (*Tabla) ColorearCabeceras

func (t *Tabla) ColorearCabeceras(estilos ...Estilo) *Tabla

ColorearCabeceras pone estilo a la fila de títulos.

func (*Tabla) ColorearColumna

func (t *Tabla) ColorearColumna(columna int, valor string, estilos ...Estilo) *Tabla

ColorearColumna es igual que Colorear pero solo mira una columna. Sirve cuando el mismo texto significa cosas distintas según dónde salga.

func (*Tabla) ColorearColumnaEntera

func (t *Tabla) ColorearColumnaEntera(columna int, estilos ...Estilo) *Tabla

ColorearColumnaEntera pinta una columna entera, mire lo que mire.

func (*Tabla) ColorearFila

func (t *Tabla) ColorearFila(fila int, estilos ...Estilo) *Tabla

ColorearFila pinta una fila entera. El índice es el de la fila de datos: la primera es la 0, sin contar la cabecera.

func (*Tabla) ColorearSi

func (t *Tabla) ColorearSi(cumple func(fila, columna int, valor string) []Estilo) *Tabla

ColorearSi decide con una función, para lo que no sea un valor exacto: números por encima de un umbral, prefijos, expresiones regulares.

t.ColorearSi(func(_, columna int, valor string) []estentor.Estilo {
    if columna == 2 && strings.HasPrefix(valor, "10.20.") {
        return []estentor.Estilo{estentor.Cian}
    }
    return nil
})

Devolver nil deja pasar a la regla siguiente.

func (*Tabla) Escribir

func (t *Tabla) Escribir(w io.Writer) error

Escribir pinta la tabla en w.

func (*Tabla) Fila

func (t *Tabla) Fila(celdas ...string) *Tabla

Fila añade una fila y devuelve la tabla, para poder encadenar llamadas. Las filas con menos celdas de la cuenta se completan con huecos, y las celdas que sobran se ignoran.

func (*Tabla) Filtrar

func (t *Tabla) Filtrar(cumple func(fila []string) bool) *Tabla

Filtrar devuelve una tabla nueva con las mismas cabeceras y solo las filas que cumplan. No toca la original.

func (*Tabla) InsertarFila

func (t *Tabla) InsertarFila(i int, celdas ...string) *Tabla

InsertarFila mete una fila en la posición i y corre las demás. Si i se pasa del final, la añade al final. Un índice negativo no hace nada.

func (*Tabla) Limpiar

func (t *Tabla) Limpiar() *Tabla

Limpiar quita todas las filas y deja las cabeceras.

func (*Tabla) Obtener

func (t *Tabla) Obtener(fila, columna int) (string, bool)

Obtener devuelve una celda. El segundo valor dice si existía, para poder distinguir una celda vacía de una que no está.

func (*Tabla) Ordenar

func (t *Tabla) Ordenar(columna int) *Tabla

Ordenar ordena las filas por el texto de una columna, manteniendo el orden original entre las que empatan.

func (*Tabla) String

func (t *Tabla) String() string

String pinta la tabla. Al llamarse así, fmt la usa sola: basta con fmt.Println(t) o fmt.Printf("%v", t).

type Trabajo

type Trabajo struct {
	ID     int
	Nombre string
	Estado EstadoTrabajo
	Inicio time.Time
	Fin    time.Time
	Err    error
}

Trabajo es una copia del estado de una tarea de fondo. Es una copia y no un puntero a propósito: quien la recibe no puede tocarla mientras corre.

func (Trabajo) Duracion

func (t Trabajo) Duracion() time.Duration

Duracion es lo que lleva o lo que duró.

Directories

Path Synopsis
cmd
analizar command
Analiza el proyecto: formato, vet, linter y tests con cobertura.
Analiza el proyecto: formato, vet, linter y tests con cobertura.
ejemplo command
Programa de ejemplo de la biblioteca estentor.
Programa de ejemplo de la biblioteca estentor.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL