Programación del Shell Linux

Aquí tenemos un libro libre y completo sobre Shell

La sed del "conocimiento libre" es muy bienvenida.

Comandos Shell Script

Diagrama Sintáctico
Júlio Neves
Júlio Neves
Home | Artículos Português InglesAgradecimientos | Links Amigos

Comprar el libro

Changelogs

  Conversación de Bar 1  

  Conversación de Bar 2  

  Conversación de Bar 3  

  Conversación de Bar 4  

  Conversación de Bar 5  

  Conversación de Bar 6  

  Conversación de Bar 7  

  Conversación de Bar 8  

  Conversación de Bar 9  

  Conversación de Bar 10  

  Conversación de Bar 11  

  Aperitivo  

Conversación de Bar - Parte V

- Que hay amigo! Ordenaste ya tus ideas?, Se fundió ya tu cabeza, o todavía aguanta más Shell?

- Aguanto, claro! Me esta gustando mucho! Me gustó tanto que hasta me esmeré en el ejercicio que me dejaste. Te acuerdas que me pediste que hiciera un programa que recibiría como parámetro el nombre de un archivo y que cuando se ejecutara salvara este archivo con el nombre original seguido de una tilde (~) ademas de colocarlo dentro del vi?

- Claro que me acuerdo, muéstramelo y explica como lo hiciste.

$ cat vira #!/bin/bash # # vira - vi grabando el archivo anterior # == = =

# Verificando si fue pasado 1 parámetro if [ "$#" -ne 1 ] then echo "Erro -> Uso: $0 " exit 1 fi

Arq=$1 # En caso de que el archivo no exista, no hay copia para grabar if [ ! -f "$Arq" ] then vi $Arq exit 0 fi

# Si no puedo alterar el archivo, para que voy a usar el vi? if [ ! -w "$Arq" ] then echo "Usted no tiene privilegios de grabación en $Arq" exit 2 fi

# Ya que está todo OK, voy a salvar la copia y llamar el vi cp -f $Arq $Arq~ vi $Arq exit 0

- Bárbaro, muy bien! Pero dime una cosa: porque terminaste el programa con un exit 0?

- Ahhh! Descubri que el número después del exit da el código de retorno del programa (o $?, te acuerdas?), y de esta forma, si todo se ejecuto bien, se cerraría con el $? = 0. Sin embargo, si observas, verás que en el caso de que el programa no reciba el nombre del archivo o en el caso de que el operador no tenga privilegios de grabación sobre este archivo, el código de retorno ($?) sería diferente de cero.

- Grande!, aprendiste bien, pero es bueno dejar claro que el exit 0, simplemente exit, o no colocar exit, producen igualmente un código de retorno ($?) igual a cero, si el programa fue bien ejecutado. Ahora vamos a hablar sobre las instrucciones de loop o lazo, pero antes voy a pasar el concepto de bloque de programa.

Hasta ahora ya vimos algunos bloques de programa, como cuando te mostré un ejemplo para hacer un cd hacia dentro de un directorio, y que era así:

cd lmb 2> /dev/null ||
    {
    mkdir lmb
    cd lmb
    }

El fragmento contenido entre las dos llaves ({}), forma un bloque de comandos. También en este ejercicio que acabamos de ver, en que salvamos el archivo antes de editarlo, existen varios bloque de comandos comprendidos entre los then y los fi del if.

Un bloque de comandos también puede estar dentro de un case, o entre un do y un done.

- Espera ahí Julio, que do y done son esos, no me acuerdo de que hayas hablado de ellos y mira que estoy prestando mucha atención...

- Claro, todavía no había hablado de ellos porque no había llegado el momento adecuado. Todas las instrucciones de loop o lazo, ejecutan los comandos del bloque comprendido entre el do y el done.

Comandos de Loop (o lazo)

Las instrucciones de loop o lazo son el for, el while y el until que pasaré a explicarte una a una a partir de hoy.

El comando for

Si ya estás habituado a programar, con seguridad conoces el comando for, pero lo que no sabes es que el for, que es una instrucción intrínseca del Shell (esto significa que el código fuente del comando es parte del código fuente del Shell, o sea en buen idioma "programés" es un built-in), es mucho más poderoso que los semejantes de otras lenguajes.

Vamos a entender su sintaxis, primero en español y después como funciona realmente.

    para var en val1 val2 ... valn
    haga
        cmd1
        cmd2
        cmdn
    hecho

Donde la variable var asume cada uno de los valores de la lista val1 val2 ... valn y para cada uno de esos valores ejecuta el bloque de comandos formado por cmd1, cmd2 y cmdn

Ahora que ya vimos el significado de la instrucción en español, veamos la sintaxis correcta:

Primera sintaxis del comando for:

    for var in val1 val2 ... valn
    do
        cmd1
        cmd2
        cmdn
    done

Vamos directo a los ejemplos, a fin de entender el funcionamiento de este comando. Vamos a escribir un script para listar todos los archivos de nuestro directorio separados por dos puntos, pero mira primero:

$ echo * ArchDoDOS.txt1 confuso incusu logado musexc musicas musinc muslist

O sea, el Shell vio el asterisco (*), lo expandió con el nombre de todos los archivos del directorio y el comando echo los mostró en la pantalla separados por espacios en blanco. Visto esto vamos a ver como resolver el problema que nos propusimos:

$ cat testefor1 #!/bin/bash # 1o. Prog didáctico para entender el for

for Arch in * do echo -n $Arq: # La opción -n es para no saltar la línea done

Ahora vamos a ejecutarlo:

$ testefor1 ArchDoDOS.txt1:confuso:incusu:logado:musexc:musicas:musinc:muslist:$

Como viste, el Shell transformó el asterisco (es odioso ser llamado por un asterisco) en una lista de archivos separados por espacios en blanco. cuando el for vio aquella lista, se dijo: "Opa!, lista separadas por espacios es mi especialidad!"

El bloque de comandos para ejecutar era solamente el echo, que con la opción -n listó la variable $Arch seguida de dos puntos (:), sin saltar de línea. El signo de ($) del final de la línea de ejecución es el prompt. que permaneció en la misma línea también en función de la opción -n. Otro ejemplo simple (por ahora):

$ cat testefor2 #!/bin/bash # 2o. Prog didáctico para entender el for

for Palabra in Conversa de Bar do echo $Palabra done

Y ejecutando resulta:

$ testefor2 Conversa de Bar

Como viste, este ejemplo es tan bobo y simple como el anterior, pero sirve para mostrar el comportamiento básico del for.

Fíjate en la fuerza del for: todavía estamos en la primera sintaxis del comando y ya estoy mostrando nuevas formas de usarlo. Allá atrás, te había hablado que el for usaba listas separadas por espacios en blanco, pero eso es una verdad a medias, era sólo para facilitar la compresión.

En realidad, las listas no son obligatoriamente separadas por espacios, pero antes de seguir, déjame mostrarte como se comporta una variable del sistema llamada $IFS. Observa su contenido:

$ echo "$IFS" | od -h 0000000 0920 0a0a 0000004

O sea, mandé la variable (protegida de la interpretación del Shell por las comillas) para un dump hexadecimal (od -h) y resultó:

Contenido de la Variable $IFS
0a <ENTER>
  Hexadecimal     Significado  
09 <TAB>
20 <ESPACIO>

Donde el último 0a fue originado por el <ENTER> dado al final del comando. Para mejorar la explicación, vamos a ver eso de otra forma:

$ echo ":$IFS:" | cat -vet : ^I$ :$

Presta atención a lo siguiente para entender la construcción del comando cat:

Pinguim com placa de dica En el comando cat, la opción -e representa el <ENTER> como un signo de pesos ($) y la opción -t representa el <TAB> como un ^I. Usé los dos puntos (:) para mostrar el inicio y el fin del echo. y de esta forma, otra vez podemos notar que los tres caracteres están presentes en aquella variable.

Ahora, IFS significa Inter Field Separator o, traduciendo, separador entre campos. Una vez entendido eso, puedo afirmar (porque lo voy a probar) que el comando for no usa listas separadas por espacios en blanco, sino por el contenido de la variable $IFS, cuyo valor por defecto (default) son esos caracteres que acabamos de ver. Para comprobarlo, vamos a mostrar un script que recibe el nombre del artista como parámetro y lista las músicas que este ejecuta, pero primero veremos como está nuestro archivo musicas:

$ cat musicas album 1^Artista1~Musica1:Artista2~Musica2 album 2^Artista3~Musica3:Artista4~Musica4 album 3^Artista5~Musica5:Artista6~Musica6 album 4^Artista7~Musica7:Artista1~Musica3 album 5^Artista9~Musica9:Artista10~Musica10

En base a este esquema mostrado arriba fue desarrollado el script que sigue:

$ cat listartista #!/bin/bash # Dado un artista, muestra sus músicas

if [ $# -ne 1 ] then echo Usted debería haber pasado un parámetro exit 1 fi

IFS=" :"

for ArtMus in $(cut -f2 -d^ musicas) do echo "$ArtMus" | grep $1 && echo $ArtMus | cut -f2 -d~ done

El script, como siempre, comienza testando si los parámetros fueron pasados correctamente, en seguida el IFS fue configurado para <ENTER> y dos puntos (:) (como demuestran las comillas en líneas diferentes), porque es él el que separa los bloques Artistan~Musicam. De esta forma, la variable $ArtMus irá a recibir cada uno de estos bloques del archivo (observa que el for ya recibe los registros sin el álbum en virtud del cut en su línea). En el caso de que encuentre el parámetro ($1) en el bloque, el segundo cut listará solamente el nombre de la música. Vamos a ejecutarlo:

$ listartista Artista1 Artista1~Musica1 Musica1 Artista1~Musica3 Musica3 Artista10~Musica10 Musica10

Epa! Pasaron dos cosas no deseadas: los bloques también fueron listados y la Musica10 también. Además de eso, nuestro archivo de músicas es muy simple, en la vida real, tanto la música como el artista tienen más de un nombre. Suponte que el artista fuera una dupla de música folclórica llamada Clitandro & Eduviges (no me atrevo ni a dar la idea, por miedo a que se haga realidad smile ). En este caso el $1 sería Clitandro y el resto de este lindo nombre sería ignorado en la búsqueda.

Para que eso no ocurriese, debería pasar el nombre del artista entre comillas (") o alterar $1 por $@ (que significa todos los parámetros pasados), que es la mejor solución, pero en este caso tendría que modificar la crítica de los parámetros y el grep. La nueva crítica no actuaria si yo pasase un parámetro, o por lo menos un parámetro y en cuanto al grep, mira lo que resultaría después de la substitución del $* (que entraría en lugar del $1) por los parámetros:

    echo "$ArtMus" | grep clitandro & eduviges

Lo que resultaría en un error. Lo corretco sería:

    echo "$ArtMus" | grep -i "clitandro & eduviges"

Donde fue colocada la opción -i para que la búsqueda ignorase mayúsculas y minúsculas y las comillas también fueron insertadas para que el nombre del artista fuera visto como una cadena única y monolítica.

Todavia falta arreglar el error de haber listado al Artista10. Para esto, lo mejor es decirle al grep que la cadena está en el início ( forma cuya expresión regular es ^) de $ArtMus y luego después viene una tilde (~). Es necesario también que se redireccione la salida del grep para /dev/null para que los bloques no sean listados más . Observa entonces la nueva (y definitiva) cara del programa:

$ cat listartista #!/bin/bash # Dado un artista, muestra sus músicas # versao 2

if [ $# -eq 0 ] then echo Usted debería haber pasado un parámetro exit 1 fi

IFS=" :"

for ArtMus in $(cut -f2 -d^ musicas) do echo "$ArtMus" | grep -i "^$@~" > /dev/null && echo $ArtMus | cut -f2 -d~ done

Que ejecutado da:

$ listartista Artista1 Musica1 Musica3

Segunda sintáxis del comando for:

    for var
    do
        cmd1
        cmd2
        cmdn
    done

- Espera ahí!, sin el in como va a saber que valor asumir?

- Eso mismo, no? Esta construcción a primera vista parece extraña pero es bastante simple. En este caso, var asumirá uno a uno cada uno de los parámetros pasados para el progama.

Vamos rapidito a los ejemplos para entenderlo mejor. Vamos a hacer un script que reciba como parámetro una cantidad de músicas y liste sus autores:

$ cat listamusica #!/bin/bash # Recibe parte de los nombres de músicas como parámetro y # lista los intérpretes. Si el nombre es compuesto, debe # ser pasado entre comillas. # ex. "No soy tu perrito, no!" "Asadito de Madre" # if [ $# -eq 0 ] then echo Uso: $0 musica1 [musica2] ... [musican] exit 1 fi IFS=" :" for Musica do echo $Musica Str=$(grep -i "$Musica" musicas) || { echo " No encontrada" continue } for ArtMus in $(echo "$Str" | cut -f2 -d^) do echo " $ArtMus" | grep -i "$Musica" | cut -f1 -d~ done done

De la misma forma que los otros, comenzamos el ejercício con una crítica sobre los parámetros recibidos, en seguida hicimos un for en que la varible $Musica recibirá cada uno de los parámetros pasados, colocando en $Str todos los álbums que contienen las músicas pasadas. En seguida, el otro for coge cada bloque Artista~Musica de los registros que están en $Str y lista cada artista que ejecute aquella música.

Como siempre vamos a ejecutarlo para ver si realmente funciona:

$ listamusica musica3 Musica4 "Yegüita Pocotó" musica3 Artista3 Artista1 Musica4 Artista4 Yegüita Pocotó No encontrada

La lista quedó fea porque todavia no sabemos dar formato a la salida, pero cualquier día de estos, cuando sepas posicionar el cursor, hacer negritas, trabajar con colores, etc, haremos esta lista nuevamente usando todas estas perfumerías y entoces quedará bien coqueto.

A esta altura de los acontecimientos debes estar preguntandote: "Y aquél for tradicional de los otros lenguajes en que sale contando a partir de un número, con un determinado incremento hasta alcanzar una condición?"

Y es ahí donde te respondo: "Yo no te dije que nuestro for es más completo que los otros?" Para hacer eso existen dos formas:

1 - con la primera sintáxis que vimos, como en los siguientes ejemplos directamente en el prompt:

$ for i in $(seq 9) > do > echo -n "$i " > done 1 2 3 4 5 6 7 8 9

Aquí, la variable i asumió los enteros del 1 al 9 generados por el comando seq y la opción -n del echo fue usada para no saltar de línea con cada número listado (me siento ecologicamente correcto por no gastar una cantidad de papel de la revista cuando eso puede ser evitado). Además usando el for con seq:

$ for i in $(seq 3 9) > do > echo -n "$i " > done 4 5 6 7 8 9

O todavia en la forma más completa del seq:

$ for i in $(seq 0 3 9) > do > echo -n "$i " > done 0 3 6 9

2 – La otra forma de hacer lo deseado es con una sintáxis muy parecida al for del lenguaje C, como veremos a continuación.

Tercera sintáxis del comando for:

    for ((var=ini; cond; incr))
    do
        cmd1
        cmd2
        cmdn
    done

Donde:

var=ini - Significa que la variable var comenzará a partir de un valor inicial ini;
cond    - Significa que el loop o lazo del for será ejecutado en cuanto la var no cumpla la condición cond;
incr    - Significa el incremento que la variable var sufrirá en cada pasada del loop.

Como siempre vamos a los ejemplos y la cosa quedara más fácil:

$ for ((i=1; i<=9; i++)) > do > echo -n "$i " > done 1 2 3 4 5 6 7 8 9

En este caso la variable i partió del valor inicial 1, el bloque de comando (aqui solamente el echo) será ejecutado en cuanto i sea menor o igual (<=) a 9 y el incremento de i será de 1 a cada pasada del loop.

Fíjate que en el for propiamente dicho (y no en el bloque de comandos) no coloqué un signo de pesos ($) antes del i, y la notación para incrementar (i++) es diferente de la que vimos hasta ahora. Esto es porque el uso de paréntesis dobles (así como el comando let) llama el interpretador aritmético del Shell, que es más tolerante.

Como me referí al comando let, y sólo para mostrar como funciona, vamos hacer lo mismo, omitiendo sin embargo, la última parte del for, pasándola hacia el bloque de comandos, así ademas veras la versatilidad del for.

$ for ((; i<=9;)) > do > let i++ > echo -n "$i " > done 1 2 3 4 5 6 7 8 9

Observa que el incremento desapareció del cuerpo del for y pasó dentro del bloque de comandos, fíjate también que cuando usé el let, no fue necesario siquiera inicializar la varible $i. Observa los siguientes comandos escritos directamente en el_prompt_ para mostrar lo que acabo de decir:

$ echo $j

$ let j++ $ echo $j 1

O sea, la variable $j ni siquiera existía y en el primero let asumió el valor 0 (cero) para, después del incremento, tener el valor 1.

Fíjate en lo simples que son las cosas:

$ for arq in * > do > let i++ > echo "$i -> $Arq" > done 1 -> ArqDoDOS.txt1 2 -> confuso 3 -> incusu 4 -> listamusica 5 -> listartista 6 -> logado 7 -> musexc 8 -> musicas 9 -> musinc 10 -> muslist 11 -> testefor1 12 -> testefor2

- Y hasta aqui amigo!, tengo la seguridad que hoy tomaste una buena dosis de jarabe del comando for. Por hoy es suficiente, la próxima vez que nos encontremos hablaremos sobre otras instruciones de loop, pero me gustaria que hasta entonces, hicieses un pequeño script para contar la cantidad de palabras de un archivo texto, cuyo nombre sería recibido por parámetro.

OBS: Esa cuenta tiene que ser hecha usando el comando for para que te habitues a su uso. No vale usar o wc -w.

- Chico! Tráeme, por favor la del estribo!

Y no te olvides, cualquer duda o falta de compañia para tomar una cerveza o hasta para hablar mal de los políticos lo único que tienes que hacer es mandarme un e-mail para julio.neves@gmail.com. Voy aprovechar también para mandar mi aviso publicitario: puedes decirle a los amigos que quien quiera hacer un curso nota diez de programación en Shell que mande un e-mail para julio.neves@uniriotec.br para informarse.

Gracias y hasta la próxima

-- HumbertoPina - 20 Oct 2006

Licencia Creative Commons - Reconocimiento y no comercial (CC) 2009 Por Visitantes del Bar de Júlio Neves.
Todo el contenido de esta página puede ser usada de acuerdo a la Creative Commons License: Atribuição-UsoNãoComercial-PermanênciaDaLicença.