Oct 302007
 

Comandos internos y externos

Hay una serie de comandos que la propia shell ejecuta sin ninguna ayuda, a esos comandos les llamaremos comandos internos.
A modo meramente informativo puedo poner una relación de los comandos internos de bash: ., :, [, alias, bg, bind, break, builtin, caller, cd, command, compgen, complete, continue, declare, dirs, disown, echo, enable, eval, exec, exit, export, false, fc, fg, getopts, hash, help, history, jobs, kill, let, local, logout, popd, printf, pushd, pwd, read, readonly, return, set, shift, shopt, source, suspend, test, times, trap, true, type, typeset, ulimit, umask, unalias, unset, wait

Cualquier otro comando que no se encuentre entre estos, bash no sabe ejecutarlo, y entenderá que es un programa externo, así que lo buscará en una serie de directorios que se la han configurado.

Por ejemplo si tecleamos:

$ ls

El bash ejecutará un programa que está en el directorio /bin/ y se lama ls.

Hay que decir también que existen ciertas redundancias, bash maneja internamente ciertos comandos que también son ejecutables externos, otras shells quizá no los manejen internamente, por eso son necesarios como ejecutables externos. Por ejemplo el comando ‘pwd’ nos informa de en que directorio nos encontramos, y es un comando interno de bash, pero también es un programa externo que está en /bin/. En casos así siempre se ejecuta primero el interno.

$ pwd
/home/redy

Si existe esta coincidencia, pero por la razón que sea necesitamos ejecutar el comando externo y no el interno debemos hacerlo indicando su path completo:

$ /bin/pwd

Aparentemente el resultado es el mismo, pero se ejecutó un programa externo en vez del comando interno. Prueba

$ /bin/pwd --help
Modo de empleo: /bin/pwd [OPCIÓN]
 
Muestra el nombre de fichero completo del directorio de trabajo actual.
   –help muestra esta ayuda y finaliza
   –version informa de la versión y finaliza
NOTE: your shell may have its own version of pwd, which usually supersedes
the version described here. Please refer to your shell’s documentation
for details about the options it supports.

Otra forma de evitar los comandos internos es usando el comando enable que permite activar o desactivar el manejo interno del comando que queramos. Si tecleamos por ejemplo

$ enable -n pwd

A partir de ese momento y hasta que salgamos de la shell o lo volvamos a activar pwd nunca será tratado como comando interno, se ejecutará en su lugar el programa externo. Para volver a activarlo como comando interno deberás ejecutar

$ enable pwd

Para ver todos los comandos que se manejan internamente, puedes usar ‘enable’ sin parámetros o ‘enable -p’. Y para ver los que están desactivados puedes usar ‘enable -n’. Por último ‘enable -a’ te los muestra todos mezclados.

$ enable
$ enable -p
$ enable -n
$ enable -a

Una orden muy útil para ver como tratará bash un comando es ‘type’. Type nos informa de si un comando es manejado internamente o externamente.
$ type man
man is /usr/bin/man
$ type -t man
file
$ type pwd
pwd is a shell builtin
$ type -t pwd
builtin
$ enable -n pwd
$ type pwd
pwd is /bin/pwd
$ type -t pwd
file

Todo comando que el bash no sepa manejar internamente, lo busca en los directorios dónde residen los programas. Busca un programa con el mismo nombre que el comando que tecleamos, puede ser una utilidad del sistema operativo, o un programa de usuario, no hay diferencia, si lo encuentra lo ejecuta, y si no nos avisa del error diciendo 'command not found.'.
Existe una variable que tiene un significado especial, su nombre es PATH, y contiene la lista de directorios separados por ':' en los que la shell busca los programas.

Cuando hablamos de 'comando' sin más, sin definir si es externo o interno, nos referimos tanto a los comandos que el propio bash interpreta, como a los programas que tenemos instalados en nuestro sistema operativo.

Concepto de programa y proceso

Quizá este sea el momento de definir lo que es un programa. Un programa es un archivo o un conjunto de archivos que contienen instrucciones y datos, el ordenador de alguna forma puede ejecutar las instrucciones de ese programa. Por ejemplo ls es un programa, contenido en un archivo, que está en el directorio /bin/ y que realiza una tarea que es la de mostrar el contenido de un directorio.

Fundamentalmente hay dos tipos de programas: unos está escritos en código binario, en lenguaje, que el propio procesador puede entender. No es que se escriban así, generalmente se escriben en un lenguaje de programación de alto nivel, más inteligible por seres humanos y un programa (compilador) los traduce al lenguaje binario del procesador. En cambio hay otro tipo de programas que están escritos en un lenguaje que el procesador no entiende directamente, generalmente son archivos de texto que tendrán que ser interpretados por otro programa binario que los va traduciendo línea a línea y los va ejecutando. Tal es el caso de los scripts que escribimos en las anteriores entregas, son archivos de texto que el programa binario /bin/bash, tiene que interpretar. Pues bien en nuestro sitema habrá de los dos tipos. Por ejemplo en el mío:
$ file /bin/ls
/bin/ls: ELF 64-bit LSB executable, AMD x86-64, version 1 (SYSV), for GNU/Linux 2.6.9, dynamically linked (uses shared libs), for GNU/Linux 2.6.9, stripped
$ file /bin/egrep
/bin/egrep: Bourne shell script text executable
$ file /usr/bin/partmon
/usr/bin/partmon: perl script text executable
$ file /usr/bin/helpviewer
/usr/bin/helpviewer: a python script text executable
$ file /usr/bin/spamassassin
/usr/bin/spamassassin: awk script text

Ya veis que hay tanto programas binarios, como scripts en formato texto tanto de bash como de otros intérpretes como pueden ser el perl o el python, el awk..

En un entorno multitarea y multiusuario como Linux hay que distinguir programa y proceso. Se denomina proceso a un programa que se encuentra en ejecución. La multitarea permite que un mismo programa sea ejecutado simultáneamente varias veces, ya sea porque varios usuarios lo han lanzado, o porque un único usuario lo ha lanzado varias veces. Esto es: un mismo programa puede generar varios procesos. En linux cada proceso tiene un número de identificador (PID). Un proceso (proceso padre) puede desencadenar otros (procesos hijos), y un proceso no puede finalizar mientras haya procesos hijos que no hayan terminado. En linux un único proceso llamado init, que siempre tiene el PID 1, es el padre (bueno mejor dicho el ancestro) de todos los demás procesos. Para ver los procesos en ejecución se utiliza el comando ps, pero no adelantemos acontecimientos porque dedicaremos un capítulo a la gestión de procesos.

Lo que si quiero reseñar es que cada proceso tiene un área de memoria, a la que llamamos entorno del proceso dónde se guardan sus parámetros y variables. Cada proceso tiene su entorno y el hecho de modificar un parámetro o variable en un proceso no afectará para nada a los parámetros de otro proceso, aunque se llamen igual. Cuando un proceso finaliza su entorno desaparece con él. Ahora bien cuando se crea un proceso hijo se copia parte del entorno del padre, ese entorno que se hereda es una copia y como dije antes se destruye al acabar el proceso hijo, sin afectar para nada al entorno del padre, ni a otros entornos de otros procesos. Si hablamos de variables; podemos decidir que variables serán copiadas en los procesos hijos y cuales no marcándolas como exportables. El comando para hacer esto es export y su sintáxis es más o menos:
'export VAR1[=valor1] ... [VAR_N=[valor_n]]'.
Si se usa export con una lista de una o más variables estas se marca para ser exportadas a los procesos hijos. Se puede asignar un valor a la variable al mismo tiempo.

$ A=5
$ B=6
$ export B # Los procesos hijos tendrán una variable B que copiará su valor del proceso padre
$ bash # Abrimos un proceso bash hijo
$ echo $A # A no tiene valor en el proceso hijo
$ echo $B # Pero B si, ha copiado el valor del padre en el entorno del hijo
$ B=7 # Modificamos B en el hijo
$ echo $B # Vemos que se ha modificado
$ exit # Finalizamos el proceso hijo
$ echo $A # Las variables del proceso padre siguen conservando sus valores
$ echo $B # Incluso la B que había sido modificada en el hijo, pero claro, era una copia.

Si queremos desmarcar una variable que ha sido previamente marcada como exportable usaremos ‘export -n variable’, esto hará que no se copie a los procesos hijos que se puedan desencadenar desde ese momento. Y si queremos obtener una lista de todas las variables, que se copiarán al entorno de los procesos hijos cuando estos se inicien usaremos el comando export sin ningún argumento o con el argumento -p

$ export -p

Los comandos internos se ejecutan normalmente en el mismo proceso en el que estamos, pero para ejecutar comandos externos bash genera un proceso hijo en el que se ejecuta el comando. Es importante reseñar esto ya que si el comando necesita acceder al valor de una variable es necesario que ésta esté en el entorno del comando (proceso hijo). Por ejemplo si trabajamos con varios displays (varios monitores, o varias consolas gráficas virtuales, y queremos ajustar la gamma de uno de ellos podemos usar el comando xgamma que por defecto ajustará el display indicado en la variable $DISPLAY


$ export -n DISPLAY # por si acaso :-)
$ DISPLAY=:1
$ xgamma 1.2

No funcionará como queremos porque al ejecutarse xgamma en un proceso hijo, en su entorno la variable DISPLAY no está definida. Tendríamos que hacer:


$ DISPLAY=:1
$ export DISPLAY
$ xgamma 1.2

Así cuando se genera el proceso hijo, la variable DISPLAY, marcada para exportación se copiará con el valor que tenga del entorno del proceso padre all entorno del proceso hijo. En lugar de dos órdenes también se puede usar una sola, que es más cómodo, para exportar y asignar valor:

$ export DISPLAY=:1
$ xgamma 1.2

O simplemente usar la forma de asignar variables en la misma línea del comando que ya hemos visto:

$ DISPLAY=:1 xgamma 1.2

Decíamos en el capítulo anterior que esta es una asignación temporal del valor de la variable; en realidad lo que hacemos con ese comando es asignar valor a una variable en el entorno del proceso hijo que vamos a iniciar para ejecutar el comando xgamma.

Cuando una variable está marcada para exportación y se copia a un proceso hijo conserva todos sus atributos, marca de esportación incluída, así que si no hacemos algo que lo evite también se copiará a los proceso hijos del hijo y así sucesivamente.

Hay ciertas variables que se definen en los ficheros de configuración del bash, y que normalmente tienen el atributo de exportables. Un ejemplo es PATH que mencionábamos antes que es uasad por la shel para saber dónde buscar programas ejecutables, es decir comandos externos.

Órdenes compuestas

Hablábamos ya en la entrega 1 de las órdenes simples, y decíamos: «Si comparamos el lenguaje bash con el castellano, podríamos decir que cada orden es como un frase. Cada orden consta de varios elementos (palabras) y tiene significado propio, puede ser interpretada y ejecutado por la shell. Como en nuestro idioma, toda frase debe tener normalmente un verbo, en bash los verbos son los comandos, que generalmente van al principio de la orden. […] Hay ordenes que no tienen más que el comando, y hay otras que llevan argumentos adicionales. […] Cuando estudiábamos lengua en el cole, había oraciones simples y compuestas. Las simples tenían un solo verbo y las compuestas, se formaban combinando dos o más oraciones simples, y normalmente tenían más de un verbo. Además había varios tipos de oraciones compuestas, coordinadas, subordinadas… En el lenguaje de bash pasa lo mismo, hay órdenes simples con un solo comando, y hay órdenes compuestas que llevan varios comandos. En el castellano para unir varias frases simples en una farse compuesta se usan la conjunciones, en bash se usan también unos signos que denominaremos separadores.

El caso más simple de ordenes compuestas consiste en juntar varias ordenes simples, para que se ejecuten una tras otra separándolas por un punto y coma ‘;’ tal que así

$ echo "Este es el listado del directorio"; ls; echo "Buenos días"

Hay otras formas de formar órdenes compuestas, que iremos viendo poco a poco, pero antes tengo que explicar algunos conceptos:

Ejecución de programas, código de terminación

Siempre que se ejecuta un comando, ya sea este interno o externo, al finalizar el proceso obtenemos un código de terminación, que es un número de 0 a 65535. Normalmente, y salvo que el programador haya decidido otra cosa, se sigue el convenio de que si el programa finalizó sin ningún error se obtiene un código 0 y si hubo algún problema un código distinto de 0.

Podemos consultar el código de terminación del último comando ejecutado viendo el valor del parámetro $? por ejemplo con el comando echo, tal que así:
$ echo $?

Prueba a hacer un ls de un fichero existente y de otro que no existe y verás como el código de terminación cambia:

$ cd ~/ejercicios_curso_bash
$ touch fichero_que_existe
$ ls fichero_que_existe
$ echo $?
0
$ ls fichero_que_no_existe
$ echo $?
1

Ordenes compuestas condicionadas

Una vez entendido esto volvemos a las órdenes compuestas. Más arriba veíamos como en una sola orden podíamos agrupar varias ordenes simples usando el separador ‘;’. Hay dos separadores que hacen lo mismo pero la segunda (y sucesivas ordenes si las hubiese), se ejecutará o no en función del código de terminación de la orden anterior.

Para ejecutar una orden si el código de terminación de la anterior fue 0 usaremos el separador ‘&&’ dos signos de andpersand consecutivos de la siguiente forma.


$ ls fichero_que_existe && echo "El fichero existe"
fichero_que_existe
El fichero existe

$ ls fichero_que_no_existe && echo "Esto no se imprimirá"
ls: fichero_que_no_existe: No existe el fichero o el directorio

Para ejecutar una orden si el código de terminación de la anterior fue distinto de 0, usaremos el separador ‘||’ dos barras verticales consecutivas tal que así:


$ ls fichero_que_existe || echo "Esto no se imprime"
fichero_que_existe
$ ls fichero_que_no_existe || echo "El comando ls terminó con error $?"
ls: fichero_que_no_existe: No existe el fichero o el directorio
El comando ls terminó con error 1

Los ejemplos vistos unen dos ordenes simples en una compuesta, pero podemos concatenar más de dos combinando los separadores como nos interese.

$ ls fichero_que_existe && ls fichero_que_no_existe || echo "hola"
fichero_que_existe
ls: fichero_que_no_existe: No existe el fichero o el directorio
hola

 
$ ls fichero_que_existe && echo "CT: $?"; echo "El fichero existe"
fichero_que_existe
CT: 0
El fichero existe

Como pueden combinarse muchas oŕdenes simples usando varios separadores hay que establecer unas prioridades para le evaluación de estos, es como en matemáticas que la multiplicación tiene prioridad sobre la suma y la resta por ejemplo, y por tanto si ponemos 3 + 2 * 5, se entiende que primero se multiplica el dos por el cinco y a continuación se suma a tres el resultado de ese producto. Aquí es igual, los separadores anteriores se ejecutan en un orden de prioridad: primero se ejecuta ‘&&’ y ‘||’ que tienen la misma prioridad, y se ejecutarán por órden de izquierda a derecha. Y luego está ‘;’. En caso de duda podemos hacer agrupaciones con llaves o con paréntesis.

Agrupación con llaves: Toda lista de ordenes que agrupemos entre llaves funcionará, en cuanto a prioridad se refiere como si fuese una sola orden simple. Hay que dejar espacios siempre entre las llaves y las ordenes (para que la shell no las confunda con una expansión de llaves) , y la lista tiene que acabar en un ‘;’ antes de cerrar la última llave. Por ejemplo:

$ ls fichero_que_no_existe && { echo hola ; ls pepe || echo adios ; }
Dada la prioridad de cada separador, si no pusiesemos ninguna llave llave sería como ejecutar:
${ ls fichero_que_no_existe && echo hola ; } ; { ls pepe || echo adios ; }

Los comandos entre llaves se agrupan y se ejecutan como procesos de la shell actual y el código de terminación obtenido como resultado de la evaluación de las llaves es el del último comando ejecutado dentro de las llave.

Agrupación con paréntesis: funciona de manera similar. Como el paréntesis no puede confundirse con una expansíon como era el caso de las llaves, no es necesario respetar los espacios. La agrupación con paréntesis no se ejecuta en la misma shell sino que se abre en un proceso hijo otra subshell y se ejecuta dentro de esta, por lo que si las órdenes modifican el entorno en la subshell, el entorno de la shell original permanece invariable al finalizar. No hace falta acabar en ‘;’ antes de cerrar el último paréntesis.

Las llaves se ejecutan en la misma shell con la siguiente orden compuesta podéis ver que entre nuestra lista de procesos, solo hay un bash.

$ { echo Comandos entre llaves ; ps f; }
Comandos entre llaves
PID TTY STAT TIME COMMAND
14911 pts/2 S 0:00 bash
15358 pts/2 R 0:00 ps f

Sin embargo en una orden entre paréntesis se abre otro proceso con otro bash en el que se ejecutan las órdenes.
$ (echo Comandos entre paréntesis ; ps f)
Comandos entre paréntesis
PID TTY STAT TIME COMMAND
14911 pts/2 S 0:00 bash
17637 pts/2 S 0:00 bash
17638 pts/2 R 0:00 \_ ps f

$ A=5; { A=6; echo 'Dentro $A vale' $A; } ; echo 'Fuera $A vale' $A
Dentro $A vale 6
Fuera $A vale 6

$ A=5; (A=6; echo 'Dentro $A vale' $A ) ; echo 'Fuera $A vale' $A
Dentro $A vale 6
Fuera $A vale 5

Algo de lógica

Para los que sepan programar en C u otro lenguaje similar habrán visto en los separadores «||» y «&&» una relación con los operadores de C de igual grafía que representan a los operadores lógicos and y a or respectivamente.

Que no se asuste nadie. No hace falta saber C, para entender lo que voy a intentar explicar. Eso sí, si no se presta atención puede ser algo lioso de entender, así que espero muchas dudas y consultas de este capítulo, porque ya sabéis que el que no pregunta nada, o lo sabe todo, o no sabe nada.

Decíamos que si todo iba sin errores el estado de terminación era 0 y si había error era algo distinto a 0. Podemos asumir que cero es ‘Éxito’ y no cero es ‘Fallo’. Para abstraernos a algo más booleano (de Boole, el padre de la lógica), podemos entender que ‘Éxito’ es como un resultado ‘Cierto’ y ‘Fallo’ es como un resultado ‘Falso’, o sea que cero es Cierto, y no cero es Falso. (A veces cuesta pensar en lógica negativa, ya que estamos acostumbrados a que el cero sea falso, pero en bash el cero es cierto).

En lógica de Boole tenemos dos operadores muy usados.

A veces a estos dos operadores se les llama suma lógica (O) y producto lógico (Y) pero yo creo que se entiende más con la nomenclatura de las conjunciones Y y O

El operador Y (and en inglés) que dadas dos proposiciones nos devuelve ‘Cierto’ si ambas son Ciertas y ‘Falso’ Si alguna de ellas es falsa.

(Yo soy usuario de Fotolibre) Y (Me gusta Windows)

Es falso porque una de las dos proposiciones es falsa pero…

(Yo soy usuario de Fotolibre) Y (Sé algo de bash)

Es cierto porque ambas son ciertas.

El otro operador lógico es O (or en inglés) que nos da como resultado Cierto si alguna de las proposiciones es cierta, y falso solo si ambas son falsas.

(Yo soy usuario de Fotolibre) O (Me gusta Windows)

Es cierto porque al menos una de las dos es cierta, pero…

(Me gusta Windows) O (Odio Linux)

Es falso porque ambas son falsas.

En realidad el separador && es un operador lógico Y. Entonces ¿Porque sólo se ejecuta la segunda sentencia si la primera devuelve un error 0?

Tal como decíamos antes 0=’Éxito’=’Cierto’. Aplicando un principio de comodidad, si la primera orden nos da cierto, tenemos que evaluar si se cumple la segunda para saber si se ambas son ciertas. Pero si la primera no es cierta ya no necesitamos seguir evaluando, ya sabemos que al menos una no es cierta, para que vamos a ejecutar la otra.

La tabla de esta operación lógica sería:

   En lógica:                               En bash:
   -----------------------------            ----------
   Cierto Y Cierto     = Cierto             0 && 0 = 0
   Cierto Y Falso      = Falso              0 && 1 = 1
   Falso  Y lo_que_sea = Falso              1 && X = 1

De forma análoga el separador ‘||’ es un operador lógico O. Así que si la primera orden nos da un resultado de 0, es decir cierto, no se ejecuta la segunda, porque ya sabemos que al menos una de las dos es cierta. Por el contrario si el resultado de la primera es no cero, o sea falso, debemos seguir ejecutando porque, para saber si alguna es cierta, debemos saber el resultado de la segunda. La tabla de la operación lógica O sería:

   En lógica:                               En bash:
   -----------------------------            ----------
   Cierto O lo_que_sea = Cierto             0 || X = 0
   Falso  O Cierto     = Cierto             0 || 1 = 0
   Falso  O Falso      = Falso              1 || 1 = 1

El resultado de la evaluación Y es 0 (Cierto) si ambas ordenes devuelven 0. En realidad se devuelve el resultado de la última orden ejecutada, si la primera es ‘no 0’ (Falso) se devuelve este valor, ya que la siguiente no se ejecuta. Y si la primera es 0 y la segunda no, se devuelve el valor de terminación de la segunda. Así que efectivamente funciona como una operación lógica Y.

En una evaluación O también se devuelve siempre el código de terminación de la última orden ejecutada, deducirá el lector fácilmente que también funciona como una operación lógica O.

Si te has liado con lo último, no te preocupes, recuerda lo dicho en el apartado anterior:

&& (and ó Y) ejecuta una orden si la anterior devolvió 0 (no error)
|| (or ó O) ejecuta una orden si la anterior no devolvió 0 (error)

Como regla nemotécnica para recordar que cual es Y y cual es O solo tenéis que recordar que el & en los nombres de sociedades significa Y, como en ‘Manolo Fdez. & Hijos’. :-) Y para entender el funcionamiento puedes remplazar el && (Y) por un ‘Y (si ha funcionado)’ o el || (O) por un ‘O (si no)’ por ejemplo:

$ ls fichero_que_existe && echo "El fichero existe"

Lo leeríamos como ‘(ls fichero_que_existe) y si ha funcionado (echo «El fichero existe»)’ y

$ ls fichero_que_no_existe || echo "El fichero no existe"

Lo leeríamos como ‘(ls fichero_que_no_existe) o si no (echo «El fichero no existe)»

Me interesaría bastante recibir opiniones sobre este capítulo, porque pienso que es especialmente lioso ¿o no?