Platica en el Bar

Page 1

PLAT PLATICA EN EL BAR BAR El shell de de Linux


PLATICA EN EL BAR Diálogo escuchado entre un Linuxer y un empujador de mouse: - Quién es el Bash? - El Bash es el hijo mas nuevo de la familia Shell. - Espera ahí! Quieres volverme loco? Tenía una duda y ahora me dejas con dos! - No, loco ya lo eras antes de aparecer por aqui. Desde que decidiste usar aquél sistema operativo con el cual tienes que reiniciar tu máquina unas diez veces por dia y no tienes dominio ninguno sobre lo que está pasando en el computador. Pero deja eso de lado, te voy a explicar lo que es el Shell y los componentes de su familia y al final de la explicación me dirás: "Mi Dios del Shell! Porque no opté antes por Linux?".

El Ambiente Linux Para que entiendas lo que es y como funciona el Shell, primero te mostraré como funciona el ambiente en capas de Linux. Da una mirada atenta en el gráfico que sigue:

En este gráfico se ve que la capa de hardware es la mas profunda estando formada por los componentes físicos de tu computador. Envolviendo a ésta, viene la capa del kernel


que es el corazón de Linux, su núcleo, y es quien hace que el hardware funcione, efectuando su manejo y control. Los programas y comandos que envuelven el kernel, lo utilizan para realizar las tareas especificas para las cuales fueron desarrolladas. Encerrando todo eso viene el Shell que tiene este nombre porque en ingles, Shell significa concha, envoltura, o sea que, queda entre los usuarios y el sistema operativo, de forma que todo lo que interacciona con el sistema operativo, tiene que pasar por su filtro.

El Ambiente Shell Bueno, si para llegar al núcleo de Linux, o sea al kernel que es lo que le interesa a cualquier aplicacion, es necesario el filtro del Shell, vamos entonces a entender como funciona este, y la forma de sacar el mayor provecho de las innúmerables facilidades que él nos ofrece. El Linux por definición es un sistema multiusuario - no podemos nunca olvidar ésto – y para permitir el acesso de determinados usuarios e impedir la entrada de otros, existe un archivo llamado /etc/passwd que además de proveer datos para esta función, especie de "guardián de puerta" del Linux, también pasa información para el login de aquellos que consiguieron pasar por esta primera barrera. El último campo de sus registros, informa al sistema cual es el_Shell_ que la persona va a recibir cuando se "loguee" (Ajjjjj!!!). Cuando dije que el último campo del /etc/passwd informa al sistema cual es el Shell que el usuario va a recibir al "loguearse", es para ser interpretado literalmente, o sea, si en este campo de su registro está prog, la persona al acceder al sistema recibirá la pantalla de ejecución del programa prog y al terminar la ejecución saldrá inmediatamente con un logout. Imagina cuanto podemos aumentar la seguridad con este simples truco. Te acuerdas que te mencioné de la familia Shell? Exactamente, vamos a comenzar a entender esto: el Shell, que se vale de la imagen de una concha envolviendo el sistema operativo propiamente dicho, es el nombre genérico para tratar los hijos de esta idea que, con el correr de los años de existencia del sistema operativo Unix fueron apareciendo. Actualmente existen diversos “sabores” de Shell, entre ellos destaco el sh (Bourne Shell), el ksh (Korn Shell), bash (Bourne Again Shell) y el csh (C Shell).

Una visión rápida em los Principales Sabores de Shell Bourne Shell (sh) Desarrollado por Stephen Bourne de la Bell Labs (de AT&T donde también fue desarrollado el Unix), este fue durante muchos años el Shell patrón del sistema operativo Unix. Es también llamado de Standard Shell por haber sido durante varios años, el único y hasta hoy es el mas utilizado ya que fue transportado para todos los ambientes Unix y distros Linux. Korn Shell (ksh)


Desarrollado por David Korn, también de la Bell Labs, es un superconjunto del sh, o sea, posee todas las facilidades del sh y a ellas se agregaron muchas otras. La compatibilidade total con el sh esta atrayendo a muchos usuarios y programadores de Shell para este ambiente. Boune Again Shell (bash) Este es el Shell mas moderno y cuyo número de adeptos crece mas en todo el mundo, sea por ser el Shell default de Linux, su sistema operativo natural, o sea por su gran diversidad de comandos, que incorpora inclusive diversas instrucciones características del C Shell. C Shell (csh) Desarrollado por Bill Joy de la Berkley University es el Shell mas utilizado en ambientes *BSD e Xenix. La estrutura de sus comandos es bastante similar al del lenguage C. Su gran pecado fue ignorar la compatibilidad con el sh, partiendo por un camino propio. Además de estos Shells existen otros, pero contigo voy a hablar solamente sobre los tres primeros, tratandolos genéricamente por Shell y señalando las peculiaridades de cada uno que eventualmente tengan.

Explicando el funcionamento de Shell El Shell es el primer programa al que accedes al "loguearte" en Linux. Es él quien va a resolver una cantidad de cosas para no cargar al kernel con tareas repetitivas, aliviandolo para tratar asuntos mas importantes. Como cada usuario posee su propio Shell interponiendose entre él y el Linux, es el Shell quien interpreta los comandos que son tecleados y examina sus sintaxis, pasándolos desmenuzados para su ejecución. - Alto ahí, no corra tanto! Ese trabajo de interpretar comandos no tiene nada que ver con intérprete, no es cierto? - Tiene que ver si, la verdad el Shell es un interpretador (o intérprete realmente) que trae consigo un poderoso lenguaje con comandos de alto nivel, que permite la construcción de loops (lazos), tomas de decisión y de almacenamiento de valores en variables, como te voy mostrar ahora. Voy a explicarte las principales tareas que el Shell cumple, por su orden de ejecución. Prestale mucha atención a este orden porque es fundamental para comprender el resto de nuestra conversación. Exámen de la Línea de Comandos En este exámen el Shell identifica los caracteres especiales (reservados) que tienen significado para la interpretación de la línea, inmediatamente después verifica si la línea pasada es una asignación o un comando. Asignación


Si el Shell encuentra dos campos separados por un símbolo de igual (=) sin espacios en blanco entre ellos, identifica esta secuencia como una asignación. Exemplos $ ls linux linux

En este ejemplo el Shell identificó el ls como un programa y el linux como un parámetro pasado para el programa ls. $ valor=1000

En este caso, por no haber espacios en blanco (y así identificamos que el espacio en blanco es un de los caracteres reservados) el Shell identificó una asignación y colocó 1000 en la variable valor. Nunca Haga: $ valor = 1000 bash: valor: not found

Aqui el Bash tomo la palabra valor aislada por espacios en blanco y juzgó que tu habías mandado ejecutar un programa llamado valor, al cual estarías pasando dos parámetros: = e 1000. Comando

Cuando se escribe una línea en el prompt de Linux, esta es dividida en pedazos separados por espacios en blanco: el primer pedazo es el nombre del programa y su existencia sera comprobada; identifica en seguida, en este orden: opciones/parámetros, redireccionamentos y variables. Si el programa identificado existe, el Shell verifica los permisos de los archivos involucrados (inclusive el propio programa), dando un señal de error en caso de que tu no estés autorizado a ejecutar esta tarea. Resolución de Redireccionamentos

Después de identificar los componentes de la línea que tecleaste, el Shell parte para la resolución de redireccionamentos. El Shell tiene incorporado a su elenco de ventajas lo que llamamos el redireccionamento, que puede ser de entrada (stdin), de salida (stdout) o de errores (stderr), de acuerdo a como te explicaré a continuación. Substitución de variables

En este punto, el Shell verifica si las eventuales variables (parámetros comenzados por $), encontradas en el campo del comando, están definidas y las substituye por sus valores actuales.


Substitución de Meta caracteres

Si algún metacaracter (*, ? ou []) es hallado en la línea de comando, es aquí que será substituido por sus posibles valores. Suponiendo que el único archivo que comienza por la letra n en su actual directorio sea un directorio llamado nombregrandeparacaramba, si tu haces: $ cd n*

Como hasta aquí quien está trabajando su línea es el Shell y el comando (programa) cd todavía no fue ejecutado, el Shell transforma el n* en nombregrandeparacaramba y el comando cd será ejecutado con éxito. Pasa línea de Comando para el kernel

Completadas las anteriores tareas, el Shell monta la línea de comandos, ya con todas las substituciones hechas, llama el kernel para ejecutarla en un nuevo Shell (Shell hijo), recibiendo un número de proceso (PID o Process IDentification) y permanece inactivo, durmiendo una siestecita, durante la ejecución del programa. Una vez finalizado este proceso (junto con el Shell hijo), recibe nuevamente el control y, exhibiendo un prompt, muestra que está listo para ejecutar otros comandos.

Descifrando la Piedra de Roseta Para sacarte aquella sensación que tienes cuando ves un script Shell, que mas parece una sopa de letras o un jeroglífico, te mostraré los principales caracteres especiales para que puedas salir por ahí como el Jean-François Champollion descifrando la Piedra de Roseta (vale la pena dar una "googlada" para descubrir quién es este tipo). caracteres que cambian el significado Por eso mismo, cuando deseamos que el Shell no interprete un carácter especial, debemos "esconderlo" de él. Eso puede hacerse de tres formas distintas: Apóstrofe o plic (')

Cuando el Shell ve una cadena de caracteres entre apostrofes ('), él saca los apostrofes de la cadena y no interpreta su contenido. $ ls linux* linuxmagazine $ ls 'linux*' bash: linux* no such file or directory

En el primer caso el Shell "abrió" el asterisco y descubrió el archivo linuxmagazine para listar. En el segundo, los apostrofes evitaron la interpretación del Shell y dio la respuesta que no existe el archivo linux*. Contrabarra o Barra Invertida (\)


Idéntico a los apostrofes excepto que la barra invertida evita la interpretación del carácter que la sigue solamente. Suponga que accidentalmente has creado un archivo llamado * (asterisco) – que algunos sabores de Unix permiten - y deseas eliminarlo. Si tu hicieras: $ rm *

Tendrías un gran problema, ya que el rm borraría todos los archivos del directorio corriente. La mejor forma de hacerlo es: $ rm \*

De esta forma, el Shell no interpretaría el asterisco, y por consiguiente no haría su abertura Haz la siguiente experiencia científica: $ $ $ $

cd /etc echo '*' echo \* echo *

Viste la diferencia? Entonces no se precisa explicar mas. Aspas (")

Exactamente igual a apostrofe excepto que, si la cadena entre aspas contiene un signo de pesos ($), un acento invertido (`), o una barra invertida (\), estos caracteres serán interpretados por el Shell. No debes preocuparte ahora con todo eso, todavía no te di ejemplos del uso de las aspas porque todavía no conoces el signo de pesos ($) ni el acento grave (`). De aquí en adelante veremos con mucho detalle el uso de estos caracteres especiales, lo mas importante es entender el significado de cada uno. caracteres de redireccionamento La mayoría de los comandos tiene una entrada, una salida y puede generar errores. Esta entrada es llamada Entrada Patrón o stdin y su default es el teclado del terminal. Análogamente, la salida del comando es llamada Salida Patrón o stdout y su default es la pantalla del terminal. Hacia la pantalla también son enviados por default los mensajes de error oriundos del comando, y que en este caso es la llamada Salida de Error Patrón o stderr. Veremos ahora como alterar este estado de cosas. Vamos a hacer un programa tartamudo. Para eso haz: $ cat


El cat es una instrucción que lista el contenido del archivo especificado para la Salida Patrón (stdout). En el caso que la entrada no esté definida, el espera los datos de la stdin. Mira, como yo no especifiqué la entrada, el está esperándola por el teclado (Entrada Patrón) y como tampoco no cité la salida, lo que yo escriba irá hacia la pantalla (Salida Patrón) haciendo de esta forma, como había propuesto un programa tartamudo. Prueba, prueba, el teclado no muerde! Redireccionamento de la Salida Patrón

Para especificar una salida de programa usamos el > (mayor que) o el >> (mayor, mayor) seguido del nombre del archivo al que se desea mandar la salida. Vamos a transformar el programa tartamudo en un editor de textos (que pretensión eh!).

$ cat > Arch

El cat continua sin tener una entrada especificada, por lo tanto está aguardando que los datos sean escritos, sin embargo su salida está siendo desviada hacia el archivo Arch. De esta forma, todo lo que esta siendo escrito está yendo hacia Arch, de forma que hicimos el editor de textos mas corto y pobre del planeta. Si ejecutas nuevamente: $ cat > Arch

Los datos contenidos en Arch se perderán, ya que antes del redireccionamento el Shell creará un Arch vacio. Para colocar mas informaciones en el final del archivo deberias haber hecho: $ cat >> Arch

Como ya te habia dicho, el Shell resuelve la linea y despues manda el comando para su ejecución. Asi, si tu redireccionas la salida de un archivo hacia el mismo, primero el Shell "vacia" este archivo y después manda el comando para su ejecución, y de esta forma acabas de perder el contenido de tu querido archivo. Con esto notamos que el >> (mayor mayor) sirve para incluir texto al final del archivo. Redireccionamento de la Salida de Error Patrón


Así como el default del Shell es recibir los datos del teclado y mandar las salidas hacia la pantalla, los errores también serán enviados hacia la pantalla si tu no especificas hacia donde deverán ser enviados. Para redireccionar los errores usa 2> SalidaDeError. Fijate que entre el número 2 y el símbolo de mayor (>) no existe espacio en blanco.

Presta atención! No confundas >> con 2>. El primero anexa datos al final de un archivo, en cuanto el segundo redirecciona la Salida de Error Patrón (stderr) hacia el archivo que ha sido designado. Esto es importante! Suponte que durante la ejecución de un script tu puedes, o no (dependiendo del rumbo tomado por la ejecución del programa), haber creado un archivo llamado /tmp/seraqueexiste$$. Para no dejar basura en tu disco, al final del script deberías colocar esta línea: $ rm /tmp/seraqueexiste$$

En caso de no existir el archivo, seria enviado hacia la pantalla un mensaje de error. Para que eso no ocurra se debe hacer: $ rm /tmp/seraqueexiste$$ 2> /dev/null

Sobre el ejemplo que acabamos de ver tengo dos consejos a dar: Consejo # 1 El $$ contiene el PID, o sea, el número de su proceso. Como Linux es multiusuario, es de buenos modales anexar siempre el $$ a los nombre de los archivos que serán usados por varias personas para que no haya problemas de propriedad, o sea, en caso que nombrases tu archivo simplemente como seraqueexiste, el primero que lo usase (creándolo entonces) seria su dueño y todos los otros obtendrian un error cuando intentasen grabar algo en él. Para que hagas un test de la Salida de Error Patrón te voy a dar otro ejemplo, escribe directamente en el prompt del Shell: $ ls noexiste bash: noexiste no such file or directory $ ls noexiste 2> archivodeerrores $ $ cat archivodeerrores bash: noexiste no such file or directory


En este ejemplo, vimos que cuando hicimos un ls en noexiste, obtuvimos un mensaje de error. Después, redireccionamos la Salida de Error Patrón hacia archivodeerros y executamos el mismo comando, recibiendo solamente el prompt en la pantalla. Cuando listamos el contenido del archivo para el cual fue redireccionada la Salida de Error Patron, vimos que el mensaje de error habia sido almacenado en él. Haz este test. Dica # 2 - Quién es ese tal de /dev/null? - En Unix existe un archivo fantasma. Llamase /dev/null. Todo lo que es enviado a este archivo desaparece. Se parece a un Agujero Negro. En el caso del ejemplo, como no me interesaba guardar el posible mensaje de error proveniente del comando rm, lo redireccioné para este archivo. Es interesante notar que estos caracteres de redireccionamento son acumulativos, o sea, si en el ejemplo anterior hicieramos: $ ls noexiste 2>> archivodeerrores

el mensaje de error proveniente del ls seria añadido al final de archivodeerrores. Redireccionamento de la Entrada Patrón

Para hacer el redireccionamento de la Entrada Patrón usamos el < (menor que). - Y para que sirve eso? - me vas a preguntar. - Déjame darte un ejemplo que vas a entender rapidito. Supone que quieres mandar un mail a tu jefe. Para el Jefe queremos lo mejor, no es así? entonces, en lugar de escribir el mail dirigiéndolo directamente al prompt de la pantalla de forma de que será imposible la corrección de una frase anterior donde, sin querer, escribiste un "nosotros va", tu editas un archivo con el contenido del mensaje y después de unas quince verificaciones sin constatar errores, decides enviarlo y para eso haces: $ mail jefe < archivoconmailparaeljefe

Tu jefe reciberá entonces el contenido del archivoconmailparaeljefe. Otro tipo de redireccionamento muy loco que el Shell te permite es el llamado here document. Es representado por << (menor menor) y sirve para indicar al Shell que el alcance de un comando comenza en la línea siguiente y termina cuando encuentra una línea cuyo contenido sea unicamente la etiqueta que sigue al símbolo <<. Observa a continuación el fragmento de un script, con una rutina de ftp:


ftp -ivn hostremoto << fimftp user $Usuario $Seña binary get archivoremoto fimftp

En este pedacito de programa tenemos una cantidad de detalles interesantes: •

Las opciones que usé para el ftp (-ivn) sirven para ir listando todo lo que está ocurriendo (—v de verbose), para no preguntar si tienes la seguridad de que deseas transmitir cada archivo (—i de interactive), y finalmente la opción —n sirve para decirle al ftp para que no solicite el usuario y su contraseña, pues estos estarán indicados por la instrucción específica (user); Cuando usé el << fimftp, estaba diciendo lo siguiente para el intérprete: - Mira aqui Shell, no hagas nada a partir de aqui hasta encontrar la etiqueta fimftp. Tu no entenderias nada, ya que son instrucciones específicas del comando ftp y tu no entiendies nada de ftp. Si solo fuera eso, seria simple, pero el mismo ejemplo dá para ver que existen dos variables ($Usuário e $Senha), que el Shell va a resolver antes del redireccionamento. Sin embargo, la gran ventaja de este tipo de construcción es que ella permite que los comandos también sean interpretados dentro del alcance del here document, lo que también contradice lo que acabe de decir. Enseguida explico como funciona esto. No es este el momento todavía, nos faltan herramientas. El comando user es del repertorio de instrucciones del ftp y sirve para pasar el usuario y la contraseña, que habian sido leídos en una rutina anterior a ese fragmento de código y colocados respectivamente en las dos variables: $Usuário y $Seña. El binary es otra instrucción del ftp, que sirve para indicar que la transferencia de archivoremoto será hecha en modo binario, o sea, el contenido del archivo no será interpretado para saber se está en ASCII, EBCDIC, ... El get archivoremoto le dice al ftp que saque ese archivo del hostremoto y lo traiga a nuestro host local. Si fuera para mandar el archivo, usariamos el comando put.

Un error muy frecuente en el uso de labels (como el fimftp del ejemplo anterior) es causado por la presencia de espacios en blanco antes o después del mismo. Estate muy atento en relación a esto, por que este tipo de errores causan muchos dolores de cabeza a los programadores, hasta que son detectados. Acuérdate: un label que se precie, tiene que tener una linea enterita solamente para él. - Está bien, está bien! Ya sé que salí de viaje y entré por los comandos del ftp, escapando de nuestro asunto, que es el Shell, pero como siempre es bueno aprender algo mas y es raro que las personas estén dispuestas para enseñar... Redireccionamento de Comandos


Los redireccionamentos que hablamos hasta aqui siempre se referían a archivos, o sea mandaban hacia archivo, recibían de archivo, simulaban archivo local, ... Lo que veremos a partir de ahora, redirecciona la salida de un comando hacia la entrada de otro. Esto es utilísimo y facilita la vida un montón. Su nome es pipe (que en inglés significa tubo, ya que él envía la salida de un comando directamente como por un caño hacia la entrada del otro) y su representación es una barra vertical (|). $ ls | wc -l 21

El comando ls pasó la lista de archivos para el comando wc, que cuando está con la opción –l cuenta la cantidad de lineas que recibió. De esta forma, podemos afirmar categóricamente que en mi diretorio existían 21 archivos. $ cat /etc/passwd |sort | lp

Esta linea de comandos manda la lista del archivo /etc/passwd hacia la entrada del comando sort. Éste la clasifica y la manda para el lp que es el gerente mandamás del spool de impresión. Caracteres de Ambiente Cuando quieres priorizar una expresión la colocas entre paréntesis, no es así? Puede ser, a causa de la aritmética es normal que pensemos así. Sin embargo, en Shell lo que prioriza realmente son los acentos graves (`) y no los paréntesis. Te voy a dar ejemplos del uso de los acentos graves para que lo entiendas mejor. Si quiero saber cuántos usuarios están "logados" en el computador que administro. Puedo hacer: $ who | wc -l 8

El comando who pasa la lista de usuarios conectados al comando wc –l que cuenta cuantas lineas recibió y lista la respuesta en la pantalla. Ahora bien, en lugar de tener un ocho suelto en la pantalla, lo que quiero es que esté colocado en medio de una frase. Entonces, para mandar frases hacia la pantalla usamos el comando echo, veamos entonces como queda: $ echo "Existen who | wc -l usuarios conectados" Existen who | wc -l usuarios conectados

Epa! Que pasó?, no funcionó! Exactamente, no funcionó y no fue por causa de las aspas que coloqué, sino porque tendria que haber ejecutado el who | wc -l antes del echo. Para resolver este problema, tengo que priorizar esta segunda parte del comando com el uso de los acentos graves, haciéndolo de la siguiente forma:


$ echo "Existen `who | wc -l` usuarios conectados" Existen 8 usuarios conectados

Para eliminar esa cantidad de espacios en blancos antes del 8 que el wc -l produjo, basta sacar las aspas. Así: $ echo Existen `who | wc -l` usuarios conectados Existen 8 usuarios conectados

Como dije antes, las aspas protejen todo lo que está dentro de sus límites, de la interpretación del Shell. Como para el Shell basta un espacio en blanco como separador, esa cantidad de espacios será cambiada por un único espacio, después de haber retirado las aspas (económico, eh?). Antes de hablar sobre el uso de los paréntesis déjame explicar rapidamente acerca del uso del punto y coma (;). Cuando estés en el Shell, siempre debes dar un comando en cada linea. Para agrupar comandos en una misma línea tenemos que separarlos por punto y coma. De esta forma: $ pwd ; cd /etc; pwd; cd -; pwd /home/midir /etc/ /home/midir

En este ejemplo, listé el nombre del directorio corriente con el comando pwd, me cambié para el directorio /etc, nuevamente listé el nombre del directorio y finalmente volví para el directorio donde estaba anteriormente (cd -), listando su nombre. Note que coloqué el punto y coma (;) de todas las formas posibles para mostrar que no importa si existen espacios en blanco antes o después de este caracter. Finalmente veamos el caso de los paréntesis. Observa el caso siguiente, muy parecido al ejemplo anterior: $ (pwd ; cd /etc ; pwd;) /home/midir /etc/ $ pwd /home/midir

- Qué es eso, por amor del Shell!? Yo estaba en /home/midir, me cambié para el verifiqué que estaba en ese directorio con el pwd seguiente y cuando el agrupamiento de comandos terminó, vi que continuaba en el /home/midir, como si nunca hubiera salido de alli! /etc,

- Ah! Será que es cosa de magos? - No me confundas, amigo!! No es nada de eso! Lo interesante del uso de paréntesis es que se llama a un nuevo Shell para ejecutar los comandos que están en su interior. De


esta forma, realmente fuimos para el directorio /etc, sin embargo cuando todos los comandos dentro de los paréntesis fueron ejecutados, el nuevo Shell que estaba en el directorio /etc murió y volvimos al Shell anterior cuyo directorio corriente era /home/midir. Haz otras pruebas usando cd, y ls para que dejes este concepto bien cimentado. Ahora que ya conocemos estos conceptos observemos este ejemplo que sigue: $ > > > > > > > > >

mail apoyo << FIM Hola apoyo, hoy a las ‘date "+%H:%M"‘ ocurrió nuevamente aquel problema que ya habia informado por teléfono. De acuerdo con su pedido ahí va una lista de los archivos del directorio: ‘ls —l‘ Abrazos a todos. FIM

Finalmente ahora tenemos el conocimiento necesario para mostrar lo que habiamos conversado sobre here document. Los comandos entre acentos graves (`) tendrán prioridad y por tanto el Shell los ejecutará antes de la instrucción mail. Cuando el apoyo reciba el e-mail, verá que los comandos date y ls fueron ejecutados inmediatamente antes del comando mail, recibiendo entonces una fotografía del ambiente en el momento en que la correspondencia fue enviada. El prompt primario default del Shell, como vimos, es el símbolo de pesos ($), sin embargo el Shell usa el concepto de prompt secundario, o de continuación de comando, que es enviado para la pantalla cuando hay un quiebro de linea y la instrucción no terminó. Ese prompt, es representado por un símbolo de mayor (>), que vemos precediendo a partir de la 2ª linea del ejemplo. Para finalizar y mezclarlo todo, debo decir que existe una construcción mas moderna que viene siendo utilizada como forma de priorizar la ejecución de comandos, como hacen los acentos graves (`). Son las construcciones del tipo $(cmd), donde cmd es un (o varios) comando(s) que será(n) ejecutado(s) con prioridad en su contexto. De esta forma, el uso de acentos graves(`) o construcciones del tipo $(cmd) sirven para el mismo fin, sin embargo para quien trabaja con sistemas operativos de diversos fabricantes (multiplataforma), aconsejo el uso de los acentos invertidos, ya que el $(cmd) no fue trasladado para todos los sabores de Shell. Aqui dentro del Bar, usaré las dos formas, indistintamente. Veamos nuevamente el ejemplo dado para los acentos invertidos, ahora con una nueva visión: $ echo Existen $(who | grep wc -l) usuarios conectados Existen 8 usuarios conectados


Mira este caso: $ Archs=ls $ echo $Archs ls

En este ejemplo, hice una asignación (=) y ejecuté una instrucción. Lo que quería era que la variable $Archs, recibiese la salida del comando ls. Como las instrucciones de un script son interpretadas de arriba hacia abajo y de izquierda a derecha, la asignación fue hecha antes de la ejecución del ls. Para hacer lo que deseamos es necesario que le dé prioridad a la ejecución de este comando en perjuicio de la asignación y esto puede ser realizado de cualquiera de las maneras siguientes: $ Archs=`ls`

o: $ Archs=$(ls)

Para cerrar este asunto con broche de oro, vamos a ver un último ejemplo. Digamos que quiero colocar dentro de la variable $Archs la lista detallada (ls -l) de todos los archivos comenzados por arch y seguidos de un único caracter (?). Para eso deberia hacer: $ Archs=$(ls -l arch?)

o: $ Achs=`ls -l arch?`

Sin embargo, ve esto: $ echo $Archs -rw-r--r-- 1 jneves jneves 19 May 24 19:41 arch1 -rw-r--r-- 1 jneves jneves 23 May 24 19:43 arch2 -rw-r--r-1 jneves jneves 1866 Jan 22 2003 archl

- Caramba! salió todo junto! - Eso mismo, como ya te dije, si tu dejas que el Shell “vea” los espacios en blanco, siempre que haya diversos espacios juntos, estos serán cambiados por uno solo. Para que la lista salga bien bonita, es necesario proteger la variable de la interpretación de Shell, así: $ echo "$Archs"


-rw-r--r-- 1 jneves jneves 19 May 24 19:41 arch1 -rw-r--r-- 1 jneves jneves 23 May 24 19:43 arch2 -rw-r--r-- 1 jneves jneves 1866 Jan 22 2003 archl

- Y ahora amigo, ve practicando esos ejemplos, porque, cuando nos encontremos nuevamente, te voy a explicar una serie de instrucciones típicas de programación Shell. Chau! Ahh! Solamente una cosita mas que me estaba olvidando de decirte. En Shell, el símbolo (#) es usado cuando deseamos hacer un comentario.


Me quedo con el grep, tú con la gripe Eso de la gripe es un sólo un decir! Simplemente un pretexto para pedir unas "Caipirinhas" (o caipiriñas, el coctel oficial del carnaval brasileño - vea cómo prepararlas). Volviendo a nuestro tema, te dije que el grep busca cadenas de caracteres dentro de una entrada definida, pero en realidad... que viene a ser una "entrada definida"? Bueno, existen varias formas de definir la entrada del comando grep. Veamos: Buscando en un archivo: $ grep rafael /etc/passwd

buscando en varios archivos: $ grep grep *.sh

Buscando en la salida de un comando: $ who | grep Pelegrino

En el 1º ejemplo, el más simple, busqué la palabra rafael en cualquier lugar del archivo /etc/passwd. Si quisiera buscarla como un login name, o sea, solamente en el principio de los registros de este archivo, debería hacer: $ grep '^rafael' /etc/passwd

Y para que sirve este acento circunflejo y los apostrofes? me vas a preguntar. Si hubieras leído los artículos anteriores sobre expresiones regulares que te dije, sabrías que el circunflejo (^) sirve para limitar la búsqueda al inicio de cada línea, y los apostrofes (') sirven para que el Shell no interprete este circunflejo, dejándolo pasar intacto para el comando grep. Mira que bien! El grep acepta como entrada, la salida de otro comando redireccionado por un pipe (esto es muy común en Shell y es un tremendo acelerador de la ejecución de comandos, ya que actúa como si la salida de un programa fuera guardada en disco y el segundo programa leyera este archivo generado), de esta forma, en el 3º ejemplo, el comando who listó las personas "logadas" en la misma máquina que tú (no te olvides jamás: el Linux es multiusuario) y el grep fue usado para verificar si Pelegrino estaba trabajando o simplemente "haciendo sebo".

La familia grep Este comando grep es muy conocido, pues es usado con mucha frecuencia. Algo que muchas personas desconocen es que existen tres comandos en la familia grep, que son: • •

grep egrep


fgrep

Las principales diferencias entre los 3 son: • •

El grep puede o no, usar expresiones regulares simples, sin embargo en caso de no usarlas, el fgrep es mejor, por ser más rápido; El egrep ("e" de extended, extendido) es muy poderoso en el uso de expresiones regulares. Por ser el más lento de la familia, solo debe ser usado cuando sea necesario la construcción de una expresión regular que no sea aceptada por el grep; El fgrep ("f" de fast, rápido, o de "file", archivo) como el nombre dice, es el rapidito de la familia, ejecuta el servicio de forma muy veloz (a veces es cerca de 30% más rápido que el grep y 50% más que el egrep), sin embargo no permite el uso de expresiones regulares en la búsqueda.

Todo lo que dije arriba sobre la velocidad, solamente se aplica a la familia de comandos grep del Unix. En Linux el grep es siempre más veloz, ya que los otros dos (fgrep y egrep) son scripts en Shell que llaman al primero y, ya me estoy adelantando, no me gusta nada esta solución. - Ahora que ya conoces las diferencias entre los miembros de la familia, dime: que te parecen los tres ejemplos que te dí antes de las explicaciones? - Me pareció que el fgrep resolvería tu problema de forma más veloz que el grep. - Perfecto! Estoy notando que estás bien atento! Estás entendiendo lo que te estoy explicando! Entonces vamos a ver más ejemplos, para aclarar de una vez por todas las diferencias del uso de los miembros de esta familia. Ejemplos Yo sé que en un archivo existe un texto hablando sobre Linux solo que no sé si está escrito con L mayúscula o l minúscula. Puedo hacer la búsqueda de dos formas: $ egrep (Linux | linux) archivo.txt

o $ grep [Ll]inux archivo.txt

En el primer caso, la expresión regular compleja "(Linux | linux)" usa los paréntesis para agrupar las opciones y la barra vertical (|) como un "o" lógico, o sea, estoy buscando Linux o linux.


En el segundo, la expresión regular [Ll]inux significa: comenzando por L o l seguido de inux. Como esta expresión es más simple, el grep consigue resolverla, por lo tanto creo que es mejor usar la segunda forma, ya que el egrep haría la búsqueda más lenta. Otro ejemplo. Para listar todos los subdirectorios del directorio actual, basta hacer: $ ls -l | grep drwxr-xr-x 3 drwxr-xr-x 11 freeciv drwxr-xr-x 3 drwxr-xr-x 3 gnome drwxr-xr-x 2 drwxrwxr-x 14 locale drwxrwxr-x 12 drwxrwxr-x 3 pixmaps drwxr-xr-x 3 scribus drwxrwxr-x 3 sounds drwxr-xr-x 3

'^d' root root

root root

4096 Dec 18 2000 doc 4096 Jul 13 18:58

root root

root root

4096 Oct 17 4096 Aug 8

root root

root root

4096 Aug 8 2000 idl 4096 Jul 13 18:58

root root

root root

4096 Jan 14 4096 Jan 17

root

root

4096 Jul

root

root

4096 Jan 17

2000

root

root

4096 Dec 18

2000 xine

2000 gimp 2000

2000 lyx 2000

2 20:30

En el ejemplo que acabamos de ver, el circunflejo (^) sirvió para limitar la busqueda a la primera posición de la salida del ls detallado. los apostrofes fueron colocados para que el Shell no "viera" el circunflejo (^). Vamos a ver otro. Sabemos que las cuatro primeras posiciones posibles de un ls -l de un archivo común (archivo común!, No directorio, ni link, ni ...) deben ser: Posición

1ª 2ª 3ª

-

Valores Posibles -

r

w

-

-

x s

(suid)

Así, para descubrir todos los archivos ejecutables en un determinado directorio debería hacer: $ ls -la | egrep '^-..(x|s)' -rwxr-xr-x 1 root root -rwxr-xr-x 1 root root rc.local -rwxr-xr-x 1 root root rc.sysinit

2875 Jun 18 19:38 rc 857 Aug 9 22:03 18453 Jul

6 17:28

Donde nuevamente usamos el circunflejo (^) para limitar la búsqueda al inicio de cada línea, entonces las líneas listadas serán las que comienzan por un trazo (-), seguido de


cualquier cosa (el punto cuando es usado como una expresión regular significa cualquier cosa), nuevamente seguido de cualquier cosa, y siguiendo un x o un s. Obtendríamos el mismo resultado si hiciéramos: $ ls -la | grep '^-..[xs]'

y agilizaríamos la búsqueda.

Vamos a montar una "CDteca" Vamos a comenzar a desarrollar programas, me parece que montar un banco de datos de músicas es muy útil y didáctico (y además práctico, en estos tiempos de downloads de mp3 y "quemadores" de CDs). No te olvides que, de la misma forma vamos a desarrollar una cantidad de programas para organizar tus CDs de música, con pequeñas adaptaciones, puedes hacer lo mismo con los CDs de software que vienen con la Linux Magazine y otros que compres o quemes, si compartes este banco de software para todos los que trabajan contigo (el Linux es multiusuario, y como tal debe ser explotado), ganarás muchos puntos con tu adorado jefe. - Un momento! De donde voy la recibir los datos de los CDs? - Inicialmente, te voy a mostrar como tu programa puede recibir parámetros de quién lo esté ejecutando y en breve, te enseñaré a leer los datos por la pantalla o de un archivo.

Pasando parámetros El visual del archivo músicas será el siguiente: nombre del álbum^intérprete1~nombre de la música1:..:intérprete~nombre de la música

o sea, el nombre del álbum será separado por un circunflejo (^) del resto del registro, que está formado por diversos grupos compuestos por el intérprete de cada música del CD y la respectiva música interpretada. Estos grupos son separados entre sí por dospuntos (:) e internamente, el intérprete será separado por una tilde (~) del nombre de la música. Escribiré un programa llamado musinc, que incluirá registros en mi archivo músicas. Pasaré el contenido de cada álbum como parâmetro en la llamada del programa de la siguiente forma: $ musinc "álbum^interprete~música:interprete~música:..."

De esta forma el programa musinc estará recibiendo los datos de cada álbum como si fuera una variable. La única diferencia entre un parámetro recibido y una variable es que los primeros reciben nombres numéricos (nombre numérico suena algo raro, no?). Lo que quise decir es que sus nombres son formados por un y solamente un algarismo), el sea $1, $2, $3, ..., $9. Vamos, antes de todo, hacer un test:


Ejemplos $ cat test #!/bin/bash # Programa para echo "1o. param echo "2o. param echo "3o. param

verificar el pasaje de parámetros -> $1" -> $2" -> $3"

Vamos a ejecutarlo: $ test pasando parámetros para verificar bash: test: cannot execute

Opa! Me olvidé de hacerlo ejecutable. Voy a hacerlo de forma que permita que todos puedan ejecutarlo y en seguida voy a testearlo: $ chmod 755 test $ test pasando parámetros para verificar 1o. param -> pasando 2o. param -> parámetros 3o. param -> para

Repara que la palabra verificar, que sería el cuarto parámetro, no fue listada. Esto sucedió justamente porque el programa test solo listaba los tres primeros parámetros. Vamos ejecutarlo de otra forma: $ test "pasando parámetros" para verificar 1o. param -> pasando parámetros 2o. param -> para 3o. param -> verificar

Las comillas no dejaron que el Shell viese el espacio en blanco entre las palabras y las consideró como un único parámetro.

Observaciones sobre parámetros Ya que estamos hablando de pasar parámetros observa bien lo siguiente: Significado de las Principales Variables Referentes a los Parámetros $* Contiene el conjunto de todos los parámetros (muy parecido con $@) Variable Significado $0 Contiene el nombre del programa $# Contiene la cuantidad de parámetros pasados Ejemplos


Vamos a alterar el programa test para usar las variables que acabamos de ver. Vamos hacerlo así: $ cat test #!/bin/bash # Programa para verifivar el paso de parámetros (2a. Versao) echo El programa $0 recibió $# parámetros echo "1o. param -> $1" echo "2o. param -> $2" echo "3o. param -> $3" echo Todos de una sola \"bolada\": $*

Repare que antes de las comillas usé una barra invertida para esconderlas de la interpretación del Shell (si no usase las contrabarras las comillas no aparecerian). Vamos a ejecutarlo: $ test pasando parámetros para verificar El programa test recibió 4 parámetros 1o. param -> pasando 2o. param -> parámetros 3o. param -> para Todos de una sola "bolada": pasando parámetros para verificar

Como ya dije, los parámetros reciben números de 1 a 9, pero eso no significa que no puedo usar más de 9 parámetros, significa solamente que solo puedo direccionar 9. Vamos a verificar eso: Ejemplo: $ cat test #!/bin/bash # Programa para verificar el pasaje de parámetros (3a. Versión) echo El programa $0 recebió $# parámetros echo "11o. param -> $11" shift echo "2o. param -> $1" shift 2 echo "4o. Param -> $1"

Vamos a ejecutarlo: $ test pasando parámetros para verificar El programa test recibió 4 parámetros que son: 11o. param -> pasando1 2o. param -> parámetros 4o. param -> verificar


Dos cosas muy interesantes en este script: 1. Para mostrar que los nombres de los parámetros varían de $1 a $9 hice un echo $11 y que pasó? El Shell interpretó como que era $1 seguido del algarismo 1 y listó pasando1; 2. El comando shift cuya sintáxis es shift n, pudiendo el n asumir cualquier valor numérico (sin embargo su default es 1, como en el ejemplo dado), desprecia los n primeros parámetros, devolviendo el parámetro de orden n+1, el primero o sea, el $1. Bueno, ahora que ya sabes más sobre pasar parámetros que yo mismo, vamos a volver a nuestra "CDteca" para hacer el script que incluira los CDs en mi banco llamado musicas. El programa es muy simple (como todo en Shell) y voy a listarlo para que lo veas: Ejemplos $ cat musinc #!/bin/bash # Incluye CDs (versión 1) # echo $1 >> musicas

El script es fácil y funcional, me limito a añadir al final del archivo musicas el parámetro recibido. Vamos a incluir 3 álbumes para ver si funciona (y para no hacerlo muy aburrido, voy a suponer que en cada CD existem solamente 2 músicas): $ musinc "album 3^Artista5~Musica5:Artista6~Musica5" $ musinc "album 1^Artista1~Musica1:Artista2~Musica2" $ musinc "album 2^Artista3~Musica3:Artista4~Musica4"

Muestro ahora el contenido de musicas. $ cat album album album

musicas 3^Artista5~Musica5:Artista6~Musica6 1^Artista1~Musica1:Artista2~Musica2 2^Artista3~Musica3:Artista4~Musica4

No es tan funcional como esperaba que quedase... podía haber quedado mejor. Los álbumes están fuera de orden, dificultando la búsqueda. Vamos a alterar nuestro script y después a probarlo nuevamente: $ cat musinc #!/bin/bash # Incluye CDs (versión 2) # echo $1 >> musicas sort musicas -o musicas


Vamos a incluir un álbum más: $ musinc "album 4^Artista7~Musica7:Artista8~Musica8"

Ahora vamos a ver lo que pasó con el archivo musicas: $ cat album album album album

musicas 1^Artista1~Musica1:Artista2~Musica2 2^Artista3~Musica3:Artista4~Musica4 3^Artista5~Musica5:Artista6~Musica5 4^Artista7~Musica7:Artista8~Musica8

Simplemente incluí una línea que clasifica el archivo musicas dándole la salida sobre si mismo (para eso sirve la opción -o), después de que cada álbum fue incluído. Opa! Ahora está quedando bien y casi funcional. Pero atención, no te desesperes! Esta no es la versión final. El programa quedará mucho mejor y más amigable, en una nueva versión que haremos después que aprendamos la adquirir los datos de la pantalla y a formatear la entrada Ejemplos Usar el comando cat para listar no es una buena idea, vamos a hacer un programa llamado=muslist=, para listar un álbum cuyo nombre será pasado como un parámetro: $ cat muslist #!/bin/bash # Consulta CDs (versión 1) # grep $1 musicas

Vamos a ejecutarlo, buscando el album 2. Como ya vimos anteriormente, para pasar la cadena de caracteres album 2 es necesario protegerla de la interpretación del Shell, así él no la interpreta como dos parámetros separados. Vamos a hacerlo de la siguiente forma: $ muslist "álbum 2" grep: can't open 2 musicas: album 1^Artista1~Musica1:Artista2~Musica2 musicas: album 2^Artista3~Musica3:Artista4~Musica4 musicas: album 3^Artista5~Musica5:Artista6~Musica6 musicas: album 4^Artista7~Musica7:Artista8~Musica8

Que desorden! Donde está el error?. Tuve buen cuidado de colocar el parámetro pasado entre comillas, para que el Shell no lo diviera en dos! Si, pero advierte ahora como el grep está siendo ejecutado:


grep $1 musicas

Aunque coloque álbum 2 entre comillas, para que fuera visto como un único parámetro, cuando el $1 fue pasado por el Shell hacia el comando grep, lo transformó en dos argumentos. Así, el contenido final de la línea que el comando grep ejecutó fue el siguiente: grep album 2 musicas

Como la sintáxis del grep es: grep <cadena de caracteres> [arch1, arch2, ..., archn]

el grep entendió que debería recuperar la cadena de caracteres album en los archivos 2 y musicas, Al no existir el archivo 2 generó el error, y como encontro la palabra album en todos los registros de musicas, los listo todos. Siempre que la cadena de caracteres a ser pasada hacia el comando grep posea blancos o TAB, y lo mismo que dentro de variables, colóquela siempre entre comillas para evitar que las palabras después del primer espacio en blanco o TAB sean interpretadas como nombres de archivos. Por otro lado, es mejor ignorar las mayúsculas y minúsculas en la búsqueda. Resolveríamos los dos problemas si el programa tuviera la siguiente forma: $ cat muslist #!/bin/bash # Consulta CDs (versión 2) # grep -i "$1" musicas

En este caso, usamos la opción -i del grep, que como vimos, sirve para ignorar mayúsculas y minúsculas, y colocamos el $1 entre comillas, para que el grep continue viendo la cadena de caracteres resultante de la expansión de la línea por el Shell como un único argumento de búsqueda. $ muslist "album 2" album2^Artista3~Musica3:Artista4~Musica4

Ahora, nota que el grep localiza la cadena buscada en cualquier lugar del registro, entonces de la forma que estamos lo haciendo, podemos buscar por álbum, por música, por intérprete o hasta por un pedazo de cualquiera de estos. Cuando conozcamos los comandos condicionales, montaremos una nueva versión de muslist que permitirá especificar por que campo buscar. Ahora me vas a decir: - Si, todo bien, pero es muy tedioso tener que colocar el argumento de búsqueda entre comillas cuando tengo que pasar el nombre del álbum. Esta forma no es nada amigable!


- Tienes toda la razón, y es por eso que te voy a mostrar otra forma de hacer lo que me pediste: $ cat muslist #!/bin/bash # Consulta CDs (versión 3) # grep -i "$*" musicas $ muslist album 2 album 2^Artista3~Musica3:Artista4~Musica4

De esta forma, el $*, que significa todos los parámetros, será substituído por la cadena album 2 (de acuerdo con el ejemplo anterior), haciendo lo que tu querias. No te olvides que el problema del Shell no es si él puede o no hacer una determinada cosa. El problema es decidir cuál es la mejor forma de hacerla, ya que para realizar cualquier tarea, la cantidad de opciones es enorme. Recuerdas aquel dia de verano fuiste a la playa?, olvidaste el CD en el automóbil, y entonces aquel "solecito" de 40 grados dobló tu CD y ahora necesitas una herramienta para borrarlo del banco de datos. No hay ningún problema, vamos a desarrollar un script llamado musexc, para excluir estos CDs. Antes de desarrollar el programa te quiero presentar una opción bastante útil de la familia de comandos grep. Es la opción -v, que cuando es usada, lista todos los registros de la entrada, excepto el(los) localizado(s) por el comando. Veamos: Ejemplos $ grep -v "album 2" musicas album 1^Artista1~Musica1:Artista2~Musica2 album 3^Artista5~Musica5:Artista6~Musica6 album 4^Artista7~Musica7:Artista8~Musica8

De acuerdo con lo que te expliqué antes, el grep del exemplo listó todos los registros de músicas excepto los referentes al album 2, porque atendía al argumento del comando. Estamos entonces preparados para desarrollar un script para retirar aquél CD doblado de tu "CDteca". Este script tiene la forma siguiente: $ cat musexc #!/bin/bash # Borra CDs (versión 1) # grep -v "$1" musicas > /tmp/mus$$ mv -f /tmp/mus$$ musicas

En la primera línea mandé hacia /tmp/mus$$ el archivo musicas, sin los registros que tuviesen la consulta hecha por el comando grep. En seguida, moví (que, en realidad, equivale a renombrarlo) /tmp/mus$$ al antiguo musicas.


Usé el archivo /tmp/mus$$ como archivo de trabajo, porque como ya habia citado en el artículo anterior, el $$ contiene el PID (Process Identification o identificación del proceso) y de esta forma cada uno que edite el archivo musicas lo hará en un archivo de trabajo diferente, de esta forma evitamos choques en su uso. - Y entonces, amigo, estos programas que hicimos hasta aquí son muy rústicos en virtud de la falta de herramientas que todavia tenemos. Pero están bien, mientras me tomo otro "chopp", puedes ir para casa a praticar en los ejemplos dados porque, te prometo, llegaremos a desarrollar un sistema bien bonito para el control de tus CDs. - Cuando nos encontremos la próxima vez, te voy a enseñar como funcionan los comandos condicionales y mejoraremos otro poco estos scripts.


Trabajando con cadenas Por el título de arriba no pienses que te voy a enseñar a ser carcelero! Me estoy refiriendo a cadenas de caracteres!

El Comando cut (que no es la central única de trabajadores) Primero te quiero mostrar, de forma eminentemente práctica, una instrucción simple de usar y muy útil: el comando cut, Esta instrucción es usada para cortar un determinado pedazo de un archivo y tiene dos formas distintas de uso: El comando cut con la opción -c Con esta opción, el comando tiene la siguiente sintáxis: cut -c PosIni-PosFin [archivo]

Donde: PosIni = Posición inicial PosFin = Posición final

$ cat números 1234567890 0987654321 1234554321 9876556789 $ cut -c1-5 números 12345 09876 12345 98765 $ cut -c-6 números 123456 098765 123455 987655 $ cut -c4- números 4567890 7654321 4554321 6556789 $ cut -c1,3,5,7,9 números 13579 08642 13542 97568 $ cut -c-3,5,8- números 1235890 0986321 1235321 9875789


Como se puede ver, en realidad existen cuatro sintaxis distintas: en la primera (-c1-5), especifiqué una franja de posiciones, en la segunda (-c-6), especifiqué todas las posiciones hasta una determinada columna, en la tercera (-c4-) de una determinada posición en adelante y en la cuarta (-c1,3,5,7,9), determinadas posiciones. La última (-c-3,5,8-) fue solo para demostrar que lo podemos mezclar todo. El comando cut con la opción -f Pero no pienses que acabó por ahí! Como debes haber notado, esta forma de cut es útil para archivos con campos de tamaño fijo, sin embargo, actualmente lo que más existe son archivos con campos de tamaño variables, donde cada campo termina con un delimitador. Vamos a dar una ojeada al archivo musicas que comenzamos a preparar en nuestra conversación de la última vez que estuvimos aquí, en el bar. $ cat album album album album

musicas 1^Artista1~Musica1:Artista2~Musica2 2^Artista3~Musica3:Artista4~Musica4 3^Artista5~Musica5:Artista6~Musica5 4^Artista7~Musica7:Artista8~Musica8

Entonces, recapitulando, su "layout" es el siguiente: nombre del album^intérprete1~nombre de la música1:...:intérpreten~nombre de la músican

O sea, el nombre del álbum será separado por un circunflejo (^) del resto del registro, que está formado por diversos grupos, compuestos por el intérprete de cada música del CD y la respectiva música interpretada. Estos grupos son separados entre sí por dos puntos (:) e internamente, el nombre del intérprete será separado por una tilde (~), del nombre de la música. Entonces para sacar los datos referentes a todas las segundas músicas del archivo debemos hacer:

musicas,

$ cut -f2 -d: musicas Artista2~Musica2 Artista4~Musica4 Artista6~Musica5 Artista8~Musica8

O sea, cortamos el segundo campo (-f de field en inglés), delimitado (-d) por dos puntos (:). En cambio, se quisiéramos solamente los intérpretes, podriamos hacer: $ cut -f2 -d: musicas | cut -f1 -d~ Artista2 Artista4 Artista6 Artista8


Para entender esto, vamos a sacar la primera línea de musicas: $ head -1 musicas album 1^Artista1~Musica1:Artista2~Musica2

Ahora observa lo que ocurrió: Delimitador del primer cut (:) album 1^Artista1~Musica1:Artista2~Musica2

De esta forma, en el primer cut, el primer campo del delimitador (-d) dos puntos (:), es album 1^Artista1~Musica1 y el segundo, que es lo que nos interesa, es Artista2~Musica2. Vamos ahora a ver lo que pasó con el segundo cut: Nuevo delimitador (~) Artista2~Musica2

Ahora, el primer campo del delimitador (-d) tilde (~), que es el que nos interesa, es Artista2 y el segundo es Musica2. Si el razonamiento que hicimos para la primera línea fuera aplicado al resto del archivo, llegaríamos a la respuesta anteriormente dada.

Si hay cut hay paste Como ya era de esperar, el comando paste sirve para pegar, sólo que aquí en Shell lo que pega son archivos. Para empezar a entenderlo, vamos a hacer esto:: paste arch1 arch2

De esta forma el comando mandará hacia la salida patrón (stdout) cada uno de los registros de arch1, al lado de los registros de arch2 correspondientes y en caso de que no se especifique ningún delimitador, usará por default el <TAB>. El paste es un comando poco usado por que su sintaxis es poco conocida. Vamos a jugar con 2 archivos creados de la siguiente forma: $ seq 10 > enteros $ seq 2 2 10 > pares

Para ver el contenido de los archivos creados, vamos a usar el paste en su forma simple que mostramos arriba: $ paste enteros pares


1 2 3 4 5 6 7 8 9 10

2 4 6 8 10

Quién está de pié, se acuesta. Ahora vamos a transformar la columna del pares en línea: $ paste -s pares 2 4 6

8

10

Usando separadores Como ya fue dicho, el separador default del paste es el <TAB>, pero eso puede ser alterado con la opción -d. Entonces para calcular la suma del contenido de pares primeramente haríamos: $ paste -s -d'+' pares # también podría ser -sd'+' 2+4+6+8+10

y después pasaríamos esta línea con pipe (|) hacia la calculadora (bc), y entonces quedaría: $ paste -sd'+' pares | bc 30

De esta forma, para calcular el factorial del número contenido en $Num, basta hacer: $ seq $Num | paste -sd'*' | bc

Con el comando paste tu también puedes montar formatos exóticos como el siguiente: $ ls | paste -s -d'\t\t\n' arch1 arch2 arch3 arch4 arch5 arch6

Lo que pasó fue lo siguiente: se le especifico al comando paste que tendría que transformar líneas en columnas (por la opción -s) y que sus separadores (si...! acepta más de uno, pero solamente uno después de cada columna creada por el comando)


serían un <TAB>, otro <TAB> y un <ENTER>, generando de esta forma su salida tabulada en 3 columnas. Ahora que ya entendiste esto, observa como hacer lo mismo, pero de forma más fácil, menos extraño y primitivo, usaremos el mismo comando pero con la siguiente sintaxis: $ ls | paste - - arch1 arch2 arch3 arch4 arch5 arch6

Y esto sucede porque si en lugar de especificar los archivos, colocamos el signo de menos (-), el comando paste los substituye por la salida o entrada patrón conforme al caso. En el ejemplo anterior los datos fueran mandados hacia la salida patrón (stdout), porque el pipe (|) estaba desviando la salida del ls hacia la entrada patrón (stdin) del paste, pero veamos el ejemplo siguiente: $ cat arch1 predisposición privilegiado profesional $ cat arch2 ver mario motor $ cut -c-3 arq1 | paste -d "" - arq2 prever primario promotor

En este caso, el cut devolvió las tres primeras letras de cada registro de arch1, el paste fue montado para no tener separador (-d"") y recibir la entrada patrón (desviada por el pipe) en el trazo (-), generando la salida junto con arch2.

El Comando tr Otro comando muy interesante es el tr que sirve para substituir, comprimir o retirar caracteres. Su sintaxis sigue el siguiente patrón: tr [opciones] cadena1 [cadena2]

El comando tr copia el texto de la entrada patrón (stdin) y cambia, las veces que encuentre, los caracteres de cadena1 por el correspondiente contenido de la cadena2, o cambia las múltiples coincidencias de los caracteres de cadena1 por solamente un carácter, o todavía puede hacer mas, puede eliminar los caracteres de la cadena1. Las principales opciones del comando son:

-d

Principales Opciones del comando tr Elimina del archivo los caracteres de la cadena1


Opción -s

Principales Opciones del comando tr Significado Comprime n coincidencias de la cadena1 en sólo una

Cambiando caracteres con tr Primero te voy a dar un ejemplo bien bobo: $ echo bobo | tr o a baba

O sea, cambié todas las coincidencias de la letra o por la letra a. Suponte que en un determinado punto de mi script, pido al operador que teclee s o n (si o no), y guardo su respuesta en la variable $Resp. El contenido de $Resp puede estar con letras mayúsculas o minúsculas, y de esta forma tendría que hacer diversos tests para saber si la respuesta dada fue S, s, N o n. Entonces lo mejor es hacer: $ Resp=$(echo $Resp | tr SN sn)

y despues de ejecutar este comando tendría la seguridad de que el contenido de $Resp seria un s o un n. Si mi archivo ArchEnt está todo escrito con letras mayúsculas y deseo pasarlas para minúsculas hago: $ tr A-Z a-z < ArchEnt > /tmp/$$ $ mv -f /tmp/$$ ArchEnt

Observa que en este caso usé la notación A-Z para no tener que escribir ABCD...YZ. Otro tipo de notación que puede ser usada son las escape sequences (preferiría escribir en español, pero en este caso como lo traduciría? Secuencias de escape? Medio sin sentido, no te parece? Pero continuemos...) que también son reconocidas por otros comandos y también en lenguaje C, y cuyo significado verás a continuación: Escape Sequences \\ Una barra invertida Secuencia Significado \t Tabulación \n Nueva línea \v Tabulación Vertical \f Nueva Página \r Início de línea <^M>

\0134 Octal \011 \012 \013 \014 \015


Sacando caracteres con tr Dejame contarte un "causo": un alumno que estaba enojado conmigo, decidió complicarme la vida y en un ejercicio práctico que pasé para ser hecho en el computador, y que valía para nota, me entregó el script con todos los comandos separados por punto y coma (recuerdas que te dije que el punto y coma servía para separar diversos comandos en una misma línea?). Te voy a dar un ejemplo simplificado e idiota de un "chorizo" así: $ cat confuso echo lea Programación Shell Linux de Julio Cezar Neves > libro;cat libro;pwd;ls;rm -f lixo 2>/dev/null;cd ~

Yo ejecutaba el programa y se ejecutaba así: $ confuso lea Programación Shell Linux de Julio Cezar Neves /home/jneves/LM confuso livro musexc musicas musinc muslist numeros

Pero nota de prueba es cosa seria (y billete de dólar todavia más ) entonces, para entender lo que el alumno habia hecho, lo llamé y delante suyo ejecuté el siguiente comando: $ tr ";" "\n" < confuso echo lea Programación Shell Linux de Julio Cezar Neves pwd ls rm -f lixo 2>/dev/null cd ~

El alumno se quedó muy triste, porque en 2 o 3 segundos le deshice la broma en la había perdido varias horas. Ahora fíjate bien! Si yo tuviera una máquina con Unix, habria hecho lo siguiente: $ tr ";" "\012" < confuso

X(com)primiendo con tr Observa ahora la diferencia entre los dos comandos date: el que hice hoy y el otro que fue ejecutado hace dos semanas: $ date # Hoy Sun Sep 19 14:59:54 2006 $ date # Hace dos semanas Sun Sep 5 10:12:33 2006


Para ver la hora debería hacer: $ date | cut -f 4 -d ' ' 14:59:54

Sin embargo, dos semanas antes ocurriría lo siguiente: $ date | cut -f 4 -d ' ' 5

Ahora observa porqué: $ date Sun Sep

# Hace dos semanas 5 10:12:33 2004

Como puedes notar, existen 2 caracteres en blanco antes del 5 (día), esto lo confunde todo porque el tercer pedazo está vacio y el cuarto es el día (5). Entonces lo ideal sería comprimir los espacios en blanco sucesivos en solamente un espacio, para poder tratar las dos cadenas resultantes del comando date de la misma forma, y eso se hace así: $ date | tr -s " "a Sun Sep 5 10:12:33 2004

Como puedes ver, no existen los dos espacios, Entonces ahora podria cortar: $ date | tr -s " " | cut -f 4 -d " " 10:12:33

Viste como el Shell ya está solucionando problemas! Observa este archivo que fue bajado de una máquina con aquél sistema operativo que sufre de todos los vírus: $ cat -ve ArqDoDOS.txt Este archivo^M$ fue generado por^M$ DOS/Rwin y fue^M$ bajado por un^M$ ftp mal hecho.^M$

y ahora te quiero dar dos consejos: Consejo #1 - La opción -v del cat muestra los caracteres de control invisibles, con la notación ^L, donde ^ es la tecla control y L es la respectiva letra. La opción -e muestra el final de la línea como un signo de pesos ($).


Consejo #2 - Esto ocurre porque en formato DOS (o rwin), el fin de los registros está formado por un carriage-return (\r) y un line-feed (\n). En Linux sin embargo, el final del registro tiene solamente el line-feed. Vamos entonces a limpiar este archivo. $ tr -d '\r' < ArchDeDOS.txt > /tmp/$$ $ mv -f /tmp/$$ ArchDeDOS.txt

Ahora vamos a ver lo que pasó: $ cat -ve ArchDeDOS.txt Este archivo$ fue generado por el$ DOS/Rwin y fue$ bajado por un$ ftp mal hecho.$

Bien, la opción -d del tr retira los caracteres especificados de todo el archivo. De esta forma retiré los caracteres no deseados, grabandolo en un archivo de trabajo temporal y posteriormente lo renombré con su nombre original. Obs: En Unix debería hacer: $ tr -d '\015' < ArchDeDOS.txt > /tmp/$$

Esto pasó porque el ftp fue hecho de modo binario (o image), o sea, sin la interpretación del texto. Si antes de la transmisión del archivo hubiera sido estipulada la opción ascii del ftp, esto no habría ocurrido. - Mira, después de este consejo, estoy comenzando a disfrutar de ese tal Shell, pero todavia hay muchas cosas que no consigo hacer. - Claro!, si hasta aqui no te hablé casi nada sobre programación en Shell, tenemos muchas cosas aun por avanzar, sin embargo, con lo que aprendiste, ya te da para resolver muchos problemas, hasta que tú adquieras el “modo Shell de pensar”. Serías capaz de hacer un script para decirme quienes son las personas que están “logadas” desde hace más de un dia en tu servidor? - Claro que no! Para eso seria necesario que conociera los comandos condicionales que todavia no me explicaste como funcionan. - Dejame intentar cambiar un poco tu lógica y atraerla hacia el “modo Shell de pensar”, pero antes es mejor tomar un chope... Chico!, traeme otros dos...


- Ahora que ya moje el gaznate, vamos a resolver el problema que te propuse. Presta atención a como funciona el comando who: $ who jneves rtorres rlegaria lcarlos

pts/1 pts/0 pts/1 pts/3

Sep Sep Sep Sep

18 20 20 20

13:40 07:01 08:19 10:01

Y mira también el date: $ date Mon Sep 20 10:47:19 BRT 2004

Ves que el mes y el dia están en el mismo formato en ambos comandos? Algunas vezes un comando tiene la salida en portugués y el otro en inglés. Cuando eso pase, puedes usar el siguiente artificio: $ date Mon Sep 20 10:47:19 BRT 2004 $ LANG=pt_BR date Seg Set 20 10:47:19 BRT 2004 Y así pasas la salida del comando date hacia portugués, o hacia cualquier otro idioma que quieras. Entonces, si en algún registro del who no encuentro la fecha de hoy, significa que el individuo está "logado" hace más de un día, ya que él no puede haberse "logado" mañana... Vamos a guardar el pedazo que importa de la fecha de hoy para buscarla en la salida del who: $ Fecha=$(date | cut -c 5-10)

Aquí usé la construcción $(...), para dar prioridad a la ejecución de los comandos antes de atribuir a su salida a la variable $Fecha. Vamos a ver si funcionó: $ echo $Fecha Sep 20

Muy bien! Ahora, lo que tenemos que hacer es buscar con el comando who los registros que no poseen esta fecha. - Ah! Me parece que estoy entendiendo! Ahora que mencionaste lo de buscar, se me ocurrió el comando grep, acerté? - Correctísimo! Solo que tengo que usar el grep con aquella opción que solamente lista los registros en los quales no encontró la cadena. Te acuerdas que opción es esa? - Claro, es la opción -v...


- Eso mismo! Estás quedando un lujo! Entonces vamos a ver: $ who | grep -v "$Fecha" jneves pts/1 Sep 18 13:40

- Y si quisiera un poco mas de adornos,haría así: $ who | grep -v "$Fecha" | cut -f1 -d ' ' jneves

- Te diste cuenta? No fue necesario usar ningún comando condicional, porque además nuestro comando condicional más usado, el famoso if, no verifica condición sino instrucciones, como veremos ahora.

Comandos Condicionales Observa las líneas de comando que siguen: $ ls musicas musicas $ echo $? 0 $ ls ArchInexistente ls: ArchInexistente: No such file or directory $ echo $? 1 $ who | grep jneves jneves pts/1 Sep 18 13:40 (10.2.4.144) $ echo $? 0 $ who | grep juliana $ echo $? 1

- y que hace ese $? por ahí? Comenzando por pesos ($) parece ser una variable, correcto? - Si, es una variable que contiene el código de salida de la última instrucción ejecutada. Te puedo garantizar que si esta instrucción fué bien ejecutada, $? tendrá el valor cero, en caso contrario su valor será diferente de cero.

El Comando if Lo que nuestro comando condicional if hace es testar la variable $?. Veamos entonces a ver su sintaxis: if cmd then cmd1 cmd2


cmdn else cmd3 cmd4 cmdm fi

o sea: en caso que el comando cmd haya sido ejecutado con éxito, los comandos del bloque del then (cmd1, cmd2 y cmdn) serán ejecutados, en caso contrario, los comandos ejecutados serán los del bloque opcional del else (cmd3, cmd4 y cmdm), terminando con un fi. Vamos a ver en la prática como funciona eso usando un scriptisiño que sirve para incluir usuários en el /etc/passwd: $ cat incusu #!/bin/bash # Versión 1 if grep ^$1 /etc/passwd then echo Usuario \'$1\' ya existe else if useradd $1 then echo Usuario \'$1\' incluído en /etc/passwd else echo "Problemas en el catastro. Usted es root?" fi fi

Nota que el if está verificando diretamente el comando grep y ésta es su finalidad. En caso de que el if sea exitoso, o sea, el usuário (cuyo nombre está en $1) fuera encontrado en /etc/passwd, los comandos del bloque del then serán ejecutados (en este ejemplo es solamente el echo) y en el caso contrario, las instrucciones del bloque del else son las que serán ejecutadas, entonces un nuevo if verifica si el comando useradd fué bien ejecutado , creando el registro del usuario en /etc/passwd, o no, y es entonces cuando dará el mensaje de error. Veamos su ejecución, primero pasando un usuario ya existente: $ incusu jneves jneves:x:54002:1001:Julio Neves:/home/jneves:/bin/bash Usuario 'jneves' ya existe

Como ya vimos diversas veces, pero siempre es bueno insistir en el tema para que te quede claro, en el ejemplo anterior surgió una línea no deseada, esta es la salida del comando grep. Para evitar que eso pase, debemos desviar la salida de esta instrucción para /dev/null, quedando así: $ cat incusu #!/bin/bash # Versión 2


if grep ^$1 /etc/passwd > /dev/null # o: if grep -q ^$1 /etc/passwd then echo Usuario \'$1\' ya existe else if useradd $1 then echo Usuario \'$1\' incluído en /etc/passwd else echo "Problemas en el catastro. Usted es root?" fi fi

Ahora vamos a verificarlo, pero como usuario normal (no root): $ incusu JuanNadie ./incusu[6]: useradd: not found Problemas en el catastro. Usted es root?

Epa!, aquél error no tenia que pasar! Para evitar que eso suceda debemos mandar también la salida de error (strerr, te acuerdas?) del useradd hacia /dev/null, quedando la versión final así: $ cat incusu #!/bin/bash # Versión 3 if grep ^$1 /etc/passwd > /dev/null then echo Usuario \'$1\' ya existe else if useradd $1 2> /dev/null then echo Usuario \'$1\' incluído en /etc/passwd else echo "Problemas en el catastro. Usted es root??" fi fi

Después de estas alteraciones y de hacer un su – (volverme root) veamos su comportamiento: $ incusu perez Usuario 'perez' incluído en /etc/passwd

Y nuevamente: $ incusu perez Usuario 'perez' ya existe


Recuerdas que te dije que a lo largo de nuestras conversaciones y "choppes" nuestros programas irían mejorando? Entonces veamos ahora como podríamos mejorar nuestro programa para incluir músicas: $ cat musinc #!/bin/bash # Incluye CDs (versión 3) # if grep "^$1$" musicas > /dev/null then echo Este álbum ya está incluído else echo $1 >> musicas sort musicas -o musicas fi

Como viste, es una pequeña evolución de la versión anterior, de forma que, antes de incluir un registro (que con la versión anterior podría ser duplicado), verificamos si el registro comenzaba (^) y terminaba ($) igual al parámetro pasado ($1). El uso del circunflejo (^) en el inicio de la cadena y el pesos ($) en el fin, son para verificar si el parámetro pasado (el álbum y sus datos) son exactamente iguales a algún registro anteriormente incluído y no unicamente igual a un pedazo de alguno de los registros. Vamos a ejecutarlo pasando un álbum ya anteriormente incluído: $ musinc "album 4^Artista7~Musica7:Artista8~Musica8" Este álbum ya está incluído

Y ahora uno no incluído: $ musinc "album 5^Artista9~Musica9:Artista10~Musica10" $ cat musicas album 1^Artista1~Musica1:Artista2~Musica2 album 2^Artista3~Musica3:Artista4~Musica4 album 3^Artista5~Musica5:Artista6~Musica5 album 4^Artista7~Musica7:Artista8~Musica8 album 5^Artista9~Musica9:Artista10~Musica10

- Como viste, el programa mejoró un poquito, pero todavia no está listo. A medida que te vaya enseñando a programar en shell, nuestra CDteca va a ir quedando cada vez mejor. - Entendí todo lo que me explicaste, pero todavia no sé como hacer un if para verificar condiciones, o sea el uso normal del comando. - Mira, para eso existe el comando test, él es quien verifica condiciones. El comando if verifica el comando test. Pero eso está medio confuso y como ya hablé mucho, estoy necesitando unos "choppes" para mojar las palabras. Vamos a parar por aqui y la próxima vez te explico claramente el uso del test y de diversas otras sintáxis del if.


Y entonces amigo mio, intentaste hacer el ejercicio que te pedí para reforzar tus conocimientos? - Claro, que si! En programación, si no se practica, no se aprende. Me pediste que hiciera un scriptisiño para informar si un determinado usuario, que será pasado como parámetro, esta logado (ajjjj!) o no. Hice lo siguiente: $ cat logado #!/bin/bash # Busca si una persona está logada o no if who | grep $1 then echo $1 está logado else echo $1 no se encuentra en la vecindad fi

- Calma amigo! Ya vi que hoy llegaste lleno de deseos de trabajar, primero vamos a pedir nuestros "choppes" de costumbre y después vamos al Shell. Chico!, tráeme dos "choppes", uno sin espuma! - Ahora que ya mojamos nuestros labios, vamos a echar un vistazo a la ejecución de tu programa: $ logado jneves jneves pts/0 jneves está logado

Oct 18 12:02 (10.2.4.144)

Realmente funcionó. Pasé mi login como parámetro y él me informó que estaba logado, sin embargo, al mismo tiempo salió una línea que no pedí. Esta línea es la salida del comando who, y para evitar que eso pase, lo único que hay que hacer es mandarla hacia el agujero negro que a estas altura ya sabes que es el /dev/null. Veamos entonces como quedaría: $ cat logado #!/bin/bash # Busca si una persona está logada o no (versión 2) if who | grep $1 > /dev/null then echo $1 está logado else echo $1 no se encuentra en la vecindad fi

Ahora vamos a los tests: $ logado jneves jneves está logado $ logado chico chico no se encuentra en la vecindad


Ah, ahora si! Acuérdate de esto: la mayor parte de los comandos tienen una salida patrón y una salida de errores (el grep es una de las pocas excepciones, ya que no da mensajes de error cuando no encuentra una cadena) y es necesario estar muy atentos para redirecionarlas hacia el agujero negro cuando sea necesario. Bueno, ahora vamos a cambiar de asunto: la última vez que nos encontramos aquí en el Bar, te estaba mostrando los comandos condicionales y cuando ya estábamos con la garganta seca hablando sobre el if, me preguntaste como se verifican condiciones. Veamos entonces

El Comando test Bien, todos estamos acostumbrados a usar el if para verificar condiciones, y estas condiciones siempre son: mayor, menor, mayor o igual, menor o igual, igual y diferente. En Shell para verificar condiciones, usamos el comando test, sólo que este es mucho más poderoso de lo que estamos habituados. Primero te voy a mostrar las principales opciones (existen muchas otras), para verificar la existencia de archivos en el disco: Opciones del Comando test para archivos -x arch arch existe y con derechos de ejecución Opción Verdadero si: -e arch arch existe -s arch arch existe y tiene tamaño mayor que cero -f arch arch existe y es un archivo regular -d arch arch existe y es un directorio; -r arch arch existe y con derechos de lectura -w arch arch existe y con derechos de escritura Observa ahora las principales opciones para verificar cadenas de caracteres: Opciones del comando test para cadenas de caracteres c1 = c2 Cadena c1 y c2 son idénticas Opción Verdadero si: -z cadena Tamaño de cadena es cero -n cadena Tamaño de cadena es mayor que cero cadena La cadena cadena tiene tamaño mayor que cero


Y crees que se acabó ahí? Pues estás engañado! Ahora viene la parte a la que estás más acostumbrado, o sea las famosas comparaciones con números. Fijate en la tabla que sigue: Opciones del comando test para números less or equal n1 -le n2 n1 es menor o igual a n2 Opción Verdadero si: Significado equal n1 -eq n2 n1 y n2 son iguales not equal n1 -ne n2 n1 y n2 no son iguales greater than n1 -gt n2 n1 es mayor que n2 greater or equal n1 -ge n2 n1 es mayor o igual a n2 less than n1 -lt n2 n1 es menor que n2 Además de todo eso, se suman a las opciones que te mostré las siguientes opciones: Operadores -o

Operador Paréntesis ( ) Admiración ! -a

lógico Finalidad Agrupar Negar O

Y

lógico

Ufa! Como viste hay mucha cosa y como te dije al comienzo, nuestro if es mucho más poderoso que los de otros. Vamos a ver en unos ejemplos como funciona todo esto, primero verificaremos la existencia de un directorio: Ejemplos: if test -d lmb then cd lmb else mkdir lmb cd lmb fi

En este ejemplo, verifiqué la existencia de un directorio definido lmb, en caso negativo (else), éste seria creado. Ya sé, vas a criticar mi razonamiento diciendo que el script no está optimizado. Lo sé perfectamente, pero quería que entendieras este ejemplo, para poder usar después el signo de admiración (!) como un negador del test. Mira esto: if test ! -d lmb then mkdir lmb fi cd lmb


De esta forma el directorio lmb sería creado solamente si este no existiese, y esta negación se debe al signo de admiración (!) que precede a la opción -d. Al finalizar la ejecución de este fragmento de script, el programa estaría seguramente dentro del directorio lmb. Vamos a ver dos ejemplos para entender como se diferencia la comparación entre números y entre cadenas. cad1=1 cad2=01 if test $cad1 = $cad2 then echo Las variables son iguales. else echo Las variables son diferentes. fi

Ejecutando el fragmento del programa arriba, resulta: Las variables son diferentes.

Vamos a modificarlo un poco, de manera que la comparación esta vez sea numérica: cad1=1 cad2=01 if test $cad1 -eq $cad2 then echo Las variables son iguales. else echo Las variables son diferentes. fi

Y lo ejecutamos nuevamente: Las variables son iguales.

Como viste, en las dos ejecuciones obtuve resultados diferentes porque la cadena 01 es realmente diferente de la cadena 1, sin embargo, la cosa cambia cuando las variables son verificadas en forma numérica, ya que el número 1 es igual al número 01. Ejemplos: Para mostrar el uso de los conectores -o (O) y -a (Y), tengo un ejemplo bien grosero, hecho directamente en el prompt (pido disculpas a los zoólogos, ya que no entendiendo nada de reino, clase, orden, familia, género y especie, puede que lo que estoy llamando familia o género tenga grandes posibilidades de ser incorrecto): $ $ $ = > > >

Familia=felina Genero=gato if test $Familia = canina -a $Genero = lobo -o $Familia felina -a $Genero = leon then echo Cuidado else


> echo Se puede acariciar > fi Se puede acariciar

En este ejemplo en caso de que el animal fuera de la familia canina Y (-a) del género lobo, O (-o) de la familia felina Y (-a) del género leon, se daria un aviso de alerta, en caso contrario el mensaje sería de incentivo. Los signos de mayor (>) al inicio de las líneas internas al if son los prompts de continuación (que están definidos en la variable $PS2) y cuando el Shell identifica que un comando continuará en la línea siguiente, automáticamente los va colocado, hasta que el comando sea finalizado. Vamos a cambiar el ejemplo para ver si continúa funcionando: $ Familia=felino $ Genero=gato $ if test $Familia = felino -o $Familia = canino -a $Genero = onza -o $Genero = lobo > then > echo Cuidado! > else > echo Puede acariciar > fi Cuidado!

Obviamente la operación resultó en error, ya que la opción -a tiene prioridad sobre la o, y así lo que se evaluó primero, fué la expresión: $Familia = canino -a $Genero = onza

Que fué evaluada como falsa, y dió el seguiente resultado: $Familia = felino -o FALSO -o $Genero = lobo

Que una vez resuelta dió: VERDADERO -o FALSO -o FALSO

Como ahora todos los conectores son -o, y para que una serie de expresiones conectadas entre sí por diversos O lógicos sea verdadera, basta que una de ellas lo sea, la expresión final resultó como VERDADERO y el then fue ejecutado de forma incorrecta. Para que vuelva a funcionar hagamos lo seguiente: $ if test \($Familia = felino -o $Familia = canino\) -a \($Genero = onza -o $Genero = lobo\) > then > echo Cuidado! > else > echo Puede acariciar


> fi Puede acariciar

De esta forma, con el uso de los paréntesis agrupamos las expresiones con el conector o, dando prioridad a sus ejecuciones y resultando: VERDADERO -a FALSO

Para que sea VERDADERO el resultado de dos expresiones ligadas por el conector -a es necesario que ambas sean verdaderas, lo que no es el caso del ejemplo arriba citado. Así el resultado final fue FALSO, siendo entonces el else correctamente ejecutado. Si quisieramos escojer un CD que tenga músicas de 2 artistas diferentes, nos sentimos tentados a usar un if con el conector -a, pero siempre es bueno recordar que el bash nos dá muchos recursos y eso podría ser hecho de forma mucho más simple con un único comando grep, de la siguiente manera: $ grep Artista1 musicas | grep Artista2

De la misma forma, para escojer CDs que tengan la participación del Artista1 y del Artista2, no es necesario montar un if con el conector -o. El egrep (o grep -E, siendo éste más recomendable), también nos resuelve eso. Fijate como: $ egrep (Artista1|Artista2) musicas

O (en ese caso específico) el propio grep puro y simple podría ayudarnos: $ grep Artista[12] musicas

En el egrep arriba, fue usada una expresión regular, donde la barra vertical (|) trabaja como un O lógico y los paréntesis son usados para limitar la amplitud de éste O.Ya en el grep de la línea siguiente, la palabra Artista debe ser seguida por alguno de los valores de la lista formada por los paréntesis rectos ([ ]), o sea, 1 o 2. - Está bien, acepto el argumento, el if del Shell es mucho más poderoso que los otros conocidos, pero entre nosotros, esa construción del if test ... es muy extraña, y poco legible. - Si, tienes toda la razón, tampoco me es simpática y me parece que a nadie le gusta. Creo que fue por eso, que el Shell incorporó otra sintáxis que substituye el comando test. Ejemplos: Para esto vamos a ver nuevamente aquél ejemplo para cambiar de directorios, que era así:


if test ! -d lmb then mkdir lmb fi cd lmb

y utilizando la nueva sintáxis, vamos a hacerla así: if [ ! -d lmb ] then mkdir lmb fi cd lmb

O sea, el comando test puede ser substituído por un par de paréntesis rectos ([ ]), separados por espacios en blanco de los argumentos, lo que aumentará enormemente la legibilidad, pues el comando if quedara con una sintáxis parecida a de otras lenguajes y por este motivo, usaré el comando test de esta forma de ahora en adelante.

Querida, Encojieron el Comando Condicional! Si creees que se acabó, estás muy equivocado. Repara en la tabla (booleana) siguiente: Valores Booleanos Y O FALSO-FALSO FALSO FALSO VERDADERO-VERDADERO VERDADERO VERDADERO VERDADERO-FALSO FALSO VERDADERO FALSO-VERDADERO FALSO VERDADERO O sea, cuando el conector es Y y la primera condición es verdadera, el resultado final puede ser VERDADERO o FALSO, dependiendo de la segunda condición, ya en el conector O, en caso que la primera condición sea verdadera, el resultado siempre será VERDADERO y si la primera fuera falsa, el resultado dependerá de la segunda condición. Bueno, las personas que crearon el interpretador no son bobas y están siempre intentando optimizar al máximo los algoritmos. Por tanto, en el caso del conector Y, la segunda condición no será evaluada, en el caso de que la primera sea falsa, ya que el resultado será siempre FALSO. Ya con el O, la segunda será ejecutada solamente en el caso de que la primera sea falsa. Aprovechando eso, crearon una forma abreviada de hacer tests. Bautizaron el conector Y de && y el O de || y para ver como funciona esto, vamos a usarlos como test en nuestro viejo ejemplo de cambiar de directorio, que en su última versión estaba así: if [ ! -d lmb ] then mkdir lmb fi cd lmb

Eso también podría ser escrito de la siguiente manera:


[ ! -d lmb ] && mkdir lmb cd lmb

O inclusive sacando la negación (!): [ -d lmb ] || mkdir lmb cd lmb

En el primer caso, si el primer comando (el test que está representado por los paréntesis rectos) estuviera bien resultado, o sea, no existe el directorio lmb, el mkdir será ejecutado, porque la primera condición era verdadera y el conector era Y. En el ejemplo siguiente, verificaremos si el directorio lmb existe (en el anterior verificabamos si no existía) y en caso de que eso fuera verdadero, el mkdir no sería ejecutado porque el conector era O. Otra forma: cd lmb || mkdir lmb

En este caso, si el cd diera error, sería creado el directorio lmb pero no sería ejecutado el hacia dentro de él. Para ejecutar más de un comando de esta forma, es necesario que hagamos un agrupamiento de comandos, eso se consigue con el uso de llaves ({ }). Mira como sería la forma correcta:

cd

cd lmb || { mkdir lmb cd lmb }

Todavia no está bien, porque en el caso de que el directorio no exista, el cd dará el mensaje de error correspondiente. Entonces debemos hacer: cd lmb 2> /dev/null || { mkdir lmb cd lmb }

Como viste, el comando if nos permitió hacer un cd seguro de diversas maneras. Es siempre bueno recordar que el seguro a que me referí, es en lo referente al hecho de que al final de la ejecución, tu siempre estarás dentro de lmb, siempre que tengas permisos para entrar en lmb, permisos para crear un directorio en ../lmb, haya espacio en el disco, ...

Y toma test! Piensas que ya se acabó? Gran error! Todavia tenemos una forma de test más. Esta es muy buena porque te permite usar patrones para la comparación. Estos patrones atienden a las normas de Generación de Nombres de Archivos (File Name Generation, que son ligeramente parecidas con las Expresiones Regulares, pero no pueden ser confundidas con éstas). La diferencia de sintáxis de este para el test que acabamos de ver, es que este trabaja con dos parêntesis rectos de la siguiente forma:


[[ expresión ]]

Donde expresión es una de las que constan en la tabla siguiente: Expresiones Condicionales Para Padrones expr1 ¦¦ expr2 "O" lógico, Verdadero si expr1 o expr2 fueran Verdaderos Expresión Retorna cadena == padrón Verdadero si cadena1 es igual a padrón cadena1 = padrón cadena1 != padrón

Verdadero si cadena1 no es igual a padrón.

cadena1 < cadena1

Verdadero si cadena1 está antes de cadena1 alfabéticamente.

cadena1 > cadena1 expr1 && expr2

Verdadero si cadena1 está después de cadena1 alfabéticamente "Y" lógico, Verdadero si ambos expr1 y expr2 son Verdaderos

$ echo $H 13 $ [[ $H == [0-9] || $H == 1[0-2] ]] || echo Hora no válida Hora no válida $H=12 $ [[ $H == [0-9] || $H == 1[0-2] ]] || echo Hora no válida $

En este ejemplo,verificamos si el contenido de la variable $H esta comprendido entre cero y nueve ([0-9]) o (||) si esta entre diez y doze (1[0-2]), dando un mensaje de error en caso que no sea asi. Ejemplos: Para saber si una variable tiene el tamaño de un y solamente un caracter, haz: $ var=a $ [[ $var == ? ]] && echo var tiene un caracter var tiene un caracter $ var=aa $ [[ $var == ? ]] && echo var tiene un caracter $

Como puedes imaginar, este uso de patrones para comparación, aumenta mucho el poderío del comando test. En el inicio de esta conversación, antes del último "choppe", afirmabamos que el comando if del interpretador Shell es más poderoso que sus similares en otros lenguajes. Ahora que conocimos todo su espectro de funciones, dime: estas de acuerdo o no con esta afirmación?

Acaso Casa con case


Veamos un ejemplo didáctico: dependiendo del valor de la variable $opc el script deberá ejecutar una de las opciones: inclusión, exclusión, alteración o fin. Fijate como quedaría este fragmento de script: if [ $opc -eq 1 ] then inclusión elif [ $opc -eq 2 ] then exclusión elif [ $opc -eq 3 ] then alteración elif [ $opc -eq 4 ] then exit else echo Digite una opción entre 1 y 4 fi

En este ejemplo viste el uso del elif con un else if, esta es una sintáxis válida y aceptada, pero podríamos hacerlo mejor y sería usando el comando case, que tiene la sintáxis siguiente: case $var in patrón1) cmd1 cmd2 cmdn ;; patrón2) cmd1 cmd2 cmdn ;; patrónn) cmd1 cmd2 cmdn ;; esac

Donde la variable $var es comparada a los patrones patrón1, ..., patrónn y en el caso de que uno de ellos coincida, el bloque de comandos cmd1, ..., cmdn correspondiente será ejecutado hasta que encuentre un doble punto y coma (;;), en donde el flujo del programa se desviará hacia la instrucción inmediatamente siguiente, o sea el esac. En la formación de los patrones, son aceptados los siguientes caracteres: Caracteres Para Formacion de Padrones ¦ O lógico Caracter Significado * Cualquier caracter ocurriendo cero o más veces ? Cualquier caracter ocurriendo una vez [...] Lista de caracteres Para mostrar como realmente queda mejor, vamos a repetir el ejemplo anterior, sólo que esta vez usaremos el case y no el if ... elif ... else ... fi.


case $opc in 1) inclusión ;; 2) exclusión ;; 3) alteración ;; 4) exit ;; *) echo Digite una opción entre 1 y 4 esac

Como debes haberte dado cuenta, usé el asterisco como la última opción, o sea, si el asterisco quiere decir cualquier cosa, entonces servirá para cualquier cosa que no este en el intervalo del 1 al 4. Otra cosa a tener en cuenta es que el doble punto y coma no es necesario antes del esac. Ejemplos: Vamos ahora a hacer un script más radical. Te dará los buenos días, buenas tardes o buenas noches, dependiendo de la hora en que sea ejecutado, pero primero mira estos comandos: $ date Tue Nov 9 19:37:30 BRST 2004 $ date +%H 19

El comando date informa de la fecha completa del sistema, sin embargo tiene diversas opciones para su enmascaramiento. En este comando, la formatación comienza con un signo de más (+) y los caracteres de formatación vienen después de un signo de porcentaje (%), así el %H significa la hora del sistema. Dicho esto, vamos al ejemplo: $ cat bienvenido.sh #!/bin/bash # Programa bien educado que # da los Buenos dias, buenas tardes o # las buenas noches dependiendo de la hora Hora=$(date +%H) case $Hora in 0? | 1[01]) echo Buenos días ;; 1[2-7] ) echo Buenas tardes ;; * ) echo Buenas noches ;; esac exit

Fue pesado, verdad?. - Que vá!. Vamos a desmenuzar la resolución caso a caso (o sería case-a-case? ) 0? | 1[01] - Significa cero seguido de cualquier cosa (?), uno ([01]) o sea, esta línea pegó 01, 02, ... 09, 10 y 11;

o (|) uno seguido de cero o


1[2-7]

- Significa uno seguido de la lista de dos a siete, o sea, esta línea pegó 12,

13, ... 17; *

- Significa todo aquello que no se encuadró en ninguno de los patrones

anteriores. - Mira, hasta ahora hablé mucho y bebí poco. Ahora te voy a pasar un ejercicio para que lo hagas en tu casa y me des la respuesta la próxima vez que nos encontremos aqui en el bar, de acuerdo? - De acuerdo, pero antes informe a las personas que nos están acompañando en este curso, como pueden hacerlo para encontrarle, para hacerle críticas, hacer chistes, invitarle a una cerveza, un curso, unas charlas o hasta si quieren, para hablar mal de los políticos. - es fácil, mi e-mail es julio.neves@gmail.com, pero para de distraerme, que no me voy a olvidar de pasarte el script de los deberes, y este es: quiero que hagas un programa que recibirá como parámetro el nombre de un archivo y que cuando sea ejecutado grabe este archivo con el nombre original seguido de una tilde (~), además colocaras este archivo dentro del vi (de paso, el mejor editor del cual se tiene noticia) para ser editado. Esto sirve para tener siempre la última copia buena del archivo en el caso de que la persona haga alteraciones indebidas. Obviamente, tendrás que hacer las investigaciones necesarias, como verificar si fué pasado un parámetro, si el archivo que fué pasado existe, ... En fin, lo que se te venga a la cabeza y tu creas que deba estar en el script. Entendiste?


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 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. exit,

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: línea

# La opción -n es para no saltar la

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: 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 album album album album album

musicas 1^Artista1~Musica1:Artista2~Musica2 2^Artista3~Musica3:Artista4~Musica4 3^Artista5~Musica5:Artista6~Musica6 4^Artista7~Musica7:Artista1~Musica3 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 ). 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: $ > > > 1

for i in $(seq 9) do echo -n "$i " done 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: $ > > > 4

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

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

for i in $(seq 0 3 9) do echo -n "$i " done 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:


- Significa que la variable var comenzará a partir de un valor inicial ini; - 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. var=ini cond

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

for ((i=1; i<=9; i++)) do echo -n "$i " done 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. $ > > > > 1

for ((; i<=9;)) do let i++ echo -n "$i " done 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!


Comandos de Loop o Lazo (Continuación) - Que tal amigo mio, como estas? Ya lo sabes todo acerca del comando for?. Te dejé un ejercicio de deberes, y si no estoy equivocado, era para contar la cantidad de palabras de un archivo... Lo hiciste? - Claro! Estoy entusiasmando con ese lenguaje del shell, lo hice de la forma que me pediste, o sea sin usar el comando wc porque si no era mucho más fácil. Mira lo que hice... - Epa! Un momento! Realmente estás entusiasmando con el lenguaje, pero yo estoy deseando tomar un "chopp". Chico!, tráeme dos por favor. Uno sin espuma, como siempre! - Como te iba diciendo, mira lo que hice. Es muy fácil... $ cat contpal.sh #!/bin/bash # Script meramente pedagógico cuya # función es contar la cantidad de palabras # de un archivo. Se supone que las # palabras están separadas entre sí # por espacio, o .

if

[ $# -ne 1 ]

then echo uso: $0 /camino/del/archivo exit 2 fi Cont=0 for Palabra in $(cat $1) do Cont=$((Cont+1)) done echo El archivo $1 tiene $Cont palabras.

O sea, el programa comienza como siempre verificando si el pasaje de parámetros fue correcto, en seguida el comando for se encarga de coger cada una de las palabras


(recuerda que el $IFS patrón (default) es espacio, <TAB> y <ENTER>, que es exactamente lo que deseamos para separar las palabras), incrementando la variable $Cont. Vamos a recordar como es el archivo ArchDelDOS.txt. $ cat ArqDoDOS.txt Este archivo fue generado por el DOS/Rwin y fue bajado por un ftp mal hecho.

Ahora vamos a testear el programa pasando este archivo como parámetro: $ contpal.sh ArchDelDOS.txt El archivo ArchDelDOS.txt tiene 15 palabras

- Muy bien! funcionó correctamente!

Un Poco más de for y Matemática Volviendo a nuestro tema, la última vez que estuvimos aquí, terminamos nuestra conversación mostrando el loop del for a continuación: for ((; i<=9;)) do let i++ echo -n "$i " done

Una vez que llegamos a este punto, creo que sera bastante interesante citar que el Shell trabaja con el concepto de "Expansión Aritmética" (Arithmetic Expansion) que es accionado por una construcción de la forma $((expresión))

o let expressión

En el último for citado, usé la expansión de las dos formas, pero no podríamos seguir adelante sin saber que la expresión puede ser una de las listadas a continuación: Expansión Aritmética ||

O lógico

Expresión

Resultado

id++ id--

pós-incremento y pós-decremento de variables


Expansión Aritmética ++id -–id ** * / % + <= >= < > == != &&

pré-incremento y pré-decremento de variables exponenciación multiplicación, división, resto de la división adición, substraccuión comparación igualdad, desigualdad Y lógico

- Y si piensas que la conversación sobre loop (o lazo) se termina en el comando for, estas en un gran error amigo, vamos a partir de ahora a ver dos más.

El comando while Todos los programadores conocen este comando, ya que es común a todas los lenguajes y en ellos lo que normalmente ocurre, es que un bloque de comandos es ejecutado, en cuanto que (en cuanto que en inglés es while) una determinada condición sea verdadera. Pues bien, esto es lo que ocurre en otros lenguajes! En programación Shell, el bloque de comandos es ejecutado en cuanto que un comando sea verdadero. Y esta claro, si quisiera verificar una condición, usaria el comando while junto con el comando test, exactamente como aprendiste a hacer en el if, recuerdas? Entonces, la sintaxis del comando queda de la siguiente forma: while comando do cmd1 cmd2 ... cmdn done

y de esta forma el bloque de comandos formado por las instrucciones cmd1, cmd2,... y cmdn será ejecutado en cuanto que la ejecución de la instrucción comando sea ejecutada con éxito. Suponga la siguiente escena: tengo una tremenda gatita esperándome y estoy preso en el trabajo sin poder salir porque mi jefe, que es un tremendo rompe cocos se encuentra todavía trabajando en su escritorio, que queda bien en medio de mi salida a la calle. Él empezó a tener las antenas (probablemente instaladas en su cabeza por la esposa) atentas, después de la quinta vez que me vio pasar por su puerta y ver que todavía estaba allí. Volví a mi mesa e hice un script en el servidor de esta forma: $ cat logaute.sh #!/bin/bash


while who | grep jefe do sleep 30 done echo El rompe se fue, no te entretengas, date el piro y ves enfrente

En este scriptiziño, el comando while verifica el pipeline compuesto por el who y por el grep y que será verdadero en cuanto el grep localice la palabra jefe en la salida del who. Así, el script dormirá durante 30 segundos mientras el jefe esté logado (Argh!). Cuando él se desconecte del servidor, el flujo del script saldra del loop y dará el tan ansiado mensaje de libertad. Si lo ejecuto adivinas lo que pasa? $ logaute.sh jefe pts/0 jefe pts/0 ... jefe pts/0

Jan Jan

4 08:46 (10.2.4.144) 4 08:47 (10.2.4.144)

Jan

4 08:52 (10.2.4.144)

Pues que cada 30 segundos es enviada a mi pantalla la salida del grep, lo que no es deseable ya que lleno la pantalla del computador y ademas el esperado mensaje podría pasar desapercibido. Para evitar eso ya sabemos que la salida del pipeline tiene que ser redireccionada hacia /dev/null. $ cat logaute.sh #!/bin/bash

while who | grep jefe > /dev/null do sleep 30 done echo El rompe se fue, no te entretengas, date el piro y ves enfrentee


Ahora quiero montar un script que reciba el nombre (y eventuales parámetros) de un programa que será ejecutado en background y que me informe de su término. Pero, para entender este ejemplo, primero tengo que mostrar una nueva variable del sistema. Mira estos comandos escritos directamente en el prompt: $ sleep 10& [1] 16317 $ echo $! 16317 [1]+ Done $ echo $! 16317

sleep 10

O sea, lance un proceso en background que se ejecutara cada 10 segundos, solo para mostrar que la variable $! guarda el PID (Process IDentification) del último proceso en background, sin embargo, fíjate que después de la línea del done, la variable continuó con el mismo valor. Bien, sabiendo eso es más fácil de controlar cualquier proceso en background. Observa como: $ cat monbg.sh #!/bin/bash

# Ejecuta y

controla un

# proceso en background

$1 &

# Coloca en backgroud

while ps | grep -q $! do sleep 5 done echo Fin del Proceso $1

Este script es bastante similar al anterior, pero tiene unos trucos más, mira: tiene que ser ejecutado en background para no retener el prompt pero el $! será el del programa pasado como parámetro ya que fue colocado en background después del monbg.sh propiamente dicho. Observa también la opción -q (quiet) del grep, sirve para transformarlo en un comando silencioso, o sea, para que el grep trabaje de forma


invisible. Este mismo resultado podría ser obtenido si la línea fuera while ps | grep $! > /dev/null, como en los ejemplos que vimos hasta ahora. No te olvides: el Bash dispone de la variable $! que posee el PID (Process IDentification) del último proceso ejecutado en background. Vamos a mejorar el musinc, que es nuestro programa para incluir registros en el archivo musicas, pero antes necesito enseñarte a capturar un dato de la pantalla, y ya voy avisando: solo voy a dar una pequeña parte del comando read (que es quien hace la captura de la pantalla) que sea lo suficiente para resolver este problema. En otra vuelta de "chopp" te lo voy a enseñar todo del asunto, inclusive como formatear la pantalla, pero hoy estamos hablando sobre loops. La sintaxis del comando read que nos interesa por ahora es la siguiente: $ read -p "prompt de lectura" var

Donde prompt de lectura es el texto que quieres que aparezca escrito en la pantalla, y cuando el operador escriba el dato éste irá a parar anla variable var. Por ejemplo: $ read -p "Título del Álbun: " Tit

Bien, una vez entendido eso, vamos a la especificación de nuestro problema: haremos un programa que inicialmente leerá el nombre del álbum y en seguida hará un loop de lectura, extrayendo la música y el artista. Este loop termina cuando se encuentre una música vacía, o sea, al ser solicitada la escritura de la música, el operador de un simple <ENTER>. Para facilitar la vida del operador, vamos a ofrecer como default el mismo nombre del artista de la música anterior (ya que es normal que el álbum sea todo del mismo artista) hasta que él desee alterarlo. Vamos a ver como quedó ahora: $ cat musinc #!/bin/bash # Catastra CDs (versión 4) # clear read -p "Título del Álbun: " Tit [ "$Tit" ] || exit 1 # Fin de la ejecución si el título= vacio if grep "^$Tit\^" musicas > /dev/null then echo Este álbum ya está catastrado exit 1 fi Reg="$Tit^" Cont=1 oArt= while true do echo Datos de la pista $Cont: read -p "Música: " Mus [ "$Mus" ] || break # Sale si vacio


read -p "Artista: $oArt // " Art [ "$Art" ] && oArt="$Art" # Si vacio Art anterior Reg="$Reg$oArt~$Mus:" # Montando el registro Cont=$((Cont + 1)) # La linea anterior también podria ser ((Cont++)) done echo "$Reg" >> musicas sort musicas -o musicas

Este ejemplo, comienza con la lectura del título del álbum, y si no es introducido, terminará la ejecución del programa. En seguida un grep busca en el inicio (^) de cada registro de músicas, el título introducido seguido del separador (^) (que está precedido de una contrabarra (\) para protegerlo de la interpretación del Shell). Para leer los nombres de los artistas y las músicas del álbum, fue montado un loop de while simple, que lo único que tiene a destacar es el hecho de estar almacenando el artista de la música anterior en la variable $oArt que solamente tendrá su contenido alterado, cuando alguno de los datos sea introducido en la variable $Art, o sea, cuando no se teclee un simple <ENTER> para mantener el artista anterior. Lo viste hasta ahora sobre el while fue muy poco. Este comando es muy utilizado, principalmente para lectura de archivos, sin embargo nos faltan conocimientos para continuar. Después que aprendamos a leer, veremos esta instrucción más a fondo. Lectura del archivo significa leer uno a uno todos los registros, lo que es siempre una operación lenta. Estate atento de no usar el while cuando su uso puede ser evitado. El Shell tiene recursos como el sed y la familia grep que buscan en los archivos de forma optimizada sin ser necesario el uso de comandos de loop para hacerlo registro a registro (o hasta palabra a palabra).

El comando until El comando until funciona exactamente igual al while, sin embargo al revés. Dije todo pero no dije nada, no es cierto? Es lo siguiente: ambos verifican comandos; ambos poseen la misma sintaxis y ambos actuan en loop, sin embargo, mientras el while ejecuta el bloque de instrucciones del loop mientras un comando este bien ejecutado, el until ejecuta el bloque del loop hasta que el comando este bien ejecutado. Parece una diferencia insignificante, pero en cambio es fundamental. La sintáxis del comando es practicamente la misma del while. Observa: until comando do cmd1 cmd2 ... cmdn done

Y así el bloque de comandos formado por las instruciones cmd1, cmd2,... y cmdn es ejecutado hasta que la ejecución de la instrucción comando sea bien ejecutada.


Como te dije, el while y el until funcionan de forma antagónica lo cual es muy fácil de demostrar: en una guerra siempre que se inventa una arma, el enemigo busca una solución para neutralizarla. Basado en este principio de la guerra es que mi jefe, creó en el mismo servidor que yo ejecutaba el logaute.sh un script para controlar el horário de mi llegada. Un dia ocurrió un problema en la red, y él me pidió que echara una mirada en su ordenador y me dejó solo en su sala. Inmediatamente comencé a revisar sus archivos porque guerra es guerra - y mira lo que descubrí: $cat llegada.sh #!/bin/bash

until who | grep julio do sleep 30 done echo $(date "+ El %d/%m a las %H:%Mh") >> haragán.log

Que cara dura! Él estaba montando un log con los horarios en que yo llegaba, y además llamó el archivo que me controlaba de haragán.log! Que será lo que quiso decir con eso? En este script, el pipeline who | grep julio, será bien ejecutado solamente cuando julio sea encontrado en el comando who, o sea, cuando yo me "logue" en el servidor. Hasta que eso pase, el comando sleep, que forma el bloque de instrucciones del until, pondrá el programa en espera por 30 segundos. Cuando este loop se cierre, será enviado un mensaje hacia el haragán.log (ARGHH!). Suponiendo que el dia 20/01 yo me loguease a las 11:23 horas, el mensaje sería el siguiente: El 20/01 a las 11:23h

Cuando vamos a buscar introducir, lo ideal sería que pudiésemos introducir diversos CDs, y en la última versión que hicimos del musinc, eso no ocurre, a cada CD que introducimos el programa termina. Veamos como mejorarlo: $ cat musinc #!/bin/bash # Catastra CDs (versión 5) # Para= until [ "$Para" ] do clear


read -p "Título del Álbum: " Tit if [ ! "$Tit" ] # Si titulo vacio... then Para=1 # Activé flag de salida else if grep "^$Tit\^" musicas > /dev/null then echo Este álbum ya está introducido exit 1 fi Reg="$Tit^" Cont=1 oArt= while [ "$Tit" ] do echo Dados de la pista $Cont: read -p "Música: " Mus [ "$Mus" ] || break # Sale si vacío read -p "Artista: $oArt // " Art [ "$Art" ] && oArt="$Art" # Si vacío Art anterior Reg="$Reg$oArt~$Mus:" # Montando registro Cont=$((Cont + 1)) # La linha anterior también podría ser ((Cont++)) done echo "$Reg" >> musicas sort musicas -o musicas fi done

En esta versión, fué agregado un loop mayor antes de la lectura del título, que solo terminará cuando la variable $Para deje de estar vacía. En el caso de que el título del álbum no se encuentre, la variable $Para recibirá valor (en este caso coloqué 1 pero podría haber colocado cualquier cosa. Lo importante es que no este vacía) para salir de este loop, y terminar el programa. En el resto, el script es idéntico a la versión anterior.

Atajos en loop No siempre un ciclo de programa, comprendido entre un do y un done, sale por la puerta del frente. En algunas oportunidades, tenemos que colocar un comando que aborte de forma controlada este loop. Al contrario, algunas veces deseamos que el fluxo de ejecución del programa vuelva antes de llegar al done. Para esto, tenemos respectivamente, los comandos break (que ya vimos rápidamente en los ejemplos del comado while) y continue, que funcionan de la siguiente forma: Lo que no habia mencionado anteriormente es que en sus sintáxis genéricas, aparecen de la siguiente forma: break [ctd loop]

y


continue [ctd loop]

Donde ctd loop representa la cantidad de loops internos sobre los que estos comandos van a actuar. Su valor default es 1.

Dudo mucho que nunca hayas borrado un archivo y enseguida no te dieras un golpe en la cabeza, maldiciéndote porque no debías haberlo hecho. Claro, pues yo la décima vez que hice esa burrada, cree un script para simular una cesta de basura, o sea, cuando mando borrar uno (o varios) archivo(s), el programa "finge" que lo borró, pero en realidad lo que hizo fue mandarlo(s) para el diretório /tmp/LoginName_del_usuario. Llamé este programa de erreeme y en el /etc/profile coloqué la siguiente línea: alias rm=erreeme

El programa era así: $ cat erreeme #/bin/bash # # Salvando Copia del Archivo Antes de Borrarlo #

if [ $# -eq 0 ] para borrar

#

Tiene que haber uno o mas archivos


then echo "Erro -> Uso: erreeme arch [arch] ... [arch]" echo " El uso de metacaracteres es permitido. Ej. erreeme arch*" exit 1 fi

MiDir="/tmp/$LOGNAME" # Variable del sist. Contiene el nombre del usuario. if [ ! -d $MiDir ] /tmp...

# Si no exister mi directorio bajo el

then mkdir $MiDir

# Voy a crearlo

fi

if [ ! -w $MiDir ] directorio...

# Si no puedo grabar en el

then echo Imposible grabar archivos en $MiDir. Cambie el permiso... exit 2 fi

Erro=0 retorno del prg for Arch parรกmetros pasados

# Variable para indicar el cod. de # For sin el "in" recibe los

do if

[ ! -f $Arch ]

# Si este archivo no existe...

then echo $Arch no existe.


Erro=3 continue

# Vuelve para el comando for

fi

DirOrig=`dirname $Arch` # Cmd. dirname informa nombre del dir de $Arch if [ ! -w $DirOrig ] en el directorio

# Verifica permiso de grabaci贸n

then echo Sin permiso de borrar en el directorio de $Arch Erro=4 continue

# Vuelve para el comando for

fi

if [ "$DirOrig" = "$MiDir" ] # Si estoy "vaciando la basurera"... then echo $Arch quedar谩 sin copia de seguridad rm -i $Arch

# Pregunta antes de borrar

[ -f $Arch ] || echo $Arch borrado usuario lo borr贸?

# Ser谩 que el

continue fi

cd $DirOrig directorio pwd >> $Arch undelete

# Guardo al final del archivo su # original para usarlo en un script de

mv $Arch $MiDir

# Grabo y borro

echo $Arch borrado


done exit $Erro # Paso eventual número de error para el código de retorno

Como puedes ver, la mayor parte del script es formada por pequeñas críticas a los parámetros hallados, pero como el script puede haber recibido diversos archivos para borrar, a cada archivo que no se encaja dentro del especificado, hay un continue, para que la secuencia vuelva para el loop del for de forma que pueda recibir otros archivos. Cuando estás en Windows (con perdón de la mala palabra) y tratas de borrar aquella cantidad de basura con nombres extraños como HD04TG.TMP, si te da un error en uno de ellos, los otros no serán borrados, no es así? Entonces, el continue fue usado para evitar que un barbaridad de estas ocurra, o sea, aunque dé un error en el borrado de un archivo, el programa continuará borrando los otros que le fueron pasados. - Me parece que a esta altura ya debes tener curiosidad por ver el programa que restaura el archivo borrado, no es así? Entonces, ahí va un desafio: hazlo en casa y me lo traes para discutirlo en nuestro próximo encuentro aqui en el bar. - Caramba, me parece que en ese voy a fracasar, no sé ni como comenzar... - Amigo mio, este programa es como todo lo que se hace en Shell, extremamente fácil, es para ser hecho en no más de 10 líneas. No te olvides que el archivo borrado está grabado en /tmp/$LOGNAME y que su última línea es el directorio en que estaba antes de ser "borrado". Tampoco te olvides de comprobar si fue pasado el nombre del archivo a ser borrado. - En fin, voy a tratar, pero no sé... - Ten fé hombre, te estoy diciendo que es fácil! Cualquier duda me pasas un e-mail para julio.neves@gmail.com. Ahora basta de conversación que ya estoy con la garganta seca de tanto hablar. Me acompañas en el próximo "chopp" o vas a salir corriendo para hacer el script que pasé? - Déjame pensar un poco... - Mozo, trae otro "chopp" mientras él piensa!


- Como dijiste? Repite que no te entiendo! Se te derritieron los pensamientos para hacer el scriptiziño que te pedí? - Si, realmente tuve que colocar mucha materia gris en la pantalla, pero creo que lo conseguí! Bueno, por lo menos en los tests que hice la cosa funcionó, pero tu siempre me colocas piedras en el camino! - No sera tanto, programar en shell es muy fácil, lo que vale son los consejos y los detalles de programación que te doy, que no son triviales. Las correcciones que te hago, son justamente para mostrarlos. Pero vamos a pedir dos "chopps" y le echo una ojeada a tu script. - Mozo, trae dos. No te olvides que uno es sin espuma!

$ cat restaura #!/bin/bash # # Restaura archivos borrados vía erreeme #

if [ $# -eq 0 ] then echo "Uso: $0 " exit 1 fi # Lee el nombre del directorio original en la última línea Dir=`tail -1 /tmp/$LOGNAME/$1` # O grep -v borra la última línea y crea el # archivo con directorio y nombres originales grep -v $Dir /tmp/$LOGNAME/$1 > $Dir/$1 # Borra el archivo que ya estaba moribundo rm /tmp/$LOGNAME/$1


- Un momento, déjame ver se lo entendí. Primero colocas en la variable Dir la última línea del archivo cuyo nombre está formado por /tmp/nombre del operador ($LOGNAME)/parámetro pasado con el nombre del archivo a ser restaurado ($1). Enseguida el grep -v que montaste borra esa línea en que estaba el nombre del directorio, o sea, siempre es la última y manda lo que resta del archivo, que sería el archivo ya limpio, hacia el directorio original para después borrar el archivo del "cubo de la basura"; S E N S A C I O N A L! Impecable! Ningun error! Lo viste? ya le estás tomando las medidas al shell! - Entonces vamos a continuar, basta ya de bla-bla-bla, de que vas a hablar hoy? - Ah! estoy viendo que el bichito del Shell se te contagió. Que bueno, vamos a ver como se pueden (y deben) leer datos y formatear pantallas, pero primero vamos a conocer un comando que te da todas las herramientas para que formatees tu pantalla de entrada de datos.

El comando tput Este comando se usa principalmente para posicionar el cursor en la pantalla, sin embargo también es muy usado para borrar datos de la pantalla, saber la cantidad de líneas y columnas de la pantalla, posicionar correctamente un campo, borrar un campo cuya entrada se detectó como error. En fin, casi toda la formatación de la pantalla es hecha por este comando. Unos pocos atributos del comando tput pueden eventualmente no funcionar, esto en el caso de que el modelo de terminal definido por la variable $TERM, no tenga esta posibilidad incorporada. En la tabla siguiente, se presentan los principales atributos del comando y los efectos ejecutados sobre la pantalla, pero debes saber que existen muchos más que esos, mira sino: $ tput it 8

Este ejemplo devolvió el tamaño inicial del <TAB> ( Initial T ab), y dime una cosa: para que quiero saber eso? Si quieres saber todo sobre el comando tput (y mira que es de nunca acabar), vea a: http://www.cs.utah.edu/dept/old/texinfo/tput/tput.html#SEC4. Principales Opciones del Comando tput rc Restore Cursor position - Coloca el cursor en la posición marcada por el último sc Opciones de Efecto tput cup lin col bold

CUrsor Position - Posiciona el cursor en la línea lin y columna col. El origen es cero Coloca la pantalla en modo de realce


rev smso smul blink sgr0 reset

Principales Opciones del Comando tput Coloca la pantalla en modo de vídeo inverso Idéntico al anterior A partir de esta instrucción, los caracteres tecleados aparecerán sublineados en la pantalla Los caracteres tecleados aparecerán intermitentes Después de usar uno de los atributos de arriba, se usa este para restaurar la pantalla a su modo normal Limpia la pantalla y restaura sus definiciones de acuerdo con el terminfo o sea, la pantalla vuelve al patrón definido por la variable $TERM

lines cols el ed il n dl n ech n sc

Devuelve la cantidad de líneas de la pantalla en el momento de la instrucción Devuelve la cantidad de columnas de la pantalla en el momento de la instrucción Erase Line - Borra la línea a partir de la posición del cursor Erase Display -

Borra la pantalla a partir de la posición del cursor Insert Lines - Introduce n líneas a partir de la posición del cursor Delete Lines - Borra n líneas a partir de la posición del cursor Erase CHaracters - Borra n caracteres a partir de la posición del cursor Save Cursor position - Graba la posición del cursor

Vamos a hacer un programa bien sencillo para mostrar algunos atributos de este comando. Es el famoso usado y abusado Hola Mundo, sólo que esta frase será escrita en el centro de la pantalla y en vídeo inverso y después de eso, el cursor volverá hasta la posición en que estaba antes de escribir esta frase tan creativa. Observa: $ cat hola.sh #!/bin/bash # Script bobo para testar # el comando tput (versión 1)

Columnas=`tput cols`

#

Grabando cantidad columnas

Líneas=`tput lines`

#

Grabando cantidad

líneas

Línea=$((Líneas / 2)) pantalla?

#

Cual es la línea del medio de la

Columna=$(((Columnas - 11) / 2)) # Centrando el mensaje en la pantalla tput sc

#

tput cup $Línea $Columna #

Grabando posición del cursor Posicionándose para escribir


tput rev

#

Vídeo inverso

tput sgr0

#

Restaura el vídeo a normal

tput rc original

#

Restaura el cursor a la posición

echo Hola Mundo!

Como el programa ya está todo comentado, creo que la única explicación necesaria sería para la línea en que es creada la variable Columna y lo extraño allí es aquél número 11, este numero es el tamaño de la cadena que pretendo escribir (Hola Mundo). De esta forma, este programa solamente conseguiría centrar cadenas de 11 caracteres, sin embargo, mira esto: $ var=Conversa $ echo ${#var} 8 $ var="Conversa de Bar" $ echo ${#var} 15

Ahhh, mejoró! Entonces ahora sabemos que la construcción ${#variable} devuelve la cantidad de caracteres de variable. De esta forma, vamos a optimizar nuestro programa para que escriba en vídeo inverso y en el centro de la pantalla, la cadena pasada como parámetro y que después el cursor vuelva a la posición en que estaba antes de la ejecución del script. $ cat hola.sh #!/bin/bash # Script bobo para testar # el comando tput (versión 2)

Columnas=`tput cols`

#

Grabando cantidad columnas

Líneas=`tput lines`

#

Grabando cantidad líneas

Línea=$((Líneas / 2)) pantalla?

# Cual es la línea del medio de la

Columna=$(((Columnas - ${#1}) / 2)) #Centrando el mensaje en la pantalla put sc tput cup $Línea $Columna

# Grabando posición del cursor #

Posicionándose para escribir


tput rev

#

Vídeo inverso

tput sgr0

#

Restaura vídeo a normal

tput rc original

#

Restaura cursor en la posición

echo $1

Este script es igual al anterior, sólo que cambiamos el valor fijo de la versión anterior (9), por ${#1}, donde éste 1 es el $1 o sea, esta construcción devuelve el tamaño del primer parámetro pasado para el programa. Si el parámetro que yo quisiese pasar tuviese espacios en blanco, tendría que colocarlo todo entre comillas, sino el $1 sería solamente el primer pedazo. Para evitar este problema, es solo necesario substituir el $1 por $*, que como sabemos es el conjunto de todos los parámetros. Entonces aquella línea quedaría así: Columna=`$(((Columnas - ${#*}) / 2))` #Centrando el mensaje en la pantalla

y la línea echo $1 pasaría a ser echo $*. Pero no te olvides de que cuando lo ejecutes, tienes que pasar la frase que deseas centrar como un parámetro.

Y ahora podemos leer los dados de la pantalla Bien, a partir de ahora vamos a aprender todo sobre lectura, solo que no te puedo enseñar a leer las cartas o el futuro, porque sino ya seria rico, estaria en un pub de Londres, tomando scotch y no en un bar tomando "chopp". Pero vamos a continuar. La última vez que nos encontramos aquí ya te dí una introducción sobre el comando read. Para comenzar su análisis más detallada. fíjate en esto: $ read var1 var2 var3 Conversa de Bar $ echo $var1 Conversa $ echo $var2 de $ echo $var3 Bar $ read var1 var2 Conversa de Bar $ echo $var1 Conversa $ echo $var2 de Bar


Como viste, el read recibe una lista separada por espacios en blanco y coloca cada ítem de esta lista en una variable. Si la cantidad de variables es menor que la cantidad de ítems, la última variable recibe el resto de los parámetros. Yo mencioné una lista separada por espacios en blanco? Pero ahora que ya lo conoces todo sobre el $IFS (Inter Field Separator) que te presenté cuando hablamos del comando for, todavía crees eso? Vamos a verificarlo directamente en el prompt: $ oIFS="$IFS" $ IFS=: $ read var1 var2 var3 Conversa de Bar $ echo $var1 Conversa de Bar $ echo $var2

$ echo $var3

$ read var1 var2 var3 Conversa:de:Bar $ echo $var1 Conversa $ echo $var2 de $ echo $var3 Bar $ IFS="$oIFS"

Te diste cuenta, estaba equivocado! La verdad es que el read lee una lista, así como el for, separada por los caracteres de la variable $IFS. Fíjate entonces como esto puede facilitarte la vida: $ grep julio /etc/passwd julio:x:500:544:Julio C. Neves - 7070:/home/julio:/bin/bash $ oIFS="$IFS" # Grabando IFS $ IFS=: $ grep julio /etc/passwd | read lname lixo uid gid coment home shell $ echo -e "$lname\n$uid\n$gid\n$coment\n$home\n$shell"


julio 500 544 Julio C. Neves - 7070 /home/julio /bin/bash $ IFS="$oIFS" # Restaurando IFS

Como viste, la salda del grep fue redireccionada hacia el comando read que leyó todos los campos de una sola vez. La opción -e del echo fue usada para que el \n fuera entendido como un salto de línea (new line), y no como un literal. En el Bash existen diversas opciones del read que sirven para facilitarte la vida. Observa la siguiente tabla:

-s

Opción -p prompt -n num -t seg

Opciones del comando read en Bash Lo que está siendo tecleado no aparece en la pantalla Acción Escribe el prompt antes de hacer la lectura Lee hasta num caracteres Espera seg segundos para que concluya la lectura

Y ahora directo a los ejemplos cortos para demostrar estas opciones. Para leer un campo "Matrícula": $ echo -n "Matricula: "; read Mat # -n no salta línea Matricula: 12345 $ echo $Mat 12345

O simplificando con la opción -p: $ read -p "Matricula: " Mat Matricula: 12345 $ echo $Mat 12345

Para leer una determinada cantidad de caracteres: $ read -n5 -p"CEP: " Num ; read -n3 -p- Compl CEP: 12345-678$ $ echo $Num 12345 $ echo $Compl 678


En este ejemplo hicimos dos read: uno para la primera parte del CEP y otra para su complemento y de este modo formateamos la entrada de datos. El signo de pesos ($) después del último número tecleado, es porque el read no tiene el new-line implicito por default como lo tiene el echo. Para leer hasta que un determinado tiempo termine (conocido como time out): $ read -t2 -p "Digite su nombre completo: " Nom || echo 'Ah perezoso!' Escriba su nombre completo: JAh perezoso! $ echo $Nom

$

Obviamente esto fue una broma, ya que solo tenía 3 segundos para escribir mi nombre completo y sólo me dio tiempo de teclear una J (aquella pegada al Ah), pero me sirvió para mostrar dos cosas: 1. El comando después del par de barras verticales (||) (el o lógico, te acuerdas?) será ejecutado en el caso que la escritura no haya terminado en el tiempo estipulado; 2. La variable Nom permaneció vacía. Esta tendrá valores solamente cuando el <ENTER> sea tecleado. Para leer un dato sin ser mostrado en la pantalla: $ read -sp "Seña: " Seña: $ echo $REPLY secreto :)

Aprovecho un error para mostrarte un detalle de programación. Cuando escribi la primera línea, me olvidé de colocar el nombre de la variable que iría a recibir la contraseña, y sólo noté eso cuando fui a listar su valor. Por suerte la variable $REPLY del Bash, posee la última cadena leída y me aproveché de esto para no perder el viaje. Verifica tu mismo lo que acabo de hacer. Pero el ejemplo que dí, era para mostrar que la opción -s impide que lo que está siendo tecleado se vea en la pantalla. Como en el ejemplo anterior, la falta del new-line hizo con que el prompt del comando ($) permaneciese en la misma línea. Bien, ahora que sabemos leer de la pantalla, veamos como se leen los datos de los archivos.

Vamos a leer archivos?


Como ya te habia dicho y te debes de acordar, el while verifica un comando y ejecuta un bloque de instrucciones mientras este comando de una respuesta correcta. Cuando estás leyendo un archivo que te dá permiso de lectura, el read sólo dará una respuesta errónea cuando alcance el EOF (end of file), de esta forma podemos leer un archivo de dos maneras: 1 - Redireccionando la entrada del archivo hacia el bloque del while así: while read Línea do echo $Línea done < archivo

2 - Redireccionando la salida de un cat hacia el while, de la siguiente forma: cat archivo | while read Línea do echo $Línea done

Cada uno de los procesos tiene sus ventajas y desventajas: Ventajas del primer proceso: • •

Es más rápido; No necesita de un subshell para asistirlo;

Desventaja del primer proceso: •

en un bloque de instrucciones grande, el redireccionamento queda poco visible, lo que a veces perjudica la visualización del código;

Ventaja del segundo proceso: •

Como el nombre del archivo está antes del while, es más fácil la visualización del código.

Desventajas del segundo proceso: •

El Pipe (|) llama un subshell para interpretarlo, volviendo el proceso más lento, pesado y a veces problemático (mira los ejemplos que siguen).

$ cat readpipe.sh #!/bin/bash # readpipe.sh # Ejemplo de read pasando archivo por pipe.

Ultimo="(vacío)"


cat $0 | # Pasando el arch. del script ($0) p/ while while read Línea do Ultimo="$Línea" echo "-$Ultimo-" done echo "Acabó, Último=:$Ultimo:"

Vamos a ver su ejecución: $ readpipe.sh -#!/bin/bash-# readpipe.sh-# Ejemplo de read pasando archivo por pipe.--Ultimo="(vacío)"-cat $0 | # Pasando el arch. del script ($0) p/ while-while read Línea-do-Ultimo="$Línea"-echo "-$Ultimo-"-done-echo "Acabó, Último=:$Ultimo:"Acabó, Último=:(vacío):

Como viste, el script lista todas sus própias líneas con un signo de menos (-) antes y otro después y al final muestra el contenido de la variable $Ultimo. Sin embargo, observa que el contenido de esta variable permanece como (vacío). - Será que la variable no fue actualizada? - Lo fue, y eso puede ser comprobado porque la línea echo "-$Ultimo-" lista correctamente las líneas. - Entonces que paso? - Pues que como ya te dije, el bloque de instrucciones redireccionado por el pipe (|) es ejecutado en un subshell y allí las variables son actualizadas. Cuando este subshell termina, las actualizaciones de las variables se van junto con él, para los quintos infiernos. Observa que voy a hacer un pequeño cambio pasando el archivo por redireccionamento de entrada (<) y así las cosas pasarán a funcionar de una forma más perfecta: $ cat redirread.sh


#!/bin/bash # redirread.sh # Ejemplo de read pasando archivo por redireccionamento de entrada (<).

Ultimo="(vacío)" while read Línea do Ultimo="$Línea" echo "-$Ultimo-" done < $0 # Pasando el arch. del script ($0) p/ while echo "Acabó, Último=:$Ultimo:"

Y mira su ejecución sin errores: $ redirread.sh -#!/bin/bash-# redirread.sh-# Ejemplo de read pasando archivo por redireccionamento de entrada (<).--Ultimo="(vacío)"-while read Línea-do-Ultimo="$Línea"-echo "-$Ultimo-"-done < $0 # Pasando el arch. del script ($0) p/ while-echo "Acabó, Último=:$Ultimo:"Acabó, Último=:echo "Acabó, Último=:$Ultimo:":

Bien amigos de la Red Shell, para finalizar el comando read sólo falta un pequeño e importante detalle que voy a mostrar utilizando un ejemplo práctico. Imagina que quieres listar en pantalla un archivo y que cada diez registros esta lista se detenga para que el operador pueda leer el contenido de la pantalla y sólo volverá a funcionar (scroll) después que el operador pulse cualquier tecla. Para no gastar absurdamente papel (de la Linux Magazine), voy a hacer esta lista en la horizontal y mi archivo (numeros), que tiene 30 registros solamente con números secuenciales. Mira: $ seq 30 > numeros $ cat 10porpag.sh #!/bin/bash # Prg de test para escribir # 10 líneas y parar para leer


# Versión 1

while read Num do let ContLin++

# Contando...

echo -n "$Num "

# -n para no saltar línea

((ContLin % 10)) > /dev/null || read done < numeros

Como forma de hacer un programa genérico creamos la variable $ContLin (por que en la vida real, los registros no son solamente números secuenciales) y pararemos para leer cuando el resto de la división por 10 sea cero (mandando la salida para /dev/null de forma de que no aparezca en la pantalla, ensuciandola). Sin embargo, cuando fui a ejecutarlo me dio el siguiente error: $ 10porpag.sh 1 2 3 4 5 6 7 8 9 10 12 13 14 15 16 17 18 19 20 21 23 24 25 26 27 28 29 30

Fíjate que falta el número 11 y que la lista no se paro en el read. Lo que paso es que la entrada del loop estaba redireccionada desde el archivo numeros y de esta forma, la lectura fue hecha encima de este archivo, así perdimos el 11 (y tambiém el 22). Vamos a mostrar entonces como debería quedar para funcionar correctamente: $ cat 10porpag.sh #!/bin/bash # Prg de test para escribir # 10 líneas y parar para leer # Versión 2

while read Num do let ContLin++ echo -n "$Num "

# Contando... # -n para no saltar línea

((ContLin % 10)) > /dev/null || read < /dev/tty


done < numeros

Observa que ahora la entrada del read fue redireccionada desde /dev/tty, que no es nada más que el terminal corriente, forzando de esta forma que la lectura sera hecha del teclado y no de números. Es bueno resaltar que esto no sucede solamente cuando usamos el redireccionamento de entrada, se hubieramos usado el redireccionamento via pipe (|), habría pasado lo mismo. Observa ahora su ejecución: $ 10porpag.sh 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

Esto está casi bien, pero falta un poco para quedar excelente. Vamos a mejorar un poco el ejemplo para que lo reproduzcas y verifiques (pero antes de verificar, aumenta el número de registros de numeros o reduce el tamaño de la pantalla, para que haya un salto de página). $ cat 10porpag.sh #!/bin/bash # Prg de test para escribir # 10 líneas y parar para leer # Versión 3

clear while read Num do ((ContLin++))

# Contando...

echo "$Num" ((ContLin % (`tput lines` - 3))) || { read -n1 -p"Teclee Algo " < /dev/tty # para leer cualquier caracter clear pantalla despues de la lectura }

# limpia la


done < numeros

El cambio principal hecho en este ejemplo, es con relación al salto de página, ya que esta hecho en cada cantidad-de-líneas-de-pantalla (tput lines) menos (-) 3, o sea, si la pantalla tiene 25 líneas, listará 22 registros y parará para su lectura. En el comando read también fue hecha una alteración, incluyendo un -n1 para leer solamente un caracter sin ser necesariamente un <ENTER> y la opción -p para dar el mensaje. - Bien amigo mio, por hoy ya basta porque me parece que estás saturado de esto... - No, no lo estoy, realmente puede continuar... - Si tu no lo estás, yo sí... Pero ya que estás tan entusiasmado con el Shell, te voy a dejar un ejercicio de aprendizaje que mejorara tu CDteca y que es bastante simple. Reescribe tu programa que registra CDs para montar toda la pantalla con un único echo y que después vaya posicionandose frente a cada campo para recibir los valores que serán tecleados por el operador.


- Hola amigo, como estás? - Muy bien!, quería mostrarte lo que hice, pero ya sé que tu quieres ir rápido a lo que interesa, no? - Solo para llevarte la contraria, hoy voy a dejar que me muestres tu "programita". Venga, muéstrame lo que hiciste. - Ahhh ... el ejercicio que me pasaste es muy extenso. Yo lo resolví así: $ cat musinc5 #!/bin/bash # Registra CDs (versión 5) # clear LineaMesg=$((`tput lines` - 3)) # Linea que define cuantos mensajes serán pasados de una vez a la pantalla TotCols=$(tput cols) # Cantidad de columnas de la pantalla para encuadrar mensajes echo " Inclusión de Músicas ========= == ======= Título del Álbum: | Este campo fue Pista:

<

creado

solamente para | orientar como llenar Nombre de la Música: Intérprete:" # Pantalla montada con un único echo while true do tput cup 5 38; tput el # Posiciona y limpia linea read Albun [ ! "$Albun" ] && # Operador pulso <ENTER> { Msg="Desea Terminar? (S/n)" TamMsg=${#Msg} Col=$(((TotCols - TamMsg) / 2)) # Centra mensaje en la linea tput cup $LineaMesg $Col echo "$Msg" tput cup $LineaMesg $((Col + TamMsg + 1)) read -n1 SN tput cup $LineaMesg $Col; tput el # Borra mensaje de la pantalla [ $SN = "N" -o $SN = "n" ] && continue # $SN es igual a N o (-o) n? clear; exit # Fin de la ejecución } grep "^$Albun\^" musicas > /dev/null &&


{ Msg="Este álbum ya está incluido" TamMsg=${#Msg} Col=$(((TotCols - TamMsg) / 2)) # Centra mensaje en la linea tput cup $LineaMesg $Col echo "$Msg" read -n1 tput cup $LineaMesg $Col; tput el # Borra mensaje de la pantalla continue # Vuelve para leer otro álbum } Reg="$Albun^" # $Reg recibirá los datos para grabación elArtista= # Variable que graba artista anterior while true do ((Track++)) tput cup 7 38 echo $Track tput cup 9 38 # Posiciona para leer música read Musica [ "$Musica" ] || # Si el operador escribio ... { Msg="Fin del Álbum? (S/n)" TamMsg=${#Msg} Col=$(((TotCols - TamMsg) / 2)) # Centra mensaje en la linea tput cup $LineaMesg $Col echo "$Msg" tput cup $LineaMesg $((Col + TamMsg + 1) read -n1 SN tput cup $LineaMesg $Col; tput el # Borra mensaje de la pantalla [ "$SN" = N -o "$SN" = n ]&&continue # $SN es igual a N o (-o) n? break # Sale del loop para grabar } tput cup 11 38 # Posiciona para leer Artista [ "$elArtista" ]&& echo -n "($elArtista) " # Artista anterior es default read Artista [ "$Artista" ] && elArtista="$Artista" Reg="$Reg$elArtista~$Musica:" # Montando registro tput cup 9 38; tput el # Borra Música de la pantalla tput cup 11 38; tput el # Borra Artista de la pantalla done echo "$Reg" >> musicas # Graba registro en el fin del archivo sort musicas -0 musicas # Clasifica el archivo done


- Si, el programa esta bien, esta todo bien estructurado, pero me gustaría comentarte un poco lo que hiciste: •

Solo para recordarte, las siguientes construcciones: [ ! $Albun ] && y [ $Musica ] || representan lo mismo, en el primer caso, comprobamos si la variable $Album no (!) tiene nada dentro, entonces (&&) ... y en el segundo, comprobamos lo mismo en $Musica, si no (||) ... Si te quejaste por el tamaño, es porque todavía no te pase algunos trucos. Fíjate que la mayor parte del script es para dar mensajes centrados en la penúltima linea de la pantalla. Fíjate también que algunos mensajes piden un S o un N y otros son sólo de advertencia. Sería el caso típico del uso de funciones, que serían escritas solamente una vez y llamadas para ejecutar en diversos puntos del script . Voy a hacer dos funciones para resolver estos casos y vamos a incorporarlas a tu programa para ver el resultado final.

Funciones - Mozo! Ahora tráeme dos "chops" bien helados, uno sin espuma, para que me de inspiración. Pregunta () { # La función recibe 3 parámetros en el siguiente orden: # $1 - Mensaje que será mostrado en la pantalla # $2 - Valor que será aceptado como respuesta por defecto # $3 - Otro valor aceptado # Suponiendo que $1=Acepta?, $2=s y $3=n, la linea a # seguir colocaría en Msg el valor "Acepta? (S/n)" local Msg="$1 (`echo $2 | tr a-z A-Z`/`echo $3 | tr A-Z a-z`)" local TamMsg=${#Msg} local Col=$(((TotCols - TamMsg) / 2)) # Centra mensaje en la linea tput cup $LineaMesg $Col echo "$Msg" tput cup $LineaMesg $((Col + TamMsg + 1)) read -n1 SN [ ! $SN ] && SN=$2 # Si esta vacía coloca por defecto en SN echo $SN | tr A-Z a-z # La salida de SN será en minúscula tput cup $LineaMesg $Col; tput el # Borra mensaje de la pantalla return # Sale de la función }

Como podemos ver, una función es definida cuando hacemos nombre_de_la_función y todo su cuerpo esta entre llaves ({}). Así como charlamos aquí en el Bar sobre pasar parámetros, las funciones los reciben de la misma forma, o sea, son parámetros de posición ($1, $2, ..., $n) y todas las reglas que se aplican al pase de parámetros para programas, también valen para funciones, pero es muy importante aclarar que los parámetros pasados hacia un programa no se mezclan con aquellos que éste pasó hacia sus funciones. Esto significa, por ejemplo, que el $1 de un script es diferente del $1 de una de sus funciones. ()


Fíjate que las variables $Msg, $TamMsg y $Col son de uso restringido de esta rutina, y por eso fueron creadas como local. La finalidad de esto es simplemente economizar memoria, ya que al salir de la rutina, todas serán destruidas y si no hubiese usado esta opción, se quedarían residentes en la memoria. La linea de código que crea local Msg, junta el texto recibido ($1) abre paréntesis, la respuesta default ($2) en mayúscula, una barra, la otra respuesta ($3) en minúscula y finaliza cerrando el paréntesis. Uso esta forma para, que al mismo tiempo, pueda mostrar las opciones disponibles y destacar la respuesta ofrecida como default. Casi al final de la rutina, la respuesta recibida ($SN) se pasa a minúscula de forma que en el cuerpo del programa no se necesite hacer esta prueba. Veamos ahora como quedaría la función para presentar un mensaje en la pantalla: function MandaMsg { # La función recibe solamente un parámetro # con el mensaje que se desea mostrar, # para no obligar al programador que pase # el mensaje entre comillas, usaremos $* (todos # los parámetros, te acuerdas?) y no $1. local Msg="$*" local TamMsg=${#Msg} local Col=$(((TotCols - TamMsg) / 2)) # Centra el mensaje en la linea tput cup $LineaMesg $Col echo "$Msg" read -n1 tput cup $LineaMesg $Col; tput el # Borra el mensaje de la pantalla return # Sale de la función }

Esta es otra forma de definir una función: no la llamamos como en el ejemplo anterior usando una construcción con la sintaxis nombre_de_la_función (), sino como function nombre_de_la_función. No tiene ninguna diferencia con la anterior, excepto que, como consta en los comentarios, usamos la variable $* que como ya sabemos es el conjunto de todos los parámetros pasados, para que el programador no necesite usar comillas envolviendo el mensaje que desea pasar para la función. Para terminar con este blá-blá-blá vamos a ver entonces las alteraciones que el programa necesita cuando usamos el concepto de funciones: $ cat musinc6 #!/bin/bash # Registra CDs (versión 6) #

# Área de las variables globales


LineaMesg=$((`tput lines` - 3)) dados para el operador

# Linea que mensajes serán

TotCols=$(tput cols) # Cantidad de columnas de la pantalla para encuadrar mensajes

# Área de las funciones Pregunta () { # La función recibe 3 parámetros en el siguiente orden: #

$1 - Mensaje que será mostrado en la pantalla

#

$2 - Valor que será aceptado como respuesta default

#

$3 - Otro valor aceptado

#

Suponiendo que $1=Acepta?, $2=s y $3=n, la linea a

#

seguir colocaría en Msg el valor "Acepta? (S/n)"

local Msg="$1 (`echo $2 | tr a-z A-Z`/`echo $3 | tr A-Z a-z`)" local TamMsg=${#Msg} local Col=$(((TotCols - TamMsg) / 2)) mensaje en la linea

# Centra

tput cup $LineaMesg $Col echo "$Msg" tput cup $LineaMesg $((Col + TamMsg + 1)) read -n1 SN [ ! $SN ] && SN=$2 coloca default en SN

# Si vacia

echo $SN | tr A-Z a-z SN será en minúscula

# La salida de

tput cup $LineaMesg $Col; tput el de la pantalla

# Borra mensaje

return función

# Sale de la


} function MandaMsg { # La función recibe solamente un parametro # con el mensaje que se desea mostrar, # para no obligar al programador que pase # el mensaje entre comillas, usaremos $* (todos # los parametros, te acuerdas?) y no $1. local Msg="$*" local TamMsg=${#Msg} local Col=$(((TotCols - TamMsg) / 2)) # Centra mensaje en la linea tput cup $LineaMesg $Col echo "$Msg" read -n1 tput cup $LineaMesg $Col; tput el de la pantalla

# Borra mensaje

return función

# Sale de la

}

# El cuerpo del programa propiamente dicho comienza aqui clear echo " Inclusión de Músicas ========= == =======

Título del Álbun:


| Este campo fue Pista:

<

creado

solamente para | orientar como llenar Nombre de la Música:

Intérprete:" montada con un único echo

# Pantalla

while true do tput cup 5 38; tput el limpia linea

# Posiciona y

read Albun [ ! "$Albun" ] &&

# Operador dió

{ Pregunta "Desea Terminar" s n [ $SN = "n" ] && continue verifico minúsculas

# Ahora sólo

clear; exit ejecución

# Fin de la

} grep -iq "^$Albun\^" musicas 2> /dev/null && { MandaMsg Este álbun ya esta catastrado continue otro álbun

# Vuelve para leer

} Reg="$Albun^" datos de grabación

# $Reg recibirá los


elArtista= anterior

# Grabará artista

while true do ((Track++)) tput cup 7 38 echo $Track tput cup 9 38 leer música

# Posiciona para

read Musica [ "$Musica" ] ||

# Si el operador

dio ... { Pregunta "Fin de Álbun?" s n [ "$SN" = n ] && continue prueba la minuscula

# Ahora solo

break para grabar datos

# Sale del loop

} tput cup 11 38 Posiciona para leer Artista

#

[ "$elArtista" ]&& echo -n "($elArtista) " Artista anterior es default

#

read Artista [ "$Artista" ] && elArtista="$Artista" Reg="$Reg$elArtista~$Musica:" Montando registro

#

tput cup 9 38; tput el Musica de la pantalla

# Borra

tput cup 11 38; tput el Artista de la pantalla

# Borra

done


echo "$Reg" >> musicas en el fin del archivo

# Graba registro

sort musicas -o musicas archivo

# Clasifica el

done

Fijate que la estructura del _script_esta como en el gráfico de abajo: Cuerpo del Programa Variables Globales Funciones Esta estructuración es debida a que el Shell es un lenguaje interpretado y así el programa es leído de izquierda a derecha y de arriba para abajo. De esa forma, para que una variable sea vista simultáneamente por el script y sus funciones, debe ser declarada (o inicializada) antes de cualquier otra cosa. Las funciones deben ser declaradas antes del cuerpo del programa propiamente dicho para que en el lugar en que el programador mencione su nombre, el interprete Shell ya lo haya localizado antes y registrado que es una función. Una cosa muy útil en el uso de funciones es tratar de hacerlas lo más generales posible, de forma que sirvan para otras aplicaciones, sin necesidad de tener que reescribirlas. Esas dos que acabamos de ver tienen uso general, pues es dificil hallar un script que tenga una entrada de datos por teclado que no use una rutina del tipo de la MandaMsg o no interaccione con el operador a través de algo parecido a Pregunta. Consejo de amigo: crea un archivo y cada función nueva que programes, añádela a este archivo. Así con el tiempo tendrás una bella biblioteca de funciones que te ahorrará mucho tiempo de programación.

El comando source Fíjate si notas algo diferente en la salida del ls siguiente: $ ls -la .bash_profile -rw-r--r-- 1 Julio unknown .bash_profile

4511 Mar 18 17:45

No mires la respuesta y vuelve a prestar atención! De acuerdo, ya que no tienes paciencia para pensar y prefieres leer la respuesta, te voy a dar una pista: me parece que ya sabes que el .bash_profile es uno de los programas que son automáticamente "ejecutados" cuando tu te logeas (ARRGGHH! Odio este término). Ahora que te dí esta ayuda, mira nuevamente la salida del ls y dime que hay de diferente en ella.


Como te dije el .bash_profile es "ejecutado" en el momento del logon y fíjate que no tiene ningúna prerrogativa de ejecución. Esto ocurre porque si tu lo ejecutaras como cualquier otro script simple, cuando terminara su ejecución, todo el ambiente generado por él moriría junto con el Shell en el cual fue ejecutado (te acuerdas que todos los scripts son ejecutados en subshells, verdad?). Pues bien, es para cosas así que existe el comando source, también conocido por . (punto). Este comando hace que no sea creado un nuevo Shell (un subshell) para ejecutar el programa que le es pasado como parámetro. Mejor un ejemplo que 10.000 palabras. Mira el scriptiziño siguiente: $ cat script_bobo cd .. ls

Simplemente debería ir hacia el directório superior del directório actual. Vamos a ejecutar unos comandos que incluyen el script_bobo y vamos a analizar los resultados: $ pwd /home/jneves $ script_bobo jneves juliana $ pwd /home/jneves

paula

silvie

Si yo mandé subir un directório, porque no subió? Subió sí! El subshell que fue creado para ejecutar el script subió y listó los directórios de los cuatro usuarios debajo del /home, solo que así que el script acabó, el subshell se fue al limbo y con él, todo el ambiente creado. Mira ahora como la cosa cambia: $ source script_bobo jneves juliana paula $ pwd /home $ cd /home/jneves $ . script_bobo jneves juliana paula $ pwd /home

silvie

silvie

Ahh! Ahora sí! Siendo pasado como parámetro del comando source o . (punto), el script fue ejecutado en el Shell corriente dejando en este, todo el ambiente creado. Ahora damos un rewind hacia el inicio de la explicación sobre este comando. Un poco antes, hablamos del .bash_profile, y a estas alturas ya debes saber que su tarea es, inmediatamente después del login, dejar el ambiente de trabajo preparado para el usuário, y ahora entendemos que por eso es ejecutado usando esta construcción.


Y ahora debes estarte preguntando, sólo sirve para eso este comando?, y yo te digo que sí, pero eso nos trae una cantidad de ventajas y una de las más usadas es tratar funciones como rutinas externas. Mira una forma diferente de hacer nuestro programa para incluir CDs en el archivo musicas: $ cat musinc7 #!/bin/bash # Registra CDs (versión7) #

# Área de varibles globales LinhaMesg=$((`tput lines` - 3)) dadas para operador

# Línea que msgs serán

TotCols=$(tput cols) para encuadrar msgs

# Qtd colunas de la tela

# El cuerpo del programa propriamente dicho comienza aqui clear echo " Inclusión de Músicas ======== == =======

Título do Álbum: | Este campo fue Pista:

<

creado

solamente para | orientar como llenar Nombre de la Música:

Intérprete:" con un único echo while true

# Pantalla montada


do tput cup 5 38; tput el línea

# Posiciona y limpa

read Album [ ! "$Album" ] &&

# Operador dió

{ source pergunta.func "Desea Terminar" s n [ $SN = "n" ] && continue verifico minúsculas

# Ahora sólo

clear; exit ejecución

# Fin de la

} grep -iq "^$Album\^" musicas 2> /dev/null && { . mandamsg.func Este álbum ya está catastrado continue otro álbum

# Vuelve para leer

} Reg="$Album^" datos de grabación

# $Reg reciberá los

oArtista= anterior

# Guardará artista

while true do ((Faixa++)) tput cup 7 38 echo $Faixa tput cup 9 38 leer música read Musica

# Posiciona para


[ "$Musica" ] || hubiese dado ...

# Si el operador

{ . pergunta.func "Fin del Álbum?" s n [ "$SN" = n ] && continue verifico minúsculas

# Ahora sólo

break para grabar datos

# Sale del loop

} tput cup 11 38 Posiciona para leer Artista

#

[ "$oArtista" ] && echo -n "($oArtista) " Artista anter. é default

#

read Artista [ "$Artista" ] && oArtista="$Artista" Reg="$Reg$oArtista~$Musica:" Montando registro

#

tput cup 9 38; tput el Musica de la pantalla

# Borra

tput cup 11 38; tput el Artista de la pantalla

# Borra

done echo "$Reg" >> musicas en el fin del archivo

# Graba registro

sort musicas -o musicas archivo

# Clasifica el

done

Ahora el programa disminuyo considerablemente de tamaño y las funciones fueron cambiadas por archivos externos llamados pergunta.func y mandamsg.func, que de esta forma, pueden ser llamados por cualquer otro programa y con eso, reutilizando su código. Por motivos meramente didácticos las ejecuciones de pergunta.func y están siendo llamadas por source y por . (punto)

mandamsg.func


indiscriminadamente, sin embargo, prefiero el source por ser más visible, lo que le da mayor legibilidad al código y facilita su manutención posteriormente. Mira ahora como quedaron estos dos archivos: $ cat pergunta.func # La función recibe 3 parámetros en el siguiente orden: # $1 - Mensaje a ser enviado a la pantalla # $2 - Valor que sera aceptado como respuesta por defecto # $3 - El otro valor aceptado # Suponiendo que $1=Acepta?, $2=s y $3=n, en la línea # de abajo colocaría en Msg el valor "Acepta? (s/n)" Msg="$1 (`echo $2 | tr a-z A-Z`/`echo $3 | tr A-Z a-z`)" TamMsg=${#Msg} Col=$(((TotCols - TamMsg) / 2)) # Centra msg en la línea tput cup $LinhaMesg $Col echo "$Msg" tput cup $LinhaMesg $((Col + TamMsg + 1)) read -n1 SN [ ! $SN ] && SN=$2 # Si esta vacía coloca default en SN echo $SN | tr A-Z a-z # La salida de SN será en minúscula tput cup $LinhaMesg $Col; tput el # Borra msg de la pantalla $ cat mandamsg.func # La función recibe solamente un parámetro # con el mensaje que se desea exhibir, # para no obligar al programador a pasar # el msg entre comillas, usaremos $* (todos # los parámetro, recuerdas?) y no $1. Msg="$*" TamMsg=${#Msg} Col=$(((TotCols - TamMsg) / 2)) # Centra msg en la línea tput cup $LinhaMesg $Col echo "$Msg" read -n1 tput cup $LinhaMesg $Col; tput el # Borra msg de la pantalla

En ambos archivos, hice solamente dos cambios que veremos en las observaciones que siguen, sin embargo tengo tres observaciones más para hacer: 1. Las variables no están siendo declaradas como local, porque está es una directiva que solamente puede ser usada en el cuerpo de funciones y por consiguiente, estas variables permanecen en el ambiente del Shell, llenándolo de basura; 2. El comando return no está presente pero podría estarlo, sin alterar en nada la lógica, ya que sólo serviría para indicar un eventual error vía un código de retorno previamente establecido (por ejemplo return 1, return 2, ...), siendo que el return y return 0 son idénticos y significan rutina ejecutada sin errores;


3. El comando que estamos acostumbrados a usar para generar código de retorno es el exit, pero la salida de una rutina externa no puede ser hecha de esta forma, porque por estar siendo ejecutada en el mismo Shell que el script llamador, el exit simplemente cerraría este Shell, terminando la ejecución de todo el script; 4. De donde surgió la variable LinhaMesg? Ella vino del musinc7, porque habia sido declarada antes de la llamada de las rutinas (sin olvidar que el Shell que está interpretando el script y estas rutinas, es el mismo para todos); 5. Si decidiste usar rutinas externas, no seas haragán, abunda en los comentarios (principalmente sobre el pasaje de los parámetros) para facilitar la manutención y su uso para otros programas en el futuro. - Bien, ahora ya tienes una cantidad de novedades para mejorar los scripts que hicimos. Te acuerdas del programa listartista en el cual pasabas el nombre de un artista como parámetro y él devolvia sus músicas? Era así: $ cat listartista #!/bin/bash # Dado un artista, muestra sus músicas # versión 2

if

[ $# -eq 0 ]

then echo Usted debería haber pasado al menos 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

- Claro que me acuerdo!...


- Entonces para afirmar los conceptos que te pasé, hazlo con la pantalla formateada, en loop, de forma que solamente termine cuando reciba un <ENTER> puro en el nombre del artista. Ahhh! Cuando la lista llegue a la antepenúltima línea de la pantalla, el programa deberá detenerse para que el operador pueda leerlas, o sea, imagina que la pantalla tenga 25 lineas. Cada 22 músicas listadas (cantidad de líneas menos 3) el programa aguardará a que el operador teclee algo para entonces continuar. Eventuales mensajes de error deben ser pasados usando la rutina mandamsg.func que acabamos de hacer. - Mozo, trae dos más, el mio con poca presión...


- Está bien, ya sé que vas a querer un "chopp" antes de empezar, pero tengo muchas ganas de enseñarte primero lo que hice, así que voy pidiéndote ya la bebida y enseguida te lo muestro. - Mozo!, trae dos. El de él sin espuma para no ensuciarse el bigote... - Mientras el "chopp" no llega, déjame recordarte que me pediste que rehiciera el con la pantalla formateada, en loop, de forma que solamente termine cuando reciba un <ENTER> puro en el nombre del artista. Eventuales mensajes de error y preguntas deberían ser mostradas en la antepenúltima línea de la pantalla utilizando las rutina mandamsj.func y pregunta.func que acabamos de desarrollar. listartista

- Primero optimize el mandamsj.func y el pregunta.func, que quedaron así: $ cat mandamsj.func # La función recibe solamente un parámetro # con el mensaje que se desea exhibir. # Para no obligar al programador a pasar # el msj entre comillas, usaremos $* (todos # los parámetros, recuerdas?) y no $1. Msj="$*" TamMsj=${#Msj} Col=$(((TotCols - TamMsj) / 2)) # Centra msj en la línea tput cup $líneaMesj $Col read -n1 -p "$Msj " $ cat pregunta.func # La función recibe 3 parámetros en el siguiente orden: # $1 - mensaje a ser dado en pantalla # $2 - Valor a ser acepto como respuesta default # $3 - El otro valor aceptado # Suponiendo que $1=Acepta?, $2=s y $3=n, la línea # abajo colocaría en Msj el valor "Acepta? (s/n)" Msj="$1 (`echo $2 | tr a-z A-Z`/`echo $3 | tr A-Z a-z`)" TamMsj=${#Msj} Col=$(((TotCols - TamMsj) / 2)) # Centra msj en la línea tput cup $líneaMesj $Col read -n1 -p "$Msj " SN [ ! $SN ] && SN=$2 # Si vacía coloca default en SN SN=$(echo $SN | tr A-Z a-z) # La salida de SN será en minúscula tput cup $líneaMesj $Col; tput el # Borra msj de la pantalla

- Y aquí va el grandullón ahora: $ cat listartista3 #!/bin/bash # Dado un artista, muestra sus músicas # versión 3


líneaMesj=$((`tput lines` - 3)) dados al operador

# línea que msjs serán

TotCols=$(tput cols) pantalla para encuadre de msjs

# Ctd de columnas de la

clear echo " +---------------------------------------------------+ |

Lista Todas las Músicas de

|

----- ----- -- ------- --

un Determinado Artista | -- ----------- -------

| |

| |

Informe el Artista:

| +---------------------------------------------------+" while true do tput cup 5 51; tput ech 31 # ech=Erase chars (31 caracteres para no borrar barra vertical) read Nombre if

[ ! "$Nombre" ]

# $Nombre está vacío?

then . pregunta.func "Desea Salir?" s n [ $SN = n ] && continue break fi


fgrep -iq "^$Nombre~" musicas || # fgrep no interpreta ^ como expresión regular { . mandamsjg.func "No existe música de este artista" continue }

tput cup 7 29; echo '| |' LinActual=8 IFS=" :" for ArtMus in $(cut -f2 -d^ musicas) # Excluye nombre del album do if

echo "$ArtMus" | grep -iq "^$Nombre~"

then tput cup $LinActual 29 echo -n '|

'

echo $ArtMus | cut -f2 -d~ tput cup $LinActual 82 echo '|' let LinActual++ if

[ $LinActual -eq $líneaMesj ]

then . mandamsj.func "Teclee Algo para Continuar..." tput cup 7 0; tput ed a partir de la línea 7

# Borra la pantalla


tput cup 7 29; echo '| |' LinActual=8 fi fi done tput cup $LinActual 29; echo '| |' tput cup $((++LinActual)) 29 read -n1 -p "+-----------Teclee Algo para Nueva Consulta----------+" tput cup 7 0; tput ed a partir de la línea 7

# Borra la pantalla

done

- Caramba!, hoy llegaste con mucha fuerza! Pero me gustó la forma en que resolviste el problema y estructuraste el programa. Fue más trabajoso pero la presentación quedó excelente y usaste bastante las opciones del tput. Vamos a comprobar el resultado con un álbum de Emerson, Lake & Palmer que tengo registrado: +----------------------------------------------------+ | Lista Todas las Músicas de un Determinado Artista | | ----- ----- -- ------- -- -- ----------- ------- | | | | Informe el Artista: Emerson, Lake & Palmer | +----------------------------------------------------+ | | | Jerusalem | | Toccata | | Still ... You Turn Me On | | Benny The Bouncer | | Karn Evil 9 | | | +-----------Teclee Algo para Nueva Consulta----------+

Mejorando la escritura - Ufa! Ahora ya lo sabes todo sobre lectura, pero sobre escritura apenas estás gateando. Ya sé que me vas a preguntar: - Pero, no era con el comando echo y con los redireccionamentos de salida que se escribe?


Si, con estos comandos escribes el 90% de las cosas necesarias, sin embargo, si necesitas escribir algo formateado te dará mucho trabajo. Para formatear la salida veremos ahora una instrucción muy interesante - el printf - su sintaxis es la siguiente: printf formato [argumento...]

En donde: formato - es una cadena de caracteres que contiene 3 tipos de objetos: 1. caracteres simples; 2. caracteres para especificación de formato; 3. secuencia de escape en el patrón del lenguaje C. Argumento

- es la cadena a ser impresa con el control del formato.

Cada uno de los caracteres utilizados para especificación de formato está precedido por el carácter % y luego viene la especificación de formato de acuerdo con la tabla: Tabla de los Caracteres de Formatación del printf %

Letra

Imprime un %. no existe ninguna conversión La expresión será impresa como:

c

Simple caracter

d

Número en sistema decimal

e

Notación científica exponencial

f

Número con punto decimal (float)

g

El menor entre los formatos %e y %f con supresión de los ceros no significativos

o

Número en sistema octal

s

Cadena de caracteres

x

Número en sistema hexadecimal

Las secuencias de escape patrón del lenguaje C son siempre precedidas por una barra invertida (\) y las reconocidas por el comando printf son: Secuencias de Escape del printf t

Secuencia

Avanza para la próxima marca de tabulación Efecto

a

Suena el bip

b

Vuelve una posición (backspace)

f

Salta para la próxima página lógica (form feed)


Secuencias de Escape del printf n

Salta para el inicio de la línea siguiente (line feed)

r

Vuelve para el inicio de la línea actual (carriage return)

Y no se acaba aquí, todavía hay más! Hay muchas más cosas sobre esta instrucción, pero como son muchos detalles es por consiguiente aburrido de explicar y todavía peor de leer o estudiar, así que vamos directos a los ejemplos con sus comentarios, que no estoy aqui para aburrir a nadie. $ printf "%c" "1 caracter" 1$ Error! Sólo listó 1 caracter y no saltó de línea al final $ printf "%c\n" "1 caracter" 1 Saltó de línea pero todavia no listó la cadena entera $ printf "%c caracteres\n" 1 1 caracter Esta es la forma correcta, el %c recebió el 1 $ a=2 $ printf "%c caracteres\n" $a 2 caracteres O %c recibió el valor de la variable $a $ printf "%10c caracteres\n" $a 2 caracteres $ printf "%10c\n" $a caracteres 2 c

Observa que en los dos últimos ejemplos, en virtud del %c, sólo se listo un caracter de cada cadena. El 10 delante de la c, no significa 10 caracteres. Un número después del signo de porcentaje (%) significa el tamaño que la cadena tendrá depués de la ejecución del comando. Y aqui vá un ejemplo: $ printf "%d\n" 32 32 $ printf "%10d\n" 32 32 Rellena con blancos a la izquierda y con ceros $ printf "%04d\n" 32 0032 04 despues % significa 4 dígitos con ceros a la izquierda $ printf "%e\n" $(echo "scale=2 ; 100/6" | bc) 1.666000e+01 El default del %e es 6 decimales $ printf "%.2e\n" `echo "scale=2 ; 100/6" | bc` 1.67e+01 El .2 especificó dos decimales $ printf "%f\n" 32.3 32.300000 El default del %f es 6 decimales


$ printf "%.2f\n" 32.3 32.30 El .2 especificó dos decimales $ printf "%.3f\n" `echo "scale=2 ; 100/6" | bc` 33.330 El bc devolvió 2 decimales. El printf colocó 0 a la derecha $ printf "%o\n" 10 12 Convirtió el 10 en octal $ printf "%03o\n" 27 033 Así la conversión queda con más apariencia de octal, sí? $ printf "%s\n" Palabra Palabra $ printf "%15s\n" Palabra Palabra Palabra con 15 caracteres rellenados con blancos $ printf "%-15sNeves\n" Palabra Palabra Neves El menos (-) rellenó a la derecha con blancos $ printf "%.3s\n" Palabra Pal 3 Corta y deja sólo las 3 primeras $ printf "%10.3sa\n" Peteleca Peta Pet con 10 caracteres concatenado con a (después del s) $ printf "EJEMPLO %x\n" 45232 EJEMPLO b0b0 Transformó en hexa pero los zeros no combinan $ printf "EJEMPLO %X\n" 45232 EJEMPLO B0B0? Así quedó mejor (Fíjate en la X mayúscula) $ printf "%X %XL%X\n" 49354 192 10 C0CA? C0LA?

El último ejemplo no es marketing y es bastante completo, voy a comentarlo paso a paso: 1. El primer %X convirtió 49354 en hexadecimal resultando C0CA (léase "ce", "cero", "ce" y "a"); 2. En seguida viene un espacio en blanco seguido por otro %XL. El %X convirtió el 192 dando como resultado C0 que con el L hizo C0L; 3. Y finalmente el último %X transformó el 10 en A. Como puedes notar, la instrucción printf es bastante completa y compleja (por suerte el echo lo resuelve casi todo). Creo que cuando me decidí a explicar el printf a través de ejemplos, acerté plenamente, porque no sabria como enumerar tantas reglitas sin hacer la lectura aburrida.

Principales Variables del Shell


El Bash posee diversas variables que sirven para dar informaciones sobre el ambiente o alterarlo. Su número es muy grande y no pretendo mostrártelas todas sino una pequeña parte, y que pueden ayudarte en la elaboración de scripts. Ahí van las principales: Principales variables del Bash TMOUT

Variável

Si tuviera un valor mayor que cero, este valor será tomado como el patrón de timeout del comando read. En el prompt, este valor es interpretado como el tiempo de espera a una acción antes de finalizar la sesión. Suponiendo que la variable contenga 30, el Shell dará logout 30 segundos después que el prompt esté sin ninguna acción. Conteúdo

CDPATH

Contiene los caminos que serán recorridos para intentar localizar un directório especificado. A pesar de ser esta variable poco conocida, su uso debe ser incentivado por que nos ahorra mucho trabajo, principalmente en instalaciones con estructuras de directórios con bastante niveles.

HISTSIZE

Limita el número de instrucciones que caben dentro del archivo histórico de comandos (normalmente .bash_history pero efectivamente es lo que está almacenado en la variable $HISTFILE). Su valor default es 500.

HOSTNAME LANG LINENO

LOGNAME

El nombre del host actual (que también puede ser obtenido con el comando uname -n). Usada para determinar el idioma hablado en el país (más especificamente categoria de locale). El número de la línea del script o de la función que está siendo ejecutada, su uso principal es para dar mensajes de error juntamente con las variables $0 (nombre del programa) y $FUNCNAME (nombre de la función en ejecución) Almacena el nombre de login del usuário.

MAILCHECK

Especifica, en segundos, la frecuencia con que el Shell verificará la presencia de correspondencia en los archivos indicados por las variables $MAILPATH o $MAIL. El tiempo patrón es de 60 segundos. Una vez que este tiempo expira, el Shell hará esta verificación antes de exhibir el próximo prompt primario (definido en $PS1). Si esta variable estuviera sin valor o con un valor menor o igual a cero, la verificación de nueva correspondencia no será efectuada.

PATH

Caminos que serán recorridos para intentar localizar un archivo especificado. Como cada script es un archivo, en el caso de que uses el directorio actual (.) en su variable $PATH, no necesitarás usar el ./scrp para que scrp sea ejecutado. Basta hacer scrp. Este es el modo en que procedo aqui en el Bar.


Principales variables del Bash PIPESTATUS

Es una variable del tipo vector (array) que contiene una lista de valores de código de retorno del último pipeline ejecutado, o sea, un array que abriga cada uno de los $? de cada instrucción del último pipeline.

PROMPT_COMMAND Si esta variable recibe una instrucción, cada vez que tu des un <ENTER> directo en el prompt principal ($PS1), este comando será ejecutado. Es útil cuando se está repitiendo mucho una determinada instrucción. PS1

Es el prompt principal. En "Conversa de Bar" usamos sus defaults: $ para el usuário común y # para el root, pero es muy frecuente que esté personalizado. Una curiosidad es que existen hasta concursos de quien programa el $PS1 más creativo. (clique para dar una googlada)

PS2

También llamado prompt de continuación, es aquél signo de mayor (>) que aparece después de un <ENTER> sin que el comando haya sido finalizado.

PWD RANDOM

Cada vez que esta variable es llamada, devuelve un número entero, que es un número randómico entre 0 y 32767.

REPLY

Usa esta variable para recuperar el último campo leído, en caso de que no tenga ninguna variable asociada.

SECONDS

Posee el camino completo ($PATH) del directório actual. Tiene el mismo efecto que el comando pwd.

Esta variable contiene la cantidad de segundos en que el Shell actual está en uso. Úsala solamente para mostrar a un usuario aquello que llaman de sistema operacional, pero necesita de frecuentes boots.

CDPATH

$ echo $CDPATH .:..:~:/usr/local $ pwd /home/jneves/LM $ cd bin $ pwd /usr/local/bin

Como /usr/local estaba en mi variable $CDPATH, y no existía el directório bin en ninguno de sus antecesores (., .. e ~), el cd fue ejecutado para /usr/local/bin •

LANG

$ date Thu Apr 14 11:54:13 BRT 2005 $ LANG=pt_BR date


Qui Abr 14 11:55:14 BRT 2005

Con la especificación de la variable LANG=pt_BR (portugués de Brasil), la fecha pasó a ser formateada en el patrón brasileño. Es interesante observar que no se uso punto y coma (;) para separar la atribución de LANG del comando date. •

PIPESTATUS

$ who jneves pts/0 Apr 11 16:26 (10.2.4.144) jneves pts/1 Apr 12 12:04 (10.2.4.144) $ who | grep ^botelho $ echo ${PIPESTATUS[*]} 0 1

En este ejemplo mostramos que el usuário botelho no estaba "logado", en seguida ejecutamos un pipeline que lo filtraba. Se usa la notación [*] en un array para listar todos sus elementos, y de esta forma vimos que la primera instrucción (who) fue bien ejecutada (código de retorno 0) y la siguiente (grep), no (código de retorno 1). •

RANDOM

Para generar randómicamente un entero entre 0 y 100, hacemos: $ echo $((RANDOM%101)) 73

O sea, tomamos el resto de la división por 101 del número randómico generado, porque el resto de la división de cualquier número por 101 varía entre 0 y 100. •

REPLY

$ read -p "Digite S o N: " Digite S o N: N $ echo $REPLY N

Yo soy de la época en que la memoria era un bien precioso que costaba muuuuy caro. Entonces para tomar un S o un N, no acostumbro guardar un espacio especial y por lo tanto, tomo de la variable $REPLY lo que se escribio.

Expansión de parámetros Bien, mucho de lo que vimos hasta ahora son comandos externos al Shell. Estos son de gran ayuda, facilitan la visualización, manutención y depuración del código, pero no son tan eficientes como los intrínsecos (built-ins). Cuando nuestro problema sea


prestaciones, debemos dar preferencia al uso de los intrínsecos y a partir de ahora te voy a mostrar algunas técnicas para que tu programa pise el acelerador. En la tabla y ejemplos siguientes, veremos una serie de construcciones llamadas expansión (o substitución) de parámetros (Parameter Expansion), que substituyen instrucciones como el cut, el expr, el tr, el sed y otras, de forma más ágil. Expansión de parámetros ${cadena/%subcad1/subcad2}

Expresión ${var:-padrón} ${#cadena} ${cadena:posición}

Si subcad1 s igual al fin de $cadena, entonces es cambiada por subcad2 Resultado esperado Si var padrón

no tiene valor, el resultado de la expresión es

Tamaño de $cadena Extrae una sub-cadena de $cadena a partir de Origen cero

posición. ${cadena:posición:tamaño}

Extrae una sub-cadena de $cadena a partir de posición con tamaño igual a tamaño. Origen cero

${cadena#expr}

Corta la menor ocurrencia de $cadena a la izquierda de la expresión expr

${cadena##expr}

Corta la mayor ocurrencia de $cadena a la izquierda de la expresión expr

${cadena%expr}

Corta la menor ocurrencia de $cadena a la derecha de la expresión expr

${cadena%%expr}

Corta la mayor ocurrencia de $cadena a la derecha de la expresión expr

${cadena/subcad1/subcad2}

Cambia en $cadena la primera ocurrencia de subcad1 por subcad2 Cambia en $cadena todas las ocurrencias de por subcad2

${cadena//subcad1/subcad2} subcad1 ${cadena/#subcad1/subcad2} •

Si subcad1 es igual al inicio de $cadena, entonces es cambiada por subcad2

Si en una pregunta el S es ofrecido como valor default (patrón) y la salida va hacia la variable $SN, después de leer el valor podemos hacer: SN=$(SN:-S}

De esta forma si el operador dió un simple <ENTER> para confirmar que aceptó el valor default, después de ejecutar esta instrucción, la variable tendrá el valor S, en caso contrário, tendrá el valor tecleado. •

Para saber el tamaño de una cadena:


$ cadena=0123 $ echo ${#cadena} 4 •

Para extraer de una cadena de la posición uno hasta el final hacemos:

$ cadena=abcdef $ echo ${cadena:1} bcdef

Fíjate que el origen es cero y no uno. •

En la misma variable $cadena del ejemplo de arriba, para extraer 3 caracteres a partir de la 2ª posición:

$ echo ${cadena:2:3} cde

Fíjate que nuevamente el origen de la posición es cero y no uno. •

Para suprimir todo a la izquierda de la primera ocurrencia de una cadena, haz:

$ cadena="Conversa de Bar" $ echo ${cadena#*' '} de Bar $ echo "Conversa "${cadena#*' '} Conversa de Bar

En este ejemplo fue suprimido a la izquierda todo lo que estuviera antes de la ocurrencia de la expresión *' ', o sea, todo hasta el primer espacio en blanco. Estos ejemplos también podrían ser escritos sin proteger el espacio de la interpretación del Shell (pero prefiero protegerlo para facilitar a legibilidad del código), mira: $ echo ${cadena#* } de Bar $ echo "Conversa "${cadena#* } Conversa de Bar

Fíjate que en la construcción de expr está permitido el uso de metacaracteres. •

Utilizando el mismo valor de la variable $cadena, observa como haríamos para tener solamente Bar:

$ echo ${cadena##*' '} Bar $ echo "Vamos 'Chopear' en el "${cadena##*' '} Vamos 'Chopear' en el Bar


Esta vez suprimimos a la izquierda de la cadena la mayor ocurrencia de la expresión expr. Así como en el caso anterior, el uso de metacaracteres está permitido. Otro ejemplo mas útil: para que no aparezca el camino (path) completo de tu programa (que, como ya sabemos está contenido en la variable $0) en un mensaje de error, empieza tu texto de la siguiente forma: echo Uso: ${0##*/} texto del mensaje de error

En este ejemplo sería suprimido por la izquerda todo hasta la última barra (/) del camino (path), quedando solamente el nombre del programa. * El uso de porcentaje (%) es como si mirasemos el simbolo (#) en el espejo, o sea, son simétricos. Veamos un ejemplo para probarlo: $ echo $cadena Conversa de Bar $ echo ${cadena%' '*} Conversa de $ echo ${cadena%%' '*} Conversa •

Para cambiar la primera ocurrencia de una sub-cadena en una cadena por otra:

$ echo $cadena Conversa de Bar $ echo ${cadena/de/en el} Conversa en el Bar $ echo ${cadena/de /} Conversa Bar

En este caso presta atención cuando vayas a usar metacaracteres, son unos comilones! Siempre combinarán con la mayor posibilidad, mira el ejemplo siguiente donde la intención era cambiar Conversa de Bar por Charla de Bar: $ echo $cadena Conversa de Bar $ echo ${cadena/*a/Charla} Charlar

La idea era cogerlo todo hasta la primera a, pero lo que cambio fue todo hasta la última a. Esto podría resolverse de diversas formas, veamos algunas: $ echo Charla $ echo Charla

${cadena/*sa/Charla} de Bar ${cadena/????????/Charla} de Bar


* Cambiando todas las ocurrencias de una subcadena por otra. Cuando hacemos: $ echo ${cadena//a/o} Converso de Bor

Cambiamos todas las letras a por o. Otro ejemplo más útil es para contar la cantidad de archivos existentes en el directorio en uso. Observa la linea siguiente: $ ls | wc -l 30

Viste? El wc produce una cantidad de espacios en blanco al inicio. Para eliminarlos podemos hacer: $ CtdArChs=$(ls | wc -l) # CtdArchs recibe la salida del comando $ echo ${CtdArChs// /} 30

En el último ejemplo, como sabía que la salida era compuesta de blancos y números, monté esta expresión para cambiar todos los espacios por nada. Fíjate que después de las dos primeras barras existe un espacio en blanco. Otra forma de hacer la misma cosa sería: $ echo ${CtdArChs/* /} 30 •

Cambiando una sub-cadena en el inicio o en el fin de una variable. Vamos a usar como ejemplo el conocido pájaro del campo "Quero quero", conocido en otros países como "Tero tero". Para cambiarla al inicio hacemos:

$Pájaro="quero quero" $ echo $Pájaro quero quero $ echo "Como dice el gaucho - "${Pájaro/#quero/no} Como dice el gaucho - no quero

Para cambiarla al final hacemos: $ echo "Como se dice en el norte - "${Pájaro/%quero/no} Como se dice en el norte - quero no

- Ahora basta, la conversación de hoy fue muy aburrida porque hay muchas cosas para memorizar, así que lo principal es que hayas entendido lo que te dije y cuando lo necesites, consultes estas servilletas en las que escribí estas ayudas y después guárdalas


para futuras consultas. Pero volviendo a lo que importa, ha llegado la hora de tomar otro y ver el partido de futbol. Para la pr贸xima te voy a aflojar un poco y solo te voy a pedir lo siguiente: toma la rutina pregunta.func, (de la cual hablamos en el inicio de nuestra conversa de hoy) y optim铆zala para que la variable $SN reciba el valor default por expansi贸n de par谩metros, como vimos. - Mozo, no se olvide de mi y llene mi vaso.


-Que hay amigo, te lo puse mas fácil, verdad? Un ejercicio muy simple... - Si, pero en los tests que hice y de acuerdo con lo que me enseñaste sobre substitución de parámetros, me pareció que debería hacer otras alteraciones en las funciones que creamos, para dejarlas de uso más general como me dijiste que todas las funciones deberían de ser, quieres ver? - Claro, si te pedí hacerlas es porque estoy con ganas de verte aprender, pero alto! dame un momento! - Mozo! Trae dos, uno sin espuma! - Anda, enseñame lo que hiciste. - Bien, además de lo que me pediste, me fije que el programa que llamaba la función, tendría que tener previamente definida la línea en que sería dado el mensaje y la cantidad de columnas. Lo que hice fue incluir dos líneas - en las cuales emplee sustitución de parámetros - y en caso de que una de estas variables no fuese introducida, la propia función la generaría. La línea del mensaje estaría tres líneas encima del final de la pantalla y el total de columnas sería obtenido por el comando tput cols. Mira como quedó: $ cat pergunta.func # La función recibe 3 parámetros en el siguiente orden: # $1 - Mensaje a ser dado en pantalla # $2 - Valor a ser aceptado como respuesta default # $3 - Otro valor aceptado # Suponiendo que $1=Acepta?, $2=s y $3=n, la línea # abajo colocaría en Msj el valor "Acepta? (S/n)" TotCols=${TotCols:-$(tput cols)} # Si no estaba definido, ahora lo está LineaMesj=${LineaMesj:-$(($(tput lines)-3))} # Idem Msj="$1 (`echo $2 | tr a-z A-Z`/`echo $3 | tr A-Z a-z`)" TamMsj=${#Msj} Col=$(((TotCols - TamMsj) / 2)) # Para centrar Msj en la línea tput cup $LineaMesj $Col read -n1 -p "$Msj " SN SN=${SN:-$2} # Si vacio coloca default en SN SN=$(echo $SN | tr A-Z a-z) # La salida de SN será en minúscula tput cup $LineaMesj $Col; tput el # Borra msj de pantalla

- Me gustó, te anticipaste a lo que te iba a pedir. Solamente para cerrar esta conversación de sustitución de parámetros, fíjate que la legibilidad es horrible, pero la optimización, o sea, la velocidad de ejecución, está óptima. Como las funciones son cosas muy personales, ya que cada uno usa las suyas, y casi no se les da mantenimiento, yo siempre opto por la optimización. - Hoy vamos a salir de aquel aburrimiento que fue nuestra última conversación y volveremos a la lógica saliendo de la memorización, pero te vuelvo a recordar, todo lo


que te mostré la otra vez aquí en el Bar, es válido y de gran ayuda, guarda aquellas servilletas que escribimos que tarde o temprano te van a ser muy útiles.

El comando eval - Te voy a dar un problema que dudo que resuelvas: $ var1=3 $ var2=var1

- Te dí estas dos variables, y quiero que me digas como puedo, solamente refiriéndome a $var2, listar el valor de $var1 (3). - Ah! eso es fácil, es sólo hacer: echo $`echo $var2`

- Fíjate que coloqué el echo $var2 entre comillas (`), que de esta forma tendrá prioridad de ejecución y resultará en var1, montando echo$var1 que producirá 3... - A sí? Entonces ejecutalo a ver si está correcto. $ echo $`echo $var2` $var1

- Eh! Que pasó? Mi razonamiento parecía bastante lógico... - Tu razonamiento realmente fue lógico, el problema es que te olvidaste de una de las primeras cosas de que te hablé aquí en el Bar y voy a repetir. El Shell usa el siguiente orden para resolver una línea de comandos: • • • •

Resuelve los redireccionamentos; Substituye las variables por sus valores; Resuelve y substituye los meta caracteres; Pasa la línea ya toda masticada, para ejecución.

De esta forma, cuando llegó a la fase de resolución de variables, que como ya dije es anterior a la ejecución, la única variable existente era $var2 y por eso tu solución produjo como salida $var1. El comando echo identificó eso como una cadena y no como una variable. Problemas de este tipo son relativamente frecuentes y serían insolubles en caso de que no existiese la instrucción eval, cuya sintaxis es: eval cmd

Donde cmd es una línea de comando cualquiera que se podría inclusive ejecutar directamente en el prompt del terminal. Cuando pones el eval al principio, lo que


ocurre es que el Shell trata cmd como si sus datos fueran parámetros del eval y enseguida el eval ejecuta la línea recibida, sometiéndola al Shell, dando entonces en la práctica dos pasadas en cmd. De esta forma si ejecutásemos el comando que propusiste, colocando el eval en su comienzo, tendríamos la salida esperada, mira sino: $ eval echo $`echo $var2` 3

Este ejemplo también podría haber sido hecho de la siguiente manera: $ eval echo \$$var2 3

En la primera pasada la barra invertida (\) sería retirada y $var2 resuelto, produciendo var1. Para la segunda pasada habría sobrado echo $var1, que produciría el resultado esperado. Ahora voy a colocar un comando dentro de var2: $ var2=ls

Voy a ejecutar: $ $var2 10porpag1.sh 10porpag2.sh 10porpag3.sh alo1.sh

alo2.sh confuso contpal.sh incusu

listamusica listartista listartista3 logado

logaute.sh mandamsj.func monbg.sh

Ahora vamos a colocar en var2 el siguiente: ls $var1; y en var1 vamos a colocar l*, veamos: $ var2='ls $var1' $ var1='l*' $ $var2 ls: $var1: No such file or directory $ eval $var2 listamusica listartista listartista3

logado

logaute.sh

Nuevamente, al momento de la sustitución de las variables, $var1 todavía no se había presentado al Shell para ser resuelta, por eso sólo nos queda ejecutar el comando eval para dar las dos pasadas necesarias. Una vez un colega de una excelente lista sobre Shell Script, presentó una duda: quería hacer un menú que numerase y listase todos los archivos con extensión .sh y cuando el


operador escogiese una opción, el programa correspondiente sería ejecutado. Mi propuesta fue la siguiente: $ cat fazmenu #!/bin/bash # # Lista numerando los programas con extensión .sh en # directorio actual y ejecuta el escogido por el operador # clear; i=1 printf "%11s\t%s\n\n" Opción Programa CASE='case $opt in' for arq in *.sh do printf "\t%03d\t%s\n" $i $arq CASE="$CASE "$(printf "%03d)\t %s;;" $i $arq) i=$((i+1)) done CASE="$CASE *) . error;; esac" read -n3 -p "Introduce la opción deseada: " opt echo eval "$CASE"

Parece complicado porque usé mucho el printf para formatear la pantalla, pero es bastante simple, vamos a entenderlo: el primer printf fue colocado para hacer el encabezado y en seguida comencé a montar dinámicamente la variable $CASE, sobre la cual al final será hecho un eval para la ejecución del programa escogido. Observa sin embargo que dentro del loop del for existen dos printf: el primero sirve para formatear la pantalla y el segundo para montar el case (si antes del comando read colocas una línea echo "$CASE", verás que el comando case montado dentro de la variable está todo indentado. Una pasada, verdad? :). En la salida del for, fue agregada una línea a la variable $CASE, para que en el caso de que se haga una opción no válida, sea ejecutada una función externa para dar mensajes de error. Vamos a ejecutarlo para ver la salida generada: $ fazmenu.sh Opción

Programa

001

10porpag1.sh

002

10porpag2.sh

003

10porpag3.sh

004

alo1.sh

005

alo2.sh


006

contpal.sh

007

fazmenu.sh

008

logaute.sh

009

monbg.sh

010

readpipe.sh

011

redirread.sh

Introduce la opción deseada:

En este programa sería interesante tener una opción de escape, y para eso sería necesario la inclusión de una línea después del loop de montaje de la pantalla y alterar la línea en la cual hacemos la atribución final del valor de la variable $CASE. Veamos como quedaría: $ cat fazmenu #!/bin/bash # # Lista numerando los programas con extensión .sh en # directorio actual y ejecuta el escogido por el operador # clear; i=1 printf "%11s\t%s\n\n" Opción Programa CASE='case $opt in' for arq in *.sh do printf "\t%03d\t%s\n" $i $arq CASE="$CASE "$(printf "%03d)\t %s;;" $i $arq) i=$((i+1)) done printf "\t%d\t%s\n\n" 999 "Fin del programa" # línea incluida CASE="$CASE 999) exit;; # línea alterada *) ./error;; esac" read -n3 -p "Introduce la opción deseada: " opt echo eval "$CASE"

Señales de Procesos Existe en Linux una cosa llamada señal (signal). Existen diversas señales que pueden ser mandadas para (o generados por) procesos en ejecución. Vamos de aqui en adelante


a dar una ojeada a las señales enviadas hacia los procesos y más adelante vamos a dar una pasada rápida por las señales generados por procesos.

señales asesinas Para mandar una señal a un proceso, usamos normalmente el comando kill, cuya sintáxis es: kill -sig PID

Donde PID es el identificador del processo (Process IDentification o Process ID). Además del comando kill, algunas secuencias de teclas también pueden generar sig. La tabla siguiente muestra las señales más importantes para monitorear: Señales Más Importantes 15 SIGTERM Cuando recibe un kill o kill -TERM Señal Generado por: 0 EXIT Fin normal de programa 1 SIGHUP Cuando recibe un kill -HUP 2 SIGINT Interrupción por teclado (<CTRL+C>) 3 SIGQUIT Interrupción por teclado (<CTRL+\>) Además de estas señales, existe el tan abusado -9 o SIGKILL que, para el proceso que lo está recibiendo, equivale a meter el dedo en el botón de apagar el computador, lo que es altamente indeseable ya que muchos programas necesitan "limpiar el medio campo" a su término. Si el final ocurre de forma prevista, o sea si tiene un final normal, es muy fácil de hacer esta limpeza, sin embargo si el programa tiene un final brusco pueden ocurrir muchas cosas: • • • •

Es posible que en un determinado espacio de tiempo, el computador esté lleno de archivos de trabajo inútiles El procesador podrá quedar lleno de procesos zombies y defuncts generados por procesos hijos que perdieron los procesos padres; Es necesario liberar sockets abiertos para no dejar los clientes congelados; Tus bancos de datos podrán quedar corruptos porque los sistemas gestores de bancos de datos necesitan de un tiempo para grabar sus buffers en disco (commit).

En fin, existen mil razones para no usar un kill con la señal -9 y para monitorizar las terminaciones anormales de programas.

El trap no atrapa Para hacer el control de procesos descripto antes, existe el comando trap cuya sintáxis es: trap "cmd1; cmd2; cmdn" S1 S2 ... SN


o trap 'cmd1; cmd2; cmdn' S1 S2 ... SN

Donde los comandos cmd1, cmd2, cmdn serán ejecutados en caso de que el programa reciba las señales S1 S2 ... SN. Las comillas (") o los apóstrofes (') sólo son necesarias en el caso de que el trap posea más de un comando cmd asociado. Cada uno de los cmd puede también ser una función interna, una externa u otro script. Para entender el uso de las comillas (") y los apóstrofes (') vamos a recurrir a un ejemplo que trata un fragmento de un script que hace un ftp hacia una máquina remota ($RemoComp), en la cual el usuário es $Fulano, su contraseña es $Secreto y va a transmitir el archivo contenido en $Arq. Supon todavia que estas cuatro variables fueron recibidas en una rutina anterior de lectura y que este script es muy usado por diversas pesonas de la instalación. Veamos este trozo del código: ftp -ivn $RemoComp << FimFTP >> /tmp/$$ 2>> /tmp/$$ user $Fulano $Secreto binary get $Arq FimFTP

Observa que, tanto las salidas de los diálogos del ftp, como los errores encontrados, están siendo redireccionados para /tmp/$$, lo que es una construcción bastante normal para archivos temporarios usados en scripts con más de un usuário, porque $$ es la variable que contiene el número de proceso (PID), que es único, y con este tipo de construcción se evita que dos o más usuários disputen la posesión y los derechos sobre el archivo. En el caso de que este ftp sea interrumpido por un kill o un <CTRL+C>, con toda seguridad dejará basura en el disco. Es exactamente esta la forma más frecuente de usar el comando trap. Como esto es un trozo de un script, debemos hacer, al empezar y como uno de sus primeros comandos: trap "rm -f /tmp/$$ ; exit" 0 1 2 3 15

Así en el caso de que hubiese una interrupción brusca (señales 1, 2, 3 o 15), antes de que el programa finalize (en el exit dentro del comando trap), o un fin normal (señal 0), el archivo /tmp/$$ seria borrado del disco. En el caso de que en la línea de comandos del trap no tuviese la instrucción exit, al final de la ejecución de esta línea el flujo del programa volvería al punto en que estaba cuando recibió la señal que originó la ejecución de este trap. Este trap podria ser subdividido, quedando de la siguiente forma: trap "rm -f /tmp/$$" 0 trap "exit" 1 2 3 15


Así al recibir una de las señales el programa terminaría, y al terminar, generaría una señal 0, que borraría el archivo. Si la finalización es normal, la señal también será generada y el rm será ejecutado. Observa también que el Shell analiza la línea de comandos, una vez cuando el trap es interpretado (y es por eso que es usual colocarlo al inicio del programa) y nuevamente cuando se recibe alguna de las señales listadas. Entonces, en el último ejemplo, el valor de $$ será sustituído en el momento que el comando trap es leído por primera vez, ya que las comillas (") no protegen el signo de pesos ($) de la interpretación del Shell. Si deseas que la sustitución sea realizada solamente en el momento de ser recibida la señal, el comando debería estar colocado entre apóstrofes ('). Así, en la primera interpretación del trap, el Shell no vería el signo de pesos ($), sin embargo los apóstrofes (') serian retirados y finalmente el Shell podría sustituir el valor de la variable. En este caso, la línea quedaria de la siguiente manera: trap 'rm -f /tmp/$$ ; exit' 0 1 2 3 15

Suponte dos casos: tu tienes dos scripts que llamaremos script1, cuya primera línea será un trap y script2, siendo este último ejecutado por una llamada del primero, y por ser dos procesos diferentes, tendrán dos PID distintos. •

1º Caso: El ftp se encuentra en script1 En este caso, el argumento del comando trap debería estar entre comillas (") porque si ocurriese una interrupcción (<CTRL+C> o <CTRL+\>) en el script2, la línea sólo seria interpretada en este momento y el PID del script2 seria diferente del encontrado en /tmp/$$ (no te olvides que $$ es la variable que contiene el PID del proceso activo);

2º Caso: El ftp anterior se encuentra en script2 En este caso, el argumento del comando trap debería estar entre apóstrofes ('), pues en el caso de que la interrupción se diera durante la ejecución del script1, el archivo no habría sido creado, en el caso de que ocurriera durante la ejecución del script2, el valor de $$ sería el PID de este processo, que coincidiría con el de /tmp/$$.

Cuando se ejecuta el comando trap sin argumentos, lista las señales que están siendo monitoreadas en el ambiente, así como la línea de comando que será ejecutada cuando tales señales sean recibidas. Si la línea de comandos del trap es nula (o sea vacía), esto significa que las señales especificadas deben ser ignoradas cuando sean recibidas. Por ejemplo, el comando: trap "" 2

Especifica que la señal de interrupción (<CTRL+C>) debe ser ignorada. En ese caso no se desea que la ejecución sea interrumpida. En el último ejemplo fíjate que el primer argumento debe ser especificado para que la señal sea ignorada, y no es equivalente a escribir lo siguiente, cuya finalidad es de retornar la señal 2 a su estado patrón (default):


trap 2

Si se ignora una señal, todos los Subshells ignoraran esta señal. Por lo tanto, si tu especificas que acción debe ser tomada cuando se reciba una señal, entonces todos los Subshells también tomaran la misma acción cuando reciban esta señal, o sea, las señales son automáticamente exportadas. Para la señal que hemos mostrado (señal 2), significa que los Subshells serán finalizados. Suponte que ejecutes el comando: trap "" 2

y entonces ejecutes un Subshell, que volverá a ejecutar otro script como un Subshell. Si se generase una señal de interrupción, esta no tendrá efecto sobre el Shell principal ni sobre los Subshell por él llamados, ya que todos ellos ignorarán la señal. Otra forma de restaurar una señal a su patrón (default) es haciendo: trap - señal

En korn shell (ksh) no existe la opción -s del comando read para leer una señal. Lo que acostumbramos hacer es usar el comando stty con la opción -echo que inhibe la escritura en pantalla hasta que se encuentre un stty echo para restaurar esta escritura. Entonces, si estamos usando el interprete ksh, la lectura de la señal sería hecha de la siguiente forma: echo stty read stty

-n "Señal: " -echo Señal echo

El problema en este tipo de construcción es que en el caso de que el operador no supiese la señal, probablemente haría un <CTRL+C> o un <CTRL+\> durante la instrucción read para detener el programa y en el caso de que actúe así, cualquier cosa que escribiese no aparecería en la pantalla del terminal. Para evitar que eso pase, lo mejor a hacer es: echo -n "Señal: " trap "stty echo exit" 2 3 stty -echo read Señal stty echo trap 2 3

Para terminar este asunto, abre un terminal gráfico y escribe en el prompt de comando lo siguiente: $ trap "echo Cambió el tamaño de la ventana " 28

En seguida, coge el mouse (arghh!!) y arrástralo para variar el tamaño de la ventana actual. Sorprendido? Es el Shell orientado a eventos...


Uno mas, porque no me puedo resistir... Ahora escribe esto: $ trap "echo acabó" 17

En seguida haz: $ sleep 3 &

Acabas de crear un subshell que dormirá durante tres segundos en background. Al final de este tiempo, recibirás un mensaje acabó, porque la señal 17 es emitida cada vez que un subshell termina su ejecución. Para volver estas señales a sus opciones por defecto, haz: $ trap 17 28

O: $ trap - 17 28

Acabamos de ver otras dos señales que no son tan importante como las que vimos anteriormente, pero voy a registrarlas en la tabla siguiente:

28 17

Señales no Muy Importantes SIGWINCH Cambio de tamaño de la ventana gráfica Señal Generada por: SIGCHLD Fin de un proceso hijo

Muy bueno este comando, verdad? Si tu descubres algun caso interesante del uso de señales, por favor informame por e-mail porque es muy rara la literatura sobre el asunto.

Comando getopts El comando getopts recupera las opciones y sus argumentos de una lista de parámetros de acuerdo con la sintáxis POSIX.2, o sea, letras (o números) después de un señal de menos (-) seguidas o no de un argumento; en el caso de tener solamente letras (o números) se pueden agrupar. Debes usar este comando para "cortar en partes" opciones y argumentos pasados hacia tu script. Sintáxis: getopts cadenadeopciones nombre

La cadenadeopciones debe explicitar una cadena de caracteres con todas las opciones reconocidas por el script, así si este reconoce las opciones -a -b y -c,


debe ser abc. Si deseas que una opción sea seguida por un argumento, coloca dos puntos (:) después de la letra, como en a:bc. Ésto le dice al getopts que la opción -a tiene la forma: cadenadeopciones

-a argumento

Normalmente uno o más espacios en blanco separan el parámetro de la opción; al mismo tiempo, getopts también manipula parámetros que vienen pegados a la opción como en: -aargumento cadenadeopciones

no puede contener el signo de interrogación (?).

El nombre constante en la línea de sintaxis anterior, define una variable que cada vez que el comando getopts sea ejecutado, recibirá la próxima opción de los parámetros de posición y la colocará en la variable nombre. coloca un signo de interrogación (?) en la variable definida en nombre si encuentra una opción no definida en cadenadeopciones o si no encuentra el argumento esperado para una determinada opción.

getopts

Como ya sabemos, cada opción pasada por una línea de comandos tiene un índice numérico, así, la primera opción estará contenida en $1, la segunda en $2, y así continúa. Cuando el getopts obtiene una opción, almacena el índice del próximo parámetro a ser procesado en la variable OPTIND. Cuando una opción tiene un argumento asociado (indicado por : en almacena el argumento en la variable OPTARG. Si una opción no posee argumento o el argumento esperado no se encontró, la variable OPTARG será "matada" (unset). cadenadeopciones), getopts

El comando termina su ejecución cuando: • • •

Encuentra un parámetro que no comienza por menos (-); El parámetro especial -- marca el fin de las opciones; Cuando encuentra un error (por ejemplo, una opción no reconocida).

El ejemplo siguiente es meramente didáctico, y sirve para mostrar, en un pequeño fragmento de código, el uso pleno del comando. $ cat getoptst.sh #!/bin/sh

# Ejecute así: # #

getoptst.sh -h -Pimpressora arch1 arch2


# # y note que las informaciones de todas las opciones son exhibidas # # La cadena 'P:h' dice que la opción -P es una opción compleja # y requiere de un argumento, y que h es una opción simple que no requiere # argumentos.

while getopts 'P:h' OPT_LETRA do echo "getopts hizo la variable OPT_LETRA igual a '$OPT_LETRA'" echo "

OPTARG es '$OPTARG'"

done used_up=`expr $OPTIND - 1` echo "Ignorando los primeros \$OPTIND-1 = $used_up argumentos" shift $used_up echo "Lo que sobró de la línea de comandos fue '$*'"

Para entenderlo mejor, vamos a ejecutarlo como está sugerido en su encabezado: $ getoptst.sh -h -Pimpresora arch1 arch2 getopts hizo la variable OPT_LETRA igual a 'h' OPTARG es '' getopts hizo la variable OPT_LETRA igual a 'P' OPTARG es 'impresora' Ignorando los primeros $OPTIND-1 = 2 argumentos Lo que sobró de la línea de comandos fue 'arch1 arch2'

De esta forma, sin tener mucho trabajo, separé todas las opciones con sus respectivos argumentos, dejando solamente los parámetros que fueron pasados por el operador para un tratamiento posterior .


Fíjate que si hubiesemos escrito la línea de comando con el argumento (impresora) separado de la opción (-P), el resultado sería exactamente el mismo, excepto por el $OPTIND, ya que en este caso él identifica un conjunto de tres opciones/argumentos y en el anterior solamente dos. Mira esto: $ getoptst.sh -h -P impresora arch1 arch2 getopts hizo la variable OPT_LETRA igual a 'h' OPTARG es '' getopts hizo la variable OPT_LETRA igual a 'P' OPTARG es 'impresora' Ignorando los primeros $OPTIND-1 = 3 argumentos Lo que sobró de la línea de comandos fue 'arch1 arch2'

En el ejemplo siguiente, fíjate que si pasamos una opción inválida, la variable $OPT_LETRA recibirá un signo de interrogación (?) y la $OPTARG será "matada" (unset). $ getoptst.sh -f -Pimpresora arch1 arch2 # La opción no es válida ./getoptst.sh: illegal option -- f getopts hizo la variable OPT_LETRA igual a '?' OPTARG es '' getopts hizo la variable OPT_LETRA igual a 'P' OPTARG es 'impresora' Ignorando los primeros $OPTIND-1 = 2 argumentos Lo que sobró de la línea de comandos fue 'arch1 arch2'

f

- Dime una cosa: no podrías haber usado un case para evitar el getopts? - Podría si, pero para que? Los comandos están ahí para ser usados... El ejemplo dado fue didáctico, pero imagina un programa que aceptase muchas opciones y sus parámetros podrían no estar pegados a las opciones, sus opciones también podrían o no estar pegadas, iba a ser un case infernal y con=getopts= es sólo seguir los pasos que vimos anteriormente. - Realmente... Viéndolo de esta forma, me parece que tienes razón. Sera porque ya estoy medio cansado con tanta información nueva en mi cabeza. Vamos a tomar la del estribo o todavia quieres explicar alguna particularidad mas del Shell? - Ni lo uno ni lo otro, yo también me cansé, pero hoy no voy a tomar la del estribo porque estoy yendo a dar clases en la UniRIO, que es la primera universidad federal del Brasil que está preparando a sus alumnos del curso de graduación en informática, en el uso del Software Libre. Pero antes te voy a dejar un problema para embarullar tu cabeza: quando tu varías el tamaño de una ventana gráfica, en el centro no aparece dinámicamente en vídeo inverso la cantidad de líneas y columnas? Entonces! Quiero que reproduzcas eso usando el lenguaje Shell. - Mozo, traeme rapidito mi cuenta! Voy a contar hasta uno y si no me la trajiste me voy!


- Ei!, como va amigo, todo bien? - Más o menos.. te acuerdas que me pediste que hiciera un programa que cuando el tamaño de la pantalla variase, en el centro apareciese dinamicamente y en vídeo inverso, la cantidad de líneas y columnas, de la misma forma que el Linux hace normalmente?. Bueno, lo hice, pero la apariencia no quedó igual. - No estoy preocupado por la apariencia, lo que yo quería es que ejercitases lo que aprendimos. Déjame ver lo que hiciste. $ cat tamtela.sh #!/bin/bash # # Colocar en el centro de la pantalla, con vídeo inverso, # una cantidad de columnas y lineas # cuando el tamaño de la pantalla es alterado. # trap Muda 28 # 28 = señal generada por el cambio de tamaño # de la pantalla y Muda es la función que hace eso.

Bold=$(tput bold)

# Negrita, modo de énfasis

Rev=$(tput rev)

# Modo de vídeo inverso

Norm=$(tput sgr0) defecto

# Restaura la pantalla al valor por

Muda () { clear Cols=$(tput cols) Lins=$(tput lines) tput cup $(($Lins / 2)) $(((Cols - 7) / 2)) # Centro de la pantalla echo $Bold$Rev$Cols X $Lins$Norm }

clear


read -n1 -p "Cambie el tamaño de la pantalla o teclee algo para terminar"

- Perfecto!, que se joda la apariencia, después te enseño otras formas de mejorarlo, lo que vale es el programa, está funcionando y esta todo optimizado. - Pero perdí la mayor parte del tiempo intentando descubrir como aumentar el tamaño de la fuente.- ... - Deja eso para otro día, hoy vamos a ver unas cosas bastante interesantes y útiles..

Named Pipes Otro tipo de pipes es el "named pipes", que también es llamado por FIFO. FIFO es un acrónimo de First In First Out, que se refiere a la propiedad de que los bytes salen con el mismo orden que entran. El "name" en named pipe es en verdad el nombre de un archivo. Los archivos tipo named pipe son mostrados por el comando "ls" como cualquier otro, con pocas diferencias, mira: $ ls -l pipe1 prw-r-r-1 julio dipao 0 Jan 22 23:11 pipe1|

La p en la columna del lado izquierdo indica que pipe1 es un named pipe. El resto de los bits de control de permisos, que se pueden leer o grabar al pipe, funcionan como un archivo normal. En los sistemas mas modernos una barra vertical (|) colocada al final del nombre del archivo, es otra pista y en los sistemas LINUX, donde la opción de color esta habilitada, el nombre del archivo se escribe en rojo por defecto. En los sistemas mas antiguos, los named pipes son creados por el programa mknod, normalmente situado en el directorio /etc. En los sistemas mas modernos, la misma tarea es hecha por mkfifo. EL programa mkfifo recibe uno o mas nombres como argumento y crea pipes con esos nombres. Por ejemplo , para crear un named pipe con nombre pipe1 haz: $ mkfifo pipe1

Como siempre la mejor forma de mostrar que algo funciona es dando ejemplos. Suponga que hayamos creado el named pipe mostrado anteriormente. Vamos ahora a trabajar con dos secciones o dos consolas virtuales o una de cada una. En una de ellas haz: $ ls -l > pipe1

en la otra haz:


$ cat < pipe1

Voilá! La salida del comando ejecutado en la primera consola fue mostrada en la segunda. Fíjate que el orden en que los comandos ocurrieron no importa. Si prestaste atención, viste que el primer comando ejecutado parecía estar "colgado". Esto sucede por que la otra punta del pipe todavía no estaba conectada, y entonces el sistema operativo suspendió el primer proceso hasta que el segundo proceso "abriera" el pipe. Para que un proceso que usa el _pipe no quede en modo wait, es necesario que en una punta del pipe tenga un proceso "que habla" y en el otro un proceso " que escucha" y en el ejemplo que dimos , el ls era el que hablaba y el cat era el que escuchaba.Una aplicación muy útil de los named pipes es permitir que programas sin ninguna relación se puedan comunicar entre sí, los named pipes también son usados para sincronizar procesos, ya que en un determinado punto puedes colocar un proceso para "escuchar" o "hablar" en un determinado named pipe y solo saldrá de allí , si otro proceso "habla" o "escucha" en aquel pipe. Viste que el uso de esta herramienta es ideal para sincronizar los procesos y para bloquear archivos y poder evitar así perdida o corrupción de información debido a actualizaciones simultáneas (concurrencia). Veamos ejemplos para ilustrar estos casos.

Sincronización de procesos. Imagina que lanzas paralelamente dos programas (procesos), los diagramas de bloque de sus rutinas son como muestra la siguiente figura:

Los dos procesos son lanzados en paralelo y en el BLOCO1 del Programa1 las tres clasificaciones son lanzadas de la siguiente manera: for Arq in BigFile1 BigFile2 BigFile3 do if sort $Arq then Manda=va else Manda=pare


break fi done echo $Manda > pipe1 [ $Manda = pare ] && { echo Error durante la clasificación de los archivos exit 1 } ...

De esta forma, el comando if verifica cada clasificación que está siendo efectuada. En caso de que ocurra un problema, las clasificaciones siguientes serán abortadas, un mensaje conteniendo la cadena pare es enviada por el pipe1 y el programa1 es finalizado con un fin anormal. Mientras el Programa1 ejecutaba su primero bloque (las clasificaciones) el Programa2 ejecutaba su bloque BLOCO1, procesando sus rutinas de apertura y menú paralelamente al Programa1, ganando de esta forma un buen intervalo de tiempo. El fragmento del código del Programa2 que vemos a continuación, muestra la transición del BLOCO1 hacia el BLOCO2: OK=`cat pipe1` if [ $OK = va ] then ... Rutina de impresión ... else # Recibí "pare" en OK exit 1 fi

Después de la ejecución del primer bloque , el Programa2 pasará a "escuchar" el pipe1, quedando parado hasta que las clasificaciones del Programa1 terminen, comprobando a continuación el mensaje pasado por el pipe1 para decidir si los archivos están preparados para ser impresos , o si el programa debería terminarse. De esta forma es posible lanzar programas de forma sincronizada y no sincronizada cuando es necesario, ganando bastante tiempo de procesamiento.

Bloqueo de archivos Supón que escribes una CGI (Common Gateway Interface) en Shell para contar cuantos hits recibe una determinada URL y la rutina del contador es la siguiente: Hits="$(cat page.hits 2> /dev/null)" || Hits=0 echo $((Hits=Hits++)) > page.hits

De esa forma si la página recibe dos o mas accesos concurrentes, uno o mas podrá(n) perderse, basta que el segundo acceso sea hecho después de una lectura del archivo page-hits y antes de su grabación, es decir, basta que el segundo acceso sea hecho después de que el primero haya ejecutado la primer línea del _script_y antes de ejecutar la segunda. Entonces que hacemos? Para resolver el problema de concurrencia vamos a


utilizar un named pipe. Creamos el siguiente scritp que será el daemon que recibirá la pagina en nuestro site que necesita de un contador. $ cat contahits.sh #!/bin/bash

PIPE="/tmp/pipe_contador" # archivo llamado pipe # dir donde serán colocados los archivos contadores de cada pagina DIR="/var/www/contador"

[ -p "$PIPE" ] || mkfifo "$PIPE"

while : do for URL in $(cat < $PIPE) do FILE="$DIR/$(echo $URL | sed 's,.*/,,')" # OBS1: en el sed arriba, como precisaba buscar #

una barra,usamos coma como separador.

# OBS2: cuando rodar como daemon comente la próxima línea echo "arquivo = $FILE"

n="$(cat $FILE 2> /dev/null)" || n=0 echo $((n=n+1)) > "$FILE" done done

Como solamente este script modifica los archivos, no existe problema de concurrencia


Este script será un daemon, esto es, correrá en background. Cuando una página sufre un acceso, el script escribirá su URL en el archivo del pipe. Para probarlo, ejecuta este comando: echo "test_pagina.html" > /tmp/pipe_contador

Para evitar errores, en cada página que quisiéramos agregar el contador añadiriamos la siguiente linea: <!--#exec cmd="echo $REQUEST_URI > /tmp/pipe_contador"-->

Observa que la variable $REQUEST_URI contiene el nombre del archivo que el navegador (browser)pidió. Este último ejemplo ,es fruto de una idea que intercambié con un amigo y maestro en Shell , Thobias Salazar Trevisan que escribió el script y lo colocó en su excelente URL. Aconsejo a todos que los que quieren aprender Shell que le echen un vistazo y la incluyan en sus favoritos. Aja! Pensaste que el asunto sobre los named pipes estaba terminado? Pues estabas engañado. A continuación te voy a mostrar un uso diferente de ellos.

Substitución de procesos Acabo de mostrarte un montón de recetas sobre los named pipes . Ahora te voy a mostrar que el Shell también usa los named pipes de una manera bastante singular, que es la sustitución de procesos (process substitution). Una sustitución de procesos ocurre cuando dentro de un pipeline de comando pones un comando entre paréntesis y un < o un > unido al paréntesis de la izquierda. Por ejemplo, tecleando el comando: $ cat <(ls -l)

Resultará que el comando ls -l ejecutado en un subshell como es normal (por estar entre paréntesis), redireccionará la salida a un named pipe temporal, que el Shell crea, usa y luego elimina. Entonces el cat tendrá un nombre de archivo válido para leer (que será este named pipe y cuyo dispositivo lógico asociado es /dev/fd/63), y tendremos la misma salida que la generada por la del ls -l, pero dando uno o mas pasos de lo usual, y esto es mas costoso para el computador. Como podremos constatar esto? Fácil .. Mira el siguiente comando: $ ls -l >(cat) l-wx------ 1 jneves jneves 64 Aug 27 12:26 /dev/fd/63 -> pipe:[7050]

Y... Realmente es un named pipe. Debes estar pensando que esto es una locura nerd , no? Entonces supongamos que tenes 2 directorios: dir y dir.bkp , y deseas saber si los dos son iguales ( aquella vieja duda:


estará mi backup actualizado? ). Basta comparar los dos archivos de los directorios con el comando cmp, haciendo: $ cmp <(cat dir/*) <(cat dir.bkp/*) || echo backup desactualizado!

o, mejor todavía: $ cmp <(cat dir/*) <(cat dir.bkp/*) >/dev/null || echo backup desactualizado!

De la última forma, la comparación fue efectuada en todas las líneas de todos los archivos de ambos directorios. Para acelerar el proceso, podríamos comparar solamente el listado largo de ambos en los directorios, pues cualquier modificación que un archivo sufra, es mostrado como alteración de la fecha/hora y/o del tamaño del archivo. Mira como quedaría: $ cmp <(ls -l dir) <(ls -l dir.bkp) >/dev/null || echo backup desactualizado!

Este es un ejemplo meramente didáctico, pues son tantos los comando que producen mas de una línea de salida que sirve como guia para otros. Quiero generar una lista de mis archivos, numerados y al final dar el total de archivos del directorio actual: while read arq do ((i++)) # así no es necesario inicializar i echo "$i: $arq" done < <(ls) echo "En el directório corriente (`pwd`) existen $i archivos"

Está bien, yo sé que existen otras formas de ejecutar la misma tarea. Usando el comando while. La forma mas común de resolver este problema sería: ls | while read arq do ((i++)) # así no es necesario inicializar i echo "$i: $arq" done echo "En el directório corriente (`pwd`) existen $i archivos"

Cuando ejecutase el script, parecería que esta todo ok, sin embargo en el comando echo después del none, vas a ver que el valor de $i se perdió. Esto se debe al hecho de que esta variable esta siendo incrementada en un subshell creado por el pipe (|) y que termina con el comando done, llevándose con él todas las variables creadas en su interior y las alteraciones hechas en todas la variables, inclusive las creadas externamente. Solamente para mostrarte que una variable creada fuera del subshell y alterada en su interior, pierde las alteraciones hechas al final, ejecuta el script siguiente: #!/bin/bash


LIST="" ls | while read FILE do LIST="$FILE $LIST" done echo :$LIST:

# Creada en el shell principal # Inicio del subshell # Alterada dentro del subshell # Fin del subshell

Al final de la ejecución vas a ver que aparecerán apenas dos puntos (::). Pero en el inicio de este ejemplo te dije que era meramente didáctico, ya que existen formas mejores de hacer la misma tarea. Mira estas dos: $ ls | ln

o entonces, usando la propia substitución de procesos: $ cat -n <(ls)

Un último ejemlo: tu deseas comparar arq1 y arq2 usando el comando comm, pero este comando necesita que los archivos estén clasificados. Entonces la mejor forma de proceder es: $ comm <(sort arq1) <(sort arq2)

Esta forma evita que tengas que hacer las siguientes operaciones: $ $ $ $

sort arq1 > /tmp/sort1 sort arq2 > /tmp/sort2 comm /tmp/sort1 /tmp/sort2 rm -f /tmp/sort1 /tmp/sort2

Gente.. nuesta Convesación llegó a su fin, . Disfruté mucho aquí y recibí diversos elogios por los trabajos realizados a lo largo de 12 meses y, lo mejor de todo, hice muchas amistades y tomé muchos chopps gratis con los lectores que encontré por los congresos y charlas que ando haciendo por nuestro querido Brasíl. Lo que voy a escribir aquí no está arreglado ni sé si será publicado, pero como los editores de esta revista son dos locos hermosos (ambos Rafael), es posible que lo dejen pasar. Es lo siguiente: si quieren que el Papo de Botequin continue, llenen la caja postal de la Linux Magazine pidiendo esto y desde ya escoja el próximo tema entre sed + expresiones regulares o lenguaje awk. De cualquier forma, en caso de que no consigamos sensibilizar la dirección de la revista, me despido de todos mandando un abrazo a los barbudos y besos a las chicas y agradezco a los mas de 100 mails que recibí (todos elogiosos) y todos debidamente respondidos. A la salud de todos nosotros: Chin, chin -Chico, cierra la cuenta que voy a cambiar de bar.


Pasando parámetros con xargs Existe un comando, cuya función principal es construir listas de parámetros y pasarlas para la ejecución de otros programas o instrucciones. Este comando es el xargs y debe ser usado de la siguiente manera: xargs [comando [argumento inicial]]

En el caso de que el comando (que puede ser inclusive un script Shell), sea omitido, será usado por default el echo. El xargs combina el argumento inicial con los argumentos recibidos de la entrada patrón, de forma de ejecutar el comando especificado una o más veces. Ejemplo:

Vamos a buscar una cadena de caracteres en todos los archivos dentro de un determinado directorio, usaremos el comando find con la opción -type f para buscar solamente los archivos normales, despreciando directorios, archivos especiales, archivos de uniones, etc, y vamos a hacer la busqueda lo más general posible, recibiendo el nombre del directorio inicial y la cadena a ser buscada como parámetros. Para eso hacemos: $ cat grepr # # Grep recursivo # Busca la cadena de caracteres definida en $2 a partir del directorio $1 # find $1 -type f -print|xargs grep -l "$2"

Ejecutando este script buscamos, a partir del directorio definido en la variable $1, todos los archivos que contengan la cadena definida en la variable $2. Exactamente lo mismo podría hacerse si la línea del programa fuera la siguiente: find $1 -type f -exec grep -l "$2" {} \;

El primer proceso tiene dos grandes desventajas sobre el anterior: •

La primera es bastante visible: el tiempo de ejecución de este método es muy superior al segundo, eso porque el grep será hecho en cada archivo que le sea pasado por el find, uno a uno, al paso que con el xargs, será pasada toda, o en la peor de las hipótesis, la mayor parte posible, de la lista de archivos generada por el find;

Dependiendo de la cantidad de archivos encontrados que atiendan al find, podemos recibir aquel famoso y fatídico mensaje de error Too many arguments indicando una sobrecarga en la pila de ejecución del grep. Como fue dicho en el ítem anterior, si usamos el xargs, éste pasará hacia el grep la mayor cantidad de


parámetros posible, suficiente para no causar este error y en caso necesario, ejecutará el grep más de una vez.

Atención aquellas personas de linux que usan el ls colorido como estándar: en los ejemplos siguientes que incluyen esta instrucción, deben usar la opción --color=none, en caso contrario, existen grandes posibilidades de que los resultados no sean los esperados (;-). Vamos ahora a analizar un ejemplo que es más o menos lo contrario de este que acabamos de ver. Esta vez, vamos a hacer un script para borrar todos los archivos del directorio actual y que pertenezcan a un determinado usuario. La primera idea que surge es, como en el caso anterior, usar un comando find, de la siguiente manera: find . -user cara -exec rm -f {} \;

Casi estaría correcto, el problema es que de esta forma se estarían borrando no solamente los archivos de cara en el directorio actual, sino también de todos los otros sub-directorios "colgados" a éste. Veamos entonces como hacerlo correctamente: ls -l | grep " cara " | cut -c55- | xargs rm

De esta forma, el grep seleccionó los archivos que contenían la cadena cara en el directorio actual listado por el ls -l. El comando cut tomo solamente el nombre de los archivos, pasándolos hacia el borrado, a cargo del rm usando el comando xargs como puente El xargs es también una excelente herramienta de creación de one-liners (scripts de solamente una línea). Mira éste para listar todos los propietarios de archivos (inclusive sus links) "colgados" en el directorio /bin y sus sub-directorios. $ find /bin -type f -follow | \ xargs ls -al | tr -s ' ' | cut -f3 -d' ' | sort -u

Muchas veces el /bin es un link (si no estoy equivocado, en Solaris es así) y la opción obliga al find a seguir el link. El comando xargs alimenta el ls -al y la secuencia de comandos siguiente es para tomar solamente el 3er campo (propietario) y clasificarlo devolviendo solamente una vez cada propietario (opción -u del comando sort, que equivale al comando uniq). follows

Opciones del xargs


Las opciones del xargs pueden se usadas para construir comandos extremamente poderosos. Opción -i Para ejemplificar esto y comenzar a entender las principales opciones de esta instrucción, vamos a suponer que tenemos que borrar todos los archivos con extensión .txt en el directorio actual y presentar sus nombres en pantalla. Veamos que podemos hacer: $ find . -type f -name "*.txt" | \ xargs -i bash -c "echo borrando {}; rm {}"

La opción -i del xargs cambia pares de llaves ({}) por la cadena que está recibiendo a través del pipe (|). Entonces en este caso las llaves ({}) serán cambiadas por los nombres de los archivos que satisfagan al comando find. Opción -n Veamos este pequeño juego que vamos a hacer con el xargs: $ ls | xargs echo > arch.ls $ cat arch.ls arch.ls arch1 arch2 arch3 $ cat arch.ls | xargs -n1 arch.ls arch1 arch2 arch3

Cuando mandamos la salida del ls hacia el archivo usando el xargs comprobamos lo que ya dijimos, o sea, el xargs manda todo lo que sea posible (lo suficiente para no generar una sobrecarga en la pila) de una vez sola. En seguida, usamos la opción -n 1 para listar uno por uno. Sólo para estar seguros, mira el ejemplo siguiente, donde listaremos dos archivos en cada línea: $ cat arch.ls | xargs -n 2 arch.ls arch1 arch2 arch3

Sin embargo, la línea de arriba podría (y debería) ser escrita sin usar el pipe (|), de la siguiente forma: $ xargs -n 2 < arch.ls

Opción -p


Otra excelente opción del xargs es -p, en la cual el sistema pregunta si tu realmente deseas ejecutar el comando. Digamos que en un directorio tengas archivos con la extensión .bug y .ok, los .bug tienen problemas que después de corregidos son grabados como .ok. Echa una mirada a la lista de este directorio: $ ls dir arch1.bug arch1.ok arch2.bug arch2.ok ... arch9.bug arch9.ok

Para comparar los archivos buenos con los defectuosos, hacemos: $ ls | xargs -p -n2 diff -c diff -c arch1.bug arch1.ok ?...y .... diff -c arch9.bug arch9.ok ?...y

Opción -t Para finalizar, el xargs también tenemos la opción -t, donde va mostrando las instrucciones que montó antes de ejecutarlas. Me gusta mucho esta opción para ayudar a depurar el comando que fue montado. Resumen Entonces podemos resumir el comando de acuerdo con la siguiente tabla: -t

Opción -i

Muestra la línea de comando montada antes de ejecutarla Acción Substituye el par de llaves ({}) por las cadenas recibidas

-nNum

Manda el máximo de parámetros recibidos, hasta el máximo de Num para que el comando sea ejecutado

-lNum

Manda el máximo de líneas recibidas, hasta el máximo de Num para que el comando sea ejecutado

-p

Muestra la línea de comando montada y pregunta si desea ejecutarla

Here Strings Primero un programador con complejo de inferioridad creó el redireccionamiento de entrada y lo representó con un signo de menor (<) para representar sus sentimientos. En seguida, otro sintiéndose todavía peor, creó el here document representándolo por dos


signos de menor (<<) porque su complejo era mayor. El tercero, pensó: "estos dos no saben lo que es estar deprimido"... Entonces creó el here strings representándolo por tres signos de menor (<<<). Bromas a parte, el here strings es utilísimo y, no sé porque, es un perfecto desconocido. En la poquísima literatura que hay sobre el tema, se nota que el here strings es frecuentemente citado como una variante del here document, teoría con la que discrepo pues su aplicabilidad es totalmente diferente de aquella. Su sintaxis es simple: $ comando <<< $cadena

Donde cadena es expandida y alimenta la entrada primaria (stdin) de comando. Como siempre, vamos directo a los ejemplos de los dos usos más comunes para que vosotros mismos saquéis las conclusiones. •

Uso #1. Substituyendo la tan usada construcción echo "cadeia" | comando, que obliga a un fork, creando un subshell y aumentando el tiempo de ejecución.

Ejemplos:

$ a="1 2 3" $ cut -f 2 -d ' ' <<< $a # Normalmente se hace: echo $a | cut -f 2 -d ' ' 2 $ echo $Nomearch Mis Documentos # Arrrghhh! $ tr "A-Z " "a-z_" <<< $Nomearch # Substituyendo el echo $Nomearch | tr "A-Z " "a-z_" mis_documentos $ bc <<<"3 * 2" 6 $ bc <<<"scale = 4; 22 / 7" 3.1428

Para mostrar la mejoría en el desempeño, vamos a hacer un loop de 500 veces usando el ejemplo dado para el comando tr: Veamos ahora esta secuencia de comandos con medidas de tiempo: $ time for ((i=1; i<= 500; i++)); { tr "A-Z " "a-z_" <<< $Nomearch >/dev/null; } real 0m3.508s user 0m2.400s sys 0m1.012s $ time for ((i=1; i<= 500; i++)); { echo $Nomearch | tr "AZ " "a-z_" >/dev/null; } real user sys

0m4.144s 0m2.684s 0m1.392s


Veamos ahora esta otra secuencia de comandos con medidas de tiempo: $ time for ((i=1;i<=100;i++)); { who | cat > /dev/null; } real 0m1.435s user 0m1.000s sys 0m0.380s $ time for ((i=1;i<=100;i++)); { cat <(who) > /dev/null; } real 0m1.552s user 0m1.052s sys 0m0.448s $ time for ((i=1;i<=100;i++)); { cat <<< $(who) > /dev/null; } real user sys

0m1.514s 0m1.056s 0m0.412s

Observando este cuadro verás que en el primero, usamos la forma convencional, en el segundo usamos un named pipe temporal para ejecutar una substitución de procesos y en el tercero usamos here strings. Notaras también que al contrario del ejemplo anterior, aqui el uso de here strings no fue lo más veloz. Pero observad también que en este último caso el comando who está siendo ejecutado en un subshell y eso aumentó el proceso como un todo. Veamos una forma rápida de insertar una línea como encabezamiento de un archivo: $ cat num 1 2 3 4 5 6 7 8 9 10 $ cat - num <<< "Impares Pares" Impares Pares 1 2 3 4 5 6 7 8 9 10 •

Uso #2. Otra buena forma de usar el here strings es juntándolo con un comando read, no perdiendo de vista lo que aprendimos sobre IFS (vedlo aquí, en la explicación del comando for). El comando cat con las opciones -vet muestra el <ENTER> como $, el <TAB> como ^I y los otros caracteres de control con la notación ^L donde L es una letra cualquiera. Veamos entonces el contenido de una variable y después vamos a leer cada uno de sus campos:

Ejemplos:

$ echo "$línea"


Leonardo Mello (21)3313-1329 $ cat -vet <<< "$línea" Leonardo Mello^I(21)3313-1329$ blanco y <TAB> (^I) $ read Nom SNom Tel <<< "$línea" $ echo "${Nom}_$S{Nom}_$Tel" cada uno de los campos Leonardo_Mello_(21)3313-1329 separadores eran iguales al IFS

# Los separadores son # Vamos a ver si leyó # Leyó porque los

También podemos leer directamente de un vector (array) vedlo: $ echo $Frutas Pera:Uva:Manzana $ IFS=: $ echo $Frutas Pera Uva Manzana # Sin las comillas el shell muestra el IFS como blanco $ echo "$Frutas" Pera:Uva:Manzana # Ahhh, ahora sí! $ read -a aFrutas <<< "$Frutas" # La opción -a del read, lee un vector $ for i in 0 1 2 > do > echo ${aFrutas[$i]} # Imprimiendo cada elemento del vetor > done Pera Uva Manzana

Rotatório de Tiago Estaba, como lo hago todos los días dandole un repaso a los e-mails de la "Lista de Shell Script" , cuando descubro algo totalmente inusitado de Tiago Barcellos Peczenyj. Cuando resolví crear este conjunto de consejos, me acordé de eso y le pedí para que me enviara aquel e-mail nuevamente. El texto siguiente es el e-mail que me mandó, solo inserté el último ejemplo y saqué las abrebiaturas. Julio descubrí una forma en la que el Shell crea combinaciones haciendo rotación con los elementos estipulados. Podemos generar todos los binarios de 0000 a 1111 de la siguiente forma: $ A={0,1} $ eval echo $A$A$A$A 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111

Una aplicación práctica que veo es para combinar valores diferentes sin tener que encadenar loops ni usar el =seq


$ A={`seq -s , -f "_%g" 3`} $ eval echo -e $A$A$A |tr ' _' '\n ' | grep -vE '.+?(\b[09]+\b).+?\1' 1 2 3 1 3 2 2 1 3 2 3 1 3 1 2 3 2 1

En este caso combiné los números de 1 a 3 eliminando repeticiones con el grep. usé un tr foca'podre' para tratar mejor los datos, saltando línea. El grep es simple como puedes notar, compruebo si una determinada parte de la combinación (.+?(\b[09]+\b).+?) existe en otra parte (\1), si existe, no la dejo imprimir por causa de la opción -v, y así 1 1 2 1 2 1 1 1 1

no son impresos. Ahora va mi ejemplo: el one-liner siguiente genererá todos los permisos posibles (en octal) para el archivo arq (el ejemplo fué interrumpido por que existen 512 combinaciones de permisos posibles). $ A={`seq -s , 0 7`} $ eval echo -e $A$A$A | tr ' "chmod {} arq; ls -l arq" ---------- 1 julio julio 100 ---------x 1 julio julio 100 --------w- 1 julio julio 100 --------wx 1 julio julio 100 -------r-- 1 julio julio 100 -------r-x 1 julio julio 100 -------rw- 1 julio julio 100 -------rwx 1 julio julio 100 . . . . . . . . . . -rwxrwxrw- 1 julio julio 100 -rwxrwxrwx 1 julio julio 100

' '\n' | xargs -i bash -c 2006-11-27 11:50 arq 2006-11-27 11:50 arq 2006-11-27 11:50 arq 2006-11-27 11:50 arq 2006-11-27 11:50 arq 2006-11-27 11:50 arq 2006-11-27 11:50 arq 2006-11-27 11:50 arq . . . . . . . . 2006-11-27 11:50 arq 2006-11-27 11:50 arq

Veamos este ejemplo paso a paso para entenderlo: $ echo $A {0,1,2,3,4,5,6,7} $ eval echo -e $A$A$A 000 001 002 003 004 005 006 007 010 ... ... 767 770 771 772 773 774 775 776 777 $ eval echo -e $A$A$A | tr ' ' '\n' # El tr cambiará cada espacio en blanco por um <ENTER> 000 001 002 003


. . . 774 775 776 777

A continuación el xargs (clique para consejos sobre el xargs) ejecuta el comando bash -c (que sirve para ejecutar una línea de comandos) que por cada vez que se ejecuta chmod y el ls -l permite mostrar que los permisos están siendo alterados.

Aritmética en Shell Antiguamente usábamos el comando expr para hacer operaciones aritméticas y mucha gente aun la usa, pues es compatible con cualquier ambiente. Ejemplo:

$ expr 7 \* 5 / 3 14

# 7 veces 5 = 35 dividido por 3 = 11

En este articulo podremos ver otras formas no tan conocidas, sin embargo más simple de usar, más elaboradas y con mayor precisión.

El uso de bc Una forma fantástica de hacer cálculos en Shell – usada normalmente cuando la expresión aritmética es más compleja, o cuando es necesario trabajar con cifras decimales – es usar la instrucción de calculo del UNIX/LINUX. El bc. Mira como: Ejemplo:

$ echo "(2 + 3) * 5" | bc preferencia 25

# Paréntesis usados para dar

Para trabajar con números reales (números no necesariamente enteros), especifique la precisión (cantidad de decimales) con la opción scale del comando bc. Así veamos el penúltimo ejemplo: $ echo "scale=2; 7*5/3" | bc 11.66

Otros ejemplos: $ echo "scale=3; 33.333*3" | bc 99.999 $ num=5 $ echo "scale=2; ((3 + 2) * $num + 4) / 3" | bc


9.66

Obviamente todos los ejemplos de arriba en el caso de linux, podrían (y deberían) ser escritos usando Here Strings. Veamos los últimos como quedarían: $ bc <<< "scale=3; 33.333*3" 99.999 $ num=5 $ bc <<< "scale=2; ((3 + 2) * $num + 4) / 3" 9.66

Una vez apareció en la lista (excelente a propósito) de Shell script en Yahoo ( "Lista de Shell Script" ) un persona con la siguiente duda: "yo tengo un archivo cuyos campos están separados por y el tercero de ellos posee números. Como puedo calcular la suma de todos los números de esta columna del archivo?" Yo envié la respuesta siguiente: $ echo $(cut -f3 num | tr '\n' +)0 | bc 20.1

Vamos por partes para entenderlo mejor y primero vamos ver como era el archivo que hice para comprobarlo: $ cat num a b a z w e q w

3.2 4.5 9.6 2.8

Como se puede ver, está dentro del patrón del problema, donde yo tengo como tercer campo números reales. A ver lo que haría la primera parte de la línea de comandos, donde yo transformo los caracteres (new-line) en un señal de más (+): $ cut -f3 num | tr '\n' + 3.2+4.5+9.6+2.8+

Si yo mandase de esa manera hacia el bc, él me devolvería un error por causa de aquel signo de más (+) suelto al final del texto. Mi solución fue poner un cero al final, pues sumando cero el resultado no se alterará. Veamos entonces como quedó: $ echo $(cut -f3 num | tr -s '\n' +)0 3.2+4.5+9.6+2.8+0


Eso es lo que se acostumbra llamar one-liner, esto es, códigos que serían complicados en otros lenguajes (normalmente sería necesario crear contadores y hacer uno loop de lectura sumando el tercer campo al contador) y en Shell son escritos en una única línea. Hay también gente que llama eso de método KISS, que es el acrónimo de Keep It Simple Stupid. Pero el uso potencial de esta calculadora no se acaba ahí, existen diversas facilidades proporcionadas por ella. Veamos sólo este ejemplo: $ echo "obase=16; 11579594" | bc B0B0CA? $ echo "ibase=16; B0B0CA? " | bc C, e A 11579594

# B, zero, B, zero,

En estos ejemplos vimos como hacer cambios de base de numeración con el uso del bc. En la primera ponemos la base de salida (obase) como 16 (hexadecimal) y en la segunda, dijimos que la base de la entrada (ibase) era 10 (decimal).

Otras formas de trabajar con enteros Otra forma mucho mejor de hacer cálculos es usar la notación $((exp aritmética)). Es bueno estan atento, sin embargo, al hecho de que esta sintaxis no es universal. Bourne Shell (sh), por ejemplo, no la reconoce. Ejemplo:

Usando el mismo ejemplo que ya habíamos usado: $ echo $(((2+3)*5)) internos priorizaran o 2+3 25

# Los paréntesis mas

Fíjate ahora en esta locura: $ tres=3 $ echo $(((2+tres)*5)) precedida por $ 25 $ echo $(((2+$tres)*5)) precedida por $ 25

# Variable tres no # Variable tres

Ei!! No es el signo ($) que la precede lo que caracteriza una variable? Si, pero e todos los sabores UNIX que probé, solo bash o ksh, las dos formas producen una buena aritmética. Presta atención a esta secuencia:


$ $ 0 $ 1 $ 2 $ 2

unset i echo $((i++))

# $i mooorreu!

echo $i echo $((++i)) echo $i

Fíjate que a pesar que la variable no esta definida, pues fue hecho uno unset en ella, ninguno de los comandos dió error, porque, como estamos usando construcciones aritméticas, siempre que una variable no existe, es inicializada con cero (0). Fíjate que el i++ produjo cero (0). Esto ocurre porque este tipo de construcción se llama pos-incremento, esto es, primeramente el comando es ejecutado y sólo entonces la variable es incrementada. En el caso del ++i, fue hecha un pre-incremento: primero incrementó y solo despues el comando fue ejecutado. También son válidos: $ echo 5 $ echo 5 $ echo 15 $ echo 15 $ echo 1 $ echo 1

$((i+=3)) $i $((i*=3)) $i $((i%=2)) $i

Estas tres operaciones serian lo mismo que: i=$((i+3)) i=$((i*3)) i=$((i%2))

Y esto seria válido para todos los operadores aritméticos, lo que en resumen produciría la siguiente tabla: Expansión Aritmética ||

Or lógico

Expresión

Resultado

id++ id--

pós-incremento y pós-decremento de variables

++id -–id

pré-incremento y pré-decremento de variables


Expansión Aritmética ** * / % + <= >= < > == != &&

exponenciación multiplicación, división, resto de la división adición, sustracción comparación igualdad, desigualdad And lógico

Pero el máximo exponente de esta forma de construcción con doble paréntesis es la siguiente: $ echo $var 50 $ var=$((var>40 ? var-40 : var+40)) $ echo $var 10 $ var=$((var>40 ? var-40 : var+40)) $ echo $var 50

Este tipo de construcción debe ser usado de la siguiente forma: en caso que la variable var sea mayor que 40 (var>40), entonces (?) hace var igual a var menos 40 (var-40), si no (:) hacer var igual a var más 40 (var+40). Lo que quiero decir es que los caracteres punto-de-interrogación (?) y dos-puntos (:) hacen el papel de "entonces" y "si no", sirviendo así para montar una operación aritmética condicional. Al igual que usamos la expresión $((...)) para hacer operaciones aritméticas, también podríamos usar la intrínseca (built-in) let o construcciónes del tipo $[...]. Los operadores son los mismos para estas tres formas de construcción, lo que varía un poco es la operación aritmética condicional con el uso del let. Veamos como sería: $ echo $var 50 $ let var='var>40 ? var-40 : var+40' $ echo $var 10 $ let var='var>40 ? var-40 : var+40' $ echo $var 50

Baseando Si quieres trabajar con bases diferentes de la decimal, basta con usar el formato: base#numero


Donde base es un número decimal entre 2 y 64 en nombre del sistema de numeración, y numero es un número en el sistema numérico definido por base. Si base# fuese omitida, entonces 10 se asume por defecto default. Los guarismos mayores que 9 son representados por letras minúsculas, mayúsculas, @ y _, en esta orden. Si base fuese menor o igual a 36, mayúsculas o minúsculas pueden ser usadas indiferentemente para definir guarismos mayores que 9 (no está mal escrito, los guarismos del sistema hexadecimal, por ejemplo, varían entre 0 (cero) y F). Veamos como funciona: $ echo 3 $ echo 10 $ echo 10 $ echo 13 $ echo 10 $ echo 36 $ echo 62 $ echo 63

$[2#11] $((16#a)) $((16#A)) $((2#11 + 16#a)) $[64#a] $[64#A] $((64#@)) $((64#_))

En estos ejemplos usé las notaciones $((...)) y $[...] indistintamente, para demostrar que ambas funcionan. Funciona también un cambio automático para la base decimal, si estas usando la convención numérica de C, esto es, en 0xNN, el NN será tratado como un hexadecimal y en 0NN, el NN será visto como uno octal. Vea el ejemplo: Ejemplo

$ echo 10 $ echo 8 $ echo 16 $ echo 64

$((10))

# decimal

$((010))

# octal

$((0x10))

# hexadecimal

$((10+010+0x10))

# Decimal + octal + hexadecimal

Ah, se me olvidaba! Las expresiones aritméticas con los formatos $((...)), $[...] y con el mando let usan los mismos operadores usados en la instrucción expr, además de los operadores unários (++, --, +=, *=, ...) y condicionales que acabamos de ver.

Pruebas usando expresiones regulares


En las Conversaciones de Bar 004, lo comentamos todo sobre comandos condicionales, pero faltó uno que no existía a aquella época. En esta misma Conversación de Bar, en la sección Y toma de test llegamos a hablar de una construcción del tipo: [[ Expresión ]] && cmd

Donde el comando cmd será ejecutado en el caso que la expresión condicional Expressión sea verdadera. Dije que aunque Expresión podría ser definida de acuerdo con las reglas de Generación de Nombre de Archivos (File Name Generation). A partir del bash versión 3, fue incorporado a esta forma de test un operador representado por =~, cuya finalidad es hacer comparaciones con Expresiones Regulares. Ejemplo:

$ echo $BASH_VERSION # Asumiendo que la versión igual o superior a la 3.0.0 3.2.17(15)-release $ Cargo=Senador $ [[ $Cargo =~ ^(Governa|Sena|Verea)dora?$ ]] && político É político $ Cargo=Senadora $ [[ $Cargo =~ ^(Governa|Sena|Verea)dora?$ ]] && político É político $ Cargo=Diretor $ [[ $Cargo =~ ^(Governa|Sena|Verea)dora?$ ]] && político $

de Bash es

echo É

echo É

echo É

Vamos desmenuzar la Expresión Regular ^(Governa|Sena|Verea)dora?$: esta acepta todo lo que empieza (^) por Governa, o (|) Sena, o (|) Verea, seguido de dor y seguido de una a opcional (?). El signo ($) sirve para marcar el fin de la expresión. En otras palabras esta Expresión Regular sera verdadera si recibe una de las siguientes palabras "Governador, Senador, Vereador, Governadora, Senadora e Vereadora". (En este ejemplo y su explicación hemos omitido la traducción de las constantes para hacer mas comprensible la expresión)

Coloreando la pantalla Como usted ya había visto en la Conversación de Bar 007, el comando tput sirve para hacer casi todo lo referente la formatación de la pantalla, pero lo que no dije es que con él también se pueden usar colores frontales (de los carácteres) y de fondo. Existen también otras formas de hacer lo mismo, creo sin embargo, que la que veremos ahora, es más intuitiva (o menos “desintuitiva”). La tabla siguiente muestra los comandos para especificar los patrones de colores frontales (foreground) o de fondo (background): Obteniendo colores con el comando tput tput setab n

Comando

Especifica n como color de fondo (background) Efecto


Obteniendo colores con el comando tput tput setaf n

Especifica n como color frontal (foreground)

Bien, ahora ya sabes como especificar la combinación de colores, pero todavía no sabes los colores. La tabla siguiente muestra los valores que la n (de la tabla anterior) debe asumir para cada color: Valores de los colores en el comando tput 7

Gris

Valor

Color

0

Negro

1

Rojo

2

Verde

3

Marron

4

Azul

5

Purpura

6

Cian

En este punto ya puedes empezar a jugar con los colores. - Pero caramba, todavía son mucho pocos! - Y, tienes toda la razón... El problema es que todavía no te dije que si pones el terminal en modo de énfasis (tput bold), estos colores generan otros ocho. Vamos a mostar entonces la tabla definitiva de los colores: Valores de los colores con el comando tput 7 Valor

Ejemplo

Gris claro Blanco Color

Color despues de tput bold

0

Negro

Gris oscuro

1

Rojo

Rojo claro

2

Verde

Verde claro

3

Marron

Amarillo

4

Azul

Azul Brillante

5

Púrpura

Rosa

6

Ciano

Ciano claro


Como ejemplo, veamos un script que cambiara el color de la pantalla de acuerdo a tus preferencias. $ cat mudacor.sh #!/bin/bash tput sgr0 clear

# Cargando los 8 colores básicos para un vector Colores=(Negro Rojo Verde Marron Azul Púrpura Cian "Gris claro")

# Listando el menu de colores echo " Opc

Cor ==="

=

#

La siguiente linia significa: para i empezando desde 1;

#+ cuando i es menor o igual al tamaño del vector Colores; #+ incremente el valor de i de 1 en 1 for ((i=1; i<=${#Cores[@]}; i++)) { printf "%02d

%s\n" $i "${Cores[i-1]}"

}

CL= until [[ $CL == 0[1-8] || $CL == [1-8] ]] do read -p " Escoje el colorr de la letra: " CL done


#

Para los que tienen un bash a partir de la version 3.2

#+ el test do until de arriba podria hacerse #+ usando Expresiones Regulares. Veamos como: #+ until [[ $CL =~ 0?[1-8] ]] #+ do #+

read -p "

#+ Escoje el color de la letra: " CL #+ done

CF= until [[ $CF == 0[1-8] || $CF == [1-8] ]] do read -p " Escoje el color de Fondo: " CF done

let CL-- ; let CF-siete

# Porque los colores varian de cero a

tput setaf $CL tput setab $CF clear

Ganando la partida con más comodines Estaba leyendo mis correos electrónicos cuando recibo uno de Tiago enviado a la lista de Shell Script (ya hablé de la lista y de Tiago en el Rotatório Peczenyj). Aquí va el contenido del correo electrónico:


No sé si es conocido de todos pero el shell posee, ademas del globbing normal (la expansión *, ? y [la-z] de nombres de archivos y directorios), un globbing extendido. Creo que, en algunos casos, podria ser MUY util, eliminando uno pipe por un grep por ejemplo. Estos son: ?(patron)

Debe coincidir cero o una ocurrencia de un determinado patron *(patron)

Debe coincidir cero o mas ocurrencias de un determinado patron +(patron)

Debe coincidir una o mas ocurrencias de un determinado patron @(patron)

Debe coincidir con exactamente una ocurrencia de un determinado patron !(patron)

Debe coincidir con cualquier cosa, excepto con patron Para poder utilizár las es necesario ejecutar shopt conforme al ejemplo siguiente: $ shopt -s extglob $ ls file filename filenamename fileutils $ ls file?(name) file filename $ ls file*(name) file filename filenamename $ ls file+(name) filename filenamename $ ls file@(name) filename $ ls file!(name) # divertido esse file filenamename fileutils $ ls file+(name|utils) filename filenamename fileutils $ ls file@(name|utils) # "lembra" um {name,utils} filename fileutils

Usando el awk para buscar por equivalencias Ahí va una más que Tiago mandó a la lista de Shell Script de Yahoo (ya hablé de la lista y de Tiago en el Rotatório Peczenyj y en Ganando la partida con mas comodines) Quién no pasó ya por ello: Buscar una palabra, sin embargo una letra acentuada, o no, estorbó la búsqueda? No descubrí como hacer que el grep o el sed acepten algo semejante, pero el gawk acepta clases de equivalencia!


Mejor explicar con un ejemplo, donde voy listar el número de la línea y la ocurrencia encontrada: $ cat éco eco èco êco ëco eço $ awk 2 eco $ awk 2 eco 6 eço $ awk 1 éco 2 eco 3 èco 4 êco 5 ëco

dados

'/^eco/{print NR,$1}' dados '/^e[[=c=]]o/{print NR,$1}' dados '/^[[=e=]]co/{print NR,$1}' dados

Es decir, usar [=X=] permite que la expresión encuentre la letra X estando acentuada o no (es sensible a la localización corriente!). La sintaxis es parecida con la de las clases POSIX, cambiando los dos-puntos (:) antes y después de la clase por señales de igual (=). Lo creí curioso y debe servir para algún caso semejante al descrito.

find – Buscando archivo por características Si estas como el Sr. Magoo, buscando en vano un archivo, usa el mando find que sirve para buscar archivos no sólo por el nombre, sino que ademas busca por diversas características. Su sintaxis es la siguiente: find [camino ...] expresión [acción]

Parametros: Path del directorio a partir del cual (porque es recursivo, siempre intentará entrar en los subdirectórios "colgados" de este) irá buscando en los archivos; expresión Define los critérios de busqueda. Puede ser una combinación entre vários tipos de busqueda; acción Define que acción ejecutar con los archivos que coincidan con los critérios de busqueda definidos por expresión. camino

los principales critérios de busqueda definidos por expresión son: -name

Busca archivos que tengan el nombre especificado. Aquí pueden ser usados metacaracteres o carácteres comodines, sin embargo estos


carácteres deberán estar entre comillas, apóstrofos o inmediatamente precedidos por una contrabarra, eso se debe a que quien tienen que expandir los comodines en el find. Si fuese el Shell que los expandiese, esto solo se haria con respecto al directorio corriente, lo que echaría por tierra la característica recursiva del find; -user Busca archivos que tengan al usuario como propietario; -group Busca archivos que tengan al grupo como propietario; -type c Busca archivos que tengan el tipo c, correspondiente a la letra del tipo de archivo. Los tipos aceptados están en la siguiente tabla: Valores de c Tipo de archivo buscado b Archivo especial accedido por el bloque c Archivo especial accedido por el caracter d Directório p Named pipe (FIFO) f Archivo normal l Link simbólico s Socket Busca archivos que usan mas (+n) de n unidades unid de espacio o size ±n[unid] menos (-n) de n unidades unid de espacio. Unidades

Valor Bloque de 512 bytes (valor default) Caracteres Kilobytes (1024 bytes) Palabras (2 bytes) Busca archivos a los que se accedio hace mas (+d) de d dias o menos (-d) de d dias; Busca archivos cuyo status cambio hace mas (+d) de d dias o menos (-d) de d dias; Busca archivos cuyos datos fueron modificados hace mas (+d) de d dias o menos (-d) de d dias; b c k w

-atime ±d -ctime ±d -mtime ±d

Para usar mas de un critério de busqueda, haz: expresión1 expresión2

o expresión1 –a expresión2

para atender a los criterios especificados por expresión1 y expresión2; expresión1 –o expresión2

para atender a los criterios especificados por expresión1 o expresión2. Las principales acciones definidas para acción son: Esta opción hace que los archivos encontrados sean mostrados en la pantalla. Esta es la opción default en el Linux. En los otros sabores Unix que conozco, si no se especificase ninguna acción, ocurrirá un error; -exec cmd {} \; Ejecuta el comando cmd. El objetivo del comando es considerado finalizado cuando se encuentra un punto-y-coma (;). La cadena {} es substituida por el nombre de cada archivo que satisface al criterio de investigación y la línea así formada es ejecutada. tal y como -print


dijimos para la opción –name, el punto-y-coma (;) debe ser precedido por una contrabarra (\), o debe estar entre comillas o apóstrofos; -ok cmd {} \; Lo mismo que el anterior sin embargo pregunta si se puede ejecutar la instrucción cmd sobre cada archivo que atiende al criterio de busqueda Permite que se elija los campos que serán listados y formatea la printf formato salida de acuerdo con lo especificado en formato. Ejemplos:

Para listar en pantalla (-print) todos los archivos, a partir del diretório actúal, terminados por .sh, haz: $ find . -name \*.sh Acción no especificada –print el default ./undelete.sh ./ntod.sh estos cuatro primeros archivos fueron ./dton.sh encontrados en el directório actual. ./graph.sh ./tstsh/cotafs.sh ./tstsh/data.sh Estos cuatro fueron encontrados en el ./tstsh/velha.sh directório tstsh, bajo el directório corrente ./tstsh/charascii.sh

Necesito obtener espacio en un determinado file system con mucha urgencia, entonces voy a borrar los archivos con más de un megabyte y cuyo último acceso fue hay más de 60 días. Para eso, voy a este file system y hago: $ find . –type f –size +1000000c –atime +60 –exec rm {} \;

Observa que en el ejemplo anterior use tres criterios de búsqueda, a saber: Todos los archivos regulares (normales) -size +1000000c Tamaño mayor de 1000000 de caracteres (+1000000c) -atime +60 Último acesso hace mas de 60 (+60) dias. -type f

Observa que entre estos tres criterios use el conector e, esto es, archivos regulares e mayores que 1MByte e sin acceso hace más de 60 días. Para listar todos los archivos del disco terminados por .sh o .txt, haría: $ find / -name \*.sh –o –name \*.txt –print

En este ejemplo debemos resaltar además de las contrabarras (\) antes de los asteriscos (*), el uso del –o para una u otra extensión y que el directorio inicial era el raíz (/);


siendo así, que esta búsqueda se hizo en todo el disco (lo que frecuentemente es bastante lento). Con el printf es posible formatear la salida del comando find y especificar los dados deseados. El formateo del printf es muy semejante a la del mismo comando en el lenguaje C e interpreta caracteres de formateo precedidos por un símbolo de porcentaje (%). Veamos sus efectos sobre el formateo: Caracter

Significado

%U

Número del usuário (UID) propietario del archivo

%f

Nombree del archivo (el path completo no aparece)

%F

Indica a que tipo de file system o archivo pertence

%g

Grupo al cual el archivo pertence

%G

Grupo al cual el archivo pertence (GID- Numérico)

%h

Path completo del archivo (todo menos el nombre)

%i

Número del inode del archivo (en decimal)

%m

Permisos del archivo (en octal)

%p

Nombre del archivo

%s

Tamaño del archivo

%u

Nombre del usuário (username) propietario del archivo

También es posible formatear fechas y horas obedeciendo las tablas siguientes: Carácter

Significado

%a

Fecha del último acceso

%c

Fecha de creación

%t

Fecha de Modificación

Los tres caracteres anteriores producen una fecha semejante a al del comando date. Veamos un ejemplo: $ find . -name ".b*" -printf '%t %p\n' Mon Nov 29 11:18:51 2004 ./.bash_logout Tue Nov 1 09:44:16 2005 ./.bash_profile Tue Nov 1 09:45:28 2005 ./.bashrc Fri Dec 23 20:32:31 2005 ./.bash_history

En ese ejemplo, el %p fue el responsable por poner los nombres de los archivos. En caso de ser omitido, solamente las fechas serían listadas. Observe que aunque al final se puso una /n. Sin él no habría salto de línea y la lista anterior sería un gran berenjenal.


Esas fechas también pueden ser formateadas, para eso basta pasar las letras de la tabla anterior a mayúsculas (%La, %C y %T) y usar uno de los formateadores de las dos tablas siguientes: Tabla de formatación de tiempo Z

Huso horário (en mi maravillosa ciudad BRST)

Carácter

Significado

H

Hora (00..23)

I

Hora (01..12)

k

Hora (0..23)

l

Hora (1..12)

M

Minuto (00..59)

p

AM or PM

r

Horário de 12 horas (hh:mm:ss) seguido de AM o PM

S

Segundos (00 ... 61)

T

Horário de 24-horas (hh:mm:ss) Tabla de formatación de fechas

Y

Carácter

Año con 4 dígitos Significado

a

Dia de la semana abreviado (Dom...Sab)

A

Dia de la semana completo (Domingo...Sábado)

b

Nombre del mes abreviado (Jan...Dez)

B

Dia del mes completo (Enero...Diciembre)

c

Fecha y hora completa (Fri Dec 23 15:21:41 2005)

d

Dia del mes (01...31)

D

Fecha no formateada mm/dd/aa

h

Idéntico a b

j

Dia secuencial del año (001…366)

m

Mes (01...12)

U

Semana secuencial del año. Domingo como 1º dia de la semana (00...53)

w

Dia secuencial de la semana (0..6)

W

Semana secuencial del año. Lunes como 1º dia de la semana (00...53)

x

y

Representación de la fecha en el formato del país (definido por $LC_ALL) Año con 2 dígitos (00...99)


Para mejorar la situación, veamos unos ejemplos; sin embargo, veamos primero cuáles son los archivos del directorio corriente que empiezan por .b: $ ls -la .b* -rw------1 .bash_history -rw-r--r-1 .bash_logout -rw-r--r-1 .bash_profile -rw-r--r-1 .bashrc

d276707

ssup

21419 Dec 26 17:35

d276707

ssup

d276707

ssup

194 Nov

1 09:44

d276707

ssup

142 Nov

1 09:45

24 Nov 29

2004

Para listar esos archivos en orden de tamaño, podemos hacer: $ find . -name ".b*" -printf '%s\t%p\n' | sort -n 24 ./.bash_logout 142 ./.bashrc 194 ./.bash_profile 21419 ./.bash_history

En el ejemplo que acabamos de ver, el \t fue substituido por un a la salida de forma que la lista fuera más legible. Para listar los mismos archivos clasificados por fecha y hora de la última alteración: $ find . -name ".b*" -printf '%TY-%Tm-%Td %TH:%TM:%TS %p\n' | sort 2004-11-29 11:18:51 ./.bash_logout 2005-11-01 09:44:16 ./.bash_profile 2005-11-01 09:45:28 ./.bashrc 2005-12-26 17:35:13 ./.bash_history

Y no te olvides, cualquer duda o falta de compañia para tomar una cerveza, lo único que tienes que hacer es mandarme un e-mail a julio.neves@uniriotec.br para informarse. Gracias y hasta la próxima


Turn static files into dynamic content formats.

Create a flipbook
Issuu converts static files into: digital portfolios, online yearbooks, online catalogs, digital photo albums and more. Sign up and create your flipbook.