6 oct 2010

Curso de Korn Shell scripting

Shell Scripting

Se denomina scripting a la habilidad de encadenar expresiones interpretadas por la shell.

Es una forma de programación esencial para quien quiera dedicarse a la administración de sistema, o para quien trabaje programando o desarrollando tareas de cierta complejidad con archivos y directorios en un sistema UNIX.

La sección se llama Korn Shell scripting porque la mayoría de scripts de mi vida los he escrito en Korn Shell, pero habrá quien se decante por programar en Bash que es actualmente ubicua en los sistemas Linux.

Antes de empezar, decir tres cosas:
  • que a programar en shell sólo se aprende escribiendo scripts, aunque ayuda mucho leer scripts de otros bien hechos y comentados.
  • que no se requieren más herramientas que un editor, y que este, en sistemas UNIX, debe ser el vi, aunque para mayor productividad, viene bien tener un editor gráfico como nedit.
  • que además de la shell, se asume que el programador debe conocer ciertos comandos del sistema, como sed, grep o awk que permiten que los scripts hagan cosas realmente interesantes.

El editor vi

Aunque existen en UNIX editores con interfaz gráfica de usuarios, un técnico de sistemas debe saber utilizar el editor vi. El vi es un editor que pese a su nombre muy visual no es, pero hay que tener en cuenta su fecha de nacimiento: 1976.

¿Qué editores hay en UNIX?

vi - todo técnico de sistemas debería saber usarlo
e, ed, o ex - un editor que sólo gestionaba una línea cada vez
emacs - un editor que se creó para programar en lisp y era más fácil que el vi
gráficos - nedit, xedit, gedit, kedit, etc. Si quieres ser productivo, y no está ya instalado uno de ellos, pide a tu administrador que te instale uno de estos en los que el copiar/pegar se haga con el ratón :)

Lo que más choca al principio de usar el vi es que no se puede editar directamente, que hay que "entrar" en un modo u otro según estemos dando comandos, editando, o ejecutando:
  • modo comando: es el modo en que se entra por defecto cuando se comienza la sesión de vi, cualquier cosa que se escriba se interpreta como un comando
  • modo inserción: cualquier cosa que se escriba en él, se interpreta como que se quiere meter al fichero, menos la tecla ESC que interpreta que queremos irnos al modo comando.
  • modo ejecución: cualquier cosa que se escriba en el se interpreta como que se quiere ejecutar algo. Se invoca con el :
Si queremos entrar en el vi escribimos simplemente el comando vi o

$ vi fichero

Para salir del vi, tendremos que escribir en el modo comando (ESC para asegurarnos de que estamos en él) el comando

:q! ---> salir sin grabar
:q ---> salir grabando, igual que :wq!

Meternos en el modo inserción requiere elegir si nos metemos en la línea que estamos o no:

a -> insertamos donde está el cursor
A -> idem pero al final de la linea
i -> insertamos debajo del cursor
I -> idem pero al final de la línea
o -> por encima
O -> al final

Vamos a escribir algo

luciag@luna.acme.com:~> vi fichero.txt
(a ---> hola ---> ESC ---> :wq!)

Ha sido fácil, vamos a aprendernos algunos comandos.

Para mover el cursor por la pantalla, hay que aprenderse h, j, k, l, que son los comandos para moverse a la izquierda, derecha, arriba, abajo, aunque algunos teclados nos dejan usar las flechas normales. Irse moviendo de palabra en palabra requiere usar la w o la b, para ir alante o atrás.

Para borrar, está la x, que borra el carácter que está donde el cursor, y la d, que borra la línea actual, como por ejemplo en dd, que borra la línea, también podemos dw (borrar el resto de la palabra), dG (borrar el resto del fichero), d$ (borrar hasta el final de la línea). Podemos 4dd para borrar 4 líneas.

El copia pega es un poco más complicado. Básicamente yy (yank) y p (paste por delante del cursor) y P (paste por detrás).

Un comando bastante intereante es el u. Y el . repite el último cambio.

En modo ejecución podemos hacer búsquedas, si hacemos
/texto --> Buscamos el texto
n -> Siguiente ocurrencia

Y también cambiar texto
;1,$s/algo/otracosa/g

A continuación vamos a ver una "chuleta" completa del editor

OperatorsDescription
d operanddelete the operand into the (delete) buffer
ppaste the contents of the (delete) buffer after the cursor
y operandyank the operand into the (delete) buffer
i operandinserts the operand (before current character)
a operandappends the operand (insert after current character)
r operandreplaces current character with operand
s operandsubstitute the operand with typed-in text
c operandchange the operand to typed-in text
! operandpass the operand to a (Unix) shell as standard input;
standard output replaces the operand.
Common MacrosDescription
Iinsert at beginning of line (same as ^i)
Aappend at end of line (same as $a)
Ddelete to end of line (same as d$)
Cchange to end of line (same as c$)
xdelete one character (same as dl)
ZZsave and exit
:w filenamesave as filename without exiting
:q!quit immediately (without save)
Miscellaneous
Renter replace (overstrike) mode
oopen line below current line
Oopen line above current line
" nn is 0-9: delete buffers
" xx is lowercase a-z: replace user buffer
" xx is uppercase A-Z: append to user buffer
.perform last change again
uundo last change
Uundo all changes to current line
OperandsDescription
h j k lleft, down, up, right; one character/line at a time
w b enext word, back word, end of word
W B E(same as above, but ignores punctuation)
/stringsearch for string (use ? for reverse search)
nsearch for string again (see /, above)
%find matching ( ), { }, or [ ]
( )beginning of current/previous sentence and beginning of next sentence
{ }beginning of current/previous paragraph (two adjacent newlines) and beginning of next paragraph (see also set paragraphs)
[[ ]]beginning of current/previous section and beginning of next section (mostly user-defined; see also set sections)
line Ggoto particular line number (defaults to end-of-file)
0 ^ $move to column 0, move to first non-whitespace, move to end of line
f xforward to character x on same line (inclusive)
t xto character x on same line (not inclusive)
;last f or t again in the same direction
,last f or t again in the opposite direction
m xset mark x at current position
' xmove to line containing mark x
` xmove to exact position of mark x
''move to line of last jump point
``move to exact position of last jump point


Interesting examples of numeric prefixes would be 36i-*, 8i123456789-, and 20r_.


Ex (colon-mode) commands

In the following commands, file may be either a filename, or a shell command if prefixed with !. Filenames are globbed by the shell before vi uses them (shell wildcards are processed before the filenames are used). Address ranges may be used immediately after the colon in the commands below. Example address ranges are:

RangeDescription
1,$From line 1 to the end of the file.
10,20From line 10 to line 20, inclusive.
.,.+10From the current line to current line + 10 (11 lines total).
'a,'dFrom the line containing mark a to the line containing mark d.
/from/,/to/From the line containing "from" to the line containing "to", inclusive.
Commands which change the file being edited.
:e filenameChange from the current file being edited to filename. "%" means current file, and "#" means alternate file.
Use :e # to edit the file most recently edited during the same session.
:n [filename(s)]Edits the next file from the command line. With optional list of filenames, changes command parameters and edits the first file in the list. Filenames are passed to the shell for wildcard substitution. Also consider command substitution:
:n `grep -l pattern *.c`
:argsLists the files from the command line (possibly as modified by :n, above).
:rewRestarts editing at the first filename from the command line.
Commands which modify the text buffer or disk file being edited.
:g/RE/cmdGlobally search for regular expression and execute cmd for each line containing the pattern.
:s/RE/string/optSearch-and-replace; string is the replacement. Use opt to specify options c (confirm), g (globally on each line), and p (print after making change).
:w fileWrite the contents of the buffer to file. If file starts with an exclamation mark, the filename is interpreted as a shell command instead, and the buffer is piped into the command as stdin.
:r fileReads the contents of the file into the current buffer. If file starts with an exclamation mark, the filename is interpreted as a shell command instead, and the stdout of the command is read into the buffer.
These commands control the environment of the vi session.
:set optTurns on boolean option opt.
:set nooptTurns off boolean option opt.
:set opt=valSets option opt to val.
:set opt?Queries the setting of option opt.
Miscellaneous commands.
:abbr string phraseCreates abbreviation string for the phrase phrase. Abbreviations are replaced immediately as soon as recognized during text or command input. Use :unab string to remove an abbreviation.
:map key stringCreates a mapping from key to string. This is different from an abbreviation in two ways: abbreviations are recognized as complete units only (for example, a word with surrounding whitespace) while mappings are based strictly on keystrokes, and mappings can apply to function keys by using a pound-sign followed by the function key number, i.e. #8 would map function key 8. If the terminal doesn't have an key, the mapping can be invoked by typing "#8" directly (doesn't work in the AIX 5L version of vi).


Comandos típicos para usar en scripts

Los siguientes comandos manipulan líneas, y en general cadenas.

head, tail - listan respectivamente el comienzo y el final de un fichero al stdout. Por defecto 10 líneas, pero con -n se puede cambiar.

Ejercicio: Obtener las 10 últimas alertas que se registraron en el fichero /var/adm/messages (Solaris, Linux).

cut, paste, join -
cut sirve para extraer campos de una línea (veremos que awk se usa mucho para eso también). Las opciones importantes son -d (delimitador de campos) y -f (campos que queremos)

Ejemplo: Sacar con el comando uname una cadena que sólo tenga el sistema operativo y la versión

luciag@luna.acme.com:~> uname -a
Linux luna.acme.com 2.6.18-128.el5 #1 SMP Wed Dec 17 11:41:38 EST 2008 x86_64 x
86_64 x86_64 GNU/Linux
luciag@luna.acme.com:~> uname -a |cut -d" " -f1,3
Linux 2.6.18-128.el5

Ejemplo: Sacar los usuarios y las UIDs
luciag@luna.acme.com:~> cat /etc/passwd | cut -d: -f1,3
root:0
bin:1

tr: El tr viene de "translation filter", pero yo durante mucho tiempo pensé que era tr de trocear.
Sirve para muchas cosas.

Por ejemplo, convertir a mayusculas

luciag@luna.acme.com:~> cat /etc/passwd |tr a-z A-Z
ROOT:X:0:0:ROOT:/ROOT:/BIN/BASH
BIN:X:1:1:BIN:/BIN:/SBIN/NOLOGIN

Cambiar todas las letras por asteriscos
luciag@luna.acme.com:~> cat /etc/passwd | tr "[a-z][A-Z]" "*"
****:*:0:0:****:/****:/***/****
***:*:1:1:***:/***:/****/*******


wc: Hace una "cuenta de palabras" (word count) sobre un fichero o I/O stream

luciag@luna.acme.com:~> wc /etc/passwd
35 56 1619 /etc/passwd

Se muestran palabras, líneas y bytes.
Si sólo queremos estas por separado sacamos opciones -w, -l, -c.

Un ejemplo típico:

luciag@luna.acme.com:~> who
pepe pts/1 2010-10-18 15:32 (pepepc.dominio.com:0.0)
luciag pts/4 2010-10-20 14:49 (pluton)
luciag@luna.acme.com:~> who | wc -l
2

Hay tres comandos que me dejo explícitamente para el final: grep, sed, awk


"Hola mundo" en Korn Shell

Escribiremos un primer script que saludará al mundo en general, como siempre que se aprende a programar. Iniciaremos el editor escribiendo a continuación el nombre del fichero que contendrá el script.

$ vi helloworld.ksh

El llamar helloworld.ksh al script es por manías propias, en UNIX como todo el mundo sabe, no hace falta una extensión para que el script se ejecute.

La primera línea en todo script comienza por el carácter #, seguido de una admiración ! (a esta combinación de caracteres se le conoce como shabang o sharp+bang) y la ruta a la shell con la que se ejecutará.

#!/bin/ksh

El carácter "comentario" en Korn Shell es el #, y podemos utilizarlo para crear una cabecera que explique lo que hace el script.

El carácter @(#) es opcional, sirve para que una utilidad de programación llamada "what" (no estándar en todos los sistemas) nos dé información sobre el script.

#!/bin/ksh
#
# @(#)FileDescription: Mi primer script 
# @(#)Language:        Korn Shell
# @(#)Version:         1.0

Ahora decir hola mundo es tan fácil como usar el comando echo.

#!/bin/ksh
#
# @(#)FileName:        helloworld.ksh
# @(#)FileDescription: Mi primer script
# @(#)Language:        Korn Shell
# @(#)Version: 1.0
#
echo "Hello world"

Se guarda el script y para ejecutarlo o bien le damos permisos de ejecución y escribimos su path completo, o bien indicamos la shell seguida del nombre del script.

Es decir:

$ chmod +x helloworld.ksh
$ ./helloworld.ksh

donde como se puede observar, el directorio actual (.) no está en el PATH (en algunos entornos muy confiados sí se incluye . en el PATH, pero esto no es así por defecto en la mayoría de los sistemas).

$ ksh helloworld.ksh

Más sintaxis de Korn Shell

En un script de Korn shell podemos utilizar la siguiente sintaxis:

- Ejecución de comandos en serie: $ who -l; df -v; ps aux
- Ejecución de comandos en paralelo: $ who & df -k & ps -ef &
- Ejecución de comandos en tubería: $ who | sort | lp
- Sustitución de metacaracteres: $ ls f*
- Definición de variables: export OPCION=a (sin espacios)
- Consulta de variables: echo $OPCION

- Usar acentos graves para ejecutar comandos

#!/bin/ksh
echo "Hay `who | grep $u | wc -l` usuarios conectados"

- Modos de la shell: El verboso: set -x (set -o xtrace)

#!/bin/ksh
# Descomentar si tenemos problemas con el script
# set -x


- Sustitución de comandos: expr
- Variables leidas de la linea: read var

Ejemplo

#!/bin/ksh
#
# @(#)FileName:        exprsample.ksh
# @(#)FileDescription: Uso de expr
# @(#)Language:        Korn Shell
# @(#)Version:         1.0

echo Dime un numero 
read a 
echo Dime otro 
read b 
echo He leido primero 
echo $a 
echo He leido después 
echo $b echo 
La suma es echo suma='expr $a+$b'

- Declarar enteros para usar aritmética de enteros

#!/bin/ksh
#
# @(#)FileName:        integers.ksh
# @(#)FileDescription: Uso de expr
# @(#)Language:        Korn Shell
# @(#)Version:         1.0

typeset -i i=12            integer j=44



- Manejar los argumentos del script con las variables tipo dólar:

$0nombre del script
$1...$nargumentos de 1 a n
$#número de argumentos
$?código de salida (exit)

- Terminar la ejecución
exit [n]

- Escapar caracteres utilizando metacaracteres de escape como La \ y el --
Ejemplo: Cómo crear un fichero llamado *
Ejemplo: Cómo borrar un fichero llamado -i

Sentencias de control en la programación de la shell
Las cuatro más famosas: if case while y for

1) if comandos then comandos else comandos fi
2) case valor in
1) comandos;;
2) comandos;;
*) comandos;;
esac
3) while comandos do comandos done
4) for variable in argumentos do comandos done

Se pueden necesitar read y shift.

El comando test

Para empezar un if siempre hay que poner una condición que se comprueba con el comando test o simplemente []
Las condiciones sobre cadenas de caracteres pueden ser

[ string1 = string2 ]
[ string1 != string2 ]

Hay que tener cuidado con la cadena vacía, que no se puede testear así

Si son enteros

[ int1 -eq int2 ] [-ne ] [- gt ][ -ge ][ -lt ][ -le ]

Si son ficheros
-r fichero (legible) -w fichero (escribible) -x fichero (ejecutable) -f fichero (regular) -d fichero (directorio) -s fichero (no vacío) -t fichero (abierto en una terminal)

Condiciones lógicas
[ algo -a algo ]
[ algo -o algo ]

La sentencia while
Ejercicio: Escribir un programa que le pasas un número y saca algo por la pantalla ese número de veces

La sentencia for
Ejercicio: Crear un programa que saluda a todos los usuarios del sistema por su nombre de usuario

El comando grep y expresiones regulares

El comando grep es una herramienta de búsqueda que nos permite usar expresiones regulares (de ahi su nombre, global regular expresion printer).

luciag@luna.acme.com:~> ps -ef| grep luciag
luciag 27421 27420 2 14:49 pts/4 00:00:00 -tcsh
luciag 27451 27421 0 14:49 pts/4 00:00:00 ps -ef
luciag 27452 27421 0 14:49 pts/4 00:00:00 grep luciag

luciag@luna.acme.com:~> grep root /etc/passwd
root:x:0:0:root:/root:/bin/bash
operator:x:11:0:operator:/root:/sbin/nologin

Opción -i: case insensitive
Opción -w: Palabras completas
Opción -l: Sólo lista los ficheros donde encontró
Opción -n: Saca la línea donde lo encontró

Una expresión regular es un conjunto de caracteres que especifican patrones de búsqueda o de sustitución. Tienen una sintaxis un poco compleja y la única forma de saber si una expresión regular busca lo que queremos es comprobarla con un ejemplo.

Caracteres utilizados en la sintaxis de las expresiones regulares:

El asterisco -- * -- busca cualquier número de repeticiones de la cadena que le precede
El punto -- . -- busca 1 carácter
El gorrito -- ^ -- busca el comienzo de la línea
El signo dólar -- $ -- busca al final de una línea

"[xyz]" busca uno de los tres caracteres x, y, or z.
"[c-n]" busca caracteres en el rango de c a n.
"[a-z0-9]" busca minúsculas o alfanuméricos.

Los angulitos escapados buscan palabras enteras -- \<...\> --

Ejemplos:

grep luciag /etc/passwd {busca la cadena luciag en el fichero /etc/passwd}
grep '^ luciag' files {'luciag' al principio de una linea}
grep 'luciag$' files {'luciag' al final}
grep '^smug$' files {lineas que empiezan y terminan por 'luciag', sólo contienen eso}
grep '\^l' files {lineas que empiezan por '^l', "\" es para escapar ^}
grep '[Ll]ucia' files {busca 'Lucia' or 'lucia'}
grep 'B[oO][bB]' files {busca BOB, Bob, BOb or BoB }
grep '^$' files {busca líneas que no tienen nada}

Otros patrones
[0-9] un dígito del 0 al 9
[0-9][0-9] dos dígitos
[0-9][0-9]* n dígitos

Ejemplos de expresiones regulares:
1) Vamos a sacar la máquina desde la que hemos abierto nuestra sesión (el servidor X):

luciag@luna.acme.com:~/> who -m
luciag pts/4 2010-11-03 09:45 (tpbasexp)
luciag@luna.acme.com:~/> who -m | sed 's/.*(:*0*\.*0*\(.*\))/\1/'

sed & awk

Las utilidades sed y awk sirven para procesar texto:

sed: editor no interactivo. Se usa sobre todo para "aplicar expresiones regulares".
awk: lenguaje de procesamiento de patrones. Se usa sobre todo para conversión de unas cosas en otras.

Las dos utilidades tienen una invocación similar y usan expresiones regulares, leyendo del stdin y escribiendo al stdout.

Funciones, e includes, en los scripts

Cuando un script se convierte en algo muy grande, podemos escribir "funciones" como en C.

Funcion()
{
echo "aqui mi funcion"
}

Para llamarla, simplemente

Funcion

Las funciones pueden tener argumentos, se usa la numeracíon a partir del 1

CheckSomething()
{
thing_to_check=$1
}

Para llamarla: CheckSomething ARGUMENTO

Cuando un script se nos hace muy grande podemos dividirlo en ficheros. Luego "sourceamos" el trocito pequeño desde el trocito grande

InitializeScript()
{
Init_Filename="file_to_include.ksh"
. ./$Init_Filename

# Add here specific variables
}


Scripting by example

Esta sección pretende ser un recopilatorio de ejemplos resueltos y ejercicios

Ejemplos resueltos
  • En este ejemplo quería saber todos los ficheros jar que había en un directorio para ponerlo en un classpath
for file in $APP_HOME/WEB-INF/lib/*.jar
do
JAVA_CP=`echo $JAVA_CP`:$file
done
  • En este ejemplo quería saber un número de versión de java para compararlo con otro
luciag@luna.acme.com:~/> java -version
java version "1.4.2"
gij (GNU libgcj) version 4.1.2 20080704 (Red Hat 4.1.2-44)

luciag@luna.acme.com:~/> java -version |head -1|awk '{print $3}' | sed -e 's/.//g' | tr -d \"
142

El resultado es 142, obviamente ;)

  • Aquí quería saber si el número de descriptores de archivo era 1024 por lo menos

CheckFileDescriptors()
{

sSOFT=`ulimit -n`
sHARD=`ulimit -nH`

if [ "${sSOFT}" = "unlimited" ]; then
typeset -i CURRENT_NOFILES_SOFT=1024
else
typeset -i CURRENT_NOFILES_SOFT=`echo $sSOFT`
fi

if [ "${sSOFT}" = "unlimited" ]; then
typeset -i CURRENT_NOFILES_HARD=`echo $CURRENT_NOFILES_SOFT`
else
typeset -i CURRENT_NOFILES_HARD=`echo $sSOFT`
fi

if [ ${CURRENT_NOFILES_SOFT} -lt 1024 ]; then
echo ""
echo "$0 ERROR:"
echo
echo "This application needs a file descriptors per-process limit of at least 1024"
echo "Before you install, you must set"
echo " (ksh) ulimit -n 1024 in the users .profile"
echo " (csh) limit descriptors 1024 in the users .cshrc"
echo ""
if [ ${CURRENT_NOFILES_HARD} -lt 1024 ]; then
echo ""
echo "Check with your system administrator for system wide limits"
echo ""
fi
exit
else
:
# -- all ok
fi

}

Bibliografía
El arte del bash scripting de Mendel Cooper: http://tldp.org/LDP/abs/html/
La página oficial de la Korn Shell: http://www.kornshell.com/

No hay comentarios:

Publicar un comentario