El shell és la capa més externa del sistema operatiu, on l'usuari pot interactuar usant comandaments; aquest comandaments són interpretats per un intèrpret, que en el cas de Linux pot ser un de diversos disponibles, essent l'anomenat bash el més usat, i el que tractem a continuació no des de el punt de vista de l'usuari habitual, més acostumat a treballar amb entorns gràfics d'escriptori,si no de l'usuari avançat i dels administradors de sistemes.
Execució de comandaments múltiples amb connectors
Podem unir comandament en una única línia usant el ;com a connector:
root@jordi-sve1513c5e:/media/jordi# who; date
jordi tty1 2021-03-06 12:29 (:0)
jordi pts/0 2021-03-06 12:29 (:0)
jordi pts/1 2021-03-06 13:15 (:0)
dissabte, 6 de març de 2021, 19:50:09 CET
Els espais en blanc abans o després del ; són indiferents. L'operador barra vertical | serveix per connectar el resultat d'un comandament amb l'entrada de dades d'un altre comandament, com ara:
root@jordi-sve1513c5e:/media/jordi# who | wc
3 15 132
wc ha comptat el nombre de línies, paraules i lletres del resultat de who. Ara bé, què passa si li enviem dos comandaments a wc?
root@jordi-sve1513c5e:/media/jordi# who; date | wc
jordi tty1 2021-03-06 12:29 (:0)
jordi pts/0 2021-03-06 12:29 (:0)
jordi pts/1 2021-03-06 13:15 (:0)
1 8 43
Veiem que wc només té en compte el darrer comandament dels dos que hem executat; això és per que el shell tracta a ; i | com a operadors, i el darrer operador té més precedència que el primer. Podem modificar aquest comportament usant parèntesi:
(who; date) | wc
4 23 175
Ara sí, el resultat con junt dels dos comandaments es passa a wc. En aquestes interconnexions ens pot interessar guardar resultats intermitjos en un fitxer, ho podem fer amb tee:
root@jordi-sve1513c5e:/media/jordi# (who; date) | tee qui_data | wc
4 23 175
root@jordi-sve1513c5e:/media/jordi# cat qui_data
jordi tty1 2021-03-06 12:29 (:0)
jordi pts/0 2021-03-06 12:29 (:0)
jordi pts/1 2021-03-06 13:15 (:0)
dissabte, 6 de març de 2021, 19:59:28 CET
Un altre operador de connexió de comandaments és l'ampersand & que uneix comandaments en una única línia com el ; però amb la diferència de que no espera que un acabi per llençar el següent: s'executen els comandaments en paral·lel. Una forma fàcil de provar-ho és amb el comandament sleep, que és un temporitzador que espera els segon que li diem:
sleep 5; vi
esperarà cinc segons i després executar l'editor del sistema; en canvi
sleep 5 & vi
deixarà al bash cinc segons en espera però l'editor vi s'obrirà immediatament. Un altre exemple:
root@jordi-sve1513c5e:/media/jordi# (sleep 5; date) & date
[1] 15926
dissabte, 6 de març de 2021, 20:09:48 CET
[1] Done ( sleep 5; date )
root@jordi-sve1513c5e:/media/jordi# dissabte, 6 de març de 2021, 20:09:53 CET
El primer agrupament entre () s'executa simultàniament amb el segon date, aquest darrer es mostra de seguida en pantalla, però el primer ha d'esperar els 5 segons marcats en sleep. Fixem-nos que el procés que espera queda en segon pla (en background) amb el número de procés 15926, és com si el procés quedes en espera, es pot comprovar que està en segon pla usant jobs:
root@jordi-sve1513c5e:/media/jordi# jobs
[1]+ Running ( sleep 20; date ) &
Per aquest motiu un altre ús de l'operador & és enviar un procés a 2n pla, com ara:
wget http://prdownloads.sourceforge.net/lam/ldap-account-manager_6.8-1_all &
[1] 16494
En el cas de connexions amb | els comandament connectats es consideren un únic procés encadenat, i si afegim el & afecta a tot plegat:
Metacaràcters dels shell
Els "metacaràcters" són caràcters amb significat especial, com ara * que el shell interpreta com "qualsevol caràcter":
jordi@jordi-sve1513c5e:~$ ls d*
dashboard-cluster-role-binding.ym dashboard-cluster-role-binding.yml dashboard-service-account.yml data.txt
Si volem usar un metacaràcter com a caràcter normal el posem entre cometes simples, o bé el precedim per la barra invertida \ : Així, ls * mostrarà tots els fitxers del directori actual, mentre que 'ls *' només mostrarà el fitxer anomenat * si existeix. Què creieu que mostrarà ls '*'* ? I què mostrarà ls \** ?
Alguns metacaràcters interessants:
> >> < >> | Redireccions d’entrada i sortida estàndar
|
; & | |
Connectors de comandaments en línia
|
* ? |
Caràcters «comodí»
|
$ |
Prefix de les variables d’entorn
|
\
|
Prefix per anul·lar un metacaràcter com a tal |
‘ ‘ |
No interpretat el contingut
|
&& |
Connector: executar un comandament, si acaba bé, executar el següent |
|| |
Connector: executar un comandament, si no acaba bé, executar el següent
|
Alguns exemples:
jordi@jordi-sve1513c5e:~$ (ls -d Baixades) && (ls -d samba)
Baixades
samba --> Baixades existeix, es motra també samba
jordi@jordi-sve1513c5e:~$ (ls -d Downloads) && (ls -d samba)
ls: cannot access 'Downloads': No such file or directory
--> Downloads no existeix, el comandament dóna error, no es mostra samba
jordi@jordi-sve1513c5e:~$ (ls -d Downloads) || (ls -d samba)
ls: cannot access 'Downloads': No such file or directory
samba
--> Downloads no existeix, el comandament dóna error, sí es mostra samba
Variables d'entorn i variables del shell
Tot sistema operatiu manté una llista de variables anomenades "d'entorn" que són útils per recuperar informació de configuració de l'entorn de l'usuari; en el shell aquestes variables estan en majúscules i poden accedir-se amb el prefix $, exemples:
jordi@jordi-sve1513c5e:~$ echo $HOME
/home/jordi
jordi@jordi-sve1513c5e:~$ echo $USER
jordi
jordi@jordi-sve1513c5e:~$ echo $PATH
/home/jordi/.local/bin:/home/jordi/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games
:/usr/local/games:/snap/bin
També es poden crear variables d'usuari, que poden ser en minúscules, o no, però el shell les distingirà:
jordi@jordi-sve1513c5e:~$ x=10
jordi@jordi-sve1513c5e:~$ X=100
jordi@jordi-sve1513c5e:~$ echo $x,$X
10,100
Per assignar valor no es posa el prefix $, però per accedir a la variable ja creada, llavors sí.
Les cometes invertides es poden usar per deixar en una variable el resultat d'un comandament:
pwd
/home/jordi/snap/docker
carpeta=`pwd`
echo $carpeta
/home/jordi/snap/docker
Una forma alternativa és usant el prefix $ i tancat el comandament entre ():
root@jordi-sve1513c5e:/home/jordi# carpeta=$(pwd)
root@jordi-sve1513c5e:/home/jordi# echo $carpeta
Si canviem el valor d'una variable d'entorn del sistema, només tindrà validesa en la sessió actual, quan iniciem una altre, el sistema carrega de nou el valor anterior; es pot fer un canvi permanent canviant els valors en el fitxer .profile de l'usuari. D'altre banda quan executem un comandament dins d'un fitxer de text (és a dir, un shell script), el shell obre una còpia de sí mateix (un subshell) on s'executa l'script, de forma que els canvis en les variables només afecten al subshell. Per veure-ho estudiem el següent exemple:
Aquestes línies estan en el fitxer variables.sh que té drets d'execució. Fixeu-vos en l'ús que fa de les cometes simples, dobles i invertides: quan són simples vol dir que el contingut entrecomillats es rpen tal qual, literalment, quan són dobles vol dir que el shell interpreta el contingut entrecomillat. Mostrem ara la variable $HOME i executem l'script:
echo $HOME
/home
./variables.sh
\home\jordi
echo $HOME
/home
Fixeu-vos que mentre s'executa l'script el contingut de $HOME ha variat, però en acabar l'execució, es recupera el valor inicial. Si volem que l'script no s'executi en un subshell si no en el shell actual, ho podem indicar usant un punt i un espai abans del nom de l'script:
. ./variables.sh
\home\jordi
echo $HOME
\home\jordi
Ara sí que l'script modifica la variable del shell. Observem que usem el punt dues vegades, amb significats diferents:
- el primer punt, separat per espai, indica executar l'script en el mateix shell i no en un subshell
- el segon punt, que té a continuació la barra, indica al shell que l'script està situat en el directori actual
El segon punt-barra és prescindible si l'script està situat en una carpeta llistada en la variable PATH; si afegim aquesta carpeta, per exemple així:
PATH=$PATH:`pwd`
Llavors ja no ens cal el punt-barra:
. variables.sh
\home\jordi
L'altre manera de comunicar valors de variables a subshells és usant el comandament export. Estudiem el següent exemple:
root@jordi-sve1513c5e:/home# x=100
root@jordi-sve1513c5e:/home# echo $x
100
root@jordi-sve1513c5e:/home# bash
root@jordi-sve1513c5e:/home# echo $x
root@jordi-sve1513c5e:/home# exit
exit
root@jordi-sve1513c5e:/home# echo $x
100
Veiem que quan executem un shell dintre de l'actual, la variable x no es reconeix en el subshell; en canvi si la "exportem" llavors sí:
root@jordi-sve1513c5e:/home# x=100
root@jordi-sve1513c5e:/home# export x
root@jordi-sve1513c5e:/home# bash
root@jordi-sve1513c5e:/home# echo $x
100
Entrades, sortides i errors estàndard
Tot programa té associat pel sistema operatiu tres fitxers especials: la entrada estàndard, la sortida estàndard i la sortida d'error estàndard, amb uns números associats, anomenats descriptors de fitxers, que són el 0, 1 i 2 respectivament. Per defecte el 0 s'assigna al teclat i els 1 i 2 a la pantalla, però tots es poden redigir.
La redirecció de la sortida d'error estàndard pot ser útil en encadenaments de comandaments on algun de intermig dóna missatges d'error que no es interessen:
root@jordi-sve1513c5e:/home/jordi# ls -l | find "samb" | wc
find: ‘samb’: No such file or directory
0 0 0
Si redigirim l'error ja no el veiem per pantalla:
root@jordi-sve1513c5e:/home/jordi# ls -l | find "samb" 2>/dev/null | wc
0 0 0
També pot passar al revés, que en un encadenament de comandaments algun de entre mig no mostri cap error. Un altre ús seria redirigir els errors a un fitxer de log, per exemple:
find trash 2>> error.log
Qualsevol missatge d'error s'escriu en el fitxer error.log, com usem l'operador >> s'afegirà a l final del contingut del fitxer.
Iteracions de comandaments
El shell bash és pràcticament un llenguatge de programació, amb comandaments i variables, i també estructures iteratives i condicionals. Repetir una acció sobre un conjunt de fitxers és molt usual en administració de sistemes, i ho podem fer amb la sentencia for.
for x in *
> do
> echo $x
> done
La x és la variable, que pren tots els noms de fitxers del directori actual (recordem que el metacaràcter * significa això), el for recorre tots aquests noms i els mostra amb echo.
Com a exemple més pràctic suposem que volem comparar uns fitxers amb la seva còpia de seguretat, per saber si han hagut canvis; el comandament diff compara fitxers línia a línia, si són iguals no diu res, i si són diferents indica la línia on està la diferencia
diff error.log backup/error.log no diu res: iguals
echo "hola" >> backup/error.log modifiquem un fitxer
diff error.log backup/error.log
1a2 en la fila 1
> hola
Suposem que volem comparar els fitxers anomenat error1.log, error2.log, etc amb les seves còpies en el directori backup, podem fer-ho així:
fitxers=$(ls error*.log) generem la llista de fitxers
for x in $fitxers recorrem la llista
do
diff $x backup/$x comparem un a un
done
Si resulten tots iguals l'script no genera cap sortida per pantalla, altrament mostrarà les diferencies. Potser només ens interessa saber quants fitxers han canviat, sense mostrar res més. Sabem que diff mostra la línia on hi ha diferencia, i a continuació, un ">" seguit de la diferencia
jordi@jordi-sve1513c5e:~/Documents$ ./diff.sh
1a2
> hola
Per tant si encadenem l'script amb una cerca del caràcter ">", per exemple amb grep, obtindrem només les diferències:
./diff.sh | grep ">"
> hola
Per aquest motiu al comandament grep se l'anomena un filtre, doncs filtra la sortida d'un programa; hi han diversos comandaments del bash que funcionen com a filtres, alguns fent un processament programat sofisticat usant les anomenades expressions regulars. Si tornem a encadenar-ho amb el comandament wc amb el paràmetre -l (comptar només línies) obtenim un comptador de diferencies trobades en els fitxers
./diff.sh | grep ">" | wc -l
1
En realitat podem incloure l'encadenament dintre del text de l'script:
fitxers=$(ls error*.log)
for x in $fitxers
do
diff $x backup/$x
done | grep ">" | wc -l
El Bash també disposa de les repeticions while
read a
while [ $a -gt 0 ]
do
echo "més gran que zero"
read a
done
Execució condicional
El comandament if del bash selecciona si cal executar o no comandaments depenent d'una condició; tenim vàries possibilitats.
1. La condició és que un comandament no doni error. Per exemple el comandament ls lmv* el podem incloure'l com a condició en el if:
if $(ls lmv*); then
si dóna error,
ls lmv*
ls: cannot access 'lmv*': No such file or directory
llavors NO és compleix la condició del if:
if $(ls lmv*); then echo "trobats"; else echo "cap valor"; fi
ls: cannot access 'lmv*': No such file or directory
cap valor
Com el missatge d'error no ens interessa, el redirigim:
if $(ls lmv* 2>/dev/null); then echo "trobats"; else echo "cap valor"; fi
cap valor
Un cas especial és comprovar si un fitxer o carpeta existeix amb el comandament test:
if test -f pepe.txt; then echo "existeix"; else echo "no hi és"; fi
no hi és
2. La condició compara uns valors amb altres. Per exemple per comparar dues variables numèriques usem el comparador -eq
x=1;y=2
if [ $x -eq $y ]; then echo "iguals"; else echo "diferents";fi
diferents
si les variables contenen text usem els operador "=":
x="hola";y="adeu"
if [ $x = $y ]; then echo "iguals"; else echo "diferents";fi
diferents
3. La condició comprova alguna cosa. Com ara que existeixi un fitxer usant l'operador -e
if [ -e pepe.txt ]; then echo "trobat"; fi
O el contrari, que no existeixi, usant l'operador de negació !
if [ ! -e pepe.txt ]; then echo "no trobat"; fi
Amb l'operador -d verifica si existeix una carpeta. L'operador negació es pot aplicar a qualsevol condició, com ara
if [ ! $x -eq $y ]; then echo "diferents"; else echo "iguals";fi
Per comparacions numèriques tenim també els operadors -gt (més gran que) i -lt (més petit que).
Podem incloure sentències if dins d'altres if, i també usar la condició case; veure per exemple https://ryanstutorials.net/bash-scripting-tutorial/bash-if-statements.php
Exercicis
Exercici 1. La majoria d'scripts del sistema estan en el directori /usr/bin. D'altre banda, el
comandament file mostra el tipus de fitxer, com ara:
file /usr/bin/bash
/usr/bin/bash: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linu
x-x86-64.so.2, BuildID[sha1]=a6cb40078351e05121d46daa768e271846d5cc54, for GNU/Linux 3.2.0, stripped
Escriviu un script llistascript.sh que mostri tots els scripts situats en /usr/bin/
$ ./llistascript.sh
/usr/bin/7z: POSIX shell script, ASCII text executable
/usr/bin/7za: POSIX shell script, ASCII text executable
/usr/bin/7zr: POSIX shell script, ASCII text executable
Ajuda: useu els comandaments ls, grep i file aquest darrer ens dirà de quin tipus és cada fitxer:
file /usr/bin/xzmore
/usr/bin/xzmore: POSIX shell script, ASCII text executable
Podeu usar un for per recórrer tots els fitxers del
directori i per cada un executar el comandament file, comprovant que és un script.
Exercici 2. Molts dels executables de /usr/bin en realitat són vincles simbòlics a altres fitxers; escriviu un altre script que a més a més de llistar els scripts, mostri aquells que són vincles:
$ ./script2.sh
/usr/bin/7z: POSIX shell script, ASCII text executable
/usr/bin/7za: POSIX shell script, ASCII text executable
/usr/bin/7zr: POSIX shell script, ASCII text executable
/usr/bin/add-apt-repository: Python script, ASCII text executable
/usr/bin/addr2line: symbolic link to x86_64-linux-gnu-addr2line
/usr/bin/addr2line: symbolic link to x86_64-linux-gnu-addr2line