En esta primera entrega vamos a escribir el esqueleto sobre el que iremos construyendo más funcionalidades e introduciendo algunas de las características más alucinantes de las shells. Vamos, que no esperéis ver nada alucinante por el momento, aunque diría que si bastante guay ;)
Bucle principal
El bucle principal de nuestra shell de ejemplo simplemente lee los
comandos del usuario y los ejecuta. Podríamos escribir nuestro propio
código para leer el teclado y hacer todas las maravillas que hacen las
shells permitiéndonos editar nuestra línea de comandos, pero en su lugar
vamos a utilizar la librería readline que con un par de
líneas nos va a permitir hacer que nuestra pequeña shell se parezca a
bash.
Así que el programa principal, incluyendo todos los included que necesitaremos en nuestro programa los podéis ver a continuación:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <readline/readline.h>
#include <readline/history.h>
#include <unistd.h>
#include <sys/wait.h>
static char ps1[1024] = "mesh $ ";
int
main () {
char *buffer;
printf ("MESH is a Educational Shell\n");
while (1) {
buffer = readline (ps1);
add_history (buffer);
run_cmd (buffer);
}
return 0;
}Si, hemos llamado a nuestra shell mesh que es un nombre
bastante guay, además es probable que represente muy bien el código
final, ya que lo vamos a ir construyendo sobre la marcha. Por último es
un acrónimo recursivo que es una de las cosas que más molan en el mundo
del software libre.
El programa, como veis no hace mucho. Se trata de un bucle infinito,
en el que se leen los comandos del usuario usando readline,
la cual, automáticamente nos permite editar la línea de comandos como lo
hacemos normalmente. A continuación añade el comando recién ingresado a
la historia, de forma que podamos usar los cursores arriba y
abajo o CTRL+R para buscar comandos anteriores.
Finalmente la función run_cmd es la que va a parsear la
entrada del usuario y ejecutarla. Como dijimos al principio, esta
primera versión solo permitirá ejecutar comandos simples, así que hemos
puesto todo el código junto por el momento para que sea más fácil de
leer.
Ejecutando comandos
La forma de ejecutar un comando en un sistema GNU/Linux (desde un
programa claro) es utilizando la llamada al sistema execve.
La librería C estándar nos ofrece varios wrappers a esta
llamada al sistema, los cuales podéis ver usando el comando
man exec. Fundamentalmente no ofrecen formas más
convenientes de pasar los parámetros y el entorno al programa que
queremos ejecutar.
El prototipo de la llamada al sistema execve es el
siguiente:
int execve(const char *pathname, char *const _Nullable argv[],
char *const _Nullable envp[]);El primer parámetro es el path al programa que queremos ejecutar, y
luego siguen dos arrays uno conteniendo los parámetros que le queremos
pasar al programa y otro las variables de entorno. Ambos arrays se deben
terminar con el valor NULL y para el caso de
argv, el primero debe ser el nombre del programa. Bueno,
realmente es argv[0] que es utilizado por ejemplo por
programas como ps para mostrar el nombre de los procesos.
Podemos poner lo que queramos ahí, pero como estamos escribiendo una
shell nos interesa que argv[0] represente el programa real
que estamos ejecutando, así que pathname y
argv[0] normalmente tendrán el mismo valor.
Ahora que sabemos esto, debemos parsear la entrada del usuario y
poner cada uno de los parámetros en una de las entradas del array
argv.
Parseando la entrada de usuario
Para parsear la entrada de usuario vamos a usar la función
strtok que nos permite usar varios delimitadores y además
hará el trimming (eliminar espacios al principio y final de una
cadena) de los argumentos automáticamente.
Este sería un posible código para hacer esto:
int
run_cmd (char *buffer) {
char *token;
char *delim = "\t\n ";
char *arg[10];
int i, cnt = 0;
for (i = 0; i < 10; i++) arg[i] = NULL;
token = strtok (buffer, delim);
if (token == NULL) return -1;
arg[cnt++] = token;
while (1) {
if ((token = strtok (NULL, delim)) == NULL) break;
arg[cnt++] = token;
}
arg[cnt] = NULL; // Último parámetro debe ser NULL
(...)Como podéis ver este es el principio de la función
run_cmd que llamamos desde el programa principal. Hemos
incluido todas las variables que utilizaremos en ella, incluyendo las
necesarias para ejecutar programas, cosa que haremos inmediatamente
después de procesar los argumentos.
Lo primero que hacemos es inicializar la lista de parámetros. En este
caso estamos usando una lista fija de un máximo de 10 parámetros para no
complicar el código reservando memoria dinámicamente, pero podéis
modificar el programa muy fácilmente para que trabaje con cualquier
número de argumentos, solamente debéis utilizar realloc
para añadir memoria para los argumentos nuevos.
Tras ello, hacemos la primera llamada a strtok, la cual
recibe como primer parámetro la cadena a parsear y la lista de
delimitadores. En este caso hemos seleccionado como delimitadores el
espacio, el tabulador y el retorno de carro.
El primer resultado de strtok es el nombre del programa
(al menos para esta primera shell mínima). Si el usuario ha introducido
una línea vacía (lo que incluye un montón de espacios y tabuladores),
simplemente retornamos sin hacer nada. En caso contrario añadimos al
array par el resto de argumentos. Observad como las
siguientes llamadas a strtok reciben como primer parámetro
NULL en lugar de la cadena a parsear.
Al final, cuando hemos procesado todos los parámetros añadimos una
entrada nula al final del array como requiere execve. Ahora
ya estamos en condiciones de ejecutar el programa indicado por el
usuario
Ejecutando un programa
La forma de ejecutar un programa en un sistema UNIX requiere de dos
pasos. El primer paso consiste en crear un proceso, algo que podemos
hacer con la llamada al sistema fork y, una vez que el
nuevo proceso ha sido creado, indicamos que código queremos que ejecute
(y con que argumentos), usando la llamada al sistema
execve.
La llamada al sistema fork crea un nuevo proceso
idéntico al proceso que invocó la llamada, y cuya ejecución continua en
el mismo punto, justo después de la llamada a fork. Es
literalmente hacer una copia del proceso actual, tal y como se encuentra
en ese momento. El programa puede saber si se trata del proceso original
(o proceso padre) o el nuevo proceso (o proceso hijo), comprobando el
resultado de fork. Si el valor es 0, se trata
del proceso hijo, mientras que si es un número positivo, se trata del
proceso padre, y de hecho, ese número es el identificador de proceso (o
PID) del proceso recién creado. Algo que necesitaremos en un rato.
El código que hace todo esto se muestra a continuación:
pid_t pid;
int r, status;
if ((pid = fork ()) < 0) perror ("fork:");
else if (pid == 0) { // Proceso Hijo
if ((r = execvp (par[0], arg)) < 0) {
perror ("exec:");
exit (r);
}
} else {
waitpid (pid, &status, 0);
fprintf (stderr, "\n[DEBUG:Process finished with status: %d]\n", WEXITSTATUS(status));
}
return 0;
}Observad que estamos usando la versión execvp del
execve con la que no necesitamos pasar las variables de
entorno y además comprobando su resultado para poder mostrar errores en
caso de que el programa que queremos ejecutar no exista, no tenga
permisos de ejecución o cualquier otro error que se pueda producir. En
ese caso además terminamos el proceso hijo retornando como código de
error el valor de la variable errno, que nos indica que
tipo de error se ha producido. perror muestra una
representación en lenguaje natural del valor de la variable
errno
Y eso concluye nuestra shell mínima. Ahora podemos compilarla y probarla:
$ gcc -Wall -o mesh01 mesh01.c -lreadline $ ./mesh01 MESH is a Educational Shell mesh $ pwd /home/occam [DEBUG:Process finished with status: 0] mesh $ programa_que_no_existe exec:: No such file or directory [DEBUG:Process finished with status: 2] mesh $ errno 2 ENOENT 2 No such file or directory [DEBUG:Process finished with status: 0] mesh $
Como podéis ver podemos ejecutar cualquier comando, con menos de 10 parámetros ;), y mostrar el código de error que retorna.
ANTES DE TERMINAR. COMANDOS INTERNOS
Antes de terminar vamos a introducir el concepto de comandos
internos. Hay ciertos comandos en una shell que no son programas
externos como pwd, ls o errno,
sino que se deben ser implementados por la propia shell. Para que esta
primera shell sea completa funcionalmente (aunque su funcionalidad sea
limitada), debemos añadir dos comandos internos.
Los comandos internos los podemos ejecutar directamente, no es necesario crear un nuevo proceso para ello, así que los implementaremos justo entre el parseo de la entrada de usuario y la ejecución del nuevo proceso, y vamos a implementar dos:
- El primero será
exitque nos permite salir de la shell. Este comando terminal el programa actual que es la shell, algo que no podemos hacer con un programa externo. Bueno, podríamos llamar akillcon nuestroPIDpero eso sería un poco enrevesado. - El otro comando será
cd. Sicd(cambiar directorio) es un comando interno en todas las shells. La razón es que la llamada al sistema que nos permite cambiar el directorio actualchdirsolo lo realizar en el proceso actual. Si implementamos un programa que utilizachdir, solo podrá cambiar el directorio actual para si mismo, pero no para el programa que lo invoca (la shell en este caso).
Estos dos comandos los podemos implmentar de forma muy sencilla con solo dos líneas de código:
if (!strcmp (par[0], "exit")) exit (0);
if (!strcmp (par[0], "cd")) return chdir (par[1]);Observad que el comando cd que ofrece, por ejemplo,
bash, hace muchas más cosas que esto, pero por el memento esto nos
permite cambiar de directorio en nuestra shell. Esas funciones más
avanzadas las veremos cuando introduzcamos nuevos conceptos como las
variables de entorno.
CONCLUSIÓN
Como podéis ver implementar una shell básica no es tan complicado,
apenas 70 líneas de código. Al utilizar readline hemos
incluido muy fácilmente capacidad para editar los comandos, añadir
historia y auto completado de nombres de fichero con solo dos líneas.
Hemos visto como una shell ejecuta programas y tenemos un punto de
partida para explorar las distintas maravillas que ocultan estos
interesantes programas. No dudéis en enviarnos vuestras mejoraras como
construir la lista de argumentos dinámicamente o añadir otros comando
internos. Hasta el próximo número.
■
