IFS, while read, for, if, expresii regulate, metacaractere, globbing, grep, prelucrare de date, automatizarePentru rularea demo-urilor de mai jos folosim mașina virtuală USO Demo. Mașina virtuală (în format OVA) poate fi importată în VirtualBox. Comenzile le vom rula în cadrul mașinii virtuale.
Mașina virtuală deține două interfețe de rețea:
eth0 pentru accesul la Internet (interfață de tipul NAT)eth1 pentru comunicarea cu sistemul fizic (gazdă, host) (interfață de tipul Host-only Adapter)Pentru a rula demo-ul avem două opțiuni:
eth1 a mașinii virtuale și ne conectăm prin SSH, de pe sistemul fizic, folosind comandassh student@<adresa-IP-vm-eth1>
unde <adresa-IP-vm-eth1> este adresa IP a interfeței eth1 din cadrul mașinii virtuale.
Pentru conectarea la mașina virtuală folosim numele de utilizator student cu parola student. Contul student are permsiuni de sudo. Folosind comanda
sudo su -
obținem permisiuni privilegiate (de root) în shell.
eth1 atunci folosim comanda
sudo dhclient eth1
pentru a obține o adresă IP.
Pentru parcurgerea demo-urilor, folosim arhiva aferentă. Descărcăm arhiva în mașina virtuală (sau în orice alt mediu Linux) folosind comanda
student@uso-demo:~$ wget http://elf.cs.pub.ro/uso/res/cursuri/curs-10/curs-10-demo.zip [...]
și apoi dezarhivăm arhiva
student@uso-demo:~$ unzip curs-10-demo.zip [...]
și accesăm directorul rezultat în urma dezarhivării
student@uso-demo:~$ cd curs-10-demo/ student@uso-demo:~/curs-10-demo$ ls user-results.csv
Acum putem parcurge secțiunile cu demo-uri de mai jos. Vom folosi resursele din directorul rezultat în urma dezarhivării.
Pentru splitting simplu de date tabelare putem folosi utilitarul cut. Acesta permite precizarea unui delimitator și a unui câmp sau a mai multor câmpuri (coloane) care să fie selectate din tabel.
Din fișierul user-results.csv (format CSV – Comma Separated Values) dorim să selectăm doar numele grupurilor. Pentru aceasta selectăm doar a doua coloană și folosim separatorul , (virgulă) folosind comanda
student@uso-demo:~/curs-11-demo$ cut -d ',' -f 2 < user-results.csv Liceul Teoretic Ștefan Odobleja Colegiul Național Ion Maiorescu Colegiul Tehnic Toma Socolescu Colegiul Național Nichita Stănescu Liceul Teoretic Benjamin Franklin Colegiul Național I.L. Caragiale Colegiul Național Zinca Golescu [...]
Dacă dorim să “unicizăm” rezultatele și să afișăm doar numele liceelor putem conecta comanda de mai sus la o comandă sort:
student@uso-demo:~/curs-11-demo$ cut -d ',' -f 2 < user-results.csv | sort -u Colegiul Economic Virgil Madgearu Colegiul Național Alexandru Odobescu Colegiul Național Barbu Știrbei Colegiul Național Cantemir Vodă Colegiul Național Carol I Colegiul Național Gheorghe Lazăr [...]
În urma rulării comenzii de mai sus ne sunt afișate doar numele liceelor.
Dacă ne interesează să afișăm doar identificatorii utilizatorilor și punctajele obținute, atunci folosim comanda
student@uso-demo:~/curs-11-demo$ cut -d ',' -f 1,4 < user-results.csv | head ionut.asimionesei,0 laura.matei,66 alin.dascalu,285 dragos.konnerth,42 alexandru.corneanu,247 alexandru.tittes,154 [...]
cut are două opțiuni frecvent folosite:
-d care precizează delimitatorul de câmpuri (field delimiter sau field separator)-f care precizează ce câmpuri/coloane dorim să extragem
Utilitarul cut are dezavantajul că face doar splitting și extrage câmpuri/coloane. Nu putem condiționa extragerea unor câmpuri. De exemplu, dacă dorim extragerea conturilor care au punctaj mai mare ca 500, nu vom putea folosi cut. Putem însă folosi construcția while read într-un script shell.
Pentru a extrage conturile care au punctaj mai mare ca 500, vom folosi scriptul extract-points-500 de mai jos:
#!/bin/bash IFS=',' while read uid school date points; do if test "$points" -ge 500; then echo "$uid" fi done < user-results.csv
Rulăm scriptul folosind comanda
student@uso-demo:~/curs-11-demo$ ./extract-points-500 mihaela.croitoru andreea.cismas elvis.titirca mihaela.serbana anjie.teodorescu [...]
În scriptul extract-points-500 am folosit construcția while read pentru a face split la cele patru coloane din fișierul user-results.csv. Separatorul (delimitatorul) l-am definit cu ajutorul variabilei IFS (Input Field Separator) pe care am inițializat-o la , (virgulă). După split am folosit construcția if pentru a afișa doar conturile utilizatorilor cu punctaj peste 500.
Dacă dorim să afișăm și punctajul obținut (nu doar contul) atunci trebuie doar să modificăm linia de afișare (care folosește comanda echo). Rezultatul va fi scriptul actualizat și cu rularea de mai jos:
student@uso-demo:~/curs-11-demo$ cat extract-points-500
#!/bin/bash
IFS=','
while read uid school date points; do
if test "$points" -ge 500; then
echo "$uid,$points"
fi
done < user-results.csv
student@uso-demo:~/curs-11-demo$ ./extract-points-500
mihaela.croitoru,516
andreea.cismas,803
elvis.titirca,501
mihaela.serbana,526
anjie.teodorescu,666
georgiana.ciobanica,1047
[...]
Dacă în output-ul de mai sus dorim să avem sortare în ordine descrescătoare a punctajului, înlănțuim o comandă sort care să sorteze numeric, descrescător după a doua coloană
student@uso-demo:~/curs-11-demo$ ./extract-points-500 | sort -t ',' -k 2,2rn radu.dumitru5227,21433 mihaela.catai,13623 stefania.oprea,9547 alexandra.calinescu,5266 george.ungureanu,3846 dragos.totu,2040 monica.cirisanu,1815
În comanda de mai sus, comanda sort sortează output-ul scriptului folosind ca separator virgulă (construcția -t ',' după a două coloană (construcția -k 2,2) descrescător numeric (construcția rn).
while read este folosită pentru a putea face prelucrări pe fiecare linie procesată, nu doar splitting, așa cum face comanda cut.
Construcția while read este utilă pentru realizarea de splitting și de prelucări minimale. Prelucrările pe care le poate face țin de facilitățile pe care le oferă shell-ul.
Pentru prelucrări mai avansate recomandăm folosirea utilitarului awk. Utilitarul awk are în spate un limbaj propriu, asemănător limbajului C, și are suport de expresii regulate. Este un utilitar puternic util pentru prelucrarea datelor în format text.
Pentru a obține același efect cu al scriptului extract-points-500 putem folosi oneliner-ul de mai jos:
student@uso-demo:~/curs-11-demo$ awk -F ',' '{ if ($4 >= 500) print $1;}' < user-results.csv
mihaela.croitoru
andreea.cismas
elvis.titirca
mihaela.serbana
anjie.teodorescu
georgiana.ciobanica
[...]
Pentru a afișa și punctajul folosim one liner-ul:
student@uso-demo:~/curs-11-demo$ awk -F ',' '{ if ($4 >= 500) print $1 "," $4;}' < user-results.csv
mihaela.croitoru,516
andreea.cismas,803
elvis.titirca,501
mihaela.serbana,526
anjie.teodorescu,666
georgiana.ciobanica,104
[...]
Observăm că pentru awk separatorul este dat de opțiunea -F, iar sintaxa este similară cu cea a limbajului C. Un câmp/coloană este indicat de construcția $N unde N este indexul câmpului; în cazul nostru am folosit $1 și $4 pentru cont și punctaj, respectiv.
După cum am precizat, awk are suport de expresii regulate. Dacă, de exemplu, din output-ul de mai sus dorim să extragem doar liniile care au un nume de cont al cărui nume de familie începe cu litera c, vom folosi construcția:
student@uso-demo:~/curs-11-demo$ awk -F ',' '$1 ~ /[^\.]+\.c/ { if ($4 >= 500) print $1 "," $4;}' < user-results.csv
mihaela.croitoru,516
andreea.cismas,803
georgiana.ciobanica,1047
ion.camasa,502
mihaela.catai,13623
alexandra.cismaru,860
[...]
În one liner-ul de mai sus, am selectat doar acele linii pentru care primul câmp ($1) face match pe expresia regulată [^\.]+\.c (adică numele de familie începe cu litera c).
/[^\.]+\.c/.
Același rezultat ca mai sus putea fi realizat și cu ajutorul comenzii grep legată de la comanda awk, ca mai jos:
student@uso-demo:~/curs-11-demo$ awk -F ',' '{ if ($4 >= 500) print $1 "," $4;}' < user-results.csv | grep '^[^\.]\+\.c'
mihaela.croitoru,516
andreea.cismas,803
georgiana.ciobanica,1047
ion.camasa,502
mihaela.catai,13623
alexandra.cismaru,860
[...]
Rezultatul este același și poate părea mai simplu să folosim grep. Doar că awk permite match cu expresie regulată pe un câmp specific primit la intrare; se poate forța acest lucru și cu grep dar devine mai puțin clar.
În momentul în care un script awk devine mai complicat, poate fi plasat într-un script shell, așa cum este în fișierul extract-points-500-awk:
#!/bin/bash awk -F ',' ' $1 ~ /[^\.]+\.c/ { if ($4 >= 500) print $1 "," $4; } ' < user-results.csv
Rulăm scriptul folosind comanda
student@uso-demo:~/curs-11-demo$ ./extract-points-500-awk mihaela.croitoru,516 andreea.cismas,803 georgiana.ciobanica,1047 [...]
Scriptul extract-points-500-awk face același lucru ca one liner-ul anterior doar că este mai lizibil.
Pentru prelucrări complexe sau pentru integrarea prelucrărilor cu alte componente ale unei aplicații, inclusiv awk poate fi insuficient. În acest caz programatorul va apela la un limbaj specific precum Python, Perl, Ruby, Lua, Java, JavaScript sau altul.
În fișierul extract-points-500.py de mai jos avem implementarea în Python a aceleiași funcționalități ca mai sus pentru awk
#!/usr/bin/env python import sys import re def main(): for line in open("user-results.csv", "rt"): line = line.rstrip("\n") uid, school, date, points = line.split(",") if re.match("[^\.]+\.c", uid): if int(points) >= 500: print "%s,%s" % (uid,points) if __name__ == "__main__": sys.exit(main())
În cadrul scriptului citim linie cu linie conținutul fișierului user-results.csv și apoi este splitted și se extrag liniile pentru care numele contului are un nume de familie care începe cu litere c și punctajul este mai mare ca 500.
Rularea scriptului Python conduce la același rezultat ca în cazul folosirii awk:
student@uso-demo:~/curs-11-demo$ ./extract-points-500.py mihaela.croitoru,516 andreea.cismas,803 georgiana.ciobanica,1047 ion.camasa,502 mihaela.catai,13623 alexandra.cismaru,860 [...]
Python oferă o flexibilitate superioară awk cu dezavantajul unei complexități mai mari a codului. Pentru acțiuni rapide, integrabile cu shell scripting, awk este o bună alegere (sau while read); pentru acțiuni mai complexe, un limbaj de programare dedicat, precum Python poate fi o soluție.
De exemplu, putem augmenta scriptul anterior pentru a afișa doar informații despre acele conturi care s-au autentificat prima oară în luna aprilie. Adică al treilea câmp este din luna aprilie. Pentru aceasta folosim scriptul extract-points-500-date.py din director:
#!/usr/bin/env python import sys import re from datetime import datetime def main(): for line in open("user-results.csv", "rt"): line = line.rstrip("\n") uid, school, date, points = line.split(",") date_compare = datetime.strptime("2015-04-01 00:00:00", "%Y-%m-%d %H:%M:%S") try: date_in_format = datetime.strptime(date, "%Y-%m-%d %H:%M:%S") except: continue if re.match("[^\.]+\.c", uid): if int(points) >= 500: if date_in_format > date_compare: print "%s,%s,%s" % (uid,date,points) if __name__ == "__main__": sys.exit(main())
În scriptul de mai sus am comparat data (al treilea câmp) cu data de 1 aprilie 2015 și am afișat acele câmpuri pentru care câmpul dată era mai târziu de 1 aprilie 2015. Acest lucru ar fi putut fi realizat în awk dar ar fi complicat scriptul.
Pentru rularea scriptului, folosim construcția
student@uso-demo:~/curs-11-demo$ ./extract-points-500-date.py mihaela.catai,2015-04-16 12:29:26,13623 andra.cristiev,2015-04-22 13:08:57,865
cut sau while read sau awk sau Python ține atât de datele și complexitatea problemei cât și de experiența utilizatorului/dezvoltatorului. În general, recomandăm “use the best tool for the best job”; dacă un utilitar simplu poate face ce aveți nevoie, folosiți-l pe acela, chiar dacă un utilitar mai complex are mai multe caracteristici; dacă acele caracteristici nu sunt utile, nu are sens să-l folosiți.
Mai sus am folosit cu awk, grep și Python expresii regulate. Adesea, în linia de comandă veți folosi expresii regulate folosind comanda grep. Dar și în alte situații, fie în linie de comandă, fie în scripting, fie în limbaje de programare, expresiile regulate sunt utile.
În particular, utilitarul sed folosește expresii regulate. Unul dintre cazurile frecvente de utilizare a sed este pe post de editor neinteractiv, care să înlocuiască (substituie) elemente de pe o linie cu alte elemente.
Dacă dorim, de exemplu, să înlocuim toate prenumele din numele de cont nu șirul aaa vom folosi un one liner precum cel de mai jos:
student@uso-demo:~/curs-11-demo$ sed 's/^[^\.]\+/aaa/g' < user-results.csv aaa.asimionesei,Liceul Teoretic Ștefan Odobleja,2015-03-31 16:18:01,0 aaa.matei,Colegiul Național Ion Maiorescu,2015-03-17 11:04:49,66 aaa.dascalu,Colegiul Tehnic Toma Socolescu,None,285 aaa.konnerth,Colegiul Național Nichita Stănescu,None,42 aaa.corneanu,Liceul Teoretic Benjamin Franklin,2015-03-18 10:26:21,247 [...]
La fel ca în cazul awk expresia regulată se plasează între slash-uri. Expresia regulată folosită ^[^\.]\+ face match pe începutul de linie și pe șiruri de cel puțin o literă care nu conțin punct (., dot). Adică exact pe prenumele din numele de cont.
Dacă dorim să eliminăm prenumele (împreună cu punctul) din numele de cont vom folosi o construcție precum
student@uso-demo:~/curs-11-demo$ sed 's/^[^\.]\+\.//g' < user-results.csv asimionesei,Liceul Teoretic Ștefan Odobleja,2015-03-31 16:18:01,0 matei,Colegiul Național Ion Maiorescu,2015-03-17 11:04:49,66 dascalu,Colegiul Tehnic Toma Socolescu,None,285 konnerth,Colegiul Național Nichita Stănescu,None,42 corneanu,Liceul Teoretic Benjamin Franklin,2015-03-18 10:26:21,247 [...]
În one liner-ul de mai sus am lăsat ca șir care substituie șirul vid (adică două slash-uri consecutive). Adică înlocuim partea pe care face match expresia regulată (adică prenumele din cont și caracterul punct) cu nimic, adică o ștergem.
O situație poate fi să fie înlocuită construcția prenume.nume cu nume.prenume în numele contului. Pentru aceasta folosim construcția
student@uso-demo:~/curs-11-demo$ sed 's/^\([^\.]\+\)\.\([^\,]\+\),/\2.\1,/g' < user-results.csv asimionesei.ionut,Liceul Teoretic Ștefan Odobleja,2015-03-31 16:18:01,0 matei.laura,Colegiul Național Ion Maiorescu,2015-03-17 11:04:49,66 dascalu.alin,Colegiul Tehnic Toma Socolescu,None,285 konnerth.dragos,Colegiul Național Nichita Stănescu,None,42 corneanu.alexandru,Liceul Teoretic Benjamin Franklin,2015-03-18 10:26:21,247 [...]
Expresia regulată folosită mai sus (^\([^\.]\+\)\.\([^\,]\+\),) o decodificăm în următoarele componente:
^: început de linie\([^\.]\+\): face match pe prenume, adică șirul format din cel puțin un caracter diferit de punct; reține acest șir într-o variabilă (variabila este referită prin construcția \1)\.: face match pe caracterul punct (., dot) care separă prenumele de nume în cont\([^\,]\+\): face match pe nume, adică șirul format din cel puțin un caracter diferit de virgulă; reține acest șir într-o variabilă (variabila este referită prin construcția \2),: face match pe caracterul virgulă (,, comma) care urmează după cont
Partea de înlocuit este \2.\1, adică numele de cont este înlocuit cu nume, urmat de punct, urmat de prenume, urmat de virgulă, așa cum ne-am dorit.
awk, sed este un utilitar adecvat pentru one linere, acțiuni rapide și încorporare în shell scripting. Pentru scenarii de utilizare mai complexe, recomandăm folosirea unor limbaje dedicate precum Python, Perl, Ruby, Java, JavaScript.
Folosirea shell scripting și a one line-erelor are, de principiu, 3 scenarii de utilizare:
cut, while read, tr, grep, sort, awk, sed) se prelucrează date în format textPartea de automatizare este utilă atunci când vrem să executăm o acțiune în mod repetat. Un caz de utilizare este când vrem să prelucrăm în același mod mai multe fișiere.
De exemplu, având în vedere conținutul subdirectorului horde/, vrem să creăm copii de lucru ale fișierelor de configurare de distribuție. Fișierele de distribuție au extensia .dist (de exemplu conf.php.dist); o copie de lucru este un fișier fără extensia .dist (de exemplu: conf.php).
Pentru a crea copie de lucru putem folosi următorul one liner, construit pas cu pas:
student@uso-demo:~/curs-11-demo$ find horde/ -name '*.dist'
horde/ingo/config/hooks.php.dist
horde/ingo/config/prefs.php.dist
[...]
student@uso-demo:~/curs-11-demo$ for f in $(find horde/ -name '*.dist'); do echo "$f"; done
horde/ingo/config/hooks.php.dist
horde/ingo/config/prefs.php.dist
[...]
student@uso-demo:~/curs-11-demo$ for f in $(find horde/ -name '*.dist'); do echo "${f/.dist/}"; done
horde/ingo/config/hooks.php
horde/ingo/config/prefs.php
[...]
student@uso-demo:~/curs-11-demo$ for f in $(find horde/ -name '*.dist'); do cp "$f" "${f/.dist/}"; done
În ultima comandă avem one liner-ul care creează câte o copie a fișierului de distribuție într-un fișier de lucru. Construcția ${f/.dist/} este folosită pentru a înlocui în valoarea variabilei f șirul .dist cu nimic, adică șterge acel șir, rezultând numele fișierului fără extensia .dist.
Pentru verificare folosim comanda find pentru a valida existența fișierelor de lucru:
student@uso-demo:~/curs-11-demo$ find horde/ -name '*.php' horde/ingo/config/prefs.php horde/ingo/config/backends.php [...]
One liner-ul de mai sus este util o singură dată, pentru acele fișiere și de acceea nu are sens să îl trecem într-un script pe care să îl rulăm periodic, la nevoia, ca automatizare.
Un exemplu de script folosit pentru automatizare este scriptul publish-slides folosit pentru publicarea slide-urilor de cursuri de USO. Nu veți putea rula scriptul în absența fișierelor de suport, dar este reprezentantiv pentru ceea ce înseamnă automatizarea unei sarcini repetitive: publicarea slide-urilor cursului de USO intern în cadrul echipei (în Dropbox) și studenților.
#!/bin/bash dropbox_folder=~/Downloads/Dropbox/school/uso-shared remote_end=uso@elf.cs.pub.ro:res/current/cursuri if test $# -eq 1; then id=$(printf "%02g" $1) pushd curs-$id/ > /dev/null 2>&1 make all cp *.pdf "$dropbox_folder"/curs-$id/ scp *handout*.pdf "$remote_end"/curs-$id/ popd > /dev/null 2>&1 exit 0 fi for id in $(seq -f "%02g" 0 13); do pushd curs-$id/ > /dev/null 2>&1 make all cp *.pdf "$dropbox_folder"/curs-$id/ scp *handout*.pdf "$remote_end"/curs-$id/ popd > /dev/null 2>&1 done
Scriptul compilează (folosind make) slide-urile cursului primit ca argument, sau toate slide-urile în absența argumentelor. Folosește cp pentru a copia slide-urile intern echipei, în Dropbox, și folosește scp pentru a publica slide-urile către studenți.