Inicio Apuntes FPApuntes DAMTermina-L-ndo, una shell básica en C

Termina-L-ndo, una shell básica en C

Publicado por
about termina-l-ndo

Una vez más aquí. Hace un tiempo ya había realizado un curso sobre C y Python en el que se había quedado en el tintero la idea de escribir una pequeña shell en C para practicar ciertos conceptos. Como he vuelto a otro curso, me puse a recuperar la idea. Esto es algo que hice en los últimos tres días antes de solicitar la exención de la parte teórica; de ahí nació el nombre de esta shell: Termina-L-ndo.

Aquellos que me conocen saben que soy de esa clase de gente que, siempre que puede, utiliza la terminal para todo, y que al mismo tiempo, también me pregunté toda la vida qué ocurre realmente cuando escribes ls | grep .c y pulsas Enter. Detrás de esa línea hay un intérprete de comandos que lee texto, lo analiza, crea procesos y conecta tuberías.

Termina-L-ndo es una shell en C minimalista pensada para aprender y practicar esos mecanismos sin la complejidad de bash o zsh: todo en menos de mil líneas de código, comentadas en español (creo que hay más comentarios que código real), que cubren desde fork y exec hasta historial con flechas y autocompletado con TAB. Vamos, lo que vienen siendo las cuatro funcionalidades básicas de todo… teniendo en cuenta que solo tuve 15 horas de aula y alguna extra que le dediqué en casa con un manual viejuno que tenía aparcado en una estantería, me ha gustado el resultado final.

En las siguientes líneas vamos a ver cómo funciona todo esto (hace ya algo más de una semana que no veo el código, pero así también sabré si me entero con mi propios comentarios), qué módulos del código son imprescindibles y cómo compilar y ejecutar el proyecto en Linux.

¿Qué es Termina-L-ndo y para qué sirve?

Como decía líneas atrás, Termina-L-ndo es una shell interactiva escrita en C11 para sistemas POSIX (Linux). Este pequeño shell no pretende sustituir a bash ni ser usado para nada más que practicar y servir como laboratorio: un espacio para ver en código real cómo un programa se convierte en un intérprete de comandos.

✨ Características principales

  • ⚙️ Comandos externos: Ejecución en paralelo mediante las llamadas del sistema fork + execvp.
  • 📂 Comandos internos (Builtins): Soporte nativo para órdenes que deben afectar al propio proceso de la shell, como cd, pwd, export, echo, history, help, etc.
  • 🔀 Operadores de flujo: Tuberías (|), redirecciones de entrada/salida (<, >, >>) y ejecución en segundo plano (&).
  • 💲 Dinámica de variables: Gestión de variables de entorno ($VAR, $?), sustitución de comandos ($(pwd)) y expansión de comodines (*.c).
  • ⌨️ UX de terminal: Historial en memoria con navegación mediante flechas ↑↓, autocompletado inteligente con TAB y un prompt dinámico coloreado con la ruta actual.

🛠️ Cómo compilar y ejecutar Termina-L-ndo

Los únicos requisitos son disponer del compilador GCC (o cualquiera compatible con C11) y un entorno Linux con las llamadas al sistema POSIX habituales.

  1. Descarga el proyecto: Tienes el código fuente disponible en el repositorio de desarrollo:
  2. Compilación y arranque: En la carpeta raíz del proyecto, ejecuta en tu terminal:
make
./termina-L-ndo

Nota: También puedes usar make run (compila si hace falta y lanza el shell directamente) o make clean para borrar los archivos objeto y el ejecutable generado.

Al arrancar, verás un prompt con la ruta actual en color cian y el sufijo $>. Este sufijo cambiará de color: verde si el último comando tuvo éxito o rojo si hubo un error. Escribe help para ver la ayuda integrada o exit para salir.

shel termina-l-ndo

🧠 El corazón del shell: el bucle REPL

Toda shell interactiva sigue un patrón REPL (Read–Eval–Print Loop): leer una línea, evaluarla, imprimir el resultado y volver a mostrar el prompt. En el archivo main.c ese bucle es explícito y sigue un orden estricto:

  1. Leer con shell_read_line(&history) (configura la terminal en modo raw si detecta un entorno interactivo).
  2. Guardar la entrada en el historial con history_add().
  3. Expandir sustituciones del tipo $(...) y comillas invertidas.
  4. Parsear la línea de texto convirtiéndola en un Pipeline de comandos estructurado.
  5. Expandir los globs (*.c, etc.) antes de ejecutar.
  6. Ejecutar la orden, discriminando si es un comando interno (builtin) o un pipeline externo.
  7. Actualizar el código de salida ($?) y refrescar el color del prompt.

⚠️ El orden importa: La sustitución de comandos ocurre antes del parseo, pero el globbing ocurre después. Esto es así porque la expansión de asteriscos la debe resolver la shell en su contexto, no el programa hijo que va a ser ejecutado.

📂 Anatomía del código: Partes importantes

El proyecto está modularizado para mantener un código compacto y fácil de leer

  • parser.c — De texto a estructura ejecutable: Convierte una cadena plana como ls -la | grep .c > out.txt en estructuras limpias de datos (Command y Pipeline). Separa tokens respetando comillas simples y dobles, detecta operadores (|, <, >, >>, &) y expande las variables $VAR en los argumentos. Sin un parser sólido, la shell no sabría qué está intentando hacer el usuario.
  • executor.c — Fork, exec, pipes y señales: Aquí es donde se concentra gran parte del trabajo relacionado con procesos y comunicación entre ellos. Crea tuberías (pipe) entre procesos consecutivos, invoca a fork() para bifurcar el proceso, redirige los descriptores standard (stdin/stdout) en el hijo y ejecuta execvp(). El proceso padre gestiona el cierre de descriptores sobrantes y espera con waitpid(). También captura SIGINT (Ctrl+C) para evitar que la shell se muera si cancelas un proceso hijo.
  • builtins.c — Comandos sin nuevo proceso: Órdenes como cd o export no pueden delegarse en un hijo, deben ejecutarse en el proceso de la propia shell para poder alterar su directorio de trabajo o sus variables de entorno. is_builtin() comprueba el comando y run_builtin() lo despacha directamente sin hacer fork.
  • expand.c — Más allá del lanzador: Dos funciones elevan este software de un simple «lanzador» a una herramienta útil. La sustitución $(comando) ejecuta un subproceso, captura su salida estándar mediante un pipe e inyecta el texto resultante en la línea. El Globbing aprovecha la librería glob.h para expandir patrones como src/*.c en múltiples argumentos antes del execvp.
  • input.c, history.c y complete.c — Experiencia de terminal: input.c pone la terminal en modo raw modificando la estructura termios. Así podemos pintar el prompt, bloquear el borrado del prefijo y capturar las secuencias de las flechas. history.c implementa una cola circular de 50 líneas y complete.c intercepta el TAB para sugerir comandos internos, archivos de la ruta actual o ejecutables indexados en el PATH.
  • utils.c y colors.c — Abstracción y estética: shell_print_prompt() usa getcwd y maquilla la ruta sustituyendo el prefijo del $HOME por el caracter ~. shell_set_status() almacena el estado de salida para alimentar a $? y cambiar el color del prompt. colors.c centraliza las secuencias de escape ANSI respetando el estándar moderno NO_COLOR.

💻 Ejemplos prácticos dentro de Termina-L-ndo

Una vez que tengas la shell compilada y abierta, puedes probar este flujo secuencial para ver cómo interactúan los módulos:

# 1. Consulta la ayuda interna
help

# 2. Revisa tu ubicación e inyéctala en una cadena expandida
pwd
echo "Directorio actual de trabajo: $(pwd)"

# 3. Comprueba el funcionamiento del globbing
ls src/*.c

# 4. Pon a prueba el detector de errores del prompt
false
echo $?   # Mostrará '1' y verás cómo el prompt pasa a color rojo

# 5. Navega por tu sesión
history
exit

💡 Truco: Prueba a pulsar las flechas del teclado para recuperar órdenes anteriores o escribe ec + TAB para ver cómo se autocompleta la palabra echo.

🎯 Conclusión: ¿Por qué programar tu propia shell?

Escribir una shell minimalista es uno de esos proyectos pendientes que todo desarrollador apasionado por los sistemas debería experimentar. Te obliga a tocar de golpe los pilares fundamentales de la ingeniería de software y los sistemas operativos: el ciclo de vida de los procesos, el aislamiento de descriptores de archivos, la sincronización mediante tuberías, el control de señales y el diseño de interfaces de usuario en modo texto. … y como comenté en líneas anteriores, esto es algo que tenía pendiente … y ya puedo hacer check sobre ello.

Termina-L-ndo concentra toda esa complejidad en un entorno compacto, legible y estructurado. Modificar un módulo (por ejemplo, programar tu propio comando interno) y ver el resultado de inmediato tras un make run es una forma muy didáctica de aprender.

Si te atrae el desarrollo bajo entornos Linux y el estándar POSIX, te invito a clonar el repositorio, echarle un ojo a main.c y executor.c, y trastear con el código. Al fin y al cabo, es la mejor forma de entender qué pasa realmente en tu sistema cada vez que abres una terminal.

También te puede interesar ...

Deja un comentario

* Al utilizar este formulario, aceptas que este sitio web almacene y maneje tus datos.

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Adblock Detectado!!

Ayúdanos deshabilitando la extensión AdBlocker de tu navegador para visitar esta web.
Si no sabes hacerlo en Chrome, consulta el siguiente enlace. Si utilizas Firefox, puedes consultar este otro enlace.
Esto mejorará tu experiencia en este sitio web.