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:
- Standard input (tipicamente la tastiera), canale 0, detto stdin
- Standard output (tipicamente il video), canale 1, detto stdout
- Standard error (tipicamente il video), canale 2, detto stderr
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
- l’esecuzione dei comandi avviene “per riga” (in modo diretto quella che si immette fino a INVIO)
- un “termine” (istruzione, argomento, opzione, etc.) è solitamente una stringa alfanumerica senza spazi
- spaziature multiple sono solitamente valide come singole e non sono significative se non per separare termini
- è possibile solitamente usare gli apici singoli o doppi per forzare una sequenza come termine singolo
- gli spazi iniziali e finali di una riga collassano
- le righe vuote sono ignorate
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
- >> : si comporta come > ma opera un append se la destinazione esiste (l'inserimento non sovrascrive il file)
- <<: here-document, consente all’utente di specificare un terminatore testuale (ad esempio il ?), dopodiché accetta l’input fino alla ricezione di tale terminatore. Poi invia l'input appena digitato al comando.
- <<<: here-string, consente di fornire input in maniera non interattiva. Ciò che metto dopo viene utilizzato non appena verrà richiesto un input.
Esistono molte varianti e possibilità di combinazione dei vari operatori di redirezionamento.
Un caso molto utilizzato è la “soppressione” dell’output (utile per gestire solo side-effects, come ad esempio il codice di ritorno).
Ad esempio:
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
- La shell può utilizzare delle variabili per memorizzare e recuperare valori
- I valori sono generalmente trattati come stringhe o interi: sono presenti anche semplici array (vettori di elementi)
- Il formato del nome è del tipo
\^[_[:alpha:]][_[:alnum:]]*$
- Per set (impostare) / get (accedere) al valore di una variabile:
- Il set si effettua con
[export] variabile=valore
[1] - Il get si effettua con
\$variabile
o\${variabile}
(sostituzione letterale)
- Il set si effettua con
- La shell opera in un ambiente in cui ci sono alcuni riferimenti impostabili e utilizzabili attraverso l’uso delle cosiddette “variabili d’ambiente” con cui si intendono generalmente quelle con un significato particolare per la shell stessa (v. esempi di seguito)
- Tra le variabili d’ambiente più comuni troviamo ad esempio: SHELL, PATH, TERM, PWD, PS1, HOME
- Essendo variabili a tutti gli effetti si impostano ed usano come le altre.
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:
- SHELL: contiene il riferimento alla shell corrente (path completo)
- PATH: contiene i percorsi in ordine di priorità in cui sono cercati i comandi, separati da “:”
- TERM: contiene il tipo di terminale corrente
- PWD: contiene la cartella corrente
- PS1: contiene il prompt e si possono usare marcatori speciali
- HOME: contiene la cartella principale dell’utente corrente
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
- Definizione:
lista=("a" 1 "b" 2 "c" 3)
separati da spazi! - Output completo:
${lista[@]}
- Accesso singolo:
${lista[x]}
(0-based) - Lista indici:
${!lista[@]}
- Set elemento:
lista[x]=value
- Append:
lista+=(value)
- Sub array:
${lista[@]:s:n}
(from index s, length n)
Variabili $$ e $?
Le variabili $$ e $? non possono essere impostate manualmente (la stessa sintassi lo impedisce dato che i nomi sarebbero $ e ? non utilizzabili normalmente):
- $$ : contiene il PID del processo attuale
- $? : contiene il codice di ritorno dell’ultimo comando eseguito
Esecuzione comandi e parsing
- La riga dei comandi è elaborata con una serie di azioni “in sequenza” e poi rielaborata eventualmente più volte.
- Tra le azioni vi sono:
- Sostituzioni speciali della shell (es. “!” per accedere alla “history” dei comandi)
- Sostituzione variabili
- Elaborazione subshell
- Sono svolte con un ordine di priorità e poi l’intera riga è rielaborata (v. Subshell)
Concatenazione comandi
È possibile concatenare più comandi in un’unica riga in vari modi con effetti differenti:
comando1 ; comando2
concatenazione semplice: esecuzione in sequenzacomando1 && comando2
concatenazione logica “and”: l’esecuzione procede solo se il comando precedente non fallisce (codice ritorno zero)comando1 || comando2
concatenazione logica “or”: l’esecuzione procede solo se il comando precedente fallisce (codice ritorno NON zero)comando1 | comando2
concatenazione con piping (v. prox)
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
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
- È possibile avviare una subshell, ossia un sotto-ambiente in vari modi, in particolare raggruppando i comandi tra parentesi tonde: ( …comandi… )
- Spesso si usa “catturare” l’output standard (stdout) della sequenza che viene così sostituito letteralmente e rielaborato e si può fare in due modi:
$( …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.
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
- La sintassi base per una subshell non è da confondere con l’espansione aritmetica che utilizza le doppie parentesi tonde.
- All’interno delle doppie parentesi tonde si possono rappresentare varie espressioni matematiche inclusi assegnamenti e confronti.
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 … , [ … ], …
- test … e [ … ] sono built-in equivalenti
- … è una coppia di shell-keywords
In tutti i casi il blocco di confronto genera il codice di uscita 0 in caso di successo, un valore differente (tipicamente 1) altrimenti.
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) |
< | < |
maggiore-di (ordine alfabetico) |
> | > |
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?
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
- $@ : lista completa degli argomenti passati allo script
- $# : numero di argomenti passati allo script
- $0, $1, $2, … : n-th argomento
Altri costrutti
for i in ${!lista[@]}; do echo ${lista[$i]} done
while $i < 10 ; do echo $i ; (( i++ )) done
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.