Script
Script
Încheiere
Alte surse de informaţie
În acest articol voi discuta despre ``shell'', într-una din înfăţişările pe care le are în
sistemul de operare Unix. Am mai scris articole despre shell în PC Report; le
recomand cititorilor cărora articolul de faţă le stîrneşte interesul să se uite şi peste
cele mai vechi; un articol în PC Report din iunie 1997 discută despre cum este
implementat un shell. Articolul meu din decembrie 1996 discută despre sistemele
de operare şi menţionează în treacăt şi rolul shell-ului. Dacă între timp aţi făcut
curat în bibliotecă şi nu mai aveţi numerele vechi, puteţi găsi on-line articolele
scrise de mine în pagina mea de web.
Chiar dacă 95% din umanitate folose'ste sistemul de operare Windows, de data
aceasta nu mai pot fi acuzat că ignor marea audienţă cu bună intenţie: sistemele
create de Microsoft oferă doar facilităţi rudimentare în această privinţă. Există într-
adevăr mai multe pachete software pentru Windows oferite de terţi care
implementează funcţionalităţi de shell, dar shell-ul standard rămîne cel DOS.
În primul rînd, ``shell'' înseamnă cochilie sau carapace. Numele acestui program
vine din faptul că ``înveleşte'' nucleul sistemului de operare, precum cochilia
miezul moale al melcului: utilizatorul nu are de-a face cu nucleul ci interacţionează
prin intermediul shell-ului. Pentru că nu îmi vine în minte nici o traducere potrivită,
voi continua să folosesc cuvîntul englezesc.
În Unix shell-ul oferă mai mult decît abilitatea de a lansa procese în execuţie: oferă
o sumedenie de comenzi şi facilităţi suplimentare, care-l fac un mediu ideal pentru
a activităţi de administrare a sistemului. Toate aceste comenzi formează un limbaj
de programare deosebit de puternic; shell-ul este deci un interpretor, care citeşte şi
execută comenzi.
A doua utilizare a shell-ului este pentru execuţia unor programe mai complicate,
scrise dinainte şi depozitate în fişiere. Un fişier cu comenzi pentru shell se numeşte
în engleză shell script (adică un ``scenariu pentru cochilie''), sau pe scurt ``script''.
Executarea unui script se mai numeşte ``procesare în vrac'', batch processing,
pentru că shell-ul nu se mai opreşte după fiecare comandă cu un prompt.
Ca orice alt limbaj de programare, cunoaşterea doar a unei fracţiuni din elementele
de bază se dovedeşte perfect satisfăcătoare pentru nevoile de zi cu zi. Voi ilustra
aici numai comenzile cele mai puternice, şi pe acestea le voi descrie mai mult prin
exemple decît riguros. Pentru cei doritori de aprofundarea subiectului, secţiunea
finală despre surse de informaţii suplimentare se poate dovedi un punct bun de
plecare.
Familii de shell-uri
Dacă Windows NT nu are un shell decent, în lumea Unix situaţia este chiar pe dos;
un scurt istoric este necesar pentru a lămuri babilonia de opţiuni existente.
Primul shell tradiţional pentru Unix a fost scris în 1976 de Steve Bourne, care pe
vremea aceea lucra la laboratoarele Bell ale companiei AT&T. În onoarea
creatorului său, shell-ul acesta este numit ``Bourne shell''. Programul cu pricina se
numeşte simplu ``sh'', şi se află de obicei în directorul /bin pe un sistem Unix
(/bin/sh). Să nu uităm că sistemul de operare Unix însuşi fusese creat cu puţin timp
în urmă în acelaşi loc, inspirat de sistemul de operare Multics (vedeţi şi articolul
meu despre istoria Unix-ului). Shell-ul Bourne introducea o mulţime de concepte
revoluţionare, care făceau viaţă utilizatorilor şi administratorilor mult mai simplă
decît în sistemele de operare precedente. Shell-ul Bourne era mai curînd proiectat
pentru utilizarea sub formă de interpretor, şi mai puţin pentru cea interactivă.
În 1984 David Korn, tot de la Bell Labs ale lui AT&T, şi-a propus să modernizeze
shell-ul Bourne adăugîndu-i facilităţi interactive a la csh. Astfel s-a născut shell-ul
Korn ksh, care este excelent realizat şi compatibil cu Bourne.
Din păcate ksh iniţial nu era software ``free''; aşa că unul din primele proiecte ale
fundaţiei Free Software Foundation (vedeţi articolul meu despre Open Source din
PC Report din iunie 1998) a fost să implementeze un nou shell, de data asta
complet ``liber''. Acest shell moşteneşte din ideile lui ksh, dar ia cîteva lucruri bune
de la csh. Noul shell a fost implementat original de Brian Fox, care era plătit de
FSF; numele shell-ului este ``Bourne Again SHell'' (``din nou Bourne''),
sau bash (citit aproximativ ``beş''). Acesta este shell-ul standard pe sistemele
GNU/Linux.
Prin 1981 Microsoft a lansat sistemul de operare MS-DOS. Acesta era echipat cu
un shell foarte primitiv, numit COMMAND.COM. Deşi creat ulterior Unix-ului, şi inspirat
de acesta, shell-ul MS-DOS este extrem de primitiv, inconsistent şi greu de folosit.
Din păcate a rămas, cu minore îmbunătăţiri, shell-ul standard chiar şi sub
Windows2000.
În Windows însă multe dintre funcţiunile unui shell sunt luate de un program
grafic, numit Command Manager sub Windows 3.1. Pentru utilizatori novici un
shell grafic oferă o interfaţă mult mai simplă şi intuitivă, dar pentru un ins
experimentat sau pentru un administrator facilităţile acestuia (şi ale altor programe
de administrare) sunt adesea frustrante.
De aici încolo avem de a face cu o explozie de noi shell-uri, care aduc tot felul de
înflorituri şi varietăţi; voi cita astfel: ash, zsh, scsh, rc, bsh, pdksh, es.
În articolul de faţă voi vorbi despre Bourne Shell; acest shell a evoluat şi el de-a
lungul timpului, şi a fost şi standardizat de comitetul POSIX, care a standardizat
Unix. De aceea, sh este de departe alternativa cea mai sigură: chiar dacă nu aveţi
alte shell-uri la dispoziţie, sh este sigur disponibil (pe sisteme gen GNU/Linux se
află bash, care are un mod de funcţionare în compatibilitate 100% cu sh).
Comenzi externe
Shell-ul este un program ale cărui capacităţi pot fi înzecite de faptul că poate
controla toate celelalte programe. Am spus deja că shell-ul poate porni în execuţie
orice alt program; în plus, shell-ul poate influenţa în mai multe moduri mediul în
care programul respectiv este executat. Vom vedea mai jos cîteva exemple.
Cel mai important de ştiut pentru moment este faptul numele oricărui fişier
executabil (adică un fişier care conţine o aplicaţie) este o comandă shell. Astfel,
fişierul ``netscape'' conţine imaginea executabilă a browser-ului Internet; cînd shell-
ului îi este oferită spre execuţie comanda netscape, el va lansa în execuţie
programul acesta. Un fişier executabil mai este de aceea numit ``comandă externă''.
Pentru ilustraţie vom folosi în acest text cu precădere cîteva comenzi externe
simple; iată-le rezumate aici:
ls:
Programul ls se citeşte ``list'', şi este echivalentul lui ``dir'' din DOS. Este
urmat de o listă de nume de fişiere, iar efectul lui este de a afişa chiar numele
acestor fişiere. Urmat de un director, afişează fişierele din acel director.
(Fără argumente operează pe directorul ``curent''.) Iată un exemplu:
% ls
Mail bin data lib man src tmp
% ls src
Makefile hello.c
%
wc:
Programul Word Count număra liniile, cuvintele şi caracterele din fişierele
care-i sunt date ca argumente. Iată un exemplu:
% wc src/hello.c
6 9 74 src/hello.c
%
cat:
deşi înseamnă ``pisică'', programul cat de fapt tipăreşte conţinutul fişierelor
care-i sunt oferite drept argument (echivalent cu type din DOS). Numele lui
vine de la ``CATalogue''.
% cat src/hello.c
#include <stdio.h>
main() {
printf("Hello world\n");
return 0;
}
%
Succes şi eroare
În Unix, cînd un proces se termină returnează procesului său părinte 1 un cod de
eroare, care permite părintelui să detecteze dacă odrasla-i s-a executat cu succes sau
a întîmpinat nişte probleme.
În mod surprinzător, cel puţin pentru un programator C, 0 este codul pentru succes,
şi orice valoare nenulă indică o eroare. Vom vedea mai încolo cum aceste valori pot
fi folosite de shell pentru a crea programe complicate.
Variabile shell
Ca orice limbaj de programare, limbajul shell-ului conţine variabile. În mod
tradiţional, variabilele shell sunt scrise numai cu majuscule, pentru că în Unix
programele executabile au nume scrise cu minuscule; în felul acesta se evită
confuziile. Valoarea unei variabile shell este un şir arbitrar de caractere. Numele
variabilelor sunt aceleaşi ca în limbajul C: o literă urmată de litere, cifre şi semnul
``subliniat'' _.
Atribuiri
Cea mai simplă comandă a shell-ului este cea de atribuire; ea are forma:
variabila=valoare
Efectul acestei comenzi este de a atribui valoarea din dreapta variabilei din
stînga. Atenţie: nu puteţi pune spaţii la stînga şi la dreapta semnului egal.
sh este un limbaj straniu prin faptul că pentru a accesa valoarea unei variabile
trebuie s-o prefixăm cu semnul dolar. Deci atribuirea şi citirea folosesc nume
diferite! O eroare comună este de a folosi dolar la atribuire sau de a uita dolarul la
citire. Atenţie, deci:
[1] % DIR=src
[2] % ls $DIR
[3] Makefile hello.c
[4] % ls DIR
ls: DIR: No such file or directory
[5] % $DIR=bin
sh: src=bin: command not found
[6] % DIR=ls
[7] % $DIR src
Makefile hello.c
[8] % DIR=$DIR$DIR
[9] % ls $DIR
ls: lsls: No such file or directory
[10] % DIR=src/
[11] % ls ${DIR}Makefile
Makefile
Ghilimele
Variabile interne
Shell-ul însuşi foloseşte unele variabile pentru nevoile sale interne. Schimbînd
valoarea acestor variabile putem afecta comportarea sa. De exemplu, variabila
numită PS1 este chiar prompt-ul. Variabila internă PWD este directorul curent.
% PS1="ordonati, stapine: "
ordonati, stapine: ls
Mail bin data lib man src tmp
ordonati, stapine:
Unele din variabilele interne nu pot fi atribuite, ci pot fi doar citite. Unele din
acestea au nume stranii, formate din alte caractere decît litere şi cifre; de exemplu
variabila $? conţine rezultatul cu care s-a terminat ultima comandă (0 pentru
succes).
cd:
(change directory) este urmată de numele unui director şi schimbă directorul
curent în acel director;
exit:
este urmată de o valoare numerică; shell-ul îşi termină execuţia cu valoarea
indicată. Deci exit 0 înseamnă ``succes''. Adesea programatorii utilizeaza
acest cod pentru a transmite procesului-părinte informaţii despre eroarea
petrecută;
echo:
(ecou) îşi tipăreşte argumentele. Este o comandă deosebit de utilă:
[1] % DIR=src
[2] % echo DIR $DIR
DIR src
[3] % echo DIR DIR
DIR DIR
[4] % echo "DIR DIR"
DIR DIR
[5] % echo "$DIR $DIR"
src src
[6] % echo '$DIR $DIR'
$DIR $DIR
eval:
Aceasta este o comandă extrem de puternică. Argumentul ei este un şir de
caractere, care este executat ca şi cum ar fi o comandă shell.
[1] % DIR=src
[2] % COMANDA=ls
[3] % echo "$COMANDA $DIR"
ls src
[4] % eval "$COMANDA $DIR"
Makefile hello.c
test:
e foarte utilă pentru a evalua expresii boolene (care adică generează un
rezultat ``adevărat'' sau ``fals''). Ca şi codurile de eroare ale proceselor,
``adevărat'' este 0, iar fals este orice valoare diferită de 0.
În exemplul anterior ilustrăm mai multe concepte noi: în prima linie punem
două comenzi pe aceeaşi linie, separîndu-le cu punct-şi-virgulă. În linia a
doua testăm existenţa unui fişier. În linia a treia tipărim rezultatul testului
anterior (vă amintiţi că variabila $? conţine rezultatul ultimei comenzi
executate, şi că 0 reprezintă succes).
if:
Putem face tot felul de teste, dar cum putem beneficia de rezultatul lor?
Folosind instrucţiunea de execuţie condiţională, ca în următorul exemplu:
% DIR=src; FILE=Makefile
% if [ -f $DIR/$FILE ]; then echo "Exista"; else echo "Nu exista"; fi
Exista
%
if [ $# != 1 ]; then
echo "Imi trebuie un argument"
exit 1
elif [ -f $1 ]; then
cat $1
else
echo "$1 nu exista"
fi
Acesta este deja un script interesant, care introduce cîteva elemente noi:
main() {
printf("Hello world\n");
return 0;
}
%
Read citeşte de la intrare o linie (linia care apare după comandă a fost
introdusă de la tastatură, şi nu tipărită de shell); apoi read sparge linia în
cuvinte separate de spaţii, şi fiecare cuvînt este atribuit unei variabile; ultima
variabilă primeşte restul liniei pînă la sfîrşit.
for:
permite să executăm un ciclu într-o listă de cuvinte, astfel:
for i in hello goodbye ok; do
for j in .o .c~; do
if [ -f $i$j ]; then
echo "Sterg $i$j"
rm $i$j
fi
done
done
Înainte de a prezenta alte cîteva comenzi interne foarte utile, vom discuta sumar
despre alte funcţiuni foarte importante ale shell-ului.
Expresie Semnificaţie
a* Toate fişierele al căror nume începe cu a
*.c Toate fişierele al căror nume se termină cu .c
.??*
Toate fişierele al căror nume începe cu punct şi
conţine cel puţin 3 caractere
*/src/*.c Toate fişierele terminate cu .c aflate în
subdirectorul src al oricărui director din directorul
curent.
*[0-3]*
Toate fişierele care conţin una din cifrele 0,1,2 sau 3
în nume
{ab,cd}.{c,h}
fişierele existente în
mulţimea ab.c, ab.h, cd.c, cd.h
ACEST_SCRIPT=$0
for i in *.c; do
if [ -f $i ]; then echo $i; fi
done
for i in *; do
if [ -d $i ]; then
cd $i; $ACEST_SCRIPT; cd ..
fi
done
Comenzi paralele
Am văzut că putem folosi semnul punct-şi-virgulă pentru a lansa în execuţie mai
multe comenzi succesiv. Dacă vrem să lansăm în execuţie două comenzi simultan,
punem între ele semnul &:
% netscape & xterm &
%
Mai spectaculos este că putem cupla ieşirea unui proces la intrarea altuia folosind
un singur caracter, scris | şi citit ``ţeavă'' (pipe). Pentru a afla de exemplu cîte
fişiere sunt în directorul curent, putem folosi:
% ls | wc -w
7
% ls >lista
% wc -w lista
7
% rm lista
wc -w număra doar cuvintele (words). Pentru a număra fişierele putem fie trimite
rezultatul lui ls într-un fişier lista folosind redirectare, după care putem număra
cuvintele, sau, mult mai eficient şi rapid, putem cupla ieşirea lui ls la intrarea
lui wc cu o ţeavă, ca în prima linie.
Această comandă numără liniile din toate fişierele care se termină cu .c în
directorul curent şi în toate subdirectoarele sale. Dacă unele din fişiere sunt însă
ilizibile (de exemplu pentru că avem directoare al căror nume se termină cu .c),
atunci wc va tipări nişte erori, care însă vor fi trimise spre ``null'' datorită redirectării
canalului de eroare, designat de 2>.
Accentul grav
O facilitate extrem de puternică a shell-ului este oferită de semnele de accent
grav `. Orice este cuprins între semne de accent grav este executat ca o comandă;
rezultatul acelei comenzi devine un şir de caractere, care înlocuieşte comanda între
accente grave.
% wc -w src/hello.c
9 src/hello.c
% a=`wc -w src/hello.c`
% echo $a
9 src/hello.c
%
Alte comenzi
while:
ne permite să ciclăm în mod repetat. În script-ul de mai jos, while se execută
atîta timp cît comanda read funcţionează cu succes; astfel citim fiecare linie
din fişierul text-de-prelucrat. Tipărim apoi numai liniile care au mai mult
de 10 cuvinte.
#!/bin/sh
contor=0
while [ $contor -le 100 ]; do
echo $contor
contor=`expr $contor + 1`
done
Încheiere
Aici pun capăt acestei incursiuni blitz în programarea în shell. Sper să vă fi pus la-
ndemînă suficiente scule ca să puteţi să scrieţi programe utile. Shell-urile moderne
sunt foarte sofisticate, oferă o sumedenie de facilităţi suplimentare, şi pot fi
configurate şi adaptate în mii de moduri. Dar 90% din problemele întîlnite în
practică pot fi rezolvate folosind cele 10% din limbaj pe care le-am prezentat.