Escribiendo terminal útil TUI en Linux con diálogo y jq

¿Por qué una interfaz de usuario de texto?

Muchos utilizan la terminal a diario. Una interfaz de usuario de texto (TUI) es una herramienta que minimizará los errores del usuario y le permitirá ser más productivo con la interfaz del terminal.

Permítanme darles un ejemplo: me conecto diariamente desde la computadora de mi casa a mi PC física, usando Linux. Todas las redes remotas están protegidas mediante un privadoVPN. Después de un tiempo, resultaba irritante repetir los mismos comandos una y otra vez al conectarse.

Tener una función bash como esta fue una buena mejora:

export REMOTE_RDP_USER="myremoteuser"
function remote_machine() {
/usr/bin/xfreerdp /cert-ignore /sound:sys:alsa /f /u:$REMOTE_RDP_USER /v:$1 /p:$2
}

Pero constantemente hacía esto (en una sola línea):

remote_pass=(/bin/cat/.mypassfile) remote_machine $remote_machine $remote_pass

Eso fue molesto. Sin mencionar que tenía mi contraseña en texto claro en mi máquina (tengo una unidad cifrada pero aún así…)

Así que decidí dedicar un poco de tiempo y se me ocurrió un buen guión para satisfacer mis necesidades básicas.

¿Qué información necesito para conectarme a mi escritorio remoto?

No se necesita mucha información. Sólo necesita estar estructurado para que un simple archivo JSON sirva:

"machines": [
{
"name": "machine1.domain.com",
"description": "Personal-PC"
},
{
"name": "machine2.domain.com",
"description": "Virtual-Machine"
}
],
"remote_user": "MYUSER@DOMAIN",
"title" : "MY COMPANY RDP connection"
}

JSON no es el mejor formato para archivos de configuración (ya que no admite comentarios, por ejemplo) pero tiene muchas herramientas disponibles en Linux para analizar su contenido desde la línea de comandos. Una herramienta muy útil que se destaca esjq. Déjame mostrarte cómo puedo extraer la lista de máquinas:

/usr/bin/jq --compact-output --raw-output '.machines[]| .name' 
$HOME/.config/scripts/kodegeek_rdp.json) 
"machine1.domain.com" "machine2.domain.com"

La documentación para jq está disponible.aquí. Puedes probar tus expresiones enjq jugarsimplemente copiando y pegando sus archivos JSON allí y luego use la expresión en sus scripts.

Ahora que tengo todos los ingredientes que necesito para conectarme a mi computadora remota, creemos una TUI agradable para ello.

Diálogo al rescate

Diálogoes una de esas herramientas de Linux subestimadas que desearías conocer hace mucho tiempo. Puedes crear una interfaz de usuario muy bonita y sencilla que funcionará perfectamente en tu terminal.

Por ejemplo, para crear una lista de casillas de verificación simple con mis idiomas favoritos, seleccionando Python de forma predeterminada:

dialog --clear --checklist "Favorite programming languages:" 10 30 7 1  
Python on 2 Java off 3 Bash off 4 Perl off 5 Ruby off

Le dijimos al diálogo algunas cosas:

  • Limpiar la pantalla (todas las opciones comienzan con –)
  • Crea una lista de verificación con título (primer argumento posicional)
  • Una lista de dimensiones (alto, ancho, alto de la lista, 3 elementos)
  • Entonces cada elemento de la lista es un par de Etiqueta y valor.

Sorprendentemente es muy conciso e intuitivo obtener una bonita lista de selección con una sola línea de código.

La documentación completa para el diálogo está disponible.aquí.

Juntando todo: escribiendo una TUI con Dialog y JQ

Escribí una TUI que usa jq para extraer los detalles de mi configuración de mi archivo JSON y organicé el flujo con diálogo. Solicito la contraseña cada vez y la guardo en un archivo temporal que se elimina una vez que el script termina de usarlo.

El script es bastante básico pero es más seguro y también me permite concentrarme en tareas más serias 🙂

Entonces, ¿qué hace elguion¿parece? Déjame mostrarte el código:

#!/bin/bash
# Author Jose Vicente Nunez
# Do not use this script on a public computer. It is not secure...
# https://invisible-island.net/dialog/
# Below some constants to make it easier to handle Dialog 
# return codes
: ${DIALOG_OK=0}
: ${DIALOG_CANCEL=1}
: ${DIALOG_HELP=2}
: ${DIALOG_EXTRA=3}
: ${DIALOG_ITEM_HELP=4}
: ${DIALOG_ESC=255}
# Temporary file to store sensitive data. Use a 'trap' to remove 
# at the end of the script or if it gets interrupted
declare tmp_file=$(/usr/bin/mktemp 2>/dev/null) || declare tmp_file=/tmp/test$$
trap "/bin/rm -f $tmp_file" QUIT EXIT INT
/bin/chmod go-wrx ${tmp_file} > /dev/null 2>&1
:<<DOC
Extract details like title, remote user and machines using jq from the JSON file
Use a subshell for the machine list
DOC
declare TITLE=$(/usr/bin/jq --compact-output --raw-output '.title' $HOME/.config/scripts/kodegeek_rdp.json)|| exit 100
declare REMOTE_USER=$(/usr/bin/jq --compact-output --raw-output '.remote_user' $HOME/.config/scripts/kodegeek_rdp.json)|| exit 100
declare MACHINES=$(
    declare tmp_file2=$(/usr/bin/mktemp 2>/dev/null) || declare tmp_file2=/tmp/test$$
    # trap "/bin/rm -f $tmp_file2" 0 1 2 5 15 EXIT INT
    declare -a MACHINE_INFO=$(/usr/bin/jq --compact-output --raw-output '.machines[]| join(",")' $HOME/.config/scripts/kodegeek_rdp.json > $tmp_file2)
    declare -i i=0
    while read line; do
        declare machine=$(echo $line| /usr/bin/cut -d',' -f1)
        declare desc=$(echo $line| /usr/bin/cut -d',' -f2)
        declare toggle=off
        if [ $i -eq 0 ]; then
            toggle=on
            ((i=i+1))
        fi
        echo $machine $desc $toggle
    done < $tmp_file2
    /bin/cp /dev/null $tmp_file2
) || exit 100
# Create a dialog with a radio list and let the user select the
# remote machine
/usr/bin/dialog 
    --clear 
    --title "$TITLE" 
    --radiolist "Which machine do you want to use?" 20 61 2 
    $MACHINES 2> ${tmp_file}
return_value=$?
# Handle the return codes from the machine selection in the 
# previous step
export remote_machine=""
case $return_value in
  $DIALOG_OK)
    export remote_machine=$(/bin/cat ${tmp_file})
    ;;
  $DIALOG_CANCEL)
    echo "Cancel pressed.";;
  $DIALOG_HELP)
    echo "Help pressed.";;
  $DIALOG_EXTRA)
    echo "Extra button pressed.";;
  $DIALOG_ITEM_HELP)
    echo "Item-help button pressed.";;
  $DIALOG_ESC)
    if test -s $tmp_file ; then
      /bin/rm -f $tmp_file
    else
      echo "ESC pressed."
    fi
    ;;
esac

# No machine selected? No service ...
if [ -z "${remote_machine}" ]; then
  /usr/bin/dialog 
  	--clear  
	--title "Error, no machine selected?" --clear "$@" 
       	--msgbox "No machine was selected!. Will exit now..." 15 30
  exit 100
fi

# Send 4 packets to the remote machine. I assume your network 
# administration allows ICMP packets
# If there is an error show  message box
/bin/ping -c 4 ${remote_machine} >/dev/null 2>&1
if [ $? -ne 0 ]; then
  /usr/bin/dialog 
  	--clear  
	--title "VPN issues or machine is off?" --clear "$@" 
       	--msgbox "Could not ping ${remote_machine}. Time to troubleshoot..." 15 50
  exit 100
fi

# Remote machine is visible, ask for credentials and handle user 
# choices (like password with a password box)
/bin/rm -f ${tmp_file}
/usr/bin/dialog 
  --title "$TITLE" 
  --clear  
  --insecure 
  --passwordbox "Please enter your Windows password for ${remote_machine}n" 16 51 2> $tmp_file
return_value=$?
case $return_value in
  $DIALOG_OK)
    # We have all the information, try to connect using RDP protocol
    /usr/bin/mkdir -p -v $HOME/logs
    /usr/bin/xfreerdp /cert-ignore /sound:sys:alsa /f /u:$REMOTE_USER /v:${remote_machine} /p:$(/bin/cat ${tmp_file})| 
    /usr/bin/tee $HOME/logs/$(/usr/bin/basename $0)-$remote_machine.log
    ;;
  $DIALOG_CANCEL)
    echo "Cancel pressed.";;
  $DIALOG_HELP)
    echo "Help pressed.";;
  $DIALOG_EXTRA)
    echo "Extra button pressed.";;
  $DIALOG_ITEM_HELP)
    echo "Item-help button pressed.";;
  $DIALOG_ESC)
    if test -s $tmp_file ; then
      /bin/rm -f $tmp_file
    else
      echo "ESC pressed."
    fi
    ;;
esac

Puede ver en el código que el diálogo espera argumentos posicionales y también le permite capturar las respuestas del usuario en variables. Esto efectivamente lo convierte en una extensión de Bash para escribir interfaces de usuario de texto.

Mi pequeño ejemplo anterior sólo cubre el uso de algunos de los widgets, hay mucha más documentación sobre elsitio de diálogo oficial.

¿Son el diálogo y JQ las mejores opciones?

Puedes despellejar este conejo de muchas maneras (Textual, gnomoCenit, pitónTKinker, etc.) Solo quería mostrarles una buena manera de lograr esto en poco tiempo, con solo 100 líneas de código.

No es perfecto. Específicamente, la integración con Bash hace que el código sea muy detallado, pero aún así es fácil de depurar y mantener. Esto también es mucho mejor que copiar y pegar el mismo comando largo una y otra vez.

Una última cosa: si te gustó jq para el procesamiento JSON de Bash, lo apreciarásbuena colección de recetas jq.

Artículos Relacionados