dilluns, 8 de març del 2021

El shell bash per usuaris avançats i administradors de sistemes

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
































Cap comentari:

Publica un comentari a l'entrada

Gestió d'usuaris i grups en Linux

Usuaris i grups Linux  Els comptes de Linux són com els comptes de Windows o MacOS; però els detalls no, així que cal explicar alguns detall...