Lab shell
El propósito de este lab es el de desarrollar la funcionalidad mínima que caracteriza a un intérprete de comandos shell similar a lo que realizan bash, zsh, fish.
La implementación debe realizarse en C11 y POSIX.1-2008. (Estas siglas hacen referencia a la versión del lenguaje C utilizada y del estándar de syscalls Unix empleado. Las versiones modernas de GCC y Linux cumplen con ambos requerimientos.)
A efectos de lo explicado en la página de entregas, el esqueleto para este lab se encuentra en el repositorio https://github.com/fisop/labs, rama shell (la cual no necesita ninguna integración previa).
IMPORTANTE: leer el archivo README.md que se encuentra en la raíz del proyecto. Contiene información sobre cómo realizar la compilación de los archivos, y cómo ejecutar el formateo de código.
REQUERIDO: para las entregas es condición necesaria que el check del formato de código esté en verde a la hora de realizar el PR (pull request). Para ello, se puede ejecutar make format localmente, comitear y subir esos cambios.
Índice
- Esqueleto
- Parte 1: Invocación de comandos
- Parte 2: Redirecciones
- Parte 3: Variables de entorno
- Parte 4: Extras
- Desafíos
Esqueleto
Para que no tengan que implementar todo desde cero, se provee un esqueleto de shell. Éste tiene gran parte del parseo hecho, y está estructurado indicando con comentarios los lugares en donde deben introducir el código crítico de cada punto.
Se recomienda antes de empezar leer el código para entender bien cómo funciona, y qué hace cada una de las funciones. Particularmente recomendamos entender qué significa cada uno de los campos en los structs de types.h.
Depurando con printf
Es importante mencionar que es requisito usar las funciones printf_debug y fprintf_debug si se desea mostrar información por pantalla; o bien encapsular todo lo que se imprima por stdout o stderr utilizando la macro SHELL_NO_INTERACTIVE (como ejemplo, ver las funciones definidas en utils.c).
Esto es debido a que al momento de corregir es mucho más fácil ejecutar una shell en modo no interactivo (que no imprima prompt) y así poder comparar el output de forma automática.
Cualquier mensaje que se imprima por pantalla al momento de hacer la entrega tiene que hacerse con las funciones printf_debug (en lugar de printf) o bien encapsulando el código con la directiva del preprocesador #ifndef SHELL_NO_INTERACTIVE.
Parte 1: Invocación de comandos
Búsqueda en $PATH
Los comandos que usualmente se utilizan, como los realizados en el lab anterior, están guardados (sus binarios), en el directorio /bin. Por este motivo existe una variable de entorno llamada $PATH, en la cual se declaran las rutas más usualmente accedidas por el sistema operativo (ejecutar: echo $PATH para ver la totalidad de las rutas almacenadas). Se pide agregar la funcionalidad de poder invocar comandos, cuyos binarios se encuentren en las rutas especificadas en la variable $PATH.
1
2
$ uptime
05:45:25 up 5 days, 12:02, 5 users, load average: ...
- Implementar: Ejecución de programas.
- Responder: ¿cuáles son las diferencias entre la syscall
execve(2)y la familia de wrappers proporcionados por la librería estándar de C (libc)exec(3)? - Responder: ¿Puede la llamada a
exec(3)fallar? ¿Cómo se comporta la implementación de la shell en ese caso?
Argumentos del programa
En esta parte del lab, vamos a incorporar a la invocación de comandos, la funcionalidad de poder pasarle argumentos al momento de querer ejecutarlos. Los argumentos pasados al programa de esta forma, se guardan en la famosa variable char* argv[], junto con cuántos fueron en int argc, declaradas en el main de cualquier programa en C.
1
2
3
$ df -H /tmp
Filesystem Size Used Avail Use% Mounted on
tmpfs 8.3G 2.0M 8.3G 1% /tmp
- Implementar: Ejecución de programas con argumentos (
argv[]).
Función sugerida: execvp(3)
Archivo: exec_cmd() en exec.c
Procesos en segundo plano
Los procesos en segundo plano o procesos en el fondo, o background, son muy útiles a la hora de ejecutar comandos que no queremos esperar a que terminen para que la shell nos devuelva el prompt nuevamente. Por ejemplo si queremos ver algún documento .pdf o una imagen y queremos seguir trabajando en la terminal sin tener que abrir una nueva.
1
2
3
4
5
$ evince file.pdf &
[PID=2489]
$ ls /home
patricio
Sólo se pide la implementación de un proceso en segundo plano. No es necesario que se notifique de la terminación del mismo por medio de mensajes en la shell.
Sin embargo, la shell deberá esperar oportunamente a los procesos en segundo plano. Esto puede hacerse sincrónicamente antes de mostrar cada prompt, pero el objetivo es que en una ejecución normal no se dejen procesos huérfanos.
- Implementar: Procesos en segundo plano. Sin notificación de procesos terminados, pero esperando oportunísticamente con cada prompt a los procesos en segundo plano.
- Responder: Detallar cuál es el mecanismo utilizado para implementar procesos en segundo plano.
Ayuda: Leer el funcionamiento del flag WNOHANG de la syscall wait(2)
Resumen
Al finalizar la parte 1 la shell debe poder:
- Invocar programas y permitir pasarles argumentos.
- Esperar correctamente a la ejecución de los programas.
- Ejecutar procesos en segundo plano.
- Esperar oportunísticamente a los procesos en segundo plano antes de cada prompt.
Parte 2: Redirecciones
Flujo estándar
La redirección del flujo estándar es una de las cualidades más interesantes y valiosas de una shell moderna. Permite, entre otras cosas, almacenar la salida de un programa en un archivo de texto para luego poder analizarla, como así también ejecutar un programa y enviarle un archivo a su entrada estándar. Existen, básicamente tres formas de redirección del flujo estándar:
-
Entrada y Salida estándares a archivos (
<in.txt>out.txt)Son los operadores clásicos del manejo de la redirección del stdin y el stdout en archivos de entrada y salida respectivamente. Por ejemplo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
$ ls /usr bin etc games include lib local sbin share src $ ls /usr >out1.txt $ cat out1.txt bin etc games include lib local sbin share src $ wc -w <out1.txt 10 $ ls -C /sys /noexiste >out2.txt ls: cannot access '/noexiste': No such file or directory $ cat out2.txt /sys: block class devices fs kernel module power $ wc -w <out2.txt 8
Se puede ver cómo queda implícito que cuando se utiliza el operador > se refiere al stdout y cuando se utiliza el < se refiere al stdin.
-
Error estándar a archivo (
2>err.txt)Es una de las dos formas de redireccionar el flujo estándar de error análogo al caso anterior del flujo de salida estándar en un archivo de texto. Por ejemplo:
1 2 3 4 5 6 7 8
$ ls -C /home /noexiste >out.txt 2>err.txt $ cat out.txt /home: patricio $ cat err.txt ls: cannot access '/noexiste': No such file or directory
Como se puede observar,
lsno informa ningún error al finalizar, como sí lo hacía en el ejemplo anterior. Su salida estándar de error ha sido redireccionada al archivo err.txt -
Combinar salida y errores (
2>&1)Es la segunda forma de redireccionar el flujo estándar producido por errores en la ejecución de un programa. Su funcionamiento se puede observar a través del siguiente ejemplo:
1 2 3 4
$ ls -C /home /noexiste >out.txt 2>&1 $ cat out.txt ---????---
Existen más tipos de redirecciones que nuestra shell no soportará (e.g. >> o &>)
- Implementar: Al menos, soporte para cada una de las tres formas de redirección descritas arriba:
>,<,2>y2>&1. - Responder: Investigar el significado de
2>&1, explicar cómo funciona su forma general y mostrar qué sucede con la salida decat out.txten el ejemplo. Luego repetirlo invertiendo el orden de las redirecciones. ¿Cambió algo?
Ayuda: Pueden valerse de las páginas del manual de bash: man bash.
Syscalls sugeridas: dup2(2), open(2)
Archivo: open_redir_fd() en exec.c y usarla en exec_cmd()
Tuberías simples (pipes)
Al igual que la redirección del flujo estándar hacia archivos, es igual o más importante, la redirección hacia otros programas. La forma de hacer esto en una shell es mediante el operador | (pipe o tubería). De esta forma se pueden concatenar dos o más programas para que la salida estándar de uno se redirija a la entrada estándar del siguiente. Por ejemplo:
1
2
$ ls -l | grep Doc
drwxr-xr-x 7 patricio patricio 4096 mar 26 01:20 Documentos
- Implementar: Soporte para pipes entre dos comandos.
- La shell debe esperar a que ambos procesos terminen antes de devolver el prompt:
echo hi | sleep 5ysleep 5 | echo hiambos deben esperar 5 segundos. - Los procesos de cada lado del pipe no deben quedar con fds de más.
- Los procesos deben ser lanzados en simultáneo.
- La shell debe esperar a que ambos procesos terminen antes de devolver el prompt:
Syscalls sugeridas: pipe(2), dup2(2)
Archivo: exec_cmd() en exec.c
Tuberías múltiples
Extender el funcionamiento de la shell para que se puedan ejecutar n comandos concatenados.
1
2
$ ls -l | grep Doc | wc
1 9 64
- Implementar: Soporte para múltiples pipes anidados.
- Responder: Investigar qué ocurre con el exit code reportado por la shell si se ejecuta un pipe ¿Cambia en algo? ¿Qué ocurre si, en un pipe, alguno de los comandos falla? Mostrar evidencia (e.g. salidas de terminal) de este comportamiento usando
bash. Comparar con la implementación del este lab.
Hint: Las modificaciones necesarias sólo atañen a la función parse_line() en parsing.c
Resumen
Al finalizar la parte 2 la shell debe poder:
- Redireccionar la entrada y salida estándar de los programas vía
<,>y2>.- Además se soporta específicamente la redirección de tipo
2>&1
- Además se soporta específicamente la redirección de tipo
- Concatenar la ejecución de dos o más programas mediante pipes
Parte 3: Variables de entorno
Expansión de variables
Una característica de cualquier intérprete de comandos shell es la de expandir variables de entorno (ejecutar: env para ver una lista completa de las varibles de entorno definidas), como PATH, o HOME.
1
2
$ echo $TERM
xterm-16color
Las variables de entorno se indican con el caracter $ antes del nombre, y la shell se encarga de reemplazar en la línea leída todos los tokens que comiencen por $ por los valores correspondientes del entorno. Esto ocurre antes de que el proceso sea ejecutado.
- Implementar: Expansión de variables al ejecutar un comando. Se debe reemplazar las variables que no existan con una cadena vacía (
"").
Las varibles vacías y las variables no setteadas no deben traducirse a
argumentos en el exec. Por ejemplo echo hola $VARIABLE_VACIA mundo es
equivalente a echo "hola" "mundo", dos argumentos solamente.
Función sugerida: getenv(3)
Archivos: expand_environ_var() y parse_exec() en parsing.c,
Variables de entorno temporarias
En esta parte se va a extender la funcionalidad de la shell para que soporte el poder incorporar nuevas variables de entorno a la ejecución de un programa. Cualquier programa que hagamos en C, por ejemplo, tiene acceso a todas las variables de entorno definidas mediante la variable externa environ (extern char** environ).1
Se pide, entonces, la posibilidad de incorporar de forma dinámica nuevas variables, por ejemplo:
1
2
3
4
5
6
$ /usr/bin/env
--- todas las variables de entorno definidas hasta el momento ---
$ USER=nadie ENTORNO=nada /usr/bin/env | grep =nad
USER=nadie
ENTORNO=nada
- Implementar: Variables de entorno temporales.
- Pregunta: ¿Por qué es necesario hacerlo luego de la llamada a
fork(2)? - Pregunta: En algunos de los wrappers de la familia de funciones de
exec(3)(las que finalizan con la letra e), se les puede pasar un tercer argumento (o una lista de argumentos dependiendo del caso), con nuevas variables de entorno para la ejecución de ese proceso. Supongamos, entonces, que en vez de utilizarsetenv(3)por cada una de las variables, se guardan en un array y se lo coloca en el tercer argumento de una de las funciones deexec(3).- ¿El comportamiento resultante es el mismo que en el primer caso? Explicar qué sucede y por qué.
- Describir brevemente (sin implementar) una posible implementación para que el comportamiento sea el mismo.
Ayuda: luego de llamar a fork(2) realizar, por cada una de las variables de entorno a agregar, una llamada a setenv(3).
Función sugerida: setenv(3)
Archivo: implementar set_environ_vars() en exec.c y usarla en exec_cmd()
Pseudo-variables
Existen las denominadas variables de entorno mágicas, o pseudo-variables. Estas variables son propias del shell (no están formalmente en environ) y cambian su valor dinámicamente a lo largo de su ejecución. Implementar ? como única variable mágica (describir, también, su próposito).
1
2
3
4
5
6
7
$ /bin/true
$ echo $?
0
$ /bin/false
$ echo $?
1
- Implementar: Soporte para para la pseudo-variable
$?. Esto implicará actualizar correctamente la variable globalstatuscuando se ejecute un built-in (ya que los mismos no corren en procesos separados). - Pregunta: Investigar al menos otras tres variables mágicas estándar, y describir su propósito. Incluir un ejemplo de su uso en
bash(u otra terminal similar).
Archivo: expand_environ_var() en parsing.c, ver también la variable global status.
Resumen
Al finalizar la parte 3 la shell debe poder:
- Expandir variables de entorno
- Incluyendo la pseudo-variable
$? - Ejecutar procesos con variables de entorno adicionales
Parte 4: Extras
Comandos built-in
Los comandos built-in nos dan la opurtunidad de realizar acciones que no siempre podríamos hacer si ejecutáramos ese mismo comando en un proceso separado. Éstos son propios de cada shell aunque existe un estándar generalizado entre los diferentes intérpretes, como por ejemplo cd y exit.
Es evidente que si cd no se realizara en el mismo proceso donde la shell se está ejecutando, no tendría el efecto deseado, ya que el directorio actual se cambiaría en el hijo, y no en el padre que es lo que realmente queremos. Lo mismo se aplica a exit y a muchos comandos más (aquí se muestra una lista completa de los comando built-in que soporta bash).
- Implementar: Los built-ins:
cd- change directory (cambia el directorio actual)exit- exits nicely (termina una terminal de forma linda)pwd- print working directory (muestra el directorio actual de trabajo)
- Pregunta: ¿Entre
cdypwd, alguno de los dos se podría implementar sin necesidad de ser built-in? ¿Por qué? ¿Si la respuesta es sí, cuál es el motivo, entonces, de hacerlo como built-in? (para esta última pregunta pensar en los built-in comotrueyfalse)
Funciones sugeridas: chdir(3), exit(3), getcwd(3)
Archivo: cd(), exit_shell() y pwd() en builtin.c
Desafíos
Las tareas listadas aquí no son obligatorias, pero suman para el régimen de final alternativo.
Navegación por el historial
Implementar el comando built-in history, el mismo muestra la lista de comandos ejecutados
hasta el momento. De proporcionarse como argumento n, un número entero, solamente mostrar los
últimos n comandos.
De estar definida la variable de entorno HISTFILE, la misma contendrá la ruta al archivo con los comandos ejecutados en el pasado. En caso contrario, utilizar como ruta por omisión a ~/.fisop_history.
Permitir navegar el historial de comandos con las teclas ↑ y ↓, de modo tal que se pueda volver a ejecutar alguno de ellos. Con la tecla ↑ se accede a un comando anterior, y con la tecla ↓ a un comando posterior, si este último no existe, eliminar el comando actual de modo que solo se vea el prompt.
La tecla BackSpace debe funcionar para borrar los caracteres de un comando de hasta una línea de largo. Al presionar Ctrl + d, la shell debe terminar su ejecución.
Ayuda:
- Para tener mayor control sobre la entrada de caracteres, la terminal debe configurarse en modo no canónico y sin eco. Puede usar como guía el ejemplo Noncanonical Mode presente en Low-Level Terminal Interface.
- Pueden ser de utilidad algunas secuencias de escape ANSI.
- Puede obtener información sobre la terminal con la llamada al sistema ioctl, ver: ioctl_tty(2).
Responder:
- ¿Cuál es la función de los parámetros
MINyTIMEdel modo no canónico? ¿Qué se logra en el ejemplo dado al establecer aMINen1y aTIMEen0?
Debe completar al menos una de las siguientes consignas:
- Permitir borrar con la tecla BackSpace los caracteres de un comando de cualquier número de líneas, independientemente que la escritura del mismo haya ocasionado el desplazamiento de la pantalla, esto ocurre al continuar escribiendo tras alcanzar la posición inferior derecha de la terminal.
- Desplazar el cursor de a un caracter con las teclas ← y →, de a una palabra con Ctrl + ← y Ctrl + →, al comienzo del comando con Home, y al final del mismo con End, permitiendo insertar nuevos caracteres en cualquier posición.
- Implementar los designadores de eventos
!!y!-n, ver sección Event Designators en bash(1).
-
No es necesario realizar el include de ningún header para hacer uso de esta variable. ↩︎