Linux Base - La Shell
Edizioni ByteMan



La Shell (parte 1)


Se il kernel è il nucleo del sistema operativo (interprete e gestore dell'hardware), la shell ne è il guscio, l'interfaccia (testuale) tramite la quale l'utente può interagire con le funzionalità offerte dal kernel.
La shell è un programma che gestisce la comunicazione fra utente e sistema operativo interpretanto ed eseguendo i comandi dell'utente (viene anche chiamata command interpreter), e può avere diversi modi di utilizzo:

Sono state sviluppate nel tempo diverse shell, ma sulla gran parte delle distribuzioni Linux è preimpostata di default la shell bash. La sintassi dei comandi presentati in questo seminario è basata appunto sulla shell bash.

BASH e' l'acronimo di Bourne Again SHell, si tratta di una shell compatibile con la shell Bourne, che e' stata una delle piu' usate in ambiente Unix alla quale aggiunge alcune migliorie mutuate anche da altre shell Unix.
E' in grado quindi di eseguire script pensati per la shell Bourne, mettendo a disposizione al contempo costrutti e comandi piu' complessi non presenti nella Bourne shell originale.


Configurare la Shell

La shell stessa può essere configurata ed adattata alle proprie esigenze grazie a cinque files di configurazione, non sempre per la verità usati tutti, d'altra parte come vedremo non e' difficile crearsene di propri. Questi file sono:

/etc/profile    /etc/bashrc            ~/.bash_profile    ~/.bashrc    ~/.bash_logout

I primi due sono dei file globali cioe' quelli che contengono direttive di configurazione valide per tutti gli utenti, sono quelli che si trovano infatti sotto la directory /etc.
Gli altri tre sono dei file locali cioè contengono direttive di configurazione valide solo per l'utente che possiede la cartella nella directory /home che li contiene. Infatti nell'elenco sono preceduti da un carattere tilde (~ che nei sistemi Unix e' un collegamento breve alla directory base dell'utente), e preceduti da un punto che li rende "invisibili" al comando ls senza l'argomento -a.


Le variabili

Il metodo piu' usato per archiviare temporaneamente uno o piu' valori consiste nell'usare le variabili, cioe' delle aree di memoria a cui viene assegnato un nome ed in cui vengono depositati i valori.
Vedremo meglio in seguito l'uso delle variabili per scrivere codice eseguibile dalla bash, per il momento ci interessano solo due accezioni del termine cioe' le variabili di ambiente (environmental) e le variabili locali (local).

Le variabili di ambiente sono quelle variabili create autonomamente dal sistema operativo e di norma sono definite nel file /etc/profile, come per esempio SHELL, PS1, PS2, PATH, HOME, MAIL.

Le variabili locali sono quelle variabili definite dall'utente e sono generalmente definite nel file ~/.bashrc che si trova nella /home dell'utente del quale possono influenzare l'ambiente.

La definizione di una variabile avviene in un modo intuitivo ed elementare. Consta di tre parti: il nome della variabile seguito dall'operatore di assegnazione "=" e dal valore della stessa, cioe':

nome_var=valore_var

ATTENZIONE al fatto che NON ci devono essere spazi vuoti tra i componenti dell'assegnazione.

Una volta definita la variabile, per permettere ad altri programmi (avviati dalla stessa shell) di potervi accedere, essa deve essere esportata con il comando:

export nome_var

Per accedere, infine, al valore di una variabile per usarne il valore corrente occorre premettere al nome il carattere "$". Per esempio per conoscere il valore della variabile SHELL possiamo digitare il comando:

echo $SHELL

che mostrerà sul video:

/bin/bash

Si comprende come ci stiamo avviando verso un vero e proprio linguaggio di programmazione di cui stiamo cominciando a conoscere gli elementi base. Verranno gradualmente prodotti dei file di testo chiamati script contenenti il necessario per eseguire tutta una serie di operazioni programmate.
Se mentre si redige uno di questi file di script ci si trova con una linea di codice dagli effetti dubbi invece di cancellarla la si può commentare facendola precedere dal carattere "#". Chiaramente si puo' usare il carattere "#" anche per inserire commenti che migliorino la leggibilita' dello script. Ecco alcune righe contenenti commenti:

# Questa è una riga di commento
echo "testo di prova"      # Commento laterale, sulla stessa riga
#la riga seguente e' un comando commentato che non verra' eseguito
#echo "sono un testo che non verra' visto"



La variabile PATH

Una delle prime variabili da configurare e' sicuramente PATH che definisce le directory a cui possiamo accedere da qualsiasi punto del filesystem, ad esempio per avviare un eseguibile.

In Linux/Unix un eseguibile non è caratterizzato da un particolare suffisso, ma dall'avere il permesso x attivo.
Ad esempio: chmod +x myprg rende eseguibile il programma myprg.

Supponiamo di avere un eseguibile nella directory /usr/bin chiamato myprg, se la directory /usr/bin e' presente nella variabile PATH possiamo lanciare il programma da qualsiasi posizione nel filesystem, altrimenti saremmo costretti o a digitare l'intero percorso /usr/bin/myprg oppure a portarci prima nella directory dove sta l'eseguibile con cd /usr/bin ed a digitare quindi ./myprg.

Chi proviene da Windows trova, in genere, strano il dover premettere i caratteri ./ per eseguire un programma che si trova nella stessa directory di lavoro. Si ricordi però che nella costruzione dei path relativi i caratteri ".." dirigono verso la directory di livello superiore, mentre il carattere "." fissa l'origine a partire dalla directory attuale.


La variabile PATH e' definita in /etc/profile (configurazione globale), ed ha una sintassi di assegnazione particolare: ogni percorso di directory alla destra dell'operatore "=" è separata dal carattere ":", ad esempio:

PATH=/bin:/usr/bin:/usr/local/bin
export PATH

rendera' possibile mandare in esecuzione da qualsiasi punto del filesystem tutti gli eseguibili che si trovano nelle directory dichiarate nella variabile PATH. Se noi volessimo aggiungere altri valori alla variabile PATH potremmo digitare semplicemente:

PATH=$PATH:/usr/games
export PATH

il che aggiungera' al valore originale della variabile PATH il nostro nuovo valore, infatti il contenuto $PATH /bin:/usr/bin:/usr/local/bin diventera' /bin:/usr/bin:/usr/local/bin:/usr/games.

Se si desidera modificare il path solo per l'utente desiderato basta inserire i comandi precedenti in ~/.bash_profile (configurazione locale). Quindi ogni utente sarà libero di avere una configurazione PATH personalizzata.

Il prompt

Il prompt non e' altro che il contrassegno che indica lo stato di attesa del sistema di un input dell'utente, la sua forma classica e':

nome_utente@nome_computer:[#|$]

che oltre ad indicare il nome utente e quello del computer, nel classico formato dell'indirizzo email, ricorda se si è utenti normali ($) o utenti privilegiati (#).
Queste indicazioni dipendono dal contenuto della variabile PS1 definita nel file /etc/profile, che e' di proprieta' dell'utente root. Se si volesse un prompt diverso si potrebbe però ridefinire PS1 nel file ~/.bash_profile con effetto locale, cioè relativamente all'utente proprietario di ~/.bash_profile.

La variabile PS1 accetta dei valori predefiniti che sono ottenuti facendo seguire alla barra rovesciata (backslash) dei caratteri speciali che sono:

	\t	l'ora corrente nel formato HH:MM:SS 
	\d	la data in formato esteso es. "Tue May 18"
	\n	un carattere di nuova linea
	\s	il nome della shell
	\w	la directory corrente
	\W	il percorso completo alla directory corrente
	\u	il nome dell'utente
	\h	il nome della macchina
	\#	il numero del processo associato al comando in esecuzione
	\!	la posizione numerica nel file storico dei comandi
	\$	se l'UID e' 0 mostra un "#", altrimenti un "$"

Quindi si può personalizzare l'aspetto del prompt inserendo opportunamente questi caratteri. Si veda la tabella seguente che riporta alcuni esempi classici:

PS1="[\u@\h \W]\$ "      -->     [user@computer /root]#
PS1="[\t \s]\$ "         -->     [12:18:24 bash]#

Oltre a questi caratteri speciali la variabile PS1 puo' contenere anche dei comandi, per esempio se si volesse far apparire la versione del kernel:

PS1="`uname -r` \$ "     -->     2.4.17#

Si noti l'uso delle particolari virgolette usate per definire il comando, non si tratta infatti delle virgolette classiche  '  ma del carattere ` che viene chiamato backtick o virgoletta rovescia, e si ottiene con la combinazione di tasti AltGr+virgoletta semplice.


Oltre la variabile PS1 viene definita anche la variable PS2 che determina l'aspetto del prompt secondario che viene visualizzato quando si digitano comandi incompleti. Questo succede quando abbiamo a che fare con comandi molto lunghi, possiamo digitare il comando su piu' linee, facendo precedere il comando invio da una barra rovesciata (backslash), a quel punto la shell capira' che non abbiamo terminato e ci presentera' il prompt secondario che significa che sta attendendo il completamento del comando. Se per esempio vogliamo vedere come si presenta il prompt secondario nella nostra shell possiamo digitare un comando incompleto:

if PS2 then

in questo caso non abbiamo dovuto far precedere l'invio da una barra rovescia perche' la shell riconosce i suoi costrutti di programmazione, poi premendo invio ci accorgeremo di avere un prompt diverso, tipicamente:

>

volendo si potrebbe digitare un semplice

echo $PS2

per ottenere direttamente il valore della variabile. Inoltre tutto quello che e' stato detto per PS1 vale in linea di massima per PS2.

Gli alias

Capita a volte di dover digitare frequentemente delle linee di comando lunghe e complesse, ecco allora la possibilità di ricorrere agli alias che possono essere visti come dei comandi abbreviati. Basta eseguire un'operazione di assegnazione tra il nuovo comando ed il vecchio comando ed il gioco è fatto:

alias newcmd='oldcmd'                  ad esempio:
alias lss='ls -l --sort=size'

D'ora in poi il nuovo comando lss produrrà la lista della directory ordinata per dimensione dei file.
Volendo conoscere quali sono gli alias già definiti basta digitare semplicemente:

alias 

e si otterrà in output qualcosa del genere:

alias ..='cd ..'
alias cp='cp -i'
alias l='ls -a --color=auto'
alias la='ls -la --color=auto'
alias ll='ls -l --color=auto'
alias ls='ls --color=auto'
alias lss='ls -l --sort=size'
alias mv='mv -i'
alias rm='rm -i'
alias where='type -all'
alias which='type -path'

Alcuni alias li troviamo già predefiniti a cura della distribuzione in uso, ma noi possiamo definirne molti altri, quanti ce ne servono (senza limiti), realizzando una raccolta di comandi in codice personale.

Il file storico

La shell e' in grado di ricordare i comandi progressivamente immessi dall'utente, normalmente vengono salvati nel file ~/.bash_history e possono essere richiamati premendo i tasti freccia in su' e freccia in giu'. Questo comportamento puo' essere modificato configurando le variabili:

Ricordiamo che, per consuetudine, le variabili di ambiente vengono sempre indicate usando lettere maiuscole a differenza delle locali per cui si usano le minuscole. Queste variabili sono definite in /etc/profile, ma possono essere sovrascritte da quelle ridefinite in ~/.bash_profile.

Le variabili mail

La shell utilizza le seguenti variabili che influenzano le funzioni di posta e che sono normalmente definite, globalmente, nel file /etc/profile, oppure, localmente, nel file ~/.bash_profile:



La Shell (parte 2)


La shell Bash, similmente alle altre disponibili in Linux, non solamente permette la digitazione sequenziale dei comandi, ma essendo dotata di un vero e proprio mini linguaggio di programmazione permette di codificare degli script con tanto di strutture di controllo e sequenze di istruzioni. Moltissimi dei programmi che normalmente si utilizzano in Linux sono, in effetti, script di shell e pertanto per comprenderne il funzionamento ed eventualmente modificarli e' indispensabile conoscere le basi del linguaggio e la sua sintassi.

Incidentalmente si sottolinea la differenza tra linguaggi compilati e linguaggi di scripting (interpretati): i primi sono molto piu' veloci e potenti, ma spesso questa velocita' e potenza non giustifica la complessita' ed il tempo necessario per il loro sviluppo (C, Fortran, Pascal, Cobol), i secondi sono piu' lenti, ma facilmente e velocemente modificabili dal momento che sono costituiti solo da semplici file di testo, ottimi per le piccole applicazioni. Altri esempi di linguaggi interpretati sono Html, Perl, Lisp, Tcl.

Per scrivere semplici programmi interpretabili dalla shell e' richiesta la conoscenza di almeno i principali comandi di Linux, e la capacita' di usare un editor di testo, non necessariamente visuale (anzi!) come mcedit, vim, pico, o joe. E' anche buona abitudine non provare i propri script come utente root per evitare spiacevoli danni ai file di sistema.



Primo esempio

Secondo una tradizione classica pare che non si possa prescindere dal sapere preparare un programma che stampi a video l'ignobile frase "Ciao mondo !", e pertanto ecco la ricetta in tre passi:

  1. aperto il proprio editor di testo preferito si scriverà e si salverà quanto segue


    #!/bin/bash echo "Ciao mondo !"

    la prima riga, indispensabile, avvisa Linux che vogliamo che tutto cio' che segue sia passato all'interprete bash, che in questo caso si trova nella directory /bin ma che trovandosi in un posizione non standard si indicherà con il percorso appropriato.
  2. Una volta salvato lo script col nome di ciao.sh si dovrà renderlo eseguibile impostando il permesso appropriato:

    chmod o+x ciao.sh
    

    in questo caso si è aggiunto il permesso di esecuzione +x ai permessi del proprietario (owner) del file ciao.sh e tanto basta per rendere eseguibile, agli occhi di Linux, il file in questione. Il suffisso .sh utilizzato non implica infatti nessuna caratteristica di eseguibilità per il file serve solo all'utente nel caso in cui desideri identificare facilmente una determinata categoria di file. L'esempio visto funzionerebbe egualmente anche chiamando il file semplicemente ciao.
  3. A questo punto occorrerà provare il programma avviandolo con

    ./ciao.sh 

Se tutto è andato a buon fine sul video si vedrà il messaggino "Ciao mondo !" che è il risultato della classica sequenza:

    scrittura-salvataggio-permessi-prova

Nel caso di eventuale insuccesso con rientro nell'editor per le correzioni del caso non sarà ovviamente più necessario reimpostare il permesso di esecuzione.



Secondo esempio

Scriveremo adesso un programma che spostera' tutti i file della directory corrente dentro una directory appoggio appo che dovrà essere creata, cancellera' quindi la directory appo e tutti i file al suo interno, ed alla fine la ricreera' di nuovo. Tutto cio' preparando il seguente script di comandi:


#!/bin/bash mkdir appo mv * appo rm -rf appo mkdir appo echo "Lavoro eseguito correttamente"

che verrà prima salvato con il nome di test.sh, poi gli sarà assegnato il permesso di esecuzione con chmod ed infine verrà eseguito digitando ./test.sh. Ecco quindi come compiere azioni ripetitive con poche righe di script.



Terzo esempio

Questo esempio trae la sua origine dalla necessità reale di riconfigurare periodicamente le impostazioni di rete su una workstation del laboratorio anche da parte di personale non esperto.


#!/bin/bash clear echo "Laboratorio Aula 13 - Impostazione Standard a 192.168.12.3"; echo "----------------------------------------------------------"; # Definizione Variabili IPADDR=192.168.12.76 GATEWAY=192.168.12.3 NETMASK=255.255.255.0 NETWORK=192.168.12.0 BROADCAST=192.168.12.255 DNS1=151.99.125.2 DNS2=212.216.112.112 # Attivazione Scheda rete locale ifconfig eth0 inet down ifconfig eth0 inet up ${IPADDR} netmask ${NETMASK} broadcast ${BROADCAST} route add -net 0.0.0.0/0 gw ${GATEWAY} echo "nameserver "${DNS1} > /etc/resolv.conf echo "nameserver "${DNS2} >> /etc/resolv.conf # Visualizza Situazione ifconfig eth0 route cat /etc/resolv.conf

Oltre all'uso normale di echo, già visto in precedenza, si può notare il suo utilizzo con la ridirezione dell'output sia per aggiungere una stringa ad un file (con >>) sia per creare un file contenente una stringa (con >). E ancora si osservi: la presenza di clear, per la pulizia dello schermo, la definizione delle VARIABILI, l'uso delle stesse tramite la struttura ${}, le righe di commento contrassegnate da #.



Strutture di controllo

Un vero linguaggio di programmazione dispone soprattutto di strutture di controllo, Bash non fa eccezione e infatti dispone di tutto il necessario per permettere ai programmi: di prendere delle decisioni, di essere molto piu' compatti ed eleganti, e sopratutto di gestire gli eventuali errori. Gli esempi riportati finora hanno fornito all'interprete una serie di istruzioni da eseguire in sequenza. Se adesso si crea uno script chiamato test1.sh contenente i seguenti comandi:


#!/bin/bash cp /etc/miofile . echo "Copiato."

Il programma dovrebbe copiare il file /etc/miofile nella directory corrente e stampare a video la stringa "Copiato.". Il programma però funzionera' a condizione che esista un file chiamato miofile nella directory /etc, altrimenti si otterrà dalla shell una risposta del tipo:

cp: /etc/miofile: No such file or directory
Copiato.

Sarebbe meglio se il programma verificasse l'esistenza di /etc/miofile prima di provare a copiarlo.

Bash mette a disposizione i seguenti costrutti di controllo: if, while, until, for, case. Ogni struttura di controllo necessita di un comando di apertura e uno di chiusura, un po' come i tag in html. Per esempio la struttura di controllo che inizia con if viene chiusa con fi. L'esempio precedente può allora essere riscritto come segue:


#!/bin/bash if test -f /etc/miofile then # il file esiste cp /etc/mia_var . echo "Copiato." else # il file non esiste echo "il file non esiste" exit fi

Si noti che la seconda riga, con il comando test che verifica l'esistenza del file /etc/miofile, può essere riscritta, con una sintassi alternativa, come segue:

if [ -f /etc/miofile ];

nella quale sono da evidenziare i due spazi, obbligatori, tra le parentesi quadre; tutto quello che si trova dopo il punto e virgola viene interpretato come si trovasse su una riga a se' stante.

L'indentazione non e' obbligatoria ma rende il codice piu' leggibile, aiutando a separare i blocchi di istruzioni.

I controlli che la shell ci permette di fare su di un file sono i seguenti:

-d verifica se il file e' una directory
-e verifica se il file esiste
-f verifica se il file e' un file regolare
-g verifica se il file ha il bit SGID settato
-r verifica se il file e' leggibile da chi esegue lo script
-s verifica se la dimensione del file non e' 0
-u verifica il file ha il bit SUID settato
-w verifica se il file e' scrivibile

Il costrutto else viene usato per indicare al programma l'azione da compiere se la condizione non e' soddisfatta, mentre il costrutto elif, somma dei costrutti else e if permette di evitare un susseguirsi di if quando si vogliono testare piu' condizioni. Quando si vuole testare una variabile e' buona norma includerla dentro doppie virgolette, affinche' venga espansa correttamente, ad esempio:

if [ "$mia_variabile" -eq 5 ]; then