Table of Contents

Laborator 06 - Shell script-uri 1

Suport laborator

Arhivă laborator

Introducere

Secvențierea operațiilor

a) Pentru a scrie, pe două linii, titlul jocului StarCraft 2 se pot folosi două comenzi echo :

student@uso:~$ echo "StarCraft II"
student@uso:~$ echo "Wings of Liberty"

b) Comenzile anterioare se pot rescrie pe un singur rând. Operatorul ; este folosit pentru a secvenția două comenzi.

student@uso:~$ echo "StarCraft II"; echo "Wings of Liberty"

Puteți observa cum shell-ul afișează un nou prompt doar după ce a executat ambele instrucțiuni.

Se pot rula oricâte comenzi într-o secvență, separate prin ;. Orice comenzi pot fi secvențiate în acest fel, nu doar echo. Comenzile dintr-o secvență pot fi diferite, nu trebuie să fie toate de același fel.

Multi-line commands

a) Rulați comanda de mai jos pentru a descărca și dezarhiva fișierele de suport ale laboratorului de astăzi.

Obsevați că fiecare linie se termină cu un caracter \. Acesta va face ca linia următoarea să fie considerată ca făcând parte din comanda curentă.

student@uso:~$ wget http://ocw.cs.pub.ro/courses/_media/uso/laboratoare/lab07.tar.gz;\
echo "Am luat arhiva."; \
mkdir lab07; \
tar zxvf lab07.tar.gz; \
echo "Am dezarhivat arhiva"; \
echo "Continutul arhivei:"; \
ls -l lab07

Un backslash (\) la finalul liniei semnifică faptul că linia se continuă pe rândul următor.

Promptul oferit după trecerea liniei pe rândul următor este >.

Dacă apăsați săgeată sus pentru a relua comanda anterioară, veți vedea că toate comenzile au fost alăturate pentru a fi pe o singură linie.

Scripturi simple

a) De cele mai multe ori se întâmplă ca, după ce se creează o comandă compusă pentru un task, să trebuiască să fie rezolvat acel task încă o dată. Pentru a nu rescrie toată comanda, se poate folosi un fișier în care se salvează toate comenzile componente.

Se pot copia, într-un fișier numit sc.sh, următoarele două linii:

echo "StarCraft II"
echo "Wings of Liberty"

Câteva observații:

b) Pentru a lansa script-ul, se poate folosi comanda de mai jos având ca director curent directorul unde este salvat fișierul sc.sh:

student@uso:~$ bash sc.sh

Linia de mai sus pornește un nou proces bash care nu va citi comenzile de la tastatură, ci din fișierul sc.sh. La finalul fișierului, shell-ul își va încheia execuția – putem spune că există un exit implicit.

Formularea de mai sus pornește un proces nou bash trebuie completată de faptul că, după ce procesul nou este pornit, se așteaptă ca el să se termine. Comenzile listate în fișier nu sunt executate de shell-ul inițial.

Source

O altă modalitate de a rula un script este folosind source.

student@uso:~$ source sc.sh

Alternativ, se poate folosi . ca sinonim pentru source – pentru a scrie mai puțin.

student@uso:~$ . sc.sh

Diferențele între cele două moduri de execuție (source și bash) pot fi văzute într-un exercițiu ulterior.

Executabil

a) O altă modalitate de a rula un script este aceea de a îl face executabil și de a îl rula ca un executabil oarecare.

Se pot adăuga drepturi de execuție scriptului sc.sh și apoi scriptul se poate rula cu:

student@uso:~$ ./sc.sh

Amintiți-vă ce făcea chmod.

Shebang

a) Se pot scrie fișiere script și în alte limbaje, nu doar în bash. Python, Perl, Ruby sunt câteva limbaje frecvent folosite.

Pentru a putea diferenția între limbajele din cadrul scripturilor va trebui invocat interpretorul corespunzător. De exemplu, pentru un script Python va trebui să rulăm python script.

Dacă vrem să rulăm orice script sub forma ./script, prima linie a fișierului va avea un format special: va conține șirul #! urmat de executabilul ce trebuie invocat pentru a rula scriptul. În acest caz, prima linie se numește linia shebang (sau sha-bang).

Pentru bash, linia ar arăta:

#! /bin/bash

Adăugați linia shebang scriptului sc.sh și rulați-l cu ./sc.sh.

Pentru fișierele script bash nu este obligatorie linia shebang. Se recomandă totuși s-o puneți pentru că scriptul ar putea fi folosit și în alte interpretoare de comenzi (dash, zsh, csh).

Așa cum nu este nici o diferența între . și source, la fel nu este nici o diferență între bash script.sh și ./script.sh dacă script.sh începe cu #! /bin/bash.

Finalizarea execuției unui proces

Un proces își poate termina execuția cu succes (status 0) sau cu eroare (status diferit de 0). Într-un terminal statusul cu care s-a încheiat ultimul proces lansat se poate vedea cu ajutorul comenzii

echo $?

true și false sunt două programe care întotdeauna se termină cu succes, respectiv cu eroare:

student@uso:~$ true
student@uso:~$ echo $?
0
student@uso:~$ false
student@uso:~$ echo $?
1
student@uso:~$ true && echo "Success"
Success
student@uso:~$ false && echo "Success"
 
student@uso:~$ true || echo "Fail"
 
student@uso:~$ false || echo "Fail"
Fail

Variabile Bash

bash este, de fapt, un limbaj de programare; doar că până acum l-am folosit interactiv. Ca orice limbaj de programare, bash are suport de variabile.

Pentru a defini o variabilă folosim sintaxa NUME_VARIABILA=VALOARE.

Nu lăsați niciun spațiu în jurul semnului =. Este o restricție sintactică bash.

Exemplu de utilizare a variabielor:

student@uso:~$ sc_version=2
student@uso:~$ sc_title="Wings of Liberty"
student@uso:~$ echo StarCraft $sc_version
StarCraft 2
student@uso:~$ echo $sc_title
Wings of Liberty

  • Observați faptul că inițializarea variabilei se face folosind numele ei, dar folosirea necesită prefixarea cu simbolul $.
  • Dacă valoarea pe care vreți să o atribuiți varabilei conține spații, folosiți ghilimele în jurul întregii valori pentru a proteja acele spații.

Undefined vars

Ce se întâmplă dacă vrem să folosim o variabilă nedefinită?

$ echo $undefined_var
 

Bash nu consideră o eroare faptul că o variabilă nu este definită. O va înlocui cu șirul vid.

Quoting

Vreți să afișați un tabel cu eroii StarCraft 2. Pentru a fi mai simplu de modificat, scrieți un shell script numit heroes.sh. Definiți, în script, câte o variabilă pentru fiecare facțiune.

Caracterul # marchează începutul unui comentariu. Comentariul se întinde până la finalul liniei.

$ cat heroes.sh
terran="Terran    Jim Raynor"
zerg="Zerg      Kerrigan"
protoss="Protoss   Artanis"
# Acum afisam cele trei variabile
echo $terran
echo $zerg
echo $protoss
$ bash heroes.sh
Terran Jim Raynor
Zerg Kerrigan
Protoss Artanis

Observați că spațiile în plus nu au niciun efect, drept urmare cuvintele nu sunt aliniate. Pentru a repara acest lucru trebuie să încadrați numele variabilelor între ghilimele:

$ cat heroes.sh
terran="Terran    Jim Raynor"
zerg="Zerg      Kerrigan"
protoss="Protoss   Artanis"
# Acum afisam cele trei variabile
echo "$terran"
echo "$zerg"
echo "$protoss"
$ bash heroes.sh
Terran    Jim Raynor
Zerg      Kerrigan
Protoss   Artanis

Inconveniența este cauzată de faptul că echo afișează fiecare parametru al său, separat printr-un singur spațiu. Puteți vedea în imaginea de mai jos etapele evaluării comenzii echo în cele două cazuri.

Special vars

Până acum ați învățat să rulați un script, folosind construcția bash sau source, în urma căreia scriptul se execută, efectuând anumite acțiuni. Dacă îl rulați încă o dată, aceleași acțiuni s-ar executa identic.

De multe ori am folosit comenzi căror le-am trimis anumiți parametrii specifici comenzii. De exemplu ps -e, sau cat FIŞIER. Asemănător putem rula scriptul nostru adăugând parametrii după numele script-ului:

student@uso:~$ ./script_bash parametru_1 parametru_2

Pentru a putea avea acces la parametrii transmiși unui script, bash ne pune la dispoziție următoarele variabile speciale:

Variabila Semnificație Echivalent C
$# Numărul de parametri transmiși scriptului argc
$@ Parametrii efectivi transmiși scriptului argv
$0 Numele scriptului argv[0]
$1 Primul argument argv[1]
$2 Al doilea argument argv[2]

Alte variabile speciale întâlnite de-a lungul laboratoarelor:

Variabila Semnificație Echivalent C
$$ PID-ul procesului curent -
$! PID-ul ultimului proces lansat in background -
$? Valoarea de exit a ultimei comenzi valoarea cu care se întoarce main

Exerciții

0. Descărcarea arhivei laboratorului

Descărcați arhiva de laborator. Dezarhivați-o folosind tar.

Rezolvare
tar xf lab07.tar.gz

1. Special vars

Vizualizați conținutul scriptului special_vars.sh din arhiva de laborator și apoi rulați-l astfel:

student@uso:~$ ./special_vars.sh 1 2 3 4 a

Identificați valorile parametrilor trimiși script-ului și a celorlalte variabile speciale (cele din Introducere).

2. source vs bash

Rulați următoarele comenzi.

student@uso:~$ race=Zerg
student@uso:~$ bash rush.sh
Rush!!
student@uso:~$ source rush.sh
Zerg Rush!!

Observați că în momentul folosirii source variabilele sunt definite și în interiorul scriptului.

Rulați comenzile următoare și identificați câte procese se crează în momentul folosirii bash și câte în cazul source

student@uso:~$ echo $$; bash special_vars.sh; echo $$
student@uso:~$ echo $$; source special_vars.sh; echo $$

Variabilele definite într-un shell bash sunt locale acelui shell. Rulând scriptul rush.sh cu comanda bash, se crează un alt shell bash ce execută comenzile din script.

source nu pornește proces nou, execută comenzile din script ca și cum ar fi fost introduse de la tastatură.

Fișierul .bashrc este sourced la fiecare lansare a unui proces bash. De aceea, e bine să salvați în el configurările proprii pentru bash: alias-uri, definiții de variabile, valoare umask, etc.

Un fișier similar este .vimrc sourced de fiecare dată când se lansează o nouă instanță de vim.

3. Secvențiere comenzi

Studiați ce se întâmplă dacă încercați să listați conținutul unui director care nu există. Ce valoare de ieșire are ls în acest caz?

Rezolvare
student@uso:~$ ls warcraft
ls: cannot access warcraft: No such file or directory
student@uso:~$ echo $?
2

Scrieți un one-liner care creează directorul warcraft dacă acesta nu există deja. Folosiți-vă de faptul că ls va întoarce non-zero dacă directorul nu există. Ce operator veți folosi: && sau ||?

Rezolvare
student@uso:~$ ls warcraft || mkdir warcraft

4. Filtre de text simple

Până acum știți să afișați conținutul unui fișier utilizând cat. Uneori, dorim să vizualizăm doar începutul sau finalul fișierului sau poate dorim să vizualizăm fișierul de la început spre final. Următorul tabel listează comenzile folosite în aceste cazuri.

Comanda Efect
cat Afișează tot conținutul fișierului în ordine
tac Afișează tot conținutul fișierului inversat
head Afișează doar primele linii din fișier
tail Afișează doar ultimele linii din fișier

Dacă nu primesc nici un argument, aceste comenzi citesc de la standard input ceea ce face posibilă folosirea lor prin înlănțuire cu pipe-uri.

Comenzile head și tail implicit afișează primele, respectiv ultimele 10 linii. Pentru a afișa un număr arbitrar de linii, folosim:

head -n k # primele k linii
tail -n k # ultimele k linii
# de asemenea
head -n -k # toate liniile, mai putin ultimele k
tail -n +k # toate liniile, mai putin primele (k-1)

Încercați să explicați ce face următoarea comandă. Dați o formulare mai simplă pentru ea.

tac special_vars.sh | head | tail -n +3 | head -n -1 | tac | tail -n 1

Puteți consulta paginile de manual pentru head și tail pentru a vedea cum selectați range-ul de afișat.

5. grep

Multe dintre utilitarele disponibile în linia de comandă oferă ca output o cantitate mare de text. Pentru a putea filtra output-ul se pot folosi două utilitare: grep (pentru a selecta numai anumite linii) și cut (pentru a selecta numai anumite secțiuni de pe fiecare linie).

a) Afișați în terminal conținutul fișierului /var/log/syslog. După cum observați, fișierul conține un număr mare de linii.

Rezolvare
student@uso:~$ cat /var/log/syslog

b) grep poate primi ca argumente

grep ȘABLON FIȘIER

Șablonul este reprezentat de o expresie regulată (mai multe detalii în laboratoarele viitoare).

Folosind grep, afișați doar liniile care conțin string-ul “avahi” din fișierul /var/log/syslog

Rezolvare
student@uso:~$ grep avahi /var/log/syslog

c) grep poate primi liniile pe care le filtrează și pe STDIN. Este o metodă de utilizare uzuală, care se folosește de pipe-uri (|). Folosind cat și grep, afișați doar liniile care conțin string-ul “avahi” din fișierul /var/log/syslog

Rezolvare
student@uso:~$ cat /var/log/syslog | grep avahi

6. cut

a) Utilitarul cut primește două argumente:

Șablonul este compus din două elemente: un caracter de delimitare a câmpurilor (specificat prin -d) și id-ul câmpurilor care se afișează (specificat prin -f).

cut -d DELIMITATOR -f LISTĂ_CÂMPURI FIȘIER

Dacă vrem ca pentru fiecare linie din fișierul /var/log/syslog să afișăm numai ora la care a fost înregistrată acea linie, executăm următoarea comandă:

student@uso:~$ cut -d " " -f 3 /var/log/syslog

Identificați parametrii trimiși lui cut.

b) La fel ca grep, cut poate primi liniile pe care le filtrează și pe STDIN.

Afișați toate liniile din fișierul /var/log/syslog care conțin șirul de caractere kernel. Nu uitați să folosiți cat.

Rezolvare
student@uso:~$ cat /var/log/syslog | grep kernel

Pentru fiecare din liniile anterioare afișați numai ora la care a fost înregistrată:

Rezolvare
studente@uso:~$ cat /var/log/syslog | grep kernel | cut -d " " -f 3

7. wc

Dorim să aflăm statistici despre un fișier cu privire la numărul de linii, cuvinte și caractere. Folosim wc.

Aflați câte linii are fișierul special_vars.sh.

Rezolvare
$ wc -l special_vars.sh

8. sort. uniq

Fișierul many_numbers conține numere naturale, câte unul pe fiecare linie. Vrem să aflăm câte numere există în total, numărând fiecare număr o singură dată (altfel spus, ignorând repetițiile).

Pentru asta vom sorta numeric fișierul cu sort și vom elimina duplicatele de pe liniile consecutive folosind uniq. La final, va trebui să numărăm liniile obținute.

Câte numere conține fișierul many_numbers?

Rezolvare
student@uso:~$ sort -n many_numbers | uniq | wc -l

9. Escaping. More quoting

Reveniți la script-ul special_vars.sh. Găsiți o modalitate de a transmite script-ului parametrul “Wings of Liberty” (incluzând ghilimelele). Pentru a include ghilimele într-o expresie care este deja între ghilimele, folosiți escaping: “\”Wings of Liberty\””.

O alternativă este folosirea apostroafelor pentru a delimita șirul:

student@uso:~$ echo '"Wings of Liberty"'
"Wings of Liberty"
student@uso:~$ echo "\"Wings of Liberty\""
"Wings of Liberty"

Apostroafele au, însă, și un efect neașteptat. Identificați-l urmărind pașii următori:

Rezolvare
$ faction=Terran
$ echo $faction
Terran
$ echo '$faction'
$faction

Caracterul $ are semnnificație literală între apostroafe.

10. Expandarea unei comenzi

Simbolul $ este folosit pentru expandarea unei variabile, comenzi sau expresii.

În laboratorul 5 ați întâlnit următoarea comandă:

student@uso:~$ kill -9 $(pidof signal_test)

Se executa comanda pidof signal_test, iar outputul acesteia era trimis ca parametru comenzii kill.

Asemănător putem să atribuim outpul comenzii pidof unei variabile, iar apoi să transmitem variabila ca parametru comenzii kill:

student@uso:~$ pid_signal=$(pidof signal_test)
student@uso:~$ kill -9 $pid_signal

Scrieți un script care, folosind fișierul /etc/passwd:

Ca să nu executăm de două ori comanda cut pentru a obține ultima coloană din fișierul /etc/passwd, putem folosi o variabilă intermediară în care reținem output-ul comenzi.

Hint:

Rezolvare
student@uso:~$ cat script.sh
#! /bin/bash
 
temp=$(cut -f7 -d: /etc/passwd)
 
echo "$temp" | sort | uniq | wc -l
echo "$temp" | grep $1 | wc -l

11. dd

Comanda dd are ca scop principal copierea și conversia fișierelor la nivel de octeți. Deoarece în Linux dispozitivele pot fi accesate prin fișierele din directorul /dev, putem copia partiții întregi sau doar o parte din ele cu acest utilitar.

dd if=FIȘIER_INTRARE of=FIȘIER_IEȘIRE bs=DIMENSIUNE_BLOC count=NUMĂR_BLOCURI

De obicei parametrul bs (byte size) este o putere a lui 2 (ex: 512, 1024, 2048 etc.).

Scrieți un script care construiește un fișier conținând un număr n de octeți de 0, unde n este un multiplu de 1024. Numele fișierului și numărul n scriptul le primește ca parametrii.

./script.sh NUME_FIȘIER NUMĂR_BLOCURI
Rezolvare
student@uso:~$ cat script.sh
#! /bin/bash
 
dd if=/dev/zero of=$1 bs=1024 count=$2

Modificați scriptul să afișeze doar numărul total de octeți. Folosiți filtrele de text învățate din laborator.

dd LISTĂ_PARAMETRII 2> FIȘIER
Rezolvare
student@uso:~$ cat script.sh
#! /bin/bash
 
dd if=/dev/zero of=$1 bs=1024 count=$2 2> ./tmp_file && tail -n 1 ./tmp_file | cut -d ' ' -f 1
rm ./tmp_file

O metoda mai bună de a folosi fișiere temporare este să folosim directorul /tmp. De ce?

12. Readline

Readline este o facilitate care ajută la editarea eficientă a comenzilor pe care le scriem în shell. Vom explora câteva concepte utile.

Combinația Shift-Insert este o scurtătură pentru Paste în terminal. Puteți face copy-paste pe numele de mai sus.

Rezolvare
mkdir supercalifragilisticexpialidocious
rm <Alt-.>
<Ctrl-r> + mkdir
rm -rf <Alt-#>