Bash script
Contingut
Intro
Els diversos Unix's han tingut sempre una potent interfície de comandes. La shell és la interfície que ens permet "dialogar" amb el nucli del sistema operatiu passant-li comandes.
Hi ha diversos llenguatges de shell script, entre ells el CSH (C-shell), i el KSH (Korn-shell), però sembla que el què s'ha imposat definitivament és el BASH o Bourne-Again SHell.
És important dominar algun dels editors de text per consola. El més típic ara és el nano
, tot i que convé també conèixer vi
ja que sol tenir syntax highlight (vim o vi improved) i perquè per raons històriques sempre està present i ens pot treure d'un apuro.
Referències:
- aquest tutorial està força bé
- aquest es veu una mica antic però pot servir de referència. Al capdavall, shell script és antic.
Recordeu que la font d'informació més immediata sempre és el manual!!
$ man bash
Arxius i permisos
Els scripts son arxius de text amb instruccions llegibles per la nostra shell. Es comenten amb un # tot i que la 1a línia és una excepció (comença per #!) i indica quin serà l'intèrpret que l'executarà. Edita hola.sh des del teu editor de text favorit:
#!/bin/bash
# això és un comentari
echo "hola!" # això ja és una instrucció
...i abans de començar a executar-lo cal donar permisos d'execució a l'arxiu:
$ chmod +x hola.sh
...i ara sí que ja el podem executar:
$ ./hola.sh
Entorn
Les comandes de la shell s'executen sense incloure tota la ruta mercès a la variable d'entorn $PATH
$ echo $PATH
...i podrem veure els directoris (separats per :) en els que la shell buscarà una comanda quan la teclegem. Les que (quasi) segur que hi han de ser son:
/bin:/sbin:/usr/bin:/usr/local/bin:/usr/local/sbin
En aquestes carpetes hi ha les comandes més habituals que utilitzem com ls a /bin
o ifconfig a /sbin
(a sbin hi ha les de Superusuari, d'aquí la 's' al davant).
Si algun dia volem saber on hi ha una comanda que estem utilitzant, podem esbrinar-ho amb:
$ which ls
...i ens dirà on està el binari que s'executarà amb aquesta comanda (que es buscarà als diferents directoris $PATH, en l'ordre que estan situats).
Quan obrim una shell el sistema operatiu crea unes variables d'entorn. Les podem veure fent:
$ env
Directori ~/bin
Un truc força útil és crear la carpeta bin
al nostre home directory. Aquesta es posarà automàticament al $PATH (fes logout i login després de crear la carpeta). Aquest és el lloc on posar scripts nostres i que no calgui que estiguin disponibles per altres usuaris.
$ mkdir ~/bin
...fes logout i login i comprova el $PATH
Variables
Les variables porten un $ al davant (similar a Perl i PHP), com p.ex. $PATH. En qualsevol moment i des de la pròpia shell (sense crear un script) podem crear-ne:
$ aaa=22 $ sss='els strings millor entre cometes per evitar errors amb els espais' $ echo $aaa 22
OJU!: és important que el signe "=" estigui enganxat, és a dir sense espais, al nom de la variable i al valor.
Recordem que podem veure totes les variables d'entorn amb:
$ env
Busca la nostra variable 'aaa' a la llista, veuràs que no hi és. Si ho vols fer com els pros, filtra la sortida amb grep:
$ env | grep aaa
Si volem que les nostres variables estiguin disponibles per altres programes, aplicacions, scripts, etc. caldrà exportar-les:
$ export aaa=22
Si ara filtrem la llista de variables d'entorn, veurem que sí que hi és:
$ env | grep aaa
Variables predefinides
$# - nº d'arguments $* - Tots els arguments del shell $- - Opcions subministrades al shell $? - Valor retornat per la darrera funció o comanda $$ - PID de la shell actual
Assignant a una variable el resultat d'una comanda
Si volem que una variable tingui com a valor el resultat d'una comanda, tenim 2 formes de fer-ho. Una és amb amb $() i l'altra utilitzant l'accent obert:
$ avui=$(date)
o bé utilitzant l'accent obert:
$ avui=`date` $ echo $avui dt feb 16 15:57:09 CET 2016
Expressions aritmètiques
Per efectuar càlculs matemàtics ens caldrà utilitzar la següent sintaxi:
$((expressió))
Per exemple:
$ echo $((11+5)) 16
OJU: en principi només ens permet aritmètica sencera (integers). No podem utilitzar nombres en coma flotant (floats). Si volem efectuar operacions matemàtiques en coma flotant haurem d'utilitzar l'aplicació bc (Basic Calculator). Feu-li un cop d'ull aquí a la utilització de bc.
Si algun dels elements no és un element interpretable es prendrà com un zero '0' (per exemple, variables no definides o strings no transformables a integer).
Arguments
Dintre d'un script podem accedir als arguments que ens passi l'usuari amb les variables $1, $2, etc.
Per exemple, podem crear un script args.sh com aquest:
echo El primer argument és $1
echo El segon és $2
echo el nom del script és $0
I l'executem amb:
$ ./args.sh hola que tal
Variables predefinides
Les VARIABLES PREDEFINIDES més importants són:
$# - nº d'arguments $* - Tots els arguments de la shell o script $- - Opcions subministrades a la shell o script $? - Valor retornat per la darrera funció o comanda $$ - PID de la shell actual
Arrays
Un array és un conjunt de valors arranjats consecutivament. Pel nostre cas ho veurem com una variable que pot contenir diversos valors a dins, indexats amb una posició.
Assignació:
PARAULES=( un array ple de coses )
O el que seria el mateix:
PARAULES[0]="un"
PARAULES[1]="array"
PARAULES[2]="ple"
...
Utilització. És una mica tricky, AL TANTO SON CLAUS {} !!:
echo "element 3 = ${PARAULES[2]}"
La cosa es complica quan fem expressions aritmètiques:
NOMBRES=( 3 23 5 100 )
echo $(( ${NOMBRES[1]} + ${NOMBRES[3]} ))
# Ens donarà el resultat "123"
De fet, bash no és un llenguatge molt adequat per aquest tipus d'operacions. Però poder, es poden fer.
Per iterar en un array podem fer:
for var in ${PARAULES[@]}
do
# printf ens permet escriure sense salt de línia (a diferència del echo que sí el posa)
printf $var
done
Nombre d'elements d'un array
${#array[@]}
Afegir elements a un array
De forma dinàmica, sense haver de saber quin és l'índex actual :)
arr=()
arr+=(primer)
arr+=(segon)
arr+=(tercer)
Bucles, llistes i altres gaites
És important recalcar que els bucles (loops) de bash script solen estar basats en llistes, enlloc de índexs. Com que habitualment els programadors ens posem nerviosos amb aquest canvi, us poso també com fer un bucle basat en índex (perquè quedi constància, ja que a la pràctica no es fa servir gaire ;)
Bucle indexat
Típicament:
for a in {5..10}
do
echo $a
done
Es poden posar bucles inversos {10..5}
i també amb salts {10..5..2}
.
També és molt interessant iterar per caràcters alfanumèrics, per exemple {a..z}
o {A..F}
.
També es pot fer a l'estil C:
for (( i=0; i<10; i++ ))
do
echo $a
done
Bucle de llista (a.k.a. "for each")
AQUEST ÉS EL BUCLE QUE HAURIEU D'UTILITZAR HABITUALMENT. Ja sé que us sona més la versió indexada, però per les aplicacions típiques d'administració de sistemes convé acostumar-se a utilitzar el "foreach".
Aquest és el realment útil. Hi ha moltes maneres de fer-los, però solen seguir aquesta forma:
for elem in elements
do
...
done
El típic exemple és un llistat d'arxius d'alguna carpeta, per exemple, /var
. En aquest cas posem els noms d'arxius a la variable $arxius
(tal i com ho solem fer amb la comanda ls) i després podrem iterar pels seus elements:
arxius=/var/*.jpg # triem només els arxius .jpg (no n'hi ha cap, però)
arxius=/var/l* # triem només els arxius que comencen per "l"
for elem in $arxius
do
echo "arxiu = $elem"
done
while...do
Segur que també coneixeu aquest. Per aquest, però, cal tenir clar els condicionals (IF). Segueix llegint més avall.
while...do funciona amb condició d'entrada:
while [ condició ]
do
...
done
Existeix la variant until...do. Aquesta simplement inverteix la condició (negació).
Strings, bucles i delimitadors amb IFS
Podem iterar pels diversos elements d'un array, d'una llista i fins i tot d'un string. Podem "partir" (split) un string amb un bucle.
La variable IFS indicarà al for quin serà el caràcter delimitador per iterar:
FRASE="hola que tal?,com ho veus?"
IFS=" "
for elem in $FRASE
do
echo "$elem"
done
Prova d'executar-ho tal com es mostra, i després prova-ho amb IFS=",". Comprova que el resultat és aquest:
IFS=" " | IFS="," |
---|---|
hola |
hola que tal? |
Condicionals
Les sentències condicionals executaran una sèrie d'instruccions només si es compleix la condició. Una condició s'avalua entre claudàtors o bé amb la directiva test (tot i que aquesta darrera està en desús).
Les funcions de comparació s'han millorat amb el que anomenen new test i que va amb doble claudàtors [[ ]]. Aquí pots llegir més sobre el tema.
Es pot fer composició de condicions utilitzant && (AND) i || (OR). Hi ha variants antigues utilitzant -a (AND) i -o (OR).
if...then...else
És la sentència més habitual. Per exemple, per testejar una variable:
if [ $CARRER = 'Bonavista' ]
then
echo "Estas al c. Bonavista"
else
echo "On t'has parit?"
fi
Si es tracta de nombres sencers cal utilitzar unes altres comparacions. Aprofitem per posar un exemple amb AND:
if [ $NUMERO -eq 70 ] && [ $CARRER = 'Bonavista' ]
then
echo "Estas a l'institut Esteve Terradas"
else
echo "Continua fins el final del carrer Bonavista per trobar-nos!"
fi
Les comparacions són diferents si es tracta de nombres sencers o si son strings:
Comparació | Strings | Integers |
---|---|---|
Igualtat | = | -eq |
Diferent de | != | -ne |
Menor que | < | -lt |
Major que | > | -gt |
Major o igual | -ge | |
Menor o igual | -le | |
Cadena nul·la | -z | |
No és cadena nul·la | -n |
arxius
Bash té un tractament especialment ràpid per als arxius, ja que és un cas molt comú. Així, tenim condicionals especialment pensats per arxius:
Directiva | Efecte |
---|---|
-e | l'arxiu Existeix |
-s | arxius existeix i no està buit |
-d | existeix i és un Directori |
-f | existeix i és un arxiu (File) regular o ordinari |
-O | ets propietari de l'arxiu |
Per exemple:
if [ -e /var/lib/mysql ]
then
echo "probablement tens MySQL instal·lat"
else
echo "si has d'instal·lar MySQL fes servir apt-get install mysql-server"
fi
case (a.k.a. switch)
La sentència case ens permet una sintaxi més clara quan fem molts IFs seguits. Per exemple quan tenim diverses opcions per aplicar a un script com a argument
case $1 in
start)
echo "arrencant procés"
;;
stop)
echo "parant procés"
;;
...
*)
echo "opció incorrecta"
;;
esac
select o com fer menús ultraràpids
Encara ens queda un altre tipus de condicional, el SELECT, que va de perles per fer menús. No us atabalo més, llegiu aquí si voleu saber com es fa.
stdin, stdout i redireccions a arxius
Aquests conceptes correponen als standard streams de entrada/sortida definits per la comunicació dels processos executats en una computadora.
- stdin o entrada estàndard: sol ser el teclat
- stdout o sortida estàndard: sol ser la pantalla
- stderr: sol ser també la pantalla, però està en un canal separat ja que ens dona avantatges de gestió.
stdout
Podem redirigir stdout d'un programa cap a un arxiu amb ">" , per exemple (OJU, esborrarem tots els continguts del fitxer destí):
$ ls -la > directori.txt
i podrem veure els continguts del directori fent:
$ cat directori.txt
Podem AFEGIR dades a l'arxiu amb ">>":
$ echo "xin pom!" >> directori.txt
Comprovem amb:
$ cat directori.txt
stderr
Però si cometem un error en la comanda, el missatge d'error ens apareix a stderr, que continua sent la pantalla:
$ ls -la aaa > directori.txt ls: no s’ha pogut accedir a aaa: El fitxer o directori no existeix
Si volem redirigir stderr ho podem fer amb 2>
, per exemple:
$ ls -la aaa > directori.txt 2> errors.txt
...ara el missatge d'error no ens apareix, però el trobarem dins de l'arxiu "errors.txt".
Si volem juntar stderr + stdin podem fer-ho afegint 2>&1
al final de la comanda:
$ ls -la aaa > directori.txt 2>&1
stdin
L'entrada estàndard sol ser el teclat, i la llegim amb read:
read entrada
echo "has dit \"$entrada\""
Al tanto amb els caràcters especials. Si volem imprimir una cometa doble caldra utilitzar el ESCAPE CHARACTER "\".
A stdin també podem redirigir-hi els continguts d'un arxiu. Això es pot fer de 2 maneres
- Amb la "tuberia" pipe "|"
$ cat directori.txt | grep txt
- Amb "<":
$ grep txt < directori.txt
llegir un arxiu línia a línia
Un truc important en els scripts és llegir un arxiu línia a línia per poder processar-lo i aplicar canvis en ell. Sol fer-se a l'estil de les redireccions de stdin, concretament podem llistar l'arxiu /etc/passwd
:
while read LINE
do
# processem LINE d'alguna manera
echo $LINE
done < /etc/passwd
Tal com hem vist, el mateix es pot aconseguir amb el pipe:
cat /etc/passwd | while read LINE
do
# processem LINE d'alguna manera
echo $LINE
done
ULL!
Si estem en un loop d'aquest tipus (entrant dades per STDIN a un bucle while), el read no funcionarà. Si vols assegurar el seu funcionament, hauràs de fer:
cat /etc/passwd | while read LINE
do
echo "Vols modificar $LINE? (s/n)"
read SINO << /dev/tty
done
Funcions
Podeu explorar més a fons les funcions en aquest article.
Podem encapsular funcions de la següent manera. Per passar arguments es comporta similarment a les comandes, amb $*, $1, $2, etc. i retornant el valor a $?
funcio1 () {
echo "hola, sóc una funció"
echo "el valor passat és $1"
return 33
}
# executem la funció cridant-la com si fos una comanda:
funcio1 yahuuuuuu
echo "hauria de retornar un 33 oi? =$?"
IMPORTANT: les funcions només poden fer RETURN d'un integer, ja que imita el model del STATUS CODE de les comandes de la shell. Si voleu captar informació processada per una funció, teniu diverses opcions explicades en aquest article.
Les funcions són molt importants per reutilitzar el codi i no repetir coses que ja hem fet, evitant errors i facilitant la modificació del programa.
Virgueries GUI, curses i altres mandangues vistoses
Per curses solem entendre les típiques llibreries que ens fan aparèixer menús acolorits a la pantalla (típic per exemple al configurar paquets apt). Si t'interessa pots investigar un parell de llibreries que venen ja amb moltes distribucions GNU/Linux:
- dialog : treballa només amb text. Exemple:
$ dialog --yesno "are you mad?" 10 30
- zenity : treballa amb llibreries gràfiques (no apte per ubuntu server):
$ zenity --calendar
Ambdues tenen moltes opcions de menús i no son gens complicades, i poden fer el nostre script ben resultón. Podeu consultar la sintaxi fent:
$ dialog --help $ zenity --help
Us poso un exemple per utilitzar dialog ja que sembla una mica més tricky per fer-la funcionar amb normalitat. L'he tret d'aquí.
#!/bin/bash
exec 3>&1 # fem un arreglillo creant un nou stream (3). Si no, no podem visualitzar el menú correctament
retorn=$(dialog --calendar "hello..." 5 40 30 3 2016 2>&1 1>&3)
exec 3>&- # retornem els streams com estaven (cancelem el stream 3)
if [ $? -eq 1 ]; then
echo "Has cancelat!"
else
echo "Has entrat el dia: $retorn"
fi
I ens quedarà així de xulo:
Exercicis
Scripts bàsics
Exercicis de scripts:
- Crea un script que agafi tots els arxius .JPG indicats al directori que li passem com a argument, i els canvii d'extensió a .PNG
- Consulta aquest article sobre la manipulació de strings.
- Investiga la llibreria ImageMagick i utilitza la comanda convert per transformar les imatges de la carpeta que passem al script a una resolució fixa de 800x600 pixels.
- Guarda els arxius resultats en una carpeta de sortida que indiqui l'usuari
- Fes un script que imiti el comportament de la comanda which, és a dir, que li passi un nom de comanda i em digui on està el seu executable.
- Les shells busquen les comandes a les carpetes que es troben a la variable $PATH. Utilitza aquesta variable d'entorn per buscar per les carpetes allà indicades.
- Fes un script que llegeixi el directori passat com a argument i llisti els arxius però no les carpetes.
- Utilitza els condicionals per arxius.
- Amplia l'exercici anterior fent que llisti els arxius recursivament, és a dir, que entri a les subcarpetes mostrant els continguts.
- Assegura't que no hi ha problema quan hi ha carpetes amb un nom amb espais. Consulta aquest article per solucionar-ho i aclarir què significa el IFS.
- Pista: hauràs de crear una funció recursiva, és a dir, que es cridi a sí mateixa.
- Fes un script que generi una contrasenya aleatòria amb caràcters. L'usuari passarà el nº de caràcters que ha de tenir aquesta contrasenya. Si no es passa cap argument, per defecte seran 8 caràcters. Ho pots fer amb $RANDOM que ens genera un número aleatori entre 0 i 32767.
- Fes el joc del penjat en Bash. Parteix d'una paraula fixa i demanem a l'usuari que ens entri una lletra. Hauríem d'iterar per les lletres del string de la paraula i descobrir si la lletra entrada per l'usuari figura en ella. Et pot convenir el truc següent, transforma un string en una llista de lletres (i és més fàcil iterar):
$ echo "paraula" | fold -w1
- Millora el joc del penjat afegint un diccionari de paraules en un arxiu de text (una paraula per línia), fent que el programa trii una de les paraules aleatòriament.
- Sabent que la comanda 'ps fax' mostra informació sobre tots els processos del sistema, escriu el script quants_processos de manera que en executar-se mostri: El nombre de processos és aproximadament:<n>
- Sabent que la comanda 'df -h' mostra informació sobre els dispositius d'emmagatzematge muntats escriu el script info_arrel de manera que en executar-se mostri en diferents línies: Grandària de l'arrel: <n> , Dispositiu: <dispositiu>, Espai consumit:<n>, Espai sense utilitzar: <n>, Grau d'utilització: <percentatge%>
Backups
- Crea un script per comprimir els continguts de la carpeta que es passa com a argument. Ha de demanar interactivament si utilitza compressió gzip o bz2.
- Crea un script que faci un backup de la carpeta
/var/www
i el guardi a/root/backups
comprimit en.tgz
. Fes que es guardi al directori de destí sense sobreescriure els arxius existents. Per exemple, si tens l'arxiubackup1.tgz
, el següent arxiu s'ha de guardar com abackup2.tgz
- Crea un script que faci un backup de les bases de dades MySQL amb mysqldump i les comprimeixi amb bzip2.
- Instal·la el script de l'exercici nº2 (backup /var/www) al CRON i fes que s'executi cada minut. Comprova que es realitza correctament.
Configuració de serveis
- Fes un script que canvii el FQDN d'una màquina, i que asseguri que el nom de la màquina té un domini complert (maquina.domini.tld, per exemple: mercuri.institut.local).
- Fes un script que llegeixi un arxiu passat per argument (serà un arxiu de conf) i que el tregui per stdout sense comentaris.
- Al capdavall hi ha una comanda que ja ho fa. Tot i així, realitza-ho processant línia per línia de l'arxiu.
$ grep -v "^#" <arxiu>
- Potser et convé llegir una mica sobre expressions regulars.
- Pista: llegeix aquí per saber més sobre manipulació de strings.
- Al capdavall hi ha una comanda que ja ho fa. Tot i així, realitza-ho processant línia per línia de l'arxiu.
- Crea un script que configuri un servidor Samba PDC. Ha de generar l'arxiu de configuració smb.conf d'acord amb les indicacions de l'article de Samba. De forma interactiva, l'script ha de demanar les dades necessàries: nom del domini, nom NetBIOS, si es publiquen els home dirs, si els profiles d'usuari van separats dels homes, etc.
- Crea un script que configuri una màquina Linux Ubuntu LTS (en principi 14.04) i la entri en un domini Samba (com a client) d'acord amb el què s'explica en aquest article.
- Fes un script que configuri una màquina Linux amb la següent configuració:
- Una doble xarxa (a) NAT i (b) interna amb 192.168.33.1
- El servei DNSMASQ per fer de DNS
- El servidor DHCP del DNSMASQ per servir IPs en el rang 192.168.33.100-200, i utilitzant la pròpia màquina de gateway i DNS.
- Amplia el script de configuració de smb.conf per connectar Samba amb LDAP. Pot incloure la instal·lació i/o posterior reconfiguració del paquet slapd per adequar-lo al domini.
- Fes un script que crei els arxius de configuració de smbldap-tools i que els arrangi adequadament per poder utilitzar les comandes de creació d'usuaris LDAP. Segueix l'explicat a la pràctica Samba amb LDAP#Configurem_smbldap-tools.