Shell Scripting: programación con comandos de shell |
Álvaro Uría (Fermat - fermat00 AT euskalnet DOT net) |
Después de haber dado mi primer cursillo de verano ("Iniciación a la administración de sistemas GNU/Linux"), y de haber hablado
con algunos alumnos del mismo y haber visto las encuestas, me he decidido a hacer este pequeño manual para profundizar un poco más
sobre la programación de Shell Scripts.
Es mi primer manual -así como fue mi primer cusillo ;-P - así que espero ir perfeccionándolo y añadiendo datos al mismo.
NOTA: He usado la shell BASH, porque es con la que empecé y la que está por defecto al instalar nuestro GNU/Linux. Es por esto que podéis conocerla mejor y que sepáis que hay comandos de esta shell que no se encuentran en otras (sh,ksh,..) y por ello os podría no funcionar vuestro script si usaráis otra shell para la ejecución O:-).ÍndicePara ejecutar un shell script hay que realizar 2 pasos:
~$ chmod +x script.bash && ./script.bash
El Shell Script está pensado, cuando no sabes Perl (como yo), para hacer más sencilla la administración de tu sistema. Ya sabemos todos los interesados en este manual, que GNU/Linux se administra desde consola (nada de GUI) así que en vez de realizar una tarea 100 veces por semana, mejor te haces un Shell Script y te ayudas del CRONTAB.
En este manual veremos desde la utilización de comandos básicos, como los que sueles utilizar cuando navegas por tu consola, hasta los comandos que se utilizan típicamente en shell scripts. Si detectáis que me he olvidao de alguna cosa importante o simplemente os gustaría que lo añadiera, no tenéis nada más que escribir a la lista o directamente a mí ;-)
Espero que no hayáis pulsado Av. Pág. . Empezamos..
Otras variables que podemos manejar bastante son las propias del shell, como son PATH, HOME, y algunas más que veremos más adelante. (Para ver su contenido, podemos usar el comando echo) i.e.: ~$ echo $HOME~$ man <comando>
~$ man ls
~$ man grep
etc.
Esto significa, que cuando ejecutamos un comando -por ejemplo ls - nuestro shell busca si existe ese programa en /usr/local/bin, si lo encuentra lo ejecuta y se acaba ahí la ejecución del comando. Si no lo encuentra, mira en el siguiente camino - /usr/bin - y así hasta el último. Si no está en ninguno es cuando se devuelve el mensaje..~$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/bin/X11:/usr/games
La idea está en que si se modifica esta variable, añadiendo en primer término otro camino, primero se buscará ahí dicho comando. Para saber qué binario se utiliza para la ejecución de dicho comando..~$ asdfas
bash: asdfas: command not found
Por esta razón, yo recomendaría que a la hora de utilizar un comando en nuestro sencillo shell script, demos el camino completo, para que al modificar el path, no tengamos problemas (de seguridad entre otros).~$ which ls
/bin/ls
Para modificar este tipo de variables, utilizamos el comando export. De esta forma..
buscaría los comandos empezando por el directorio /tmp/.233d/, donde un h4x0r podría tener una implementación de los comandos que están en /bin bastante distinta, con sus consecuencias }:-) . Si en nuestros scripts utilizamos el camino completo para la llamada, no necesitaremos utilizar la variable PATH (éste fue un nivel del HackIt de la semana ESIDE 2003).~$ export PATH=/tmp/.233d:$PATH
Estas variables se cargan al loguearte, por lo tanto al modificarlas con export, su valor es temporal. Para que guarden un valor habrá que mirar ficheros como /etc/profile o /etc/bashrcfermat@uritech:~/shellScriptin$ echo $HOME
/home/fermat
fermat@uritech:~/shellScriptin$ cd
fermat@uritech:~$
PS1: es la cadena de caracteres que te sale una vez logueado, en consola.
dondefermat@uritech:~$ echo $PS1
\u@\h:\w\$
\u es el nombre de usuarioPS2: prompt secundario.. en el caso de no acabar de escribir un comando, sale el valor de esta variable en consola..
\h es el nombre de la maquina
\w es el directorio donde nos encontramos.. suele empezar por ~ que equivale a /home/usuario
SHELL: contiene el path del shell que estamos ejecutandofermat@uritech:~$ echo $PS2
>
fermat@uritech:~$ for a in $(seq 1 3)
> do
> echo hola
> done
hola
hola
hola
fermat@uritech:~$
IFS (internal field separators), suele contener un espacio, un tab o nada, como valor. Que te suene esta variable por si te encuentras con ella en algún momento :-OOfermat@uritech:~$ echo $SHELL
/bin/bash
sería como hacer..fermat@uritech:~/shellScriptin$ ls $HOME
a esto me refiero con expansión de variable.fermat@uritech:~/shellScriptin$ ls /home/fermat
Una de las cosas que me hace mucha gracia en sistemas UNIX-like es las frikadas que puedes hacer en muy pocos caracteres. No sabes lo que quiero decir con frikadas??? paciencia.. ahora va una
voy a analizarlo un poco O_Ofermat@uritech:~/ekparty/videos$ \ls *.AVI* | sed 's/\(.*\).AVI\(.*\)/mv & \1.avi\2/' | bash
La barra inclinada delante de ls es para que se ejecute el comando tal cual, sin hacer uso de algún alias que tenga, como la visualización con colores (alias ls='ls --color') o de los permisos (alias ls='ls -l')El comando sed ya vimos en el curso que era para modificar grupos de caracteres completos. Por ejemplo..
fermat@uritech:~$ cat fichEn el ejemplo friki, todo lo que está delante de AVI será \1 y lo que está después, \2. Con el pipe ('|') conseguimos que ese proceso que realizamos lo ejecute bash.
vamos a correjir un error ortográfico que acabo de cometer
fermat@uritech:~$ cat fich | sed s/correjir/corregir/g
vamos a corregir un error ortográfico que acabo de cometer
el comando ejecutado nos muestra las líneas en ese fichero que comiencen por la palabra "fermat". Básicamente, el formato de esta línea es el siguientefermat@uritech:~$ grep ^fermat /etc/passwd
fermat:x:1000:1000:Debian Newbie,,,:/home/fermat:/bin/bash
Para escoger la columna 5, donde se encuentra el nombre completo del usuario..nbre_usuario:seUsaPasswdOno:uid:gid:nombre_usuario,mail_usuario,sandesces_usuario,,: dir_usuario:shell_q_usa_el_user
fermat@uritech:~$ grep ^fermat /etc/passwd | cut -d ":" -f5 | cut -d "," -f1
Debian Newbie
o bien..~$ kill `ps xww | grep "S" | cut -c1-5 | grep -v PID | xargs` 2> /dev/null
De esta forma, todos los procesos que tengan el estado "S" (sleep) serán matados (kill por defecto envía una señal SIGKILL [-9] a un proceso). Para entender los pipes, recomiendo quitar todos e ir añadiendo uno a uno, ejecutandolos ;-). En este caso, se listan los procesos que ejecutamos nosotros, entre esos procesos, escogemos los que tengan estado S, y cogemos los primeros 5 caracteres (el pid), nos quedamos con todas las líneas que no contengan la palabra PID, y dejamos lo conseguido en una sola línea, con un espacio de separación entre cada conjunto de caracteres. Los errores que se produzcan no se mostrarán.~$ kill $(ps xww | grep "S" | cut -c1-5 | grep -v PID | xargs) 2> /dev/null
NOTA: aconsejo que las pruebas se hagan con el comando echo en vez de kill que, por decirlo de alguna forma, es más inofensivo.
y así un sin fin de posibilidades, porque tenemos un directorio que es un poco caos y tiene muchas cosas diferentes: leemes y Makefiles sin extension, ficheros .pas, .c, .h, .java, .s, etc.fermat@uritech:~/bancodePruebas$ rm $(ls . | grep -v ".c")
fermat@uritech:~/bancodePruebas$ rm $(ls . | grep -v ".c" | grep -v ".h")
La idea sería, mirar sin un fichero acaba en .c y de ser así, borraremos el fichero con mismo nombre pero sin extension (tenemos holaMundo.c y entonces borraremos holaMundo)...
donde "-x" es para comprobar si tiene permisos de ejecución, "-a" equivale a una AND, y "-f" es para comprobar si es un fichero. Estos va a dar como resultado TRUE o FALSE: en caso de que dé TRUE, se escribirá el nombre de dicho fichero - echo $fich - todos los ficheros que queremos borrar los listaremos en una sola línea, gracias a xargs. Forzaremos el borrado con "-f"fermat@uritech:~/bancodePruebas$ for fich in *; do [ -x $fich -a -f $fich.c ] && echo $fich; done | xargs rm -f
NOTA: una vez más, probar el script con una salida echo que será menos dolorosa en caso de fallo.
Vamos a ver si me explico con todos los detalles posibles sobre todas las posibilidades que hay. Por si alguno no lo sabe todavía, a la hora de hacer scripts en cualquier lenguaje de scripting en unix, se avisa al principio de los ficheros, qué programa va a ejecutar nuestro script. Así, en nuestro caso deberemos poner #!/bin/bash para que sea bash quien ejecute nuestro script (en caso de programar en perl, debiéramos poner #!/usr/bin/perl).
#!/bin/bash
if [ condicion ]
then
#procesos a realizar
elif [ condicion ]
then
#procesos alternativos
else
#mas procesos alternativos
fi
-f engendro: TRUE si engendro es un ficheroComparaciones_entre_números, comparaciones_entre_strings
-d engendro: TRUE si engendro es un directorio
-r, -w, -x engendro: TRUE si engendro tiene permisos de lectura, de escritura o de ejecución.
-z string: TRUE si la cadena de caracteres es CERO.
-L engendro: TRUE si engendro es un enlace simbólico.
-O fich: TRUE si fich pertenece al usuario que está ejecutando este script
-G fich: TRUE si fich existe y su gid es igual al uid del usuario que ejecuta este script
-S fich: TRUE si fich existe y es un socket
fich1 -nt fich2: TRUE si fich1 es más nuevo que fich2
fich1 -ot fich2: TRUE si fich1 es más antiguo que fich2
fich1 -ef fich2: TRUE si existen ambos ficheros y se refieren al mismo fichero, como por ejemplo fich3
-eq, ==
-ne, !=
-lt, <
-gt, >
-le, <=
-ge, >=
#!/bin/bash
echo -n "Dame un valor numérico: "; read opt
case $opt in
1)
echo porque escogiste UNO?
;;
5)
echo por el **** teee...
;;
*)
echo $opt no me vale :\(
;;
esac
y#!/bin/bash
cont=5
while [ $cont -gt 0 ]
do
echo vuelta $cont
cont=$(expr $cont - 1) # hay que dejar espacio entre "-" y "1"
done
#!/bin/bash
for cont in $(seq 1 3)
do
echo vuelta $cont
done
#!/bin/bash
function saludar
{
echo hola
}
saludar
$0: es la llamada al script. i.e.: ./hola_mundo
$1: es el primer parmátro que se pasa
$2: es el segundo, etc.
$#: es el número de parámetros que se pasan
$@ y $*: son todos los parámetros, que se listan de una vez, en el orden que se han pasado.
$$: pid que tiene nuestro script
$?: es el valor de salida del último comando ejecutado.. en un "exit 1", se devolvería el valor 1. Si todo sale bien, se devuelve el valor 0.
$_: nos da el último argumento que hemos utilizado.
A las funciones también les podemos pasar parámetros. Y una cosa que debe quedar clara es que los parámetros de las funciones son distintos a los que se pasan en la llamada al script. Es decir, un $1 en una función representa el primer parámetro pasado a la función, que puede ser sin ningún problema, distinto al primer parámetro pasado al script ;-)#!/bin/bash
echo hemos pasado $# parámetros
echo la llamada a nuestro script se hizo con $0, y tenemos el número $$ como PID
echo el primero parametro pasado es $1
echo y el segundo, $2
echo el último argumento que hemos utilizado es $_
echo todos los parametros pasados son: $@
echo el parametro enterior acabó con valor de salida $?
echo el último argumento que hemos utilizado es $_fermat@uritech:~/shellScriptin$ ./parameters.bash hola soy edu feliz navidad
hemos pasado 5 parámetros
la llamada a nuestro script se hizo con ./parameters.bash, y tenemos el número 819 como PID
el primero parametro pasado es hola
y el segundo, soy
el último argumento que hemos utilizado es soy
todos los parametros pasados son: hola soy edu feliz navidad
el parametro enterior acabó con valor de salida 0
el último argumento que hemos utilizado es 0
fermat@uritech:~/shellScriptin$
Así..
fermat@uritech:~/shellScriptin$ cat confusionConParametros.bash
#!/bin/bash
function saluda
{
echo aupa $1
}
echo hola $1
saluda $2
fermat@uritech:~/shellScriptin$ chmod +x confusionConParametros.bash
fermat@uritech:~/shellScriptin$ ./confusionConParametros.bash Juantxo Jótxe
hola Juantxo
aupa Jótxe
fermat@uritech:~/shellScriptin$
Hay en la empresa una debian a la que se puede conectar todos los empleados de la empresa. Se han enterao que el proxy de la empresa no afecta a este servidor así que se van a conectar a saco para entrar al #ghost. Nosotros, unos newbies en administración de flipar, no sabemos iptables todavía, y tampoco es plan de meter un DROP a todo, así que nos creamos un shell script todo txungo que va a ir a por las conexiones de irc solamente ;-P
Vamos a usar CRONTAB para arrancar nuestro script cada 5 minutos.
Es un ejemplo bastante malo, lo sé, pero es que no se me ocurre ahora mismo nada que no se os pueda ocurrir. Un script para grabar CD's con cdrecord, haciendo previamente un .ISO, o un envío de correo electrónico, dirigiéndose a cada persona por su nombre (solo cambia esto), oooo.. no sé :-/#!/bin/bash
function avisar_user
{
if [ $# -ge 1 ];then
for a in $(seq 1 $#)
do
uxer=$(/bin/ps ax | grep $1 | xargs | cut -d " " -f2)
/bin/kill -9 $1
if [ $uxer != "?" ];then # BitchX por ejemplo, es desde una terminal ;-)
echo Lo siento, este tipo de conexiones no están permitidas en este momento. > /dev/$uxer 2> /dev/null
fi
shift
done
else
echo no hay conexiones al iRC \;-\)
fi
}
pids=$(/bin/netstat -putan | grep ESTABLISHED | grep ":6667" | awk {'print $6'} | cut -d "/" -f1 | cut -d "D" -f2 | xargs)
avisar_user $pids
En #euskal_linux me acaban de dar la idea de crear a modo de ejemplo un shell script que manipule los comandos que ejecutamos para recompilar nuestro kernel (tú tendrás que moverte por el resultado de ejecutar make menuconfig o xconfig). El Shell Script que dé de comer a la boca, para la próxima ;-)
recompilarKernel.bash (EXPERIMENTAL - vamos que funciona, pero el kernel es algo que me gusta manipular más de cerca)
.. así me aseguro de no ser denunciado por el maligno O:-)~$ cat lista
aktornet.ath.cx
www.borjanet.com
www.txipinet.com
eghost.deusto.es
~$
Iremos ejecutando NMAP contra cada server e iremos logueando la info en un .log y los errores en un .err
#!/bin/bash
if [ $# -ne 1 ];then
lista="./lista"
else
lista=$1
fi
function listado
{
for a in $(seq 1 $#)
do
echo "Escaneando $1 (no sé mostrará por pantalla)"
/usr/bin/nmap -sS -P0 -v $1 &> $1.log 2> $1.err
shift
done
}
if [ `/usr/bin/whoami` == "root" ];then
if [ -f $lista ];then
listado $(/bin/cat $lista | xargs)
else
echo "¿qué quieres escanear?"
fi
else
echo "Lo siento, estos escaneos hay que hacerlos como ROOT y tú parece que no tienes acceso :-("
fi
Ya hace 2 años que empecé a curiosear por sistemas unix-like y Txipi acabó reforzándome el distinto mundo del Shell Scripting
(comparado con mIRC): thx Thompson & Richie & RMS & Linus & gnu/linux-world-programmers & Txipi q('_')p
Ya paro ya paro, que entonces tendría que dar las gracias a Bell, a Cerf y a alguno más.
Ahhh!! bueno, y gracias a borjanet, aKtoR, TxipiNet, y sistemas de deusto en general, por prestarnos sus servers a modo de ejemplo X-DDD
Y ahora un pequeño flame. Este documento fue escrito desde VI (emacs sux zgor) ;-) y salu2 a fmonkey q luego se pica :-O
http://eghost.deusto.es/phpwiki/index.php/ShellScriptTambién podemos ver más ejemplos en sirio.deusto.es/garaizar
CRONTAB: demonio que ejecuta comandos programados. Lo utilizamos para la automatización de tareas.
GUI: Graphical Unit Interface.