01 May 2024, 10:54 AM
01 May 2024, 10:54 AM

1. Terminale & Bash

Terminale

Il terminale (o terminal) è l’ambiente testuale di interazione con il sistema operativo.
Tipicamente è utilizzato come applicazione all’interno dell’ambiente grafico ed è possibile avviarne più istanze, pur essendo anche disponibile direttamente all’avvio (in questo caso normalmente in più istanze accessibili con la combinazione CTRL+ALT+Fx).

Shell

All’interno del terminale, l’interazione avviene utilizzando un’applicazione specifica in esecuzione al suo interno, comunemente detta SHELL.
Essa propone un prompt per l’immissione diretta di comandi da tastiera e fornisce un feedback testuale. È anche possibile eseguire sequenze di comandi preorganizzate contenute in file testuali (script o batch). A seconda della modalità (diretta/script) alcuni comandi possono avere senso o meno o comportarsi in modo particolare.
L’insieme dei comandi e delle regole di composizione costituisce un linguaggio di programmazione orientato allo scripting.

Bash

Esistono numerose shell. Bash è una delle più utilizzate e molte sono comunque simili tra loro, ma hanno sempre qualche differenza (e anche comandi analoghi possono avere opzioni o comportamenti non identici).
Tipicamente - almeno in sessioni non grafiche - al login un utente ha associata una shell particolare.
Un paio di alternative: zsh, ksh, sh, …

POSIX

Portable Operating System Interface for Unix: è una famiglia di standard IEEE. Nel caso delle shell in particolare definisce una serie di regole e di comportamenti che possono favorire la portabilità (che però dipende anche da altri fattori del sistema!).
La shell bash soddisfa molti requisiti POSIX, ma presenta anche alcune differenze ed “estensioni” per agevolare almeno in parte la programmazione (v. costrutti per confronti logici).

Comandi interattivi

La shell attende un input dall’utente e al completamento (conferma con INVIO) lo elabora.
Per indicare l’attesa mostra all’utente un PROMPT (può essere modificato). Fondamentalmente si individuano 3 canali:

Struttura generale comandi

Solitamente un comando è identificato da una parola chiave cui possono seguire uno o più “argomenti” opzionali o obbligatori, accompagnati da un valore di riferimento o meno (in questo caso hanno valore di “flag”) e di tipo posizionale o nominale. A volte sono ripetibili.
Esempio:

ls -alh /tmp

Gli argomenti nominali sono indicati con un trattino cui segue una voce (stringa alfanumerica) e talvolta presentano una doppia modalità di riferimento: breve (tipicamente voce di un singolo carattere) e lunga (tipicamente un termine mnemonico)

Esempio:

app -h app --help

Termini

Commenti

È anche possibile utilizzare dei commenti da passare alla shell. L’unico modo formale è l’utilizzo del carattere ‘#’ per cui esso e tutto ciò che segue fino al termine della riga è considerato un commento ed è sostanzialmente ignorato.

ls -la #-r this part is ignored

Alcuni comandi fondamentali

I comandi possono essere “builtins” (funzionalità intrinseche dell’applicazione shell utilizzata) o “esterni” (applicazioni eseguibili che risiedono su disco).

clear read
pwd file
ls chown
cd chmod
wc cp/mv
date help
cat type
echo grep
alias/unalias function
test

Canali in/out

Ogni comando lavora su un insieme di canali, come standard output o un file descriptor.

ls # mostra i file della cartella corrente
ls file # mostra il nome del file se esiste, altrimenti viene stampato un messaggio d’errore

Redirezionamento di base - 1

I canali possono essere redirezionati (anche in cascata):

# redireziono lo stream di output normale in modo che inserisca tutti i file della cartella corrente all'interno di out.txt
- ls 1>/tmp/out.txt 2>./tmp/err.txt # lo stream di output d'errore verrà redirezionato all'interno di err.txt

- ls not-existent-item 1>./tmp/all.txt 2>&1 # ls + qualsiasi file che non esiste genera un errore. Lo stream dell'output normale viene redirezionato in all.txt e lo stream di output d'errore verrà redirezionato sullo stream di output normale che a sua volta scrive in all.txt

# sono due esempi diversi, non si tiene in memoria ciò che viene fatto dal comando al passaggio precedente

Redirezionamento di base - 2

# Ad esempio il comando echo accetta in input tutto ciò che è testo, quindi posso dargli in input anche un file di testo
- ‘<’: command<file.txt invia l’input al comando (file.txt read-only):
Es: bc < input.txt #Dentro a input scriviamo 1 2 3 4 5 uno sotto l'altro e lasciamo una riga vuota alla fine
Es 2: mail -s "Subject" rcpt < content.txt (anziché interattivo)

- ‘<>’: come sopra ma file.txt è aperto in read-write (raramente usato)

- source > target : command 1>out.txt 2>err.txt
redireziona source su target:
	- source può essere sottinteso (vale 1)
	- target può essere un canale (si indica con &n, ad esempio &2)

- ‘>|: si comporta come > ma forza la sovrascrittura anche se bloccata nelle configurazioni

Redirezionamento di base - 3

type command 1>/dev/null 2>&1 (per sapere se command esiste)

# se per esempio ho un comando che sposta un file ed in output mi da un risultato, se non voglio tale output posso inviarlo a /dev/null che è come se fosse un buco nero (un file che si elimina subito)

Ambiente e variabili

Variabili di sistema

Alcune variabili sono impostate e/o utilizzate direttamente dal sistema in casi particolari. Se ne vedranno alcune caratteristiche degli script/batch, ma intanto in modalità diretta se ne usano già diverse:

Esecuzione comandi e $PATH

Quando si immette un comando (o una sequenza di comandi) la shell analizza quanto inserito (parsing) e individua le parti che hanno la posizione di comandi da eseguire: se sono interni ne esegue le funzionalità direttamente altrimenti cerca di individuare un corrispondente file eseguibile: questo è normalmente cercato nel file-system solo e soltanto nei percorsi definiti dalla variabile PATH a meno che non sia specificato un percorso (relativo o assoluto) nel qual caso viene utilizzato esso direttamente. Dall’ultima osservazione discende che per un’azione abbastanza comune come lanciare un file eseguibile (non “installato”) nella cartella corrente occorre qualcosa come: ./nomefile

Array

Variabili $$ e $?

Le variabili $$ e $? non possono essere impostate manualmente (la stessa sintassi lo impedisce dato che i nomi sarebbero $ e ? non utilizzabili normalmente):

Esecuzione comandi e parsing

Concatenazione comandi

È possibile concatenare più comandi in un’unica riga in vari modi con effetti differenti:

Operatori di piping (“pipe”): | e |&

La concatenazione con gli operatori di piping “cattura” l’output di un comando e lo passa in input al successivo:

- ls | wc -l : cattura solo stdout
- ls |& wc -l : cattura stdout e stderr
Nota

Il comando ls ha un comportamento atipico: il suo output di base è differente a seconda che il comando sia diretto al terminale o a un piping. Internamente sfrutta isatty(STDOUT_FILENO) (si approfondirà in seguito).

Subshell

	$( …comandi… ) oppure ` …comandi… `

La sintassi $() in Bash viene utilizzata per eseguire un comando in una subshell e restituire il suo output come argomento per un altro comando.

echo $(( $(cat $DATA) ))

In questo caso $(cat $DATA) restituisce il contenuto del file indicato dal valore di DATA (DATA=script.sh) e lo da in input al comando echo, il quale lo stamperà a video.

Attenzione

Ricorda che le parentesi tonde sono utilizzate anche per definire array!

Esempio di alcuni comandi e sostituzione subshell

echo "/tmp" > /tmp/tmp.txt ; ls $(cat /tmp/tmp.txt)
#Il comando echo stampa la stringa /tmp e l'operatore di reindirizzamento > scrive l'output del comando echo nel file specificato

#Il comando cat /tmp/tmp.txt legge il contenuto del file e lo passa come argomento al comando ls tramite la subshell $(...). In questo caso il contenuto del file è /tmp e quindi il comando ls elenca i file e le directory presenti nella directory /tmp

Per una cartella nella directory corrente:

echo "./nomecartella" > ./nomecartella/tmp.txt ; ls $(cat ./nomecartella/tmp.txt)

I comandi sono eseguiti rispettando la sequenza:

- echo "/tmp" > /tmp/tmp.txt crea un file temporaneo con "/tmp", dentro a tmp.txt è presente "/tmp"
- ls $(cat /tmp/tmp.txt) è prima eseguita la subshell:
	- cat /tmp/tmp.txt genera in stdout "/tmp" e poi con sostituzione:
	- ls /tmp mostra il contenuto della cartella /tmp

Dato che il valore di output della subshell è /tmp, anteponendo il $ sto richiedendo il valore di quella subshell. Dato che il valore è /tmp, facendo ls di /tmp ottengo gli elementi all'interno della cartella tmp.

Espansione aritmetica

Alcuni esempi:

(( a = 7 )) (( a++ )) (( a < 10 )) (( a = 3<10?1:0 ))
b=$((c+a))

Confronti logici - costrutti

I costrutti fondamentali per i confronti logici sono il comando test e i raggruppamenti tra parentesi quadre singole e doppie: test … , [ … ],

In tutti i casi il blocco di confronto genera il codice di uscita 0 in caso di successo, un valore differente (tipicamente 1) altrimenti.

Built-in e shell-keywords

I builtins sono sostanzialmente dei comandi il cui corpo d’esecuzione è incluso nell’applicazione shell direttamente (non sono eseguibili esterni) e quindi seguono sostanzialmente le “regole generali” dei comandi, mentre le shell-keywords sono gestite come marcatori speciali così che possono “attivare” regole particolari di parsing. Un caso esemplificativo sono gli operatori “<” e “>” che normalmente valgono come redirezionamento, ma all’interno di valgono come operatori relazionali.

Confronti logici - tipologia operatori

Le parentesi quadre singole sono POSIX-compliant, mentre le doppie sono un’estensione bash. Nel primo caso gli operatori relazionali “tradizionali” (minore-di, maggiore-di, etc.) non possono usare i termini comuni (<, >, etc.) perché hanno un altro significato [2] e quindi se ne usano di specifici che però hanno un equivalente più “tradizionale” nel secondo caso. Gli operatori e la sintassi variano a seconda del tipo di informazioni utilizzate: una distinzione sottile c’è per confronti tra stringhe e confronti tra interi.

Confronti logici - interi e stringhe

Interi
[ ... ] [ [ ... ] ]
uguale-a -eq ==
diverso-da -ne !=
minore-di
minore-o-uguale-a
-lt
-le
<
maggiore-di
maggiore-o-uguale-a
-gt
-ge
>
































Stringhe
[ ... ] [ [ ... ] ]
uguale-a = o ==
diverso-da !=
minore-di
(ordine alfabetico)
&lt; <
maggiore-di
(ordine alfabetico)
&gt; >
nota: occorre lasciare uno spazio prima e dopo i
“simboli” (es. non “=” ma “ = “)

Confronti logici - operatori unari

Esistono alcuni operatori unari ad esempio per verificare se una stringa è vuota o meno oppure per controllare l’esistenza di un file o di una cartella.
Alcuni esempi:

[[ -f /tmp/prova ]]     #: è un file?
[[ -e /tmp/prova ]]     #: file esiste?
[[ -d /tmp/prova ]]     #: è una cartella?
Attenzione

Sono essenziali gli spazi dopo [[ e prima di ]]. È possibile usare sia [ che [[

Confronti logici - negazione

Il carattere “!” (punto esclamativo) può essere usato per negare il confronto seguente.
Alcuni esempi:

[[ ! -f /tmp/prova ]]
[[ ! -e /tmp/prova ]]
[[ ! -d /tmp/prova ]]

SCRIPT/BATCH

È possibile raccogliere sequenze di comandi in un file di testo che può poi essere eseguito.

Richiamando il tool “bash” e passando il file come argomento, esempio:

bash file.sh #In questo modo viene eseguito file.sh, anche se lo script non ha il permesso di esecuzione.

Impostando il bit “x” e specificando il percorso completo, o solo il nome se la cartella è in $PATH, esempio:

chmod +x file.sh && ./file.sh

In questo caso chmod +x file.sh rende il file eseguibile (al posto di file.sh si può mettere anche il percorso).
Il comando ./file.sh serve per eseguire lo script.
Il simbolo +x serve per aggiungere il permesso di esecuzione al file.
Avendo inserito && i due comandi verranno eseguiti in sequenza e quindi il file verrà sia reso eseguibile che eseguito.

Esempio 1 - subshell e PID

Creiamo un file .sh con all'interno uno script da eseguire:

# bashpid.sh
echo $BASHPID #the id of the current bash process
echo $( echo $BASHPID)

$BASHPID memorizza l'ID del processo corrente e quindi se verrà chiamato nella shell principale restituirà quello del padre, mentre se viene chiamato in una subshell restituirà quello del figlio (valori diversi). Se invece utilizziamo $$ verrà sempre utilizzato l'ID del padre.

Eseguiamo ora il seguente comando:

chmod +x ./bashpid.sh ; echo $BASHPID ; ./bashpid.sh
# Indica il tipo di permesso (x indica scrittura). Il + vuol dire che gli è stato aggiunto (con il - si toglieva tale permesso). Aggiungere una lettera prima del simbolo indicava la categoria di utenti indirizzata.

Questo comando mostrerà prima il $BASHPID e poi eseguirà lo script.

Elementi particolari negli SCRIPT

Le righe vuote e i commenti (#) sono ignorati.
La prima riga può essere un metacommento (detto hash-bang, she-bang e altri nomi simili): #!application [opts] che identifica un’applicazione cui passare il file stesso come argomento (tipicamente usato per identificare l’interprete da utilizzare).

Sono disponibili variabili speciali in particolare

Altri costrutti

For loop:
for i in ${!lista[@]}; do
	echo ${lista[$i]}
done
While loop:
while  $i < 10 ; do
	echo $i ; (( i++ ))
done
If condition:
if [ $1 -lt 10 ]; then
	echo less than 10
elif [ $1 -gt 20 ]; then
	echo greater than 20
else
	echo between 10 and 20
fi

Questi costrutti possono essere anche scritti su una sola riga e usati in un terminale.

Esempio 2 - argomenti

Creiamo un file args.sh con all'interno uno script da eseguire:

#Indica all'interprete di shell di utilizzare l'interprete bash per eseguire lo script
#!/usr/bin/env bash
nargs=$# # salva il numero di argomenti passati nella variabile nargs
while [[ $1 != "" ]]; do # controlla se il primo argomento passato è vuoto
	echo "ARG=$1" # stampa il valore dell'argomento corrente
	shift # sposta gli argomenti di posizione, cioè fa diventare il secondo argomento il primo, il terzo argomento il secondo e così via, finché non ci sono più argomenti da processare
done

# Questo script stampa ogni argomento passato sulla linea di comando

Eseguiamolo ora provando a passare prima un argomento, poi tre.

chmod +x ./args.sh
./args.sh uno
./args.sh uno due tre

Indirezione di variabile

Per accedere al valore di una variabile il cui nome è contenuto in un'altra variabile è necessario usare la sintassi:

${!nome_variabile}

Esempio indirezione di variabile (script bash)

#!/bin/bash
var1="ciao"
var2="var1"
echo ${!var2} #stampa "ciao"

Esempio indirezione di variabile (linea di comando)

$ var1="ciao"
$ var2="var1"
$ echo ${!var2} #stampa "ciao"

Funzioni

La sintassi per definire una funzione è:

function function_name () { comandi; }

Per chiamare una funzione:

function_name arg1 arg2 #call the function with 2 args

Esempio completo

Creiamo un file chiamato script.sh e ci inseriamo dentro la funzione seguente:

#!/bin/bash
function_name () {
	local var1='C' #use a local scoped variable
	echo Input: $1 $2 #normal arguments variables
	return 44 #only return codes
}

Per rendere la funzione disponibile nella shell corrente dobbiamo eseguire il comando:

source script.sh

Da terminale possiamo chiamare la funzione, passando i parametri $1 e $2:

function_name 10 20

L'output, in questo caso, sarà:

Input 10 20

Conclusioni

L’utilizzo di BASH - tramite CLI o con SCRIPT - è basilare per poter interagire attraverso comandi con il file-system, con le risorse del sistema e per poter invocare tools e applicazioni.


  1. Lo “scope” è generalmente quello del processo attuale: anteponendo “export” si rende disponibile anche agli eventuali processi figli ↩︎

  2. Salvo eventualmente utilizzare il raggruppamento con doppie parentesi tonde per le espansioni aritmetiche. ↩︎