Esta semana tuve que automatizar la configuración de un servidor mediante un script de Bash. El proceso requería ejecutar otra herramienta con varios pasos interactivos y algunos comportamientos poco intuitivos.
El problema
El flujo era el siguiente: ejecutar un comando como root, pero usando el contexto de un usuario específico; presionar varias veces Enter hasta que apareciera un prompt; seleccionar una opción y aceptar el reinicio del agente. Después había que repetir el proceso con una opción distinta y esperar a que el agente volviera a iniciar.
Al principio tuve problemas porque la herramienta tenía comportamientos inesperados. Por ejemplo, era necesario presionar Enter varias veces antes de que mostrara la opción que necesitaba. Probé varios enfoques, empezando por uno basado en esperas y entradas simuladas.
Este fue uno de ellos:
#!/bin/bash
cd /dir/agent || exit 1
./set_agent_mode
# Simular varias pulsaciones de Enter
for ((i = 0; i < 5; i++)); do
printf '\n'
sleep 1
done
# Intentar detectar el prompt y seleccionar la opción 2
while true; do
if read -r -t 1 line && [[ "$line" == *"Choose the action you want to perform:"* ]]; then
printf '2\n'
break
fi
done
El problema era que este enfoque no sincronizaba las respuestas con el estado real del programa. A veces los saltos de línea se enviaban demasiado pronto, el proceso continuaba sin tomar la opción o el while no recibía la salida que esperaba. Era un error sistemático: estábamos intentando controlar una interfaz interactiva mediante tiempos fijos.
Después intenté simplificarlo y enviar todas las respuestas mediante un heredoc:
#!/bin/bash
cd /dir/agent || exit 1
./set_agent_mode <<EOF
username
2
y
EOF
En teoría, el heredoc enviaría las tres líneas por la entrada estándar. Sin embargo, solo funcionó la primera respuesta. Este mecanismo únicamente es útil cuando el programa consume las entradas en ese orden y no necesita esperar a que aparezca cada prompt.
Después hice otra prueba usando printf como pipe:
#!/bin/bash
cd /dir/agent || exit 1
printf '%s\n' '' 'username' '2' 'y' | ./set_agent_mode
La prueba tampoco fue suficiente. Además, la presión aumentaba porque se había programado para la semana siguiente un release que incluía esta automatización. Mi manager empezaba a señalar que quizá estaba haciendo overengineering y probando alternativas fuera del alcance original, pero el problema seguía sin resolverse.
La frustración
Me sugirieron revisarlo con el equipo. Envié un mensaje con la descripción de los pasos y un diagrama para mostrar la complejidad del flujo. Explicar un problema a otra persona suele ser difícil cuando llevas horas intentando resolverlo, pero la conversación ayudó a hacer visibles algunos detalles que estaba pasando por alto.
Al día siguiente consideré probar con Go. Sin embargo, eso implicaba hacer pruebas en mi máquina, compilar un binario y trasladarlo al servidor, porque por motivos de compliance no podía instalar Go allí. Finalmente descarté esa opción, aunque este fue uno de los experimentos:
package main
import (
"bufio"
"fmt"
"os"
"os/exec"
"strings"
"time"
)
func main() {
// Define the command
cmd := exec.Command("your_command")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
stdin, err := cmd.StdinPipe()
if err != nil {
fmt.Println("Error creating stdin pipe:", err)
return
}
// Start the command
err = cmd.Start()
if err != nil {
fmt.Println("Error starting the command:", err)
return
}
// Simulate pressing Enter until prompt selection is displayed
simulateEnters(stdin)
// Select an option by pressing 1 or 2
selectOption(stdin, "1") // Replace with your desired option
// Wait for the command to finish
err = cmd.Wait()
if err != nil {
fmt.Println("Error waiting for the command:", err)
}
}
func simulateEnters(stdinPipe io.WriteCloser) {
for i := 0; i < 5; i++ {
fmt.Fprintln(stdinPipe, "")
time.Sleep(1 * time.Second) // Adjust sleep duration based on your application's response time
}
}
func selectOption(stdinPipe io.WriteCloser, option string) {
fmt.Fprintln(stdinPipe, option)
time.Sleep(1 * time.Second) // Adjust sleep duration based on your application's response time
}
Mientras trabajaba en esto también tuve varias reuniones: una sobre agregar un Docker build para una migración de Node.js 14 a Node.js 18 con TypeScript, y otra para revisar una herramienta de línea de comandos que estamos desarrollando internamente. En esta última reunión revisamos rápidamente mi problema. Una persona del equipo propuso una idea que no era la solución completa, pero sí apuntaba en la dirección correcta.
La idea
Me comentó que probablemente se trataba de un problema de la herramienta que había desarrollado el otro equipo y que podía revisar su código para buscar algún flag, igual que en la CLI que estábamos construyendo. Así que abrí el archivo:
vi set_agent_mode
El archivo era un script de Bash de más de 800 líneas. Al principio parecía un monstruo, pero siempre se me ha dado bien leer código, así que continué revisándolo. Finalmente encontré tres flags:
-u= usuario-o= opción-h= ayuda
Fue revelador porque la documentación que me habían entregado no mencionaba esas opciones. Probé primero con el usuario y la opción:
./set_agent_mode -u user -o 2
El comando no funcionó debido a un error relacionado con el flag -u. Podía intentar corregirlo, pero eso habría requerido más tiempo.
La siguiente prueba utilizó únicamente -o y envió el usuario mediante la entrada estándar:
echo "user" | ./set_agent_mode -o 2
Esta vez el usuario se introdujo correctamente y la herramienta seleccionó la opción esperada. El único paso pendiente era confirmar el reinicio: la respuesta se ignoraba porque todavía no existía una forma no interactiva de proporcionarla.
La solución
Entonces agregué un nuevo flag, -r, para indicar si el agente debía reiniciarse. Después incorporé ese valor a la validación que comprobaba si el usuario había respondido Y o N:
#!/bin/bash
user=""
restart=""
option=""
while getopts "u:o:r:h" flag; do
case "$flag" in
u) user="$OPTARG" ;;
o) option="$OPTARG" ;;
r) restart="$OPTARG" ;;
h) Usage; exit 0 ;;
*) Usage; exit 1 ;;
esac
done
printf 'Do you want to restart? [Y/N]\n'
read -r answer
if [[ "$answer" =~ ^[Yy]$ || "$restart" =~ ^[Yy]$ ]]; then
restart_agent_command
fi
Con ese cambio pude ejecutar todo el flujo de forma no interactiva:
echo "user" | ./set_agent_mode -o 2 -r y
Después hubo una discusión con mi jefe sobre si debíamos modificar ese archivo, pero esa ya es otra historia.
Conclusión
Resolver este problema fue complicado porque ya había probado varios enfoques sin éxito. Además, el otro equipo no sabía que era necesario introducir tantos saltos de línea: ese comportamiento se había convertido en un workaround durante un par de años.
Los primeros intentos con Bash y Go dependían de tiempos fijos y entradas predefinidas, pero no se sincronizaban de forma confiable con los prompts del programa. La solución apareció al revisar directamente el código de la herramienta y descubrir que ya existían flags para indicar la opción y controlar el reinicio.
Esta experiencia también me recordó la importancia de pedir una segunda opinión. Cuando estamos demasiado concentrados en un problema, la ceguera técnica puede impedirnos ver una alternativa sencilla que alguien más identifica rápidamente.
Comments