Shell Scripting: imparare le fondamenta in un’ora

by Stefano Belli on

Lo shell scripting è un’abilità essenziale per qualunque persona che utilizza la cosiddetta ‘Command Line Interface’ (CLI), andiamo a vedere nello specifico.

Shell, terminale… sono due cose uguali?

No, sono due cose differenti:

  • Il terminale: richiama la shell testuale in un programma a interfaccia grafica, con impostazioni visive (sfondo dell’emulatore, colore dei caratteri in output (e il loro font), nome della finestra, etc…), permette di eseguire comandi in essa, ‘dentro’ un ambiente grafico. Senza un emulatore della shell dovremmo richiamare la tty con la combinazione corrispondente [di solito Alt+Shift+Fx (dove x = 1,2,3,4)], quindi dovremmo uscire da interfaccia grafica e ‘andare’ in modalità testuale; scomodo, no?
    Le sessioni del terminale si chiamano pts, e vanno da 0 a n-1 (dove n è l’ultimo terminale aperto, quindi se ho 3 terminali aperti verranno identificati con 0,1,2).
    Nello specifico, con pts/0 si indica il primo, con pts/1 il secondo e così via… Mentre le tty sono le console virtuali ovvero l’emulazione della shell in modalità testuale. Vengono identificate con tty/0, tty/1, etc…
  • La Shell: l’interprete dei comandi che vengono immessi dopo il prompt (vedi in fondo per maggiori info). È fondamentale in quanto permette la comunicazione diretta con il kernel. Queste sono le shell usate in Unix:
    • Sh: la prima shell Unix in grado di interpretare comandi, ancora oggi presente in tutti i sistemi Unix (GNU, BSD, etc…).
    • Csh: shell compatibile con Sh, sintassi C like
    • GNU Bash: (Bourne Again SHell), shell avanzata, il “reborn” di Sh (Bourne AGAIN), utilizzata in tutti i sistemi GNU come shell predefinita
    • Z Shell: la shell più evoluta, ha molte funzioni avanzate ed è molto simile a Bash, è compatibile anche con script di quest’ultima. Ha un interfaccia che permette di configurare la shell la prima volta in modo semplice, temi di ogni genere, prompt destro. È, sì, simile con Bash, ma (ad esempio) le sequenze di caratteri (metacaratteri) che rimandano a determinate informazioni sono diverse.

Per cambiare la default shell di login:

chsh -s $LATUASHELLPREFERITA

È possibile utilizzare una shell in un’altra, come processo figlio del processo della prima shell,

bash-4.3$ sh
sh-4.3$ echo "Sono in sh!"

… e tornare alla shell di default

sh-4.3$ exit
È importante scegliere la mia shell predefinita?

La mia risposta è sì, le shell che consiglio sono ovviamente Bash e Z Shell, se a voi non fa né caldo né freddo modificare i vari file di configurazione, introdurre funzioni aggiuntive come l’evidenziazione della sintassi, l’auto-completamento avanzato, rimanete su Bash, altrimenti Z Shell è la scelta migliore.

Quindi, se scarico uno script che dichiara l’interprete /bin/sh e sto utilizzando /bin/zsh non è possibile utilizzare lo script?

Sì, puoi utilizzare lo script tranquillamente in quanto quando viene eseguito lo script, viene prima eseguita /bin/sh, dichiarata come interprete alla prima linea dello script (#! $INTERPRETE), poi vengono eseguite le istruzioni interpretandole una-ad-una da /bin/sh

Shell scripting… what is this?

Attuiamo lo shell scripting ogni volta che eseguiamo un comando, anche attuando semplici comandi come…

echo "Ciao"

oppure

su -c "cp $FILE /"

Quindi, quando incontrate quei file con estensione *.sh state eseguendo dei comandi, più elaborati, con determinate condizioni, in un ordine ben specifico. Anche se è possibile verificare una condizione con if anche da un terminale, lo svantaggio è che bisogna scrivere tutto su una linea… quindi non si capirà una mazza…

È buona abitudine lasciare il codice in condizioni buone, e richiedono almeno che il codice sia ben indentato (si veda sotto per maggiori info), ben commentato e che non contenga nessun errore di sintassi; in caso contrario l’interprete avvisa non appena incontra quella determinata istruzione. Tuttavia è possibile ad esempio utilizzare opzioni diverse rispetto a quelle che vogliamo, (questo dipende dal programma, che viene richiamato dall’interprete in base alla variabile $PATH) questa opzione è giusta, l’unico problema è che non esegue ciò che gli abbiamo chiesto, e questo potrebbe portare a conseguenze gravi.

*L’interprete: un programma che legge le istruzioni al momento dell’esecuzione, il programma non può essere compilato. (Python, Bash, Sh, etc…)

E’ importante imparare lo Shell Scripting?

Sì, oltre che a saper manipolare bene il sistema operativo, è anche utile per avere una prima idea di come funziona un linguaggio di programmazione come Python (C/C++, Java è troppo…), anche se la sintassi tra i due è diversa.

Il vantaggio principale?

Effettuare operazioni complesse in modo rapido. E la flessibilità.

Ogni shell ha un suo file di configurazione, sono creati non appena la shell viene caricata al primo avvio del nuovo OS. Se così non fosse è possibile creare da soli questi file. Il file più significativo, dove gli utenti inseriscono più personalizzazioni è

per Bash:

$HOME/.bashrc 

per ZSh:

$HOME/.zshrc

In questi file vengono definite le funzioni, gli aliases, etc… Che devono essere caricati all’avvio di un nuovo terminale (pts/x), è possibile reperire questi file su internet, ad esempio è possibile modificare il prompt di default con uno personalizzato: in Bash è sufficiente modificare la variabile PS1, in ZSh possiamo modificare il prompt sinistro e quello destro rispettivamente le variabili: PROMPT e RPROMPT, PS1 ha metacaratteri diversi rispetto a PROMPT e RPROMPT. Si possono trovare i metacaratteri per ogni shell cercando su Google o qualunque altro motore di ricerca.

Che cos’è il prompt?
Il prompt è quella riga che ci dice quali utenti siamo, il nostro hostname, dove ci troviamo, e attende un comando in input.

Questi sono dei metacaratteri:
[u h $, etc… <= Bash, Sh ][%h, %u, %d, %l, %?, etc… <= ZSh]

Ma che cos’è un metacarattere?
E’ una stringa che si riferisce ad un’altra stringa, numero o carattere, per esempio:
u : stefanozzz123, ovvero: il metacarattere “u” rimanda alla stringa stefanozzz123 che sarebbe il mio nome utente.
%? : 0, ovvero: il metacarattere “%?” rimanda al numero 0 che sarebbe il codice di ritorno del programma appena terminato.

Prima di cominciare:

  • Variabili d’ambiente: [export VAR=”contenuto”] se si passa da una shell a un’altra queste variabili non cambiano. E’ possibile visualizzarle tutte con il comando (una variabile d’ambiente può essere $USER, $SHELL…) [env]
  • Programmi Shell-Built-In: programmi integrati nella shell, possono essere pwd, exit, export… per altre info: GNU Bash Builtins
  • Come si rende avviabile uno script: semplice, esiste chmod (CHange MODe), una comoda utility che permette di cambiare i permessi di un file, nel nostro caso chmod +x script.sh è sufficiente per aggiungere il parametro x alle proprietà del file, per appunto renderlo eseguibile, ma con chmod è possibile anche togliere i permessi (chmod -x script.sh) e anche altre operazioni… Chmod è utile nel caso dichiariate l’interprete (vedi il primo script), altrimenti è necessario chiamare sh e passargli come argomento il file (sh script.sh)


Prendiamo un piccolo script per cominciare

Interprete, commenti, assegnazione, dichiarazione variabili, input/output, codici di ritorno, operazioni aritmetiche

Abilitiamo i permessi di esecuzione col seguente comando:

chmod +x nome_script.sh #abilitare i permessi di esecuzione

Altrimenti:

sh nome_script.sh #viene passato direttamente all'interprete
#! /bin/sh

#Assegna a UTENTE il valore della variabile $USER (scopo dimostrativo)
UTENTE=$USER

#Mostra in stdout e richiedi input in stdin, senza andare a capo
echo "Ciao $UTENTE"
echo -n "Inserisci il primo numero: "
read N
echo -n "Inserisci il secondo numero: "
read X

#Mostra il prodotto
echo "Il prodotto tra $N e $X è: $(( $N * $X ))"

#Esci con codice di ritorno 0 (true)
exit 

Analizzando questo script:

  • l’interprete utilizzato è /bin/sh che è incluso in tutti i sistemi UNIX e Unix-like
  • Ci sono dei commenti per specificare a cosa serve una determinata istruzione
  • Abbiamo dichiarato e iniziallizato UTENTE allo stesso valore di $USER
  • con echo si mostra in stdout il saluto con la variabile UTENTE preceduta dal dollaro ($)
  • Abbiamo poi eseguito sempre echo ma con l’opzione n che ci consente di evitare che il testo mostrato vada a capo per permettere all’utente di immettere in stdin dei valori dopo l’stdout
  • Abbiamo eseguito echo per mostrare in stdout che il prodotto tra $N e $X è il prodotto tra i due valori memorizzati in variabile $N e $X
  • Tutto è andato bene, uscita con codice di ritorno 0

È facoltativo dichiarare l’interprete, ma buona abitudine a parer mio!
Possiamo notare come la variabile è dichiarata, è una cosa molto importante in quanto se iniziallizate una variabile così:

*Nota: Interprete – programma che legge le istruzioni di un altro programma al momento (Python per esempio), il contrario della Compilazione in poche parole, anche se esistono dei linguaggi ibridi Compilazione-Interpretazione (Java)
*Nota: Compilazione – Processo nella quale il codice di un programma viene tradotto in linguaggio macchina, pronto per essere “masticato” dal processore. Questo processo avviene attraverso un Compilatore (GCC, G++ per esempio). Quindi non è più leggibile all’occhio umano, bytecode = binario (non quelli del treno), una serie di 0 e 1 che stanno a significare determinate operazioni, con tecnica di ingegneria inversa è possibile risalire al funzionamento di quei 010101011010.

VAR="buongiorno"

è giusto, ma se la iniziallizate così:

VAR = "buongiorno"
VAR= "buongiorno"
VAR ="buongiorno"

è sbagliato.
Una variabile si dichiara perciò in questo modo:

NOME_VARIABILE=VALORE

Dove valore può essere un numero (senza apici), o una stringa (delimitata da doppi apici), un singolo carattere (delimitata da singolo apice), o un’altra variabile.
Per riutilizzare la variabile è necessario ‘anteporgli’ il simbolo del $ (dollaro), attenzione a non anteporlo in fase di iniziallizazione.

echo "$VAR $UTENTE"
$ ~> buongiorno ste

Nel caso di stdin, si utilizza il programma read

$ read #attende finché l'utente preme invio
$ read VAR #memorizza in VAR (che viene dichiarata quando si scrive il valore) il valore che viene immesso in stdin

è possibile riutilizzare il valore di VAR allo stesso modo, come per le altre variabili.

Per effettuare operazioni aritmetiche, infine, è possibile utilizzare il seguente comando:

echo $(( 1 * 2 )) #prodotto tra 1 e 2

gli operatori sono:
* : moltiplicazione
– : sottrazione
+ : addizione
/ : divisione

rispettate gli spazi come nell’esempio o sh vi punirà. 
Ah, potete eseguire comandi all’interno di echo o in una variabile

 
echo "Il tuo kernel corrente è $(uname -s -r)"
#Oppure
CURRENT_KERNEL=$(uname -s -r)

Spiegheremo Stdin, Stdout, Stderr, redirezione dell’output qui sotto ?


Standard Output/Error/Input; Redirezione Output

 
#! /bin/sh
#Standard Output [stdout]
echo "Ciao Mondo!" #Stampa la stringa "Ciao Mondo" nel canale standard di output

#Standard Error [stderr]
>&2 echo "Errore!" #Stampa la stringa "Errore!" nel canale error dell'output

#Standard Input [stdin]
echo -n "Scrivi qui il tuo saluto: "; read SALUTO #Stampa la richiesta in stdout, poi attende l'invio da parte dell'user e memorizza il testo scritto in $SALUTO
echo $SALUTO #$SALUTO viene mostrato in stdout ;)


#redirezione output su file
FILE=$HOME/provaredirezione
echo "Ecco il mio saluto: $SALUTO" > $FILE #redirige il testo in $FILE, in questo caso CANCELLA TUTTO il file
echo "Ecco il mio saluto uguale: $SALUTO" >> $FILE #redirige il testo in $FILE, nel caso visto ora, la stringa viene posizionata all'ultima riga del file!
#echo "Questa stringa non verra' mostrata su file, e il suo contenuto verra' eliminato" 2> $FILE #redirige su $FILE la stringa (errore), ma il contenuto del file verra' eliminato ;) 

Esistono due modi per stampare del testo a schermo, o meglio due canali:

  • Standard Output [stdout]: testo normale
  • Standard Error [stderr]: canale utilizzato per stampare errori, utilizzato di solito quando un programma ritorna valore >= 1

Invece per richiedere in input qualcosa (stringa, numero, carattere) c’è il canale di Standard Input, semplicemente con l’istruzione read.
Ma attenzione, se si utilizza solamente l’istruzione read si utilizza il codice semplicemente per attendere un invio, nessun dato verrà allocato in nessuna porzione di memoria!

read NUMERO

invece, allora un numero in $NUMERO, che poi possiamo riutilizzare.
La redirezione è utile per esempio quando abbiamo un grande file di configurazione e abbiamo la necessità di fare le cose il più rapidamente possibile, quindi con l’operatore >> (che è differente da > ) possiamo evitare di cancellare tutto il file e ‘spedire’ la stringa dopo echo in quel determinato file.

echo "stefanozzz123 ALL=(ALL) ALL" >> /etc/sudoers

semplicemente, il testo non viene mostrato in stdout ma viene consegnato a /etc/sudoers
l’operatore > funziona allo stesso modo solo che sovrascrive il file quindi se eseguo

echo "stefanozzz123 ALL=(ALL) ALL > /etc/sudoers

il mio file sudoers verrà cancellato ? fate sempre attenzione quando utilizzate questo operatore.


Operatori (&&) ( || ) (;) ( | ) ( [[ -f $FILE ]] ) ( [[ -d $DIRECTORY ) ( & ) –>

#! /bin/sh

#Se clear ha codice di ritorno 0, allora esegui [...]
clear && echo "Il terminale èstato ripulito dal vecchio output."
#se clr ha codice di ritorno 0 allora esegui ls, altrimenti stampa su canale di errore [...]
clr && ls || >&2 echo "Non è stato possibile ripulirlo!"
#Se clear restituisce codice di ritorno sia 0 che 1+ esegui comunque ls
clear;ls

## operatore pipe ( | ) ##
#In poche parole
#i messaggi del kernel vengono passati a grep, che cerca l'espressione "ath9k"
dmesg | grep ath9k
#quindi con pipe è possibile passare l'output di un file a un altro per eseguire una qualsiasi operazione su di esso.
#altro esempio?
cat /etc/sudoers | grep root
#l'output di /etc/sudoers (generato da cat) viene trasferito a grep che cerca l'espressione "root"
touch random
echo b > random
echo a >> random
cat random | sort

#verifiche rapide##
[[ -f $HOME/fileExists ]] && echo "$HOME/fileExists :: esiste" || echo "Quel file non esiste!"
[[ -d $HOME ]] && echo "la tua directory home esiste!" || echo "Non esiste :( "

##processo padre e figlio##
dmesg & lspci 
#insieme a dmesg viene eseguito lspci, il processo figlio di dmesg

Volevo fare chiarezza sull’uso di &&:
non è sbagliato utilizzare &&, ma sappiate che l’operazione che richiedete verrà eseguita solo e se il codice di ritorno del programma sarà 0, se volete eseguire prima un programma e poi l’altro indifferentemente dal codice di ritorno è sufficiente utilizzare l’operatore ; ([comando];[comando1]), se desiderate che un programma esegua una determinata operazione dopo il primo programma che ha codice di ritorno >= 1 dovete utilizzare l’operatore || ([comando]||[comando1]).

  • Codice di ritorno 0: il programma è terminato a buon fine, nessun errore.
  • Codice di ritorno >= 1: il programma ha avuto problemi, qualcosa è andato storto!

L’operatore pipe ( | ) ci consente di utilizzare l’output di un programma in un altro programma. L’esempio più banale:

cat file |grep pippo

L’output del comando cat viene mostrato parzialmente se il programma grep trova l’espressione pippo, se non trova niente… niente output! È un po come la redirezione ma è possibile effettuare più operazioni utili come questa.
Il comando sort serve per ri-ordinare in ordine alfabetico un file, sostanzialmente non lo modifica, mostra solo le modifiche in stdout e basta, niente di più. Nell’esempio ho creato il file “random”, redirezionato b e a (quindi il contenuto del file è: )

~$> cat random
==============
b
a

quindi non sono in ordine alfabetico; con sort possiamo riordinarle temporaneamente, ma c’è un’opzione che consente di modificare il file.

 
~$> cat random | sort
===============
a
b

.
Ultimo ma non meno importante l’operatore &, che ci consente di eseguire un processo figlio da un processo padre, ecco il mio esempio

echo ciao & echo hello

[dmesg –> lspci (la sequenza DOVREBBE essere questa ; padre I programmi vengono eseguiti da destra verso sinistra, il motivo non l’ho mai capito onestamente :/

Verificare condizioni, utilizzare le iterazioni

#! /bin/sh

#Condizione, verifica condizione affinchè sia vera
if [[ $USER == "root" ]]; then
	echo "Sei l'utente root!"
#se la condizione presentata precedentemente è falsa, riprova con questa:
elif [[ $USER == "stefanozz123" ]]; then
	echo "Sei tu!"
#se entrambe sono false...
else
	echo "Sei qualunque altro utente mortale, $USER"
fi

#Case: simile ad if 
#verifica che in $1 siano presenti questi valori
case $1 in
	#Se hai digitato 1 
	(1)
	echo "Hai scritto 1"
	break;;
	#Se hai digitato 2
	(2) 
	echo "Hai scritto 2"
	break;;
	#Qualunque altra cosa
	(*)
	echo "Hai scritto  qualunque altro numero/stringa: $1"
	break;;
esac

#Ciclo while:
#mentre [condizione è vera] esegui
#      operazioni
#fine
#se la condizione è vera allora esegue questo blocco di codice finché non si preme CTRL+C
while [[ $USER == "root" ]]; do
	echo "E' pericoloso essere utenti root!"
	sleep 3
done

#For:
#per i che è uguale a 0, finché i è minore o uguale a 20 aggiungi a i "2" fino ad arrivare a venti
for (( i=0; i<=20; i+=2 )) 
#esegui
do
	echo "Tabellina del 2: $i" 
#fine
done

#a indice vengono assegnati i valori 1,2,3,4,5,6,7
for indice in 1 2 3 4 5 6 7
#esegui
do
	#per ogni valore in indice
	echo "In indice sono disponibili i seguenti valori (valore): $indice"
#fine
done

Per scrivere del codice elaborato, istruzioni più complesse, è necessario utilizzare:

  • If
  • Case
  • while
  • for

Sono delle istruzioni che consentono di verificare condizioni in modo più elaborato ed eseguire dei cicli in base a una determinata condizione. Se avete avuto altre esperienze di scripting/programming avrete sicuramente sentito parlare di iterazioni e condizioni.
if

if condizione; then
     operazione
fi

Questa qui sopra è la sintassi standard, semplice da capire, se condizione è vera, allora esegui operazioni.
Però con if possiamo anche eseguire operazioni se la prima condizione è falsa

if condizione; then
     operazione
else
     operazione1
fi

Il funzionamento è uguale a quello precedente solo che è stata inclusa la condizione else:
se condizione è falsa, salta operazione e esegui condizione1
Ma abbiamo un’altra possibilità, vediamo il codice:

if condizione; then
     operazione
elif condizione1; then
     operazione1
else
     operazione2
fi

notate l’elif, al contrario di else, è appunto un misto tra else e if (in C/C++, Java si scrive appunto else if ) [in italiano altrimenti se], ci consente di verificare un’altra operazione se la prima è falsa, ne possiamo implementare quante vogliamo, alla fine else ma è sempre facoltativo.
Alla fine si chiude lo statement if con la keyword fi (l’equivalente di } in Java, C/C++, JavaScript, e altri linguaggi con la sintassi C-like).

case statement
funziona in modo molto simile a if, solamente che ci consente di risparmiare tempo, di solito si utilizza per operazioni veloci:

case $1 in
    (1)
    echo "hai passato 1" 
    break;;
    (2) 
    echo "hai passato 2"
    break;;
    (*)
    echo "hai passato $1"
    break;;
esac

Questo equivale a

if [[ $1 == 1 ]]; then
     echo "hai passato 1"
elif [[ $1 == 2 ]]; then
     echo "hai passato 2"
else
     echo "hai passato $1"
fi

Quindi come possiamo vedere il case ci permette di verificare condizioni in modo veloce,
qualunque tipo di dato si identifica dentro le parentesi ( ) (che sia stringa, numero, carattere), poi, se la condizione si verifica si eseguono le operazioni richieste, alla fine si lancia l’istruzione break, necessaria a uscire dal processo di verifica del case, si termina ogni blocco con ;; .
E’ identico allo switch del C/C++ solo che il C ha una sintassi diversa:

//...
int main(int argc, char* argv[]){
//...
switch(argc){
    case 1: printf( "Hai passato 1 argomento da linea di comando"); break;
    case 2: printf("Hai passato 2 argomenti da linea di comando"); break;
    //...
}
//...

while
il while è un ciclo che verifica se una condizione è vera o falsa, se la condizione è vera, mentre è vera, esegue determinate operazioni finché quella determinata condizione diventa falsa.

while [[ $USER == "root" ]]; do
      echo "è pericoloso essere utenti root!"
      sleep 3
done

Questo blocco verifica che $USER è uguale alla stringa root per verificare che l’utente che ha eseguito il programma è, appunto, root, se così fosse, stampa in stdout la stringa, aspetta 3 secondi e la riscrive all’infinito, finchè l’utente non cambia (impossibile) o si preme CTRL+C.

for, for in
Il for è anch’esso un ciclo che permette di eseguire 3 operazioni, ma vediamo prima il for-in con un rapido esempio:

for indice in 1 2 3
do
    echo indice: $indice
done

crea una nuova variabile indice e memorizza all’interno di essa i valori 1,2,3; poi li stampa a schermo uno ad uno nell’ordine scritto. Una “specie” (tra 5 virgolette) di array (array: variabile contenente 2 o più dati omogenei, dello stesso tipo. Un esempio di array in shell /bin/sh:

ARR_VAR[1]="Ciao" #inserisce in posizione 2 la stringa Ciao
ARR_VAR[2]="Buongiorno" #inserisce in posizione 3 la stringa Buongiorno
echo ${ARR_VAR[*]} #Mostra tutto il contenuto dell'array 
echo ${ARR_VAR[1]} #Mostra un contenuto dell'array

Dov’è la posizione 1?
Gli array hanno valori da 0 a n-1 ovvero, 0=1, 1=2, 2=3. Si parte da 0 e si arriva fino a 9 per esempio, avremo 10 stringhe nell’array che vanno da 0 a 10-1 ). [chiusa la parentesi degli array]
Quindi, tornando a noi, il for-in può essere utile per svariate cose…
Il for classico, ovvero quello composto da tre istruzioni si compone in questo modo:

for (( i=0; i<=100; i++ )) 
do
    echo "N: $i"
    sleep 1
done

si legge:
“Per i che è uguale a 0; finché i è minore o uguale a 100; aggiungi a i il valore 1 fino ad arrivare a 100″
quindi esegui l’operazione nel blocco do-done finchè i verrà incrementata, nel nostro caso ogni volta che i subisce l’incremento verrà stampata in stdout N: $i, ci sarà un attesa di 1 secondo e poi verrà ri-stampata la stessa stringa, si attenderà un secondo e così fino a 100…
Soffermiamoci sull’incremento:

  • i++ : verfica i e poi aggiungi 1
  • ++i : aggiungi 1 e poi verifica i
  • i– : verifica i e sottrai 1
  • –i : sottrai 1 e verifica i
  • i+=2 : aggiungi a i il valore 2, equivale a dire: i=i+2 (ovvio che 2 può essere qualunque numero) [echo $((i=i+2)); echo $((i+=2)) ]

Il ciclo for può risultare utile in molte occasioni di necessità ? , soprattutto il for-in.

Abbiamo tralasciato until simile al while, potete cercare il suo funzionamento su Google
Il select: consente nel creare dei menù, è possibile combinarlo sia con case che con if
Questo è il select di LinuxCleaner

select YOURCHOICE in 'Show tip' 'OS.Shell' 'Update & check if there are useless packages' 'Clean temporary files /tmp' 'Clear trash'  'Update kernel' 'Add repositories' 'Install packages by your package manager' 'Search packages by package manager(from repositories)' 'Remove packages by package manager' 'Install by PKGBUILD (Arch)' 'Install package by file' 'Delete packages by manual tool' 'Installed stock kernels' 'Compile kernels' 'Update/Reinstall GRUB configuration file ' 'About your system' 'About Developer' 'About Script' 'Quit'; do
	case $YOURCHOICE in
		("Clean temporary files /tmp")
			if [[ $USER != "root" ]]; then
				echo "You need to be root"
			else
				cleanTemporaryFiles
			fi;;
		("Clear trash")
			clearTrash;;
		("Update & check if there are useless packages")
			if [[ $USER != "root" ]]; then
				echo "You need to be root"
			else
				checkUpdates
			fi;;
		('Add repositories')
			if [[ $USER != "root" ]]; then
				echo "You need to be root"
			else
				echo ""
				echo "============="
				echo "Which distro are you using/is your distro based on this listed below? "
				echo "1)Debian"
				echo "2)Red Hat"
				echo "3)Arch Linux"
				echo -n "Choice[1,2,3]--> "
				read BASEDON
				echo "============="
				case $BASEDON in
					(1)
						APTREPODIR="/etc/apt/"
						if [[ -d $APTREPODIR ]]; then
							makeChoiceDebianBased
						else
							echo "Your system doesn't have APT-GET/ APT-GET repository folder is not present"
							echo "============="
							echo ""
						fi;;
					(2)
						YUMREPODIR="/etc/yum.repos.d/"
						if [[ -d $YUMREPODIR ]]; then
							makeChoiceRedHatBased
						else
							echo "Your system doesn't have YUM / YUM repository folder is not present"
							echo "============="
							echo ""
						fi;;
					(3)
						PMREPODIR=/etc/pacman.d/
						if [[ -d $PMREPODIR ]]; then
							makeChoiceArchLinuxBased
						else
							echo "Your system doesn't have PACMAN /PACMAN repository folder is not present"
							echo "============="
							echo ""
						fi;;
					(*)
						echo "I cannot recognize this option."
						echo "============="
						echo "";;
				esac
					
			fi;;
		("Search packages by package manager(from repositories)")
			searchPackagesByPackageManager;;
		("About Developer")
			infoAboutDeveloper;;
		("About Script")
			infoAboutShellscript;;
		("Install packages by your package manager")
			if [[ $USER == "root" ]]; then
				installPackagesByPackageManager
			else
				echo "You need to be root"
			fi;;
		('Remove packages by package manager')
			if [[ $USER == "root" ]]; then
				removePackagesByPackageManager
			else
				echo "You need to be root"
			fi;;
		("Install package by file")
			if [[ $USER == "root" ]]; then
				installPackagesByFile
			else
				echo "You need to be root"
			fi;;
		('Delete packages by manual tool')
			if [[ $USER == "root" ]]; then
				deletePackagesByManTool
			else
				echo "You need to be root"
			fi;;
		("Compile kernels")
			if [[ $USER == "root" ]]; then
				echo "Good idea... Root is not needed for getting/config/compilate the kernel"
				compileKernels
			else
				echo "Root user is required for installing kernel modules, (if you want) update the bootloader configuration, (if on Arch) build a ramdisk"
				compileKernels
			fi;;
		("About your system")
			aboutSystem;;
		("Update kernel")
			if [[ $USER == "root" ]]; then
				updateKernelByPackageManager
			else
				echo "You need to be root"
			fi;;
		("Show tip")
			showTip;;
		('Quit')
			clear
			echo "[*]Bye bye! "
			exit 1
			;;

		('Update/Reinstall GRUB configuration file ')
			if [[ $USER == "root" ]]; then
				echo "[+]OK."
				updateAndReinstallGRUB
			else
				echo "[!]You need to be root."
				sleep 2
				clear 
				main
			fi;;
		('OS.Shell')
			runOSCommand;;
		('Installed stock kernels')
			installedStockKernels;;

		('Install by PKGBUILD (Arch)')
			if [[ $USER == "root" ]]; then
				echo "[-]MakePKG will not work if you run it with root access"
			else
				buildByMakePKG
			fi;;
		(*)
			echo "Retry, I cannot recognize this option";;
	esac
done 

se eseguite LinuxCleaner potete vedere quel menù creato grazie a select e a case, se viene premuta la corrispondenza per uscire, verrà eseguita l’istruzione exit e a quel punto il programma termina.
[sintassi: select VARIABILE_DA_VERIFICARE in ‘opt1′ ‘opt2′ ‘opt3′; do Operazioni con case o if done. Quindi bisogna verificare la corrispondenza di $VARIABILE_DA_VERIFICARE con le tre voci ‘opt1′ ‘opt2 ‘opt3′ che vengono ordinate in base alla loro posizione. Ultima curiosità: possiamo modificare il prompt, ovvero quella riga che chiede l’input con la variabile PS3; quindi PS3=”Select~~> “. Da notare che il select l’ho testato con sh e bash, con Z Shell la situazione dovrebbe essere leggermente diversa, ma il concetto è sempre quello ? ].


Funzioni e argomenti di funzioni, return code, exit
Che cos’è una funzione?
Una funzione si può definire come un piccolo sotto-programma che esegue specifici compiti, è un blocco di codice che esegue determinate istruzioni, lo “scopo” dell’esistenza della funzione si può identificare attraverso il nome della funzione (somma(); moltiplicazione(); doSomething()…), vedremo anche meglio lo scope, ovvero il grado di visibilità di una variabile.
Che cosa sono gli argomenti di funzioni?
sono dei parametri, che vengono passati alla funzione attraverso delle variabili speciali ( $1 , $2, $3…), vedremo a che servono guardando meglio il codice
Che cos’è il return code
il return code è un valore che un programma ritorna alla fine della sua esecuzione, questo valore è condizionato da vari aspetti, e in base al suo valore si determina se il programma è terminato correttamente, o meno. Vedremo i valori meglio dopo aver analizzato il codice.

#! /bin/sh

diCiao(){
        echo "Ciao"
}

dimmiUnTuoSaluto(){
        echo $1
}

sommaDueNumeri(){
        SUM=$(( $1 + $2 ))
        echo "Somma tra $1 e $2 = $SUM"
        return $SUM
}

#Semplice, i parametri non vengono passati
diCiao

#Scriviamo un saluto
dimmiUnTuoSaluto Buongiorno

#Scriviamo due addendi
sommaDueNumeri 5 6

if [[ $SUM == 11 ]]; then
        echo "Il programma è terminato correttamente; return code: 0"
        exit
else
        echo "C'è stato qualche errore, il programma è terminato con return code >= 1"
        exit 1
fi

In questo script abbiamo 3 funzioni:
una stampa in stdout semplicemente “Ciao”
la seconda chiede il parametro $1 per stampare un saluto da te scelto
la terza esegue un’operazione con i parametri $1 e $2 passati alla funzione. Quindi ritorna il valore di $SUM che contiene la suddetta somma.
Se ci facciamo caso nelle ultime due funzioni non c’è nessun valore significativo, solo variabili che saranno poi determinate dai parametri passati ad esse. In sostanza quei $1, $2 sono la “preview” di quello che succederà con i valori effettivi che si sostituiranno ad essi, che siano dati stringa, interi, caratteri, etc…
Notiamo come le funzioni vengono dichiarate: funzione(){ ist; ist1 }, se provenite dal C/C++,Java,JavaScript,Python,… No… tra le parentesi non va inserita alcuna variabile. Le parentesi servono all’interprete per stabilire se quella è una funzione o no. Le variabili sono $1, $2
e sono già stabilite nel codice.
Quando deve essere chiamata la funzione, la si chiama semplicemente in questo modo:

nomeFunzione var1 var2

non si utilizzano parentesi tonde per passare argomenti, si passano direttamente separate da uno spazio, dove var1 e var2 corrispondono rispettivamente a $1 e $2
$1 = primo argomento
$2 = secondo argomento
$x = x argomento .

Infine, non avendo idee per un esempio migliore ho verificato che $SUM fosse uguale a 11 (5+6), se fosse stata uguale, con l’istruzione exit il programma termina con successo (valore: 0), altrimenti avrebbe terminato con valore 1 grazie all’istruzione exit 1 dove appunto, exit è l’istruzione e 1 è l’argomento passato a esso, exit con un valore come parametro esce dal programma e ritorna quel determinato valore per determinare se il programma è terminato con successo.

  • 0 : programma terminato con successo
  • >= 1 : si sono verificati problemi durante l’esecuzione che hanno portato alla chiusura anticipata/CONTROL+C ?

*Avrei potuto determinare se $SUM è uguale a 11 semplicemente così

[[ $SUM == 11 ]] &&
       echo "Programma terminato con successo"
       exit || 
       echo "C'è stato un errore"
       exit 1

Per quanto riguarda gli scope delle variabili: lo scope come spiegato sopra è il grado di visibilità delle variabili, in questo caso x è visibile in tutte le funzioni, anche $SUM è visibile al di fuori della funzione. In Python per esempio, se ci troviamo in una funzione, possiamo dichiarare una variabile globale con la keyword global nome_var, in caso contrario la variabile ha scope locale, ovvero è visibile solo all’interno della funzione, o, se dichiarata con lo stesso nome non assume lo stesso valore. In C/C++ dobbiamo dichiarare una variabile statica al di fuori del main() per ottenere il risultato di “variabile visibile all’interno dello stesso file, ma non disponibile al di fuori di esso.”, altrimenti se vogliamo la “variabile/funzione visibile all’interno di tutto i file è necessario dichiarare la funzione con la keyword extern che stabilisce lo scope globale in tutti i file.”

Argomenti da linea di comando con $1, $2, $3…$@
è possibile passare argomenti da linea di comando utilizzando le variabili speciali $1, $2, $3… che funzionano allo stesso modo come con le funzioni.
Di un programma a linea di comando se ne può modificare l’esecuzione con queste variabili speciali che memorizzano gli argomenti passati ad esse.

#! /bin/sh

ARG_=$1
ARG_1=$2
ARG_2=$3
if [[ $ARG_ == "--somma" ]];then
        echo "Individuati gli argomenti:"
        for i in $@
        do
                echo "Parametro: $i"
        done
        echo "Somma: $(($ARG_1+$ARG_2))"
        exit
elif [[ $ARG_ == "--molt" ]]; then
        echo "Individuati gli argomenti:"
        for i in $@
        do
                echo "Parametro $i"
        done
        echo "Moltiplicazione: $(($ARG_1+$ARG_2))"
        exit
else
        echo "Richiedi --somma o --moltcome opzione, poi due numeri come argomento per la somma."
        exit 1
fi

In questo script assegno il valore di $1, $2, $3 rispettivamente a $ARG_, $ARG_1, $ARG_2 per comodità,

  • Nel parametro 1: viene richiesta la modalità di esecuzione, ovvero se il programma dovrà sommare o moltiplicare i due numeri dei parametri $2 e $3, se $1 non corrisponde né a –somma né a –molt il programma uscirà con ritorno 1 (non terminato con successo)
  • Negli altri 2: senza verifica – vengono richiesti 2 numeri, nel caso in cui $2 e $3 non fossero uguali a due numeri, il risultato è 0 ma verranno sempre individuati i due parametri indifferentemente dal valore

Quindi potrei facilmente eseguire tutto ciò:

./script.sh --somma 3 4

esegue la somma tra 3 e 4

./script.sh --molt 3 4 

esegue la moltiplicazione tra 3 e 4.
Vediamo che in ogni blocco if-elif-else si trova un blocco for-in con variabile $@: quel blocco prende i valori di $@: nella variabile sono memorizzati $@ tutti i valori di $1, $2, $3, $4… può essere infinita… il suo contenuto cambia in base ai valori passati da linea di comando, quindi quel blocco assegna a $i i valori di $@ che non è altro che un array, quindi stampa per ogni singolo valore una stringa nella quale è possibile vedere i valori contenuti in $@ (nel nostro caso $1, $2, $3). Ovviamente per quanto riguarda le funzioni: queste non assumono lo stesso valore di $1, $2 e così via se non glielo assegnate nei parametri, per ovviare a questo, basta utilizzare un altro valore qualsiasi.
Se voglio utilizzare gli stessi parametri della linea di comando utilizzo questo “stile”:

#! /bin/sh
ARG=$1

funct(){
     echo $1
}
funct $ARG

Quindi se eseguirò: ./script.sh ciao verrà stampato a schermo ciao
Altrimenti:

#! /bin/sh
ARG=$1
echo $ARG

funct(){
     echo $1
}
funct ciao


Creare delle librerie
Le librerie sono nient’altro che dei programmi che consentono ad altri programmi di effettuare determinate operazioni, esempio può essere libncurses, che ci consente di creare (se siamo developer) interfacce semi-grafiche grazie alle sue funzioni che richiameremo nel nostro source (C/C++), o ancora le librerie standard GNU/C++ che ci consentono di utilizzare gli stream per l’input e l’output, gtk per le interfacce grafiche, mechanize (python)…
Senza queste librerie non saremmo in grado di creare determinati programmi, o meglio potremmo… ma le dovremmo creare noi! Possiamo creare una “libreria” per gli shellscript e poi riutilizzarla in modo molto semplice, di solito un programmatore crea una sua libreria, per un programma grande, quindi vuole riutilizzare il codice, e/o vuole dividere il programma in moduli.

Chiudendo questa parentesi…
una libreria è un insieme di funzioni, quindi questo sarà il contenuto del file che chiameremo “lib.sh”

#! /bin/sh

doSomething(){
	echo "Ciao"
}

doSomma(){
	SOM=$(($1+$2))
	echo $SOM
	return $SOM
}

doMolt(){
	MOLT=$(($1*$2))
	echo $MOLT
	return $MOLT
}

ovviamente se lo eseguiamo non succede niente, il file contiene le funzioni che sono solamente state definite, ma non le richiama in nessun modo, e NON le deve richiamare, verranno richiamate più tardi dal programma vero e proprio. Ogni libreria deve avere un nome ben definito… chiamerei questo libcalculator.sh per definire qual è lo scopo di questa libreria.

#! /bin/sh

source "./lib.sh"

doSomething 
doSomma 1 2 
doMolt 2 3
exit 0 

Questo è invece il nostro solito script.sh
viene incluso il file ./lib.sh dalla directory corrente (se così non fosse stato avremmo dovuto scrivene il percorso) con l’istruzione source. È uguale a includere un file d’intestazione C/C++ in questo modo

#include 
#include "lib.h"

o un modulo Java con l’istruzione import

import java.lang.*;
import my.lib.hlib;

o ancora un modulo Python

import os
import miomodulo

Chiusa la parentesi, possiamo notare come script.sh sia più pulito e comprensibile, le funzioni verranno richiamate direttamente in script.sh con i relativi parametri. Avremmo potuto fare la stessa identica cosa includendo le funzioni nello stesso file e richiamandole direttamente. Ma sarebbe stato meno comprenibile. Questo invece garantisce una pulizia del codice ?

Tutti gli script sono stati testati e risultano funzionanti, a meno che ci sia un errore di copia/incolla da parte dell’autore o del lettore.

Spero che abbiate compreso come funziona una shell e gli shell script. Fatemi sapere nei commenti ?
Ovviamente questa non vuole essere un’introduzione e non un riferimento di studio ? Su Amazon ci sono molti libri gratuiti sullo shell scripting. Un aiuto, prendetela così
Comunque se avete già esperienze di programmazione siete già in grado di scrivere piccoli script, se di esperienza non ne avete vi consiglio di leggere l’articolo prima… poi studiare sui libri ? anche se leggete bene e capite ciò che ho spiegato brevemente sareste in grado di scrivere qualche script.
A presto!

Il post Shell Scripting: imparare le fondamenta in un’ora è stato pubblicato su InTheBit - Il Blog sulla Tecnologia che alimenta le tue passioni!.

Leggi il contenuto originale su InTheBit - Il Blog sulla Tecnologia che alimenta le tue passioni! » Linux

Written by: Stefano Belli