INDICE
Introduzione
Per quanto riguarda il suo svolgimento, si raccomanda di tenere un terminale sempre aperto e provare man mano i vari comandi (con dei copia/incolla) e gli script proposti. Gli script possono essere scaricati dal seguente Link e sono stati suddivisi in sottocartelle corrispondenti ai diversi capitoli che utilizzano script. per semplicità, si consiglia di partire sempre dalla cartella ~/Materiali e di creare al suo interno le cartelle ed i file indicati durante lo svolgimento del corso.
Scompattare la cartella Materiali.tar
nella propria Home ed utilizzarla come cartella di lavoro per il corso
tar xf Materiali.tar
Reindirizzare lo stdin
Prima di cominciare, facciamo una breve introduzione allo standard input
. Un comando che fa uso dello standard input è cat
(che è l'abbreviazione di concatenare).
Di solito prende uno o più file come argomenti e ne mostra il contenuto sullo schermo, unendoli.
cat ls-output.txt ls-error.txt
Tuttavia, se non vengono forniti file come argomenti, legge semplicemente le righe dallo standard input (tastiera, per impostazione predefinita) e le scrive sullo standard output (schermo, per impostazione predefinita). Prova:
cat
Scrivete un paio di righe e poi premete Ctrl-d per dire a cat
che ha raggiunto la fine del file (EOF) sullo standard input .
Se si reindirizza l'output standard a un file, lo si può utilizzare per creare file brevi. Per esempio, prova questi:
cat > lazy_dog.txt
The quick brown fox jumped over the lazy dog.
Digitate alla fine "Ctrl-d" .
cat lazy_dog.txt
Per reindirizzare lo standard input (stdin) possiamo usare l'operatore di reindirizzamento "<
", come questo:
cat < lazy_dog.txt
Abbiamo cambiato la sorgente dello standard input dalla tastiera al file lazy_dog.txt
.
LEZIONE 1
In questa lezione inizieremo a conoscere alcuni semplici comandi e le loro opzioni, impareremo a navigare ed esplorare il sistema, ecc:
- Che cos'è la shell? Provare alcuni semplici comandi (
date
,cal
,df
,free
,exit
). - Navigazione (
pwd
,ls
,cd
). Nomi di file relativi e assoluti. - Esplorazione del sistema (
ls
,file
,less
). Elenco lungo delle directory. Struttura delle directory nei sistemi Linux.
Primi comandi
1) Visualizzare la data e l’ora correnti:
date
date +%Y-%m-%d
2) Visualizzare il calendario del mese corrente:
cal
Per un altro mese:
cal 5 2020
3) Controllare quanto spazio libero c'è sulle unità disco:
df
df -h
df -h /
4) Controllare quanto spazio libero c'è sulle unità disco:
free
free -h
Navigazione
Il comando cd
(change directory) viene utilizzato per spostarsi da una cartella ad un'altra. Il comando pwd
(print working directory) mostra la cartella di lavoro corrente. Il comando ls
(lista) mostra il contenuto della directory di lavoro corrente.
1) Visualizza la directory di lavoro corrente con pwd
(print working directory ):
pwd
2) Elenca il contenuto di una cartella:
ls /
ls /usr
ls -l /usr
3) Cambia la cartella di lavoro corrente:
cd /usr
pwd
ls
cd /usr/bin
pwd
Il percorso /usr/bin
è chiamato assoluto , poiché mostra il percorso completo, a partire dalla root (/
).
4) Andare alla cartella di un livello superiore:
cd ..
pwd
Due punti (..
) rappresentano il genitore della directory corrente.
Peraltro, un singolo punto (.
) rappresenta la directory corrente:
cd .
pwd
5) Utilizzare un percorso relativo:
cd bin
pwd
La cartella bin
iè relativa a quella corrente (in questo caso
/usr
).
6) Passare alla cartella corrente precedente:
cd /var/log
cd -
cd -
7) Vai alla directory home:
cd
cd ~
La tilde (~
) rappresenta la directory home dell'utente corrente.
Opzioni dei comandi
Vediamo alcune opzioni del comando ls
.
1) Elenca solo alcuni file:
ls /bin
ls /bin/b* /bin/c*
Vengono elencati solo i file nella directory /bin
che iniziano con b
e quelli che iniziano con c
.
2) Elenco lungo:
ls -l /bin/b* /bin/c*
L'opzione -l
sta per elencazione lunga, in cui ogni file è stampato sulla propria riga con maggiori dettagli.
3) Opzioni lunghe e corte.
Si noti che la colonna centrale mostra la dimensione del file (in byte). Per rendere le dimensioni più leggibili, si può usare l'opzione:
--human-readable
:
ls -l --human-readable /bin/b* /bin/c*
Al posto di questa opzione lunga si può usare l'equivalente breve -h
, che è più comodo da scrivere:
ls -l -h /bin/b* /bin/c*
Suggerimento: Per modificare il comando precedente, è possibile utilizzare il tasto freccia in alto della tastiera per visualizzare il comando precedente e poi usare i tasti freccia sinistra_ e freccia destra per spostare il cursore, modificare il comando e quindi premere [Enter].
4) Unire le opzioni brevi.
È anche possibile unire le opzioni brevi in questo modo:
ls -lh /bin/b* /bin/c*
Per impostazione predefinita i file sono elencati in ordine alfabetico, ma è possibile ordinarli per tempo di modifica, utilizzando l'opzione -t
:
ls -lht /bin/b* /bin/c*
5) Con l'opzione --reverse
o -r
si può invertire l'ordine di visualizzazione:
ls -lt --reverse /bin/b* /bin/c*
ls -lh --reverse /bin/b* /bin/c*
ls -ltr /bin/b* /bin/c*
ls -lhr /bin/b* /bin/c*
Di solito le opzioni hanno una versione lunga (come --reverse
o --human-readable
) e una breve (come -r
o -h
), ma non tutte. Per esempio, le opzioni -l
o -t
non hanno una versione lunga.
Sembra che le opzioni brevi siano più comode quando si scrivono i comandi. Secondo te, perché ci sono anche le opzioni lunghe? Perché potrebbero essere utili?
Esplorare il sistema
Per esplorare il sistema utilizziamo i seguenti passaggi:
1) Usare cd
per spostarsi in una cartella.
2) Elencare il contenuto della cartella con ls -l
.
3) Se si vede un file interessante, determinarne il contenuto con il comando file
.
4) Se sembra che si tratti di testo, provare a visualizzarlo con less
.
Proviamo alcuni di questi:
1) Spostati in /bin
ed elencane il contenuto:
cd /bin
ls -l
ls -l b*
ls -l bzless
2) Controlla il tipo di alcuni file e il loro contenuto:
file bzless
Il file bzless
è un link simbolico, una sorta di scorciatoia, o di alias, o un riferimento a un altro file. Esistono anche hard links , che vedremo più avanti.
ls -l bzmore
file bzmore
Il file bzmore
è uno script di shell e in realtà un file di testo, quindi possiamo leggerne contenuto:
less bzmore
Premi [Spazio] un paio di volte e poi esci con q
.
Gli Shell scripts sono come programmi e contengono comandi Linux.
Il comando less
visualizza il contenuto di un file di testo pagina per pagina.
Nota: Il comando less
è una sostituzione migliorata di un precedente comando Unix che si chiamava more
. Quindi, a volte si dice che: less
è more
. Oppure: less
è più o meno more
.
3) Controlliamo un altro file:
ls -lh bash
file bash
Il file bash
è un programma eseguibile, e un file binario (non testuale). Proviamo a leggerne il contenuto:
less bash
Usciamo con q
.
Come si vede, i file di testo hanno un contenuto leggibile dall'uomo, i file non-testuali (o file binari) hanno un contenuto che non è leggibile dall'uomo (ma può essere letto e interpretato da alcuni programmi).
4) Controlliamo /etc
:
file /etc
ls -l /etc/passwd
file /etc/passwd
Si tratta di testo semplice. Controlliamo il suo contenuto:
less /etc/passwd
Questo file contiene gli account del sistema.
I file in /etc
sono solitamente file di configurazione e quasi tutti sono file di testo (leggibili e scrivibili da esseri umani).
5) Al contrario, i file in /bin
sono programmi o comandi e sono per lo più file binari o script di shell. Lo stesso vale per /sbin
, /usr/bin
, /usr/sbin
, /usr/local/bin
, ecc.
ls /sbin
ls /usr/bin
ls /usr/sbin
ls /usr/local/bin
6) Altre cartelle importanti sono:
ls /boot
ls /boot/grub
contiene il kernel Linux, l'immagine iniziale del disco RAM, il boot loader, ecc.
ls /dev
file /dev/console
file /dev/vda
contiene i nodi dei dispositivi.
ls /home
contiene le directory home degli utenti.
ls /lib
ls /usr/lib
contiene le librerie condivise.
ls /proc
less /proc/cpuinfo
è una directory speciale che espone le impostazioni e lo stato del kernel stesso.
ls /var
ls /var/log
contiene dati che possono cambiare frequentemente (come i file di log).
ls /tmp
contiene dati temporanei che potrebbero essere cancellati a ogni riavvio.
LEZIONE 2
-
Manipolare file e directory (
cp
,mv
,mkdir
,rm
,ln
). Caratteri jolly, link simbolici e hard link. -
Lavorare con i comandi (
type
,which
,help
,man
,apropos
,info
,whatis
,alias
). -
Cronologia dei comandi e trucchi da tastiera (
clear
,history
,Ctrl+a
,Ctrl+e
,Ctrl+r
).
Manipolare file e directory
Per lavorare con i file e le directory si possono usare i seguenti comandi:
-
cp
- Copia file e directory -
mv
- Sposta/rinomina file e directory -
mkdir
- Creare directory -
rm
- Rimuove file e directory -
ln
- Crea collegamenti simbolici e hard link
Utilizziamoli in alcuni esempi.
1) Creare directory:
cd
mkdir playground
cd playground
mkdir dir1 dir2
ls -l
2) Copiare file:
cp /etc/passwd .
ls -l
Nota che .
è la directory di lavoro corrente. Abbiamo quindi copiato il file /etc/passwd nella directory corrente
cp -v /etc/passwd .
L'opzione -v
rende il comando verboso.
cp -i /etc/passwd .
L'opzione -i
rende il comando interattivo. Ciò significa che chiede conferma all'utente prima di eseguire qualsiasi azione potenzialmente distruttiva. Premi y
or n
per continuare.
3) Spostare e rinominare i file:
mv passwd fun
ls -l
mv fun dir1
ls -l
ls -l dir1
mv dir1/fun dir2
ls -l dir1
ls -l dir2
mv dir2/fun .
ls -lR
mv fun dir1
mv dir1 dir2
ls -lR
ls -l dir2/dir1
mv dir2/dir1 .
mv dir1/fun .
ls -lR
4) Creare hard link:
ln fun fun-hard
ln fun dir1/fun-hard
ln fun dir2/fun-hard
ls -lR
Si noti che il secondo campo nell'elenco di fun
e fun-hard
è 4
, che indica il numero di hard link per il file. Gli hard link sono come nomi diversi per lo stesso contenuto del file.
Per essere sicuri che tutti e quattro siano lo stesso file, proviamo l'opzione -i
:
ls -lRi
Si può notare che il numero della prima colonna è lo stesso per tutti i file. Questo numero è chiamato numero di inode di un file e può essere considerato come l'indirizzo in cui si trova il file. Poiché è lo stesso per tutti i file, è dimostrato che si tratta effettivamente dello stesso file.
5) Creare collegamenti simbolici:
ln -s fun fun-sym
ls -l
I collegamenti simbolici sono un tipo speciale di file che contiene un puntatore di testo al file o alla cartella di destinazione. Sono stati creati per ovviare a due svantaggi degli hard link:
1) gli hard link non possono estendersi su dispositivi fisici
2) gli hard link non possono fare riferimento a directory, ma solo a file.
ln -s ../fun dir1/fun-sym
ln -s ../fun dir2/fun-sym
ls -lR
Questi due esempi possono sembrare un po' difficili da capire, ma ricordiamo che, quando creiamo un collegamento simbolico, stiamo facendo una descrizione testuale di dove si trova il file di destinazione, relativamente al collegamento simbolico.
E' possibile utilizzare anche percorsi assoluti di file quando si creano collegamenti simbolici:
ln -sf /root/playground/fun dir1/fun-sym
ls -l dir1/
Tuttavia, nella maggior parte dei casi, l'uso di nomi di percorso relativi è più auspicabile perché permette di rinominare un albero di directory contenente collegamenti simbolici e ai loro file di riferimento di essere rinominati e/o spostati senza interrompere i collegamenti.
Oltre ai normali file, i collegamenti simbolici possono fare riferimento anche a directory:
ln -s dir1 dir1-sym
ls -l
6) Rimuovere file e directory.
Ripuliamo un po' il campo di gioco. Per prima cosa cancelliamo uno degli hard link:
rm fun-hard
ls -l
Si noti che il numero di collegamenti per fun
è ridotto da 4 a 3 (come indicato nel secondo campo dell'elenco delle directory).
rm -i fun
Premere y
per confermare.
ls -l
less fun-sym
Il collegamento simbolico è ora interrotto.
rm fun-sym dir1-sym
ls -l
Quando si rimuove un collegamento simbolico, la destinazione non viene toccata.
rm -r dir1/
cd ..
rm -rf playground/
Alcuni comandi sui comandi
1) Il comando type
visualizza il tipo di comando:
type type
type ls
type cp
Il comando cp
è un programma eseguibile che si trova in /usr/bin/cp
.
2) Il comando which
visualizza la posizione di un eseguibile:
which ls
which cd
Il comando cd
non è un eseguibile, ma un comando incorporato nella shell.
type cd
3) Il comando help
visualizza una pagina di aiuto per i comandi integrati nella shell
help cd
help mkdir
Il comando mkdir
non è incorporato nella shell.
4) L'opzione --help
visualizza informazioni sull'uso:
mkdir --help
5) Il comando man
visualizza la pagina di manuale di un programma:
man ls
Le pagine di manuale sono organizzate in diverse sezioni, dove la sezione 1, per esempio, riguarda i comandi utente e la sezione 5 riguarda i formati dei file. Quindi, questi due comandi visualizzeranno pagine di manuale diverse:
man passwd
man 5 passwd
6) Il comando info
è un altro modo per visualizzare le pagine del manuale:
info coreutils
info passwd
7) Il comando apropos
visualizza i comandi appropriati:
apropos passwd
Ciò equivale a:
man -k passwd
Effettua una semplice ricerca nelle pagine man per il termine "passwd".
8) Il comando whatis
visualizza una breve descrizione di un comando:
whatis ls
9) Il comando alias
è usato per creare nuovi comandi.
alias --help
alias
type alias
type ls
cd /usr; ls; cd -
type foo
alias foo="cd /usr; ls; cd -"
type foo
foo
unalias foo
type foo
La cronologia (history) dei comandi
1) La cronologia dei comandi digitati è conservata in ~/.bash_history
:
echo $HISTFILE
ls $HISTFILE
Bash mantiene l'elenco dei comandi nella memoria interna durante l'esecuzione e lo scrive nel file della cronologia all'uscita. Diciamo a Bash di aggiungere subito l'elenco dei comandi al file della cronologia:
history -a
ls $HISTFILE
tail $HISTFILE
less $HISTFILE
2) Anche il comando history
può essere usato per visualizzare l'elenco della cronologia:
history
history | less
history | tail
history | tail -n 20
history | grep man
3) Possiamo rieseguire un comando precedente in questo modo:
!67
esegue nuovamente il comando che ha il numero indicato.
!ls
esegue nuovamente l'ultimo comando che inizia con ls
.
!?passwd
Esegue nuovamente l'ultimo comando che contiene passwd
.
history | grep passwd
4) Possiamo richiamare i comandi precedenti anche premendo più volte il tasto freccia in alto.
5) Tuttavia, il modo più utile per eseguire i comandi precedenti è quello di cercare interattivamente con le scorciatoie da tastiera.
Ad esempio, digitando Ctrl-r si avvia una ricerca incrementale inversa.
È inversa perché cerca a ritroso nell'elenco della cronologia, partendo dall'ultimo comando. Mentre si inizia a digitare il testo di ricerca, viene visualizzato l'ultimo comando che corrisponde ad esso.
Se siamo soddisfatti possiamo premere Invio
per eseguirla di nuovo, o possiamo usare le frecce destra e sinistra per modificarlo e poi premere [Invio] per eseguirlo.
Altrimenti si può continuare a premere "Ctrl-r" per ottenere il comando, e così via.
Proviamo:
Premi Ctrl-r.
2) Digita pass.
3) Premi Ctrl-r di nuovo.
4) Premi Ctrl-r di nuovo.
5) Premi Invio.
Trucchi da tastiera
Nella sezione precedente abbiamo visto che possiamo cercare dei comandi nella cronologia e richiamare uno dei comandi precedenti premendo Ctrl-r. Possiamo anche usare la freccia su e la freccia giù per selezionare uno dei comandi precedenti, e freccia sinistra e freccia destra per spostarsi mentre si edita un comando.
Altre combinazioni di tasti che possono essere utili durante la modifica dei comandi sono:
- Ctrl-a -- Sposta il cursore all'inizio della riga.
- Ctrl-e -- Sposta il cursore alla fine della riga.
- Alt-f -- Sposta il cursore in avanti di una parola.
-
Alt-b -- Sposta il cursore indietro di una parola.
-
Ctrl-l -- Cancella lo schermo e sposta il cursore in alto. Come il comando
clear
. -
Ctrl-k -- Cancella (taglia) il testo dalla posizione del cursore alla fine della riga.
-
Ctrl-u -- Taglia il testo dalla posizione del cursore all'inizio della riga.
-
Ctrl-d -- Cancella il carattere nella posizione del cursore.
-
Alt-d -- Taglia il testo dalla posizione del cursore alla fine della parola corrente.
-
Ctrl-y -- Strappa (incolla) il testo dal kill-ring e lo inserisce nella posizione del cursore..
Scrivete un comando e provate a utilizzare alcune di queste combinazioni di tasti.
La shell può anche aiutarci con il completamento, se si preme il tasto TAB
durante la digitazione di un comando. Per esempio, provate:
-
ls /etc
-
Ora digitate
ls /etc/n
(senza premere Invio) e premete TAB. Premete TAB una seconda volta. La shell ci mostra i possibili completamenti del comando che stiamo digitando. -
Continuate ad aggiungere altre lettere e a premere TAB (forse due o più volte) dopo ogni lettera. Quando il completamento è unico, premete Invio.
LEZIONE 3
Linux, come UNIX, è un sistema multitasking e multiutente. Questo significa che il sistema può eseguire più programmi allo stesso tempo, e può essere utilizzato da più utenti contemporaneamente. Una delle sfide di un sistema di questo tipo è che deve identificare gli utenti e proteggere gli utenti l'uno dall'altro.
-
Utenti e gruppi, permessi ( useradd, passwd, id, chmod, umask, su, sudo, chown, chgrp).
-
Processi e segnali (ps, pstree, top, jobs, bg, fg, kill, killall, shutdown).
Proprietà e permessi
Nel modello di sicurezza di Unix, un utente può essere proprietario di file e directory. Quando un utente è proprietario di un file o di una directory, ha il controllo sull'accesso (decide chi può accedervi). Per facilitare la concessione di permessi, gli utenti possono appartenere a uno o più gruppi. Se il proprietario di un file file concede i permessi a un gruppo, tutti i membri del gruppo hanno accesso al file. Oltre a concedere l'accesso a un gruppo, un proprietario può concedere alcuni diritti di accesso a tutti, che in termini Unix vengono chiamati others (altri).
1) Per prima cosa creiamo un nuovo utente:
useradd --help
useradd -m -s /bin/bash user1
ls /home/
ls -al /home/user1/
L'opzione -m
indica di creare una home directory
per l'utente, che per impostazione predefinita si trova in /home/
, mentre l'opzione -s
indica che shell usare per questo utente.
Dobbiamo anche impostare una password per user1
:
passwd user1
2) Passiamo a questo utente e proviamo alcuni comandi:
su -l user1
pwd
whoami
id
Quando viene creato un account utente, il sistema gli assegna un numero chiamato user ID o uid, che viene associato a un nome utente. A ogni utente viene assegnato anche un ID di gruppo primario (o gid) e può appartenere ad altri gruppi.
3) Gli account utente sono definiti in /etc/passwd
e i gruppi in /etc/group
.
Invece le password degli utenti sono memorizzate in /etc/shadow
:
ls -l /etc/passwd
file /etc/passwd
less /etc/passwd
Si può notare che oltre ai normali utenti ci sono anche alcuni utenti di sistema, tra cui il superutente (o root), con uid=0.
ls -l /etc/group
file /etc/group
less /etc/group
ls -l /etc/shadow
file /etc/shadow
less /etc/shadow
L'utente normale non ha il permesso di vedere il contenuto di questo file.
3) Quando si usa il comando ls -l
, la prima colonna dell'output (quella con i trattini) mostra gli attributi del file.
> foo.txt
ls -l foo.txt
ls -al
Il primo carattere degli attributi mostra il tipo di file. Se questo carattere è un -
, si tratta di un file normale, d
è per una directory, l
per un link simbolico, c
per un file speciale di caratteri
(ad esempio una tastiera o una scheda di rete) e b
per un file speciale di blocco (come un disco rigido o la RAM).
I restanti 9 caratteri indicano i diritti di accesso per il proprietario del file, il gruppo del file e il resto degli utenti.
Sono rwx
per l'utente, rwx
per il gruppo e rwx
per gli altri, dove r
sta per lettura (visualizzazione del contenuto del file), w
sta per scrittura (modifica del contenuto del file)
e x
è per esecuzione (eseguire il file come un programma o uno script).
Se c'è un meno (o un trattino) al posto di r
, w
o x
, significa che manca il diritto corrispondente.
Per le directory, l'attributo x
consente di entrare in una directory (ad esempio, cd directory
). L'attributo r
permette di elencare il contenuto di una directory (con ls
),
ma solo se è impostato anche l'attributo x
. L'attributo w
permette di creare, cancellare e rinominare i file all'interno di una directory, se è impostato anche l'attributo x
.
4) È possibile modificare i permessi di un file o di una directory con chmod
.
Solo il proprietario e il superutente possono modificare i permessi di un file o di una directory.
ls -l foo.txt
chmod 600 foo.txt
ls -l foo.txt
In questo caso stiamo usando la notazione ottale per dire a chmod
quali permessi impostare. Per esempio 7 (111
) è per rwx
, 6
(110
) è per rw-
, 5 (101
) è per r-x
, 4 (100
) è per r--
e 0 (000
) è per ---
(nessun permesso).
Possiamo anche usare la notazione simbolica con chmod
, dove u (utente) rappresenta il proprietario, g rappresenta il gruppo e o
(altri) rappresenta il mondo. Esiste anche il simbolo a (all) che è una combinazione di u, g e o.
-
Aggiungere il permesso esecuzione all'utente:
chmod u+x foo.txt
ls -l foo.txt
-
Rimuovere il permesso esecuzione dall'utente:
chmod u-x foo.txt
ls -l foo.txt
-
Aggiungere esecuzione all'utente. Il gruppo e gli altri dovrebbero avere solo i permessi di lettura ed esecuzione:
chmod u+x,go=rx foo.txt
ls -l foo.txt
-
Rimuovere il permesso esecuzione da tutti:
chmod ugo-x foo.txt
chmod a-x foo.txt
chmod -x foo.txt
ls -l foo.txt
5) Il comando umask
controlla i permessi predefiniti assegnati a un file quando viene creato:
umask
Questa notazione ottale indica quali bit saranno mascherati (rimossi) dagli attributi di un file.
rm -f foo.txt
> foo.txt
ls -l foo.txt
Il motivo per cui gli altri non hanno il permesso w
è dovuto alla maschera.
Ricordate che il numero 2 in ottale si scrive come 010
, quindi i permessi espressi da esso sono -w-
.
Ciò significa che il permesso w
per gli altri sarà rimosso dagli attributi.
Cambiamo la maschera e riproviamo:
rm foo.txt
umask 0000
> foo.txt
ls -l foo.txt
Ripristiniamo l'umask normale:
umask 0002
6) Il comando chown
può essere usato per cambiare il proprietario e/o il gruppo di un file. Proviamo:
chown root: foo.txt
whoami
Per utilizzarlo sono necessari i privilegi di superutente.
Usciamo dalla shell di utente_normale
e riproviamo come utente root
:
exit
whoami
chown root: /home/user1/foo.txt
ls -l /home/user1/foo.txt
chown user1:root /home/user1/foo.txt
ls -l /home/user1/foo.txt
Esempio con i permessi
In questo esempio verrà creata una directory condivisa tra gli utenti "bill" e "karen", dove potranno memorizzare i loro file musicali.
1) Creiamo prima questi utenti:
useradd -m -s /bin/bash bill
ls /home/
useradd -m -s /bin/bash karen
ls /home/
tail /etc/passwd
2) Dobbiamo anche creare un gruppo per questi due utenti:
groupadd music
tail /etc/group
addgroup bill music
addgroup karen music
tail /etc/group
3) Ora creiamo una directory:
mkdir -p /usr/local/share/Music
ls -ld /usr/local/share/Music
Per rendere questa directory condivisibile dobbiamo cambiare la proprietà del gruppo e i permessi del gruppo per consentire la scrittura:
chown :music /usr/local/share/Music
chmod 775 /usr/local/share/Music
ls -ld /usr/local/share/Music
Ora abbiamo una directory di proprietà di root
che consente l'accesso in lettura e scrittura al gruppo music
.
Gli utenti bill
e karen
sono membri del gruppo music
, quindi possono creare file in questa directory.
Gli altri utenti possono solo elencare i contenuti della cartella ma non possono creare file.
4) Ma abbiamo ancora un problema. Proviamo a creare un file come utente bill
.
Per prima cosa apriamo un'altra scheda del terminale:
Accedere come utente bill
:
su -l bill
Ora creiamo un file vuoto, solo per prova:
> /usr/local/share/Music/test
ls -l /usr/local/share/Music
Il gruppo del file creato è bill
(che è il gruppo primario dell'utente bill
).
In realtà vogliamo che il gruppo del file creato sia music
, altrimenti karen
non sarà in grado di accedervi correttamente.
Si può risolvere questo problema andando nella prima scheda del terminale (dove si sta lavorando come root) e dando questo comando:
chmod g+s /usr/local/share/Music
ls -ld /usr/local/share/Music
Quando abbiamo parlato di permessi non abbiamo menzionato il permesso speciale s. Quando si concede questo permesso al gruppo di una directory, i file creati in questa directory apparterranno allo stesso gruppo della directory.
Proviamo a creare un altro file di prova dalla seconda scheda del terminale, come utente `bill'.
> /usr/local/share/Music/test_1
ls -al /usr/local/share/Music
Processi
Un processo è un programma che viene eseguito dal sistema. Linux è un sistema multitasking, il che significa che può eseguire molti processi contemporaneamente. In realtà, se c'è solo un processore, solo un processo può essere eseguito in un determinato momento. Tuttavia il kernel Linux è in grado di passare rapidamente da un processo all'altro, permettendo a ciascuno di essi di girare per un breve periodo e, poiché questo avviene molto velocemente, dà l'impressione che tutti i programmi siano in esecuzione in parallelo.
Un processo in Linux viene avviato da un altro processo, quindi ogni processo ha un genitore e può avere dei figli.
Solo il processo init
non ha un genitore perché è il primo processo avviato dal kernel dopo il caricamento.
1) Possiamo usare il comando ps
per elencare i processi:
ps
Mostra solo i processi associati alla sessione corrente del terminale. corrente. TTY è l'abbreviazione di Teletype e si riferisce al terminale del processo. Ogni processo ha un PID (numero identificativo del processo).
ps a
Mostra tutti i processi associati a qualsiasi terminale. TIME mostra la
tempo di CPU consumato da un processo. STAT mostra lo stato del
del processo, dove S
sta per sleeping, R
sta per running, ecc.
ps au
Mostra anche l'utente (il proprietario del processo), la percentuale di RAM e CPU utilizzata da un processo.
ps --help
ps --help simple
ps aux | less
Questo mostra tutti i processi. Si noti che il processo numero 1 è /sbin/init
.
2) Un altro comando per elencare i processi è pstree
:
pstree
Con -p
mostra anche i PID:
pstree -p | less
Con un nome utente mostra solo i processi di quell'utente:
pstree -p bill
Con un PID mostra solo il ramo dei processi che iniziano da quel processo:
pstree -p 700
3) Il comando top
mostra una visualizzazione dinamica dei processi che viene aggiornata periodicamente:
top
La prima parte della visualizzazione mostra un riepilogo del sistema e la seconda parte mostra un elenco dei processi, con quelli più attivi (quelli che consumano più RAM, CPU e altre risorse) in cima . Premere `q' per uscire.
4) Quando si dà un comando nel terminale, la shell avvia un nuovo processo, e attende che questo processo sia terminato, prima di restituire il prompt. Ad esempio, avviamo un processo che richiede molto tempo:
sleep 100
Il processo aspetterà per circa 100 secondi. Per interrompere un comando che richiede troppo tempo si può premere Ctrl-c.
Se al comando viene aggiunto una e commerciale (&), la shell eseguirà questo comando in background. Ciò significa che non aspetterà che il comando sia terminato e restituirà immediatamente il prompt, in modo da poter eseguire altri comandi:
sleep 200 &
ps
jobs
Un comando in background è chiamato job. Possiamo spostare uno dei job in primo piano con il comando fg
:
fg %1
Se non si fornisce un numero di job come argomento, si assume il primo job.
Ora che il lavoro sleep
è in esecuzione nel terminale, possiamo interromperlo con Ctrl-c.
Se un comando sta impiegando troppo tempo, possiamo anche interromperlo con Ctrl-z e poi avviarlo in background. Per esempio:
sleep 200
Ora lo fermalo con Ctrl-z. Poi spostalo in background con bg
:
jobs
bg %1
jobs
5) Possiamo inviare segnali a un processo con il comando kill
:
sleep 100 &
ps
kill $!
ps
La variabile speciale $!
contiene il PID dell'ultimo processo in background.
Per impostazione predefinita, kill
invia il segnale termina ((15 o TERM), che chiede al processo di terminare dolcemente.
Il segnale interrupt (2 o INT) è lo stesso segnale che viene inviato da Ctrl-c. Di solito il programma termina.
sleep 100 &
ps
kill -2 $!
ps
Il segnale stop (19 o STOP) non viene inviato al processo. Al contrario, il kernel mette in pausa il processo, senza terminarlo (come Ctrl-z per un processo in primo piano).
sleep 300 &
jobs
kill -STOP $!
jobs
Il segnale continue (18 o CONT) ripristina un processo dopo che è stato messo in pausa con STOP.
kill -CONT $!
jobs
Anche il segnale kill (9 o KILL) non viene indirizzato al processo. Al contrario, il kernel termina il processo immediatamente. Questo è solitamente usato come ultima risorsa se il processo non risponde agli altri segnali.
kill -SIGKILL $!
jobs
Esistono anche molti altri segnali, come si può vedere lanciando:
kill -l
6) Il comando killall
può inviare un segnale a più processi che corrispondono a un dato programma o nome utente:
sleep 100 &
sleep 200 &
sleep 300 &
jobs
ps
killall sleep
jobs
ps
7) Se si dispone dei permessi di superutente, si possono provare anche questi comandi per spegnere o riavviare il sistema:
halt
poweroff
reboot
shutdown -h now
shutdown -r now
LEZIONE 4
I comandi e i programmi in Linux di solito producono un output. Questo output è di due tipi:
1) I risultati del programma, ovvero i dati che il programma è stato progettato per produrre
2) I messaggi di stato e di errore, che indicano come il programma sta procedendo.
Di solito i programmi inviano i risultati allo standard_output (o stdout) e i messaggi di stato allo standard error (stderr). Per impostazione predefinita, sia stdout che stderr sono collegati allo schermo (display del computer).
Inoltre, molti programmi ricevono input da una struttura chiamata standard input (stdin), che per impostazione predefinita è collegata alla tastiera.
Il reindirizzamento dell'I/O ci permette di cambiare la destinazione dell'output e dell'input. Normalmente, l'output va allo schermo e l'input proviene dalla tastiera, ma con il reindirizzamento I/O si può cambiare questa situazione.
Si possono anche concatenare diversi comandi in una pipeline, dove l'output di un comando viene inviato come input di un altro. Questa è una caratteristica potente, che ci permette di eseguire operazioni complesse sui dati combinando semplici utilità.
Reindirizzare stdout e stderr
1) Per reindirizzare lo standard output a un file si può usare l'operatore di reindirizzamento ">
".
ls -l /usr/bin
ls -l /usr/bin > ls-output.txt
ls -l ls-output.txt
less ls-output.txt
2) Proviamo a fare lo stesso esempio con una directory che non esiste:
ls -l /bin/usr
ls -l /bin/usr > ls-output.txt
ls
non invia i messaggi di errore allo standard output.
ls -l ls-output.txt
Il file ha lunghezza zero.
less ls-output.txt
L'operatore di reindirizzamento >
ha cancellato il contenuto precedente del file.
Infatti, se mai dovessimo avere bisogno di cancellare il contenuto di un file o di creare un nuovo file vuoto, potremmo usare un trucco come questo:
> ls-output.txt
3) Per aggiungere l'output rediretto al contenuto esistente del file, invece di sovrascriverlo, si può usare l'operatore:
>>
:
ls -l /usr/bin >> ls-output.txt
ls -lh ls-output.txt
ls -l /usr/bin >> ls-output.txt
ls -lh ls-output.txt
ls -l /usr/bin >> ls-output.txt
ls -lh ls-output.txt
Si noti che la dimensione del file cresce ogni volta.
4) Per reindirizzare lo stderr si possono usare gli operatori "2>
" e "2>>
". In Linux, lo standard output ha il descrittore di file (numero di filestream ) 1, e lo standard error ha il descriptor del file 2.
La sintassi è simile a quella del reindirizzamento di stdout.
ls -l /bin/usr 2> ls-error.txt
ls -l ls-error.txt
less ls-error.txt
5) Possiamo reindirizzare sia stdout che stderr allo stesso file, in questo modo:
ls -l /bin/usr > ls-output.txt 2>&1
Il reindirizzamento 2>&1
reindirizza il descrittore di file 2
(stderr) al descrittore di file 1
(stdout).
Ma prima abbiamo reindirizzato lo stdout a ls-output.txt
, quindi sia lo stdout che lo stderr saranno rediretti a questo file.
Si noti che l'ordine dei reindirizzamenti è significativo. Proviamo in questo modo:
ls -l /bin/usr 2>&1 >ls-output.txt
In questo caso si reindirizza il descrittore di file 2
(stderr) al descrittore di file 1
, che è già lo schermo,
e poi redirigiamo il descrittore di file 1
(stdout) al file. Quindi, i messaggi di errore saranno ancora inviati allo schermo e non al file.
Una scorciatoia per reindirizzare sia stdout che stderr allo stesso file è usare "&>
":
ls -l /bin/usr &> ls-output.txt
Per aggiungere al file si può usare "&>>
":
ls -l /bin/usr &>> ls-output.txt
ls -lh ls-output.txt
ls -l /bin/usr &>> ls-output.txt
ls -lh ls-output.txt
6) Per eliminare l'output o i messaggi di errore di un comando, possiamo inviarli a /dev/null
:
ls -l /bin/usr 2> /dev/null
Pipeline e filtri
1) Utilizzando l'operatore pipe "|
" (barra verticale), lo standard_output (stdout) di un comando può essere piped allo standard input (stdin) di un altro comando.
Si tratta di una potente funzione che permette operazioni complesse sui dati combinando semplici utilità. Vediamo alcuni esempi:
ls -l /usr/bin
ls -l /usr/bin | less
2) Possiamo ordinare i dati con sort
:
ls /bin /usr/bin
ls /bin /usr/bin | sort | less
3) uniq
può omettere o segnalare linee ripetute:
ls /bin /usr/bin | sort | uniq | less
Se invece vogliamo vedere l'elenco dei duplicati possiamo usare l'opzione l'opzione -d
:
ls /bin /usr/bin | sort | uniq -d | less
4) wc
conta le righe, le parole e i byte dell'input:
wc ls-output.txt
cat ls-output.txt | wc
Se vogliamo che mostri solo le righe, possiamo usare l'opzione -l
:
ls /bin /usr/bin | sort | wc -l
ls /bin /usr/bin | sort -u | wc -l
ls /bin /usr/bin | sort | uniq -d | wc -l
5) grep
stampa le righe che corrispondono a un determinato schema:
ls /bin /usr/bin | sort -u | grep zip
ls /bin /usr/bin | sort -u | grep zip | wc -l
L'opzione -v
mostra le righe che non corrispondono allo schema:
ls /bin /usr/bin | sort -u | grep -v zip
ls /bin /usr/bin | sort -u | grep -v zip | wc -l
L'opzione -i
può essere usata se si vuole che grep ignori le maiuscole e le minuscole durante la ricerca (ricerca sensibile alle maiuscole e alle minuscole).
6) head
/ tail
stampano le prime o le ultime righe dell'input:
ls /usr/bin > ls-output.txt
head ls-output.txt
tail ls-output.txt
tail -n 5 ls-output.txt
tail -5 ls-output.txt
ls /usr/bin | head -n 5
tail /var/log/syslog -n 20
tail /var/log/syslog -f
L'opzione -f
fa sì che segua le ultime modifiche del file in tempo reale. Premere "Ctrl-c" per terminarlo.
7) tee
invia il suo input sia allo stdout che a un file :
ls /usr/bin | tee ls.txt | grep zip
ls -l ls.txt
less ls.txt
LEZIONE 5
-
Ogni volta che digitiamo un comando e premiamo il tasto Invio,
bash
esegue una sorta di elaborazione del testo prima di eseguire il comando. Ad esempio, se nel comando è presente un "*
", questo viene viene sostituito dai nomi dei file corrispondenti. Questa sostituzione è chiamata espansione. -
La citazione è usata per controllare come la shell divide l'input in parti. Inoltre, disabilita alcuni tipi di espansione. Si possono usare sia apici singoli che virgolette doppie e ci sono alcune differenze tra di esse.
-
L'ambiente della shell (printenv, set, export). Personalizzazione del prompt.
Espansione della shell
1) Espansione con caratteri jolly:
echo this is a test
echo
visualizza tutti gli argomenti che gli vengono passati.
cd /usr
ls
echo *
Il carattere "*
" significa che corrisponde a qualsiasi carattere di un nome di file.
La shell espande il carattere "*
" prima di eseguire il comando echo
.
echo lib*
echo *bin
echo lib*32
echo */share
echo /*/*/bin
Il punto interrogativo "?
" corrisponde a qualsiasi carattere singolo:
echo lib??
echo lib???
I set di caratteri sono racchiusi tra parentesi quadre:
echo lib[123456789]?
echo lib[xyz][123456789]?
Intervalli di caratteri:
echo lib[1-9][1-9]
echo lib[a-z][1-9][1-9]
echo lib[1-9][!2]
Classi di caratteri:
echo lib[[:digit:]][[:digit:]]
echo lib[[:alpha:]][[:digit:]][[:digit:]]
echo lib[[:alnum:]][[:alnum:]][[:alnum:]]
echo lib[![:digit:]]*
echo lib[36[:lower:]]*
echo /etc/[[:upper:]]*
2) Il carattere tilde ("~
") si espande nella home directory:
echo ~
echo ~root
3) Espansione aritmetica:
echo $((2 + 2))
echo $(($((5**2)) * 3))
echo $(((5**2) * 3))
echo Cinque diviso due è uguale a $((5/2))
echo con il resto di $((5%2))
4) Espansione della parentesi graffa:
echo Fronte-{A,B,C}-Dietro
echo Numero_{1..5}
echo {01..15}
echo {001..15}
echo {Z..A}
echo a{A{1,2},B{3,4}}b
cd
mkdir Foto
cd Foto
mkdir {2017..2019}-{01..12}
ls
5) Espansione delle variabili:
echo $USER
printenv | less
echo $SUER
Se la variabile non esiste, viene espansa a una stringa vuota.
6) Sostituzione dei comandi:
echo $(ls)
ls -l $(which cp)
echo "$(cal)"
Virgolette della shell
1) L'uso delle virgolette in un comando influisce sugli spazi:
echo questo è un test
echo "questo è un test"
Quando la shell analizza il primo comando, trova 4 argomenti: "this", "is", "a", "test".
Quindi chiama echo
passandogli questi argomenti.
Nel secondo caso le virgolette fanno capire alla shell che esiste un singolo argomento: "questo è un test", e passa a echo
solo un argomento.
2) Le virgolette doppie non impediscono l'espansione delle variabili, mentre quelle singole lo fanno:
echo Il totale è $100.0
echo "Il totale è $100.0"
echo 'Il totale è $100.0'
Bash riconosce $1
come una variabile speciale e cerca di sostituirne il valore (che è vuoto).
L'uso di doppi apici non impedisce a bash di espandere le variabili, per quello servono le virgolette singole.
3) Le doppie virgolette non impediscono le espansioni della shell che iniziano con un "$
", ma impedisce le altre:
echo ~/*.txt {a,b} $(echo foo) $((2 + 2))
echo "~/*.txt {a,b} $(echo foo) $((2 + 2))"
echo '~/*.txt {a,b} $(echo foo) $((2 + 2))'
Sono utili per preservare gli spazi, ad esempio:
echo $(cal)
echo "$(cal)"
4) Si può anche escape
(cioè sfuggire, impedire l'espansione) a "$
" facendolo precedere da "\
":
echo Il saldo è: $5.00
echo Il saldo è: \$5.00
5) L'opzione -e
di echo
abilita anche altre sequenze di escape come \n
(per una nuova riga) e \t
(per un tab):
echo "a\nb"
echo -e "a\nb"
echo "a\tb"
echo -e "a\tb"
L'ambiente
Abbiamo visto in precedenza alcune variabili d'ambiente. Queste sono variabili mantenute dalla shell e utilizzate per memorizzare alcune impostazioni. Possono anche essere usate da alcuni programmi per ottenere valori di configurazione.
1) È possibile visualizzare un elenco di variabili d'ambiente con printenv
o set
:
printenv | less
set | less
L'elenco visualizzato da set
è più lungo perché mostra anche le variabili di shell e alcune funzioni definite nella shell.
printenv USER
echo $USER
La variabile USER
mantiene fondamentalmente il valore visualizzato dal comando whoami
.
2) Altre variabili d'ambiente interessanti sono queste:
echo $HOME
echo $PWD
echo $SHELL
echo $LANG
echo $PATH
PATH
è usato dalla shell per trovare un programma.
Ad esempio, quando si chiama ls
, shell lo cerca nella prima directory del PATH, poi nella seconda e così via.
Il comando which ls
ci mostra dove la shell trova il programma ls
.
3) Le variabili d'ambiente sono dichiarate in alcuni file di configurazione che la shell carica all'avvio. Esistono due tipi di shell: una sessione di shell login, che viene avviata quando vengono richiesti un nome utente e una password, e una sessione di shell non-login, che viene avviata quando si lancia un terminale.
Gli script di configurazione che caricano una shell login:
nano /etc/profile
nano ~/.profile
Nota: digitare Ctrl-x
per uscire da nano
.
Gli script di configurazione che caricano una shell non-login:
nano /etc/bash.bashrc
nano ~/.bashrc
Tuttavia, la shell non di login eredita le variabili d'ambiente dal processo genitore, di solito una shell di login,
e gli script di configurazione di una shell di login di solito includono gli script di configurazione di una shell di non login.
Quindi, se si vogliono apportare delle modifiche all'ambiente, il posto giusto per modificarlo è il file ~/.bashrc
.
4) Supponiamo di voler modificare la variabile HISTSIZE
, che contiene la dimensione della cronologia dei comandi.
echo $HISTSIZE
nano ~/.bashrc
Aggiungete questa riga alla fine del file:
export HISTSIZE=2000
Premere Ctrl-o e Invio per salvare le modifiche. Poi Ctrl-x per uscire.
Con HISTSIZE=2000
stiamo dando un nuovo valore alla variabile e il comando export
lo salva effettivamente nell'ambiente della shell.
La prossima volta che si avvierà una shell, questa caricherà ~/.bashrc
e un nuovo valore sarà impostato su HISTSIZE.
Ma si può anche caricare ~/.bashrc
con il comando source
, in modo da applicare subito le modifiche:
echo $HISTSIZE
source ~/.bashrc
echo $HISTSIZE
5) Una delle variabili d'ambiente è anche PS1
che definisce il prompt:
echo $PS1
Proviamo a giocarci, ma prima facciamo un backup del valore corrente:
ps1_old="$PS1"
echo $ps1_old
Se abbiamo bisogno di ripristinare il prompt, possiamo farlo in questo modo:
PS1="$ps1_old"
Proviamo altri prompt:
PS1="-->"
ls -al
PS1="\$"
ls -al
PS1="\A \h \$"
ls -al
"\A" visualizza l'ora del giorno e "\h" visualizza l'host.
PS1="<\u@\h \W>\$ "
ls -al
"\u" visualizza l'utente e "\W" visualizza il nome della directory corrente.
Per salvare questo prompt per le sessioni future della shell, si deve aggiungere questa riga a ~/.bashrc
:
export PS1="<\u@\h \W>\$"
LEZIONE 6
In questa lezione impareremo a conoscere:
-
Ricerca di file (
locate
,find
). -
Espressioni regolari (
grep
).
Le espressioni regolari
(regex) sono notazioni simboliche utilizzate per identificare i pattern
(schemi) nel testo.
Sono supportate da molti strumenti a riga di comando e dalla maggior parte dei linguaggi di programmazione per facilitare la soluzione dei problemi di manipolazione del testo.
Ricerca di file
1) Possiamo effettuare una rapida ricerca di file con locate
:
locate bin/zip
Nota: se questo comando non dà alcun risultato, è possibile che l'indicizzazione dei file del sistema non sia ancora terminata. dei file del sistema non è ancora terminata. Aspettare un po' e riprovare. Si può accelerare l'indicizzazione con il comando:
`updatedb`
Se l'esigenza di ricerca non è così semplice, è possibile combinare locate
con altri strumenti, come grep
:
locate zip | grep bin
locate zip | grep bin | grep -v overlay
2) Mentre le ricerche di locate
si basano solo sul nome del file,
con find
possiamo effettuare ricerche basate anche su altri attributi dei file.
Prende come argomenti una o più directory che devono essere esplorate:
ls -aR ~
find ~
Per trovare solo le directory si può usare l'opzione -type d
e per trovare solo i file si può usare l'opzione -type f
:
find . -type d
find . -type f
find . -type d | wc -l
find . -type f | wc -l
find . | wc -l
3) Possiamo anche effettuare una ricerca per nome e dimensione del file:
find /etc -type f | wc -l
find /etc -type f -name "*.conf" | wc -l
Il modello di ricerca è racchiuso tra doppi apici per evitare che la shell espanda "*
".
find /etc -type f -name "*.conf" -size -2k | wc -l
find /etc -type f -name "*.conf" -size 2k | wc -l
find /etc -type f -name "*.conf" -size +2k | wc -l
-2k
corrisponde ai file la cui dimensione è inferiore a 2 Kilobyte,
2k
a quelli che sono esattamente 2 Kilobyte e
+2k
a quelli che sono più di 2 kilobyte.
Oltre a k
si può usare anche M
per i Megabyte, G
per i Gigabyte, ecc.
4) find
supporta molti altri test su file e directory, come l'ora di creazione o di modifica, proprietà e permessi, ecc.
Questi test possono essere combinati con gli operatori logici per creare relazioni logiche più complesse.
Ad esempio:
find ~ \( -type f -not -perm 0600 \) -or \( -type d -not -perm 0700 \)
Questo sembra strano, ma se proviamo a tradurlo in un linguaggio più comprensibile, significa: trova nella home directory file con permessi errati o directory con permessi errati. Dobbiamo fare l'escape delle parentesi per evitare che la shell le interpreti.
5) Possiamo anche eseguire alcune azioni sui file trovati.
L'azione predefinita è quella di stamparli (print
) sullo schermo, ma possiamo anche cancellarli (delete
).
touch test{1,2,3}.bak
ls
find . -type f -name '*.bak' -delete
ls
touch test{1,2,3}.bak
find . -type f -name '*.bak' -print -delete
ls
Possiamo anche eseguire azioni personalizzate con -exec
:
touch test{1,2,3}.bak
ls
find . -name '*.bak' -exec rm '{}' ';'
ls
Qui {}
rappresenta il percorso trovato e ;
è richiesto per indicare la fine del comando.
Entrambi sono stati virgolettati per evitare che la shell li interpreti.
Se si usa -ok
invece di -exec
, ogni comando chiedera la conferma (y) prima di essere eseguito:
touch test{1,2,3}.bak
ls
find . -name '*.bak' -ok rm '{}' ';'
6) Un altro modo per eseguire azioni sui risultati di find
è quello di inviarli in pipe a xargs
:
touch test{1,2,3}.bak
ls
find . -name '*.bak' | xargs echo
find . -name '*.bak' | xargs ls -l
.
find . -name '*.bak' | xargs rm
ls
xargs
riceve l'input da stdin e lo converte in un elenco di argomenti per il comando dato.
Altri esempi con find
1) Creiamo alcuni file di prova:
mkdir -p test/dir-{001..100}
touch test/dir-{001..100}/file-{A..Z}
Il comando `touch` in questo caso crea dei file vuoti.
ls test/
ls test/dir-001/
ls test/dir-002/
2) Trova tutti i file denominati file-A
:
find test -type f -name 'file-A'
find test -type f -name 'file-A' | wc -l
3) Creare un file di timestamp:
touch test/timestamp
Viene creato un file vuoto e imposta il suo tempo di modifica all'ora corrente.
Si può verificare con stat
, che mostra tutto ciò che il sistema sa di un file:
stat test/timestamp
touch test/timestamp
stat test/timestamp
Si può notare che dopo il secondo touch
i tempi sono stati aggiornati.
4) Quindi, usiamo find
per aggiornare tutti i file denominati file-B
:
find test -name 'file-B' -exec touch '{}' ';'
5) Ora usiamo find
per identificare i file aggiornati confrontandoli con il file di riferimento timestamp
:
find test -type f -newer test/timestamp
find test -type f -newer test/timestamp | wc -l
Il risultato contiene tutte le 100 ricorrenze di file-B
.
Poiché abbiamo fatto un touch dopo l'aggiornamento di timestamp
, esse sono ora "più recenti" del file `timestamp.
6) Troviamo ora i file e le directory con permessi errati:
find test \( -type f -not -perm 0600 \) -or \( -type d -not -perm 0700 \)
find test \( -type f -not -perm 0600 \) -or \( -type d -not -perm 0700 \) | wc -l
7) Aggiungiamo alcune azioni al comando precedente per risolvere il problema dei permessi.
find test \( -type f -not -perm 0600 -exec chmod 0600 '{}' ';' \) -or \( -type d -not -perm 0700 -exec chmod 0700 '{}' ';' \)
Come già visto, il comando `chmod` imposta i permessi di un file o di una directory.
find test \( -type f -not -perm 0600 \) -or \( -type d -not -perm 0700 \)
find test \( -type f -perm 0600 \) -or \( -type d -perm 0700 \)
find test \( -type f -perm 0600 \) -or \( -type d -perm 0700 \) | wc -l
**Nota:** Questo esempio è un po' complesso solo per illustrare gli operatori logici e le parentesi,
ma avremmo potuto farlo in due passi più semplici, come questo:
find test -type f -not -perm 0600 -exec chmod 0600 '{}' ';'
find test -type d -not -perm 0700 -exec chmod 0700 '{}' ';'
8) Proviamo altri test:
Troviamo i file o le directory i cui contenuti o attributi sono stati modificati più di 1 minuto fa:
find test/ -cmin +1 | wc -l
Meno di 10 minuti fa:
find test/ -cmin -10 | wc -l
Troviamo i file o le directory i cui contenuti sono stati modificati più di 1 minuto fa:
find test/ -mmin +1 | wc -l
Meno di 10 minuti fa:
find test/ -mmin -10 | wc -l
Troviamo i file o le directory i cui contenuti o attributi sono stati modificati più di 7 giorni fa:
find test/ -ctime +7 | wc -l
Troviamo i file o le directory i cui contenuti sono stati modificati meno di 7 giorni fa:
find test/ -mtime -7 | wc -l
Troviamo i file e le directory vuote:
find test/ -empty | wc -l
Espressioni regolari
Le espressioni regolari sono notazioni simboliche utilizzate per identificare schemi nel testo. Sono supportate da molti strumenti a riga di comando e dalla maggior parte dei linguaggi di programmazione per facilitare la soluzione di problemi di manipolazione del testo.
1) Verranno testate le espressioni regolari con grep
(che significa "stampa globale di espressioni regolari").
Cerca nei file di testo la presenza di testo che corrisponde a un'espressione regolare specificata e
visualizza su standard output ogni riga che contiene una corrispondenza.
ls /usr/bin | grep zip
Per esplorare il comando grep`, creiamo alcuni file di testo da cercare:
ls /bin > dirlist-bin.txt
ls /usr/bin > dirlist-usr-bin.txt
ls /sbin > dirlist-sbin.txt
ls /usr/sbin > dirlist-usr-sbin.txt
ls dirlist*.txt
Possiamo fare una semplice ricerca su questi file in questo modo:
grep bzip dirlist*.txt
Se siamo interessati solo all'elenco dei file che contengono corrispondenze, possiamo usare l'opzione -l
:
grep -l bzip dirlist*.txt
Al contrario, se si vuole vedere un elenco di file che non contengono una corrispondenza, si può usare l'opzione -L
:
grep -L bzip dirlist*.txt
2) Anche se può non sembrare evidente, abbiamo già utilizzato le espressioni regolari nelle ricerche effettuate finora, anche se molto semplici. L'espressione regolare "bzip" significa che una riga corrisponde se contiene le lettere "b", "z", "i", "p" in questo ordine e senza altri caratteri in mezzo.
Oltre ai caratteri letterali, che rappresentano se stessi, si possono usare anche i metacaratteri in uno schema.
Ad esempio, un punto (.
) corrisponde a qualsiasi carattere:
grep -h '.zip' dirlist*.txt
L'opzione -h
sopprime l'output dei nomi dei file.
Si noti che il programma zip
non è stato trovato perché ha solo 3 lettere e non corrisponde allo schema.
3) L'accento circonflesso o caret (^
) e il segno del dollaro ($
) sono trattati come ancoraggi nelle espressioni regolari.
Ciò significa che causano la corrispondenza solo se l'espressione regolare si trova all'inizio della riga (^
) o alla fine della riga ($
).
grep -h '^zip' dirlist*.txt
grep -h 'zip$' dirlist*.txt
grep -h '^zip$' dirlist*.txt
Si noti che l'espressione regolare '^$
' corrisponde a righe vuote.
4) Usando le espressioni tra parentesi si può far corrispondere un singolo carattere di un insieme di caratteri specificato:
grep -h '[bg]zip' dirlist*.txt
Se il primo carattere di un'espressione di parentesi è un accento circonflesso (^
), allora qualsiasi carattere verrà abbinato, ad eccezione di quelli elencati:
grep -h '[^bg]zip' dirlist*.txt
Il carattere ^
invoca la negazione solo se è il primo carattere all'interno della parentesi graffa;
in caso contrario perde il suo significato speciale e diventa un normale carattere dell'insieme:
grep -h '[b^g]zip' dirlist*.txt
5) Se vogliamo trovare tutte le righe che iniziano con una lettera maiuscola, possiamo farlo in questo modo:
grep -h '^[ABCDEFGHIJKLMNOPQRSTUVWXYZ]' dirlist*.txt
Si può digitare meno se si usa un intervallo:
grep -h '^[A-Z]' dirlist*.txt
Se si vuole trovare una corrispondenza con qualsiasi carattere alfanumerico (tutte le lettere e le cifre), possiamo usare diversi intervalli, come questo:
grep -h '^[A-Za-z0-9]' dirlist*.txt
Tuttavia, il carattere trattino (-
) in questo esempio sta per se stesso, non crea un intervallo:
grep -h '^[-AZ]' dirlist*.txt
Oltre agli intervalli, un altro modo per abbinare gruppi di caratteri è l'uso di classi di caratteri POSIX:
grep -h '^[[:alnum:]]' dirlist*.txt
ls /usr/sbin/[[:upper:]]*
Altre classi di caratteri sono: [:alpha:]
, [:lower:]
, [:digit:]
, [:space:]
, [:punct:]
(per i caratteri di punteggiatura), ecc.
6) Con una barra verticale (|
) si possono definire modelli di corrispondenza alternativi:
echo "AAA" | grep AAA
echo "BBB" | grep BBB
echo "AAA" | grep 'AAA\|BBB'
echo "BBB" | grep -E 'AAA|BBB'
echo "CCC" | grep -E 'AAA|BBB'
echo "CCC" | grep -E 'AAA|BBB|CCC'
L'opzione -E
indica a grep
di utilizzare espressioni regolari estese.
Con le espressioni regolari estese la barra verticale (|
) è un metacarattere (usato per l'alternativa)
Con le espressioni regolari non estese (senza l'opzione -E
) la barra verticale è un carattere letterale e dobbiamo fare l'escape (con \
) se vogliamo usarla come metacarattere.
7) Altri metacaratteri riconosciuti dalle espressioni regolari estese e che si comportano in modo simile a |
sono:
`(`, `)`, `{`, `}`, `?`, `+`.
Ad esempio:
grep -Eh '^(bz|gz|zip)' dirlist*.txt
Si noti che questo è diverso da:
grep -Eh '^bz|gz|zip' dirlist*.txt
Nel primo esempio, tutti i pattern sono abbinati all'inizio della riga.
Nel secondo, invece, solo bz
viene confrontato all'inizio.
Altri esempi di regex
1) Supponiamo di risolvere un cruciverba e di aver bisogno di una parola italiana di sei lettere la cui terza lettera sia "c" e la quinta "r".
Proviamo a usare grep
e regex per risolverlo.
Prima di tutto assicuriamoci di avere installato un dizionario di parole (ad esempio quello italiano):
apt install witalian
ls /usr/share/dict/
less /usr/share/dict/italian
cat /usr/share/dict/italian | wc -l
Ora provate questo:
grep -i '^..c.r.$' /usr/share/dict/italian
L'opzione `-i' è usata per ignorare le maiuscole (maiuscole, minuscole).
Lo schema regex '^..c.r.$'
corrisponderà alle righe che contengono esattamente
6 lettere, dove la terza lettera è c
e la quinta è r
.
2) Supponiamo di voler verificare la validità di un numero di telefono e di considerarlo valido
se ha la forma (nnn) nnn-nnnn
dove n
è una cifra. Possiamo farlo in questo modo:
echo "(555) 123-4567" | grep -E '^\(?[0-9][0-9][0-9]\)? [0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]$'
echo "555 123-4567" | grep -E '^\(?[0-9][0-9][0-9]\)? [0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]$'
echo "AAA 123-4567" | grep -E '^\(?[0-9][0-9][0-9]\)? [0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]$'
Dal momento che stiamo usando l'opzione -E
(per esteso), dobbiamo fare l'escape delle parentesi \(
e \)
in modo che non vengano interpretate come metacaratteri.
Se si usano le espressioni regolari di base (senza -E
), allora non è necessario fare l'escape delle parentesi,
ma in questo caso dovremo fare l'escape dei punti interrogativi (\?
) in modo che vengano interpretati come metacaratteri:
echo "(555) 123-4567" | grep '^(\?[0-9][0-9][0-9])\? [0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]$'
Il punto interrogativo come metacarattere significa che la parentesi che lo precede può comparire zero o una sola volta.
3) Utilizzando il metacarattere {}
possiamo esprimere il numero di corrispondenze richieste. Ad esempio:
echo "(555) 123-4567" | grep -E '^\(?[0-9]{3})? [0-9]{3}-[0-9]{4}$'
L'espressione {3}
corrisponde se l'elemento precedente si presenta esattamente 3 volte.
Si può anche sostituire ?
con {0,1}
o {,1}
:
echo "(555) 123-4567" | grep -E '^\({0,1}[0-9]{3}){,1} [0-9]{3}-[0-9]{4}$'
echo "555 123-4567" | grep -E '^\({0,1}[0-9]{3}){,1} [0-9]{3}-[0-9]{4}$'
In generale, {n,m}
corrisponde se l'elemento precedente ricorre almeno n
volte, ma non più di m
volte.
Anche questi sono validi: {n,}
(almeno n
volte) e {,m}
(al massimo m
volte).
4) Analogamente a ?
che è equivalente a {0,1}
, esiste anche *
che è equivalente a {0,}
(zero o più occorrenze) e +
che è equivalente a {1,}
(una o più, almeno una occorrenza):
Supponiamo di voler verificare se una stringa è una frase. Questo significa che inizia con una lettera maiuscola, poi contiene un numero qualsiasi di lettere maiuscole e minuscole e spazi, e infine termina con un punto. Si potrebbe fare in questo modo:
echo "Questo funziona." | grep -E '[A-Z][A-Za-z ]*\.'
echo "Questo funziona". | grep -E '[A-Z][A-Za-z ]*\.'
echo "Questo non funziona" | grep -E '[A-Z][A-Za-z ]*\.'
Oppure in questo modo:
echo "Questo funziona." | grep -E '[[:upper:]][[:upper:][:lower:] ]*\.'
Nota: in tutti questi casi è necessario eseguire l'escape del punto (\.
) in modo che corrisponde a se stesso invece che a qualsiasi carattere.
5) Ecco un'espressione regolare che corrisponderà solo a righe composte da gruppi di uno o più caratteri alfabetici separati da singoli spazi:
echo "Questo che" | grep -E '^([[:alpha:]]+ ?)+$'
echo "a b c" | grep -E '^([:alpha:]]+ ?)+$'
echo "a b c" | grep -E '^([:alpha:]]+ ?)+$'
Non corrisponde perché ci sono due spazi consecutivi.
echo "a b 9" | grep -E '^([:alpha:]]+ ?)+$'
Non corrisponde perché c'è un carattere non alfabetico.
Se non vogliamo spazi tra l'ultima parola ed il punto:
echo "Questo funziona." |grep -E '^([[:alpha:]]+ ?)+[[:alpha:]]+\.$'
6) Creiamo un elenco di numeri di telefono casuali per i test:
echo $RANDOM
echo $RANDOM
echo ${RANDOM:0:3}
for i in {1..10}; do echo "${RANDOM:0:3} ${RANDOM:0:3}-${RANDOM:0:4}" >> elenco-telefonico.txt; done
cat elenco-telefonico.txt
for i in {1..100}; do echo "${RANDOM:0:3} ${RANDOM:0:3}-${RANDOM:0:4}" >> elenco-telefonico.txt; done
less elenco-telefonico.txt
cat elenco-telefonico.txt | wc -l
Si può notare che alcuni numeri di telefono sono malformati. Possiamo visualizzare quelli malformati in questo modo:
grep -Ev '^[0-9]{3} [0-9]{3}-[0-9]{4}$' elenco-telefonico.txt
L'opzione -v
effettua una corrispondenza inversa, il che significa che grep
visualizza solo le righe che non corrispondono allo schema dato.
7) Le espressioni regolari possono essere usate con molti comandi, non solo con grep
.
Per esempio, usiamole con find
per trovare i file che contengono caratteri non corretti nel loro nome (come spazi, segni di punteggiatura, ecc.):
touch "bad file name!"
ls -l
`find . -regex '.*[^-_./0-9a-zA-Z].*'`
A differenza di grep
, find
si aspetta che il pattern corrisponda all'intero nome del file,
ecco perché si aggiunge e si antepone .*
al pattern.
Possiamo usare le espressioni regolari con locate
in questo modo:
locate --regex 'bin/(bz|gz|zip)'
Possiamo anche usarle con less
:
less elenco-telefonico.txt
Possiamo premere /
e scrivere un'espressione regolare, e less
troverà ed evidenzierà le righe corrispondenti.
Ad esempio:
/^[0-9]{3} [0-9]{3}-[0-9]{4}$
Le righe non valide non saranno evidenziate e saranno facili da individuare.
Le espressioni regolari possono anche essere usate con zgrep
in questo modo:
cd /usr/share/man/man1
zgrep -El 'regex|regular expression' *.gz
trova le pagine man che contengono "regex" o "regular expression". Come possiamo vedere, le espressioni regolari sono presenti in molti programmi.
LEZIONE 7
Linux si basa molto sui file di testo per la memorizzazione dei dati, quindi è logico che ci siano parecchi strumenti per la manipolazione del testo. Alcuni di questi strumenti sono:
cat
-- Concatena i file e li stampa sullo standard outputsort
-- Ordina le righe dei file di testouniq
-- Segnala o omette le righe ripetutecut
-- Estrarre sezioni da ogni riga di filepaste
-- Unisce righe di filejoin
-- Unisce le righe di due file su un campo comunecomm
-- Confronta due file ordinati riga per rigadiff
-- Confronta i file riga per rigapatch
-- Applica un file diff a un originaletr
-- Tradurre o cancellare caratterised
-- Editor di flusso per filtrare e trasformare il testoaspell
-- Controllore ortografico interattivo
sort
1) Proviamo a confrontare questi comandi:
du -s /usr/share/* | less
du -s /usr/share/* | sort | less
du -s /usr/share/* | sort -r | less
du -s /usr/share/* | sort -nr | less
du -s /usr/share/* | sort -nr | head
Il comando du
ricava la dimensione (utilizzo del disco) dei file e delle directory di /usr/share
, e head
filtra i primi 10 risultati.
Poi si tenta di ordinarli con sort
e sort -r
(inverso), ma non sembra funzionare come previsto (ordinamento dei risultati in base alla dimensione).
Questo perché sort
per impostazione predefinita ordina la prima colonna in ordine alfabetico, quindi 2
è più grande di 10
(perché 2
viene dopo 1
nell'insieme dei caratteri).
Con l'opzione -n
si dice a sort di fare un ordinamento numerico. Quindi l'ultimo comando restituisce i 10 file e le directory più grandi in /usr/share
.
2) Questo esempio funziona perché i valori numerici si trovano nella prima colonna dell'output.
E se volessimo ordinare un elenco in base a un'altra colonna? Per esempio il risultato di ls -l
:
ls -l /usr/bin | head
Ignorando per il momento che ls
può ordinare i suoi risultati in base alla dimensione, potremmo usare sort
per ordinarli in questo modo:
ls -l /usr/bin | sort -nr -k 5 | head
L'opzione -k 5
dice a sort
di usare il quinto campo come chiave per l'ordinamento.
A proposito, ls
, come la maggior parte dei comandi, separa i campi del suo output con un TAB.
3) Per i test useremo il file distros.txt
, che è una sorta di cronologia di alcune distribuzioni Linux (contenente le loro versioni e le date di rilascio).
Spostiamoci innanzitutto nella cartella Materiali/lesson7
con cd .
cat distros.txt
cat -A distros.txt
L'opzione A
fa sì che vengano mostrati tutti i caratteri speciali.
Il carattere di tabulazione è rappresentato da ^I
e il carattere $
indica la fine della riga.
4) Proviamo a ordinarlo:
sort distros.txt
Il risultato è quasi corretto, ma i numeri di versione di Fedora non sono nell'ordine corretto (dato che 1
viene prima di 5
nell'insieme di caratteri).
Per risolvere questo problema, si procederà a un ordinamento su più chiavi. Vogliamo un ordinamento alfabetico sul primo campo e uno numerico sul secondo campo:
sort --key=1,1 --key=2n distros.txt
sort -k 1,1 -k 2n distros.txt
sort -k1,1 -k2n distros.txt
Si noti che se non si utilizza un intervallo di campi (come 1,1
, il che significa iniziare dal campo 1 e terminare con il campo 1), non funzionerà come ci si aspetta:
sort -k 1 -k 2n distros.txt
Questo perché in questo caso inizia dal campo 1 e va fino alla alla fine della riga, ignorando quindi la seconda chiave.
Il modificatore n
sta per ordine numerico. Altri modificatori sono r
per reverse, b
per ignore blanks, ecc.
5) Supponiamo di voler ordinare l'elenco in ordine cronologico inverso (per data di rilascio). Possiamo farlo in questo modo:
sort -k 3.7nbr -k 3.1nbr -k 3.4nbr distros.txt
L'opzione --key
permette di specificare gli offset all'interno dei campi.
Quindi 3.7
significa iniziare l'ordinamento dal 7° carattere del 3° campo, che è l'anno.
Il modificatore n
rende l'ordinamento numerico, r
esegue l'ordinamento inverso e con b
si sopprimono gli spazi iniziali del terzo campo.
In modo analogo, la seconda chiave di ordinamento 3.1
ordina in base al mese e la terza chiave 3.4
ordina per giorno.
6) Alcuni file non usano tabulazioni e spazi come delimitatori, ad esempio il file /etc/passwd
:
head /etc/passwd
In questo caso si può usare l'opzione -t
per definire il carattere di separazione del campo per definire il carattere di separazione dei campi.
Per esempio, per ordinare /etc/passwd
al settimo campo (il campo della shell predefinita dell'account), si può fare così:
sort -t ':' -k 7 /etc/passwd | head
cut
1) Il comando cut
estrae una determinata colonna (campo) dall'input, ad esempio:
cut -f 3 distros.txt
cut -f 1,3 distros.txt
cut -f 1-3 distros.txt
2) Se si vuole estrarre solo l'anno, si può fare così:
cut -f 3 distros.txt | cut -c 7-10
L'opzione -c
dice a cut
di estrarre dalla riga i caratteri, invece dei campi (come se ogni carattere fosse un campo).
cut -f 3 distros.txt | cut -c 7-10,1-2,4-5
3) Un altro modo per ottenere l'anno sarebbe il seguente:
`expand distros.txt | cut -c 23-`
Il comando expand
sostituisce le tabulazioni con il numero corrispondente di spazi, in modo che l'anno inizi sempre alla posizione 23.
4) Quando si lavora con i campi, è possibile specificare un diverso delimitatore di campo, invece della tabulazione. Ad esempio:
head /etc/passwd
cut -d ':' -f 1 /etc/passwd | head
Qui estraiamo il primo campo da /etc/passwd
.
paste
Il comando paste
fa l'opposto di cut
. Invece di estrarre una colonna di testo da un file, aggiunge una o più colonne di testo.
Per dimostrare il funzionamento di paste
, eseguiamo alcune operazioni sul nostro file distros.txt
per produrre un elenco cronologico dei rilasci.
1) Per prima cosa ordiniamo le distro per data:
head distros.txt
sort -k 3.7nbr -k 3.1nbr -k 3.4nbr distros.txt > distros-by-date.txt
head distros-by-date.txt
2) Quindi, usiamo cut
per estrarre i primi due campi/colonne dal file (il nome della distro e la versione):
cut -f 1,2 distros-by-date.txt > distros-versions.txt
head distros-versions.txt
Estraiamo anche le date di rilascio:
cut -f 3 distros-by-date.txt > distros-dates.txt
head distros-dates.txt
3) Per completare il processo, usiamo paste
per mettere la colonna delle date prima dei nomi delle distro e delle versioni, creando così un elenco cronologico:
paste distros-dates.txt distros-versions.txt > distros-chronological.txt
head distros-chronological.txt
join
Il comando join
, come paste
, aggiunge colonne a un file.
Tuttavia lo fa in un modo simile all'operazione join nei database relazionali.
Unisce i dati provenienti da più file in base a un campo chiave condiviso.
Per spiegare il comando join
, creiamo un paio di file con una chiave condivisa.
1) Il primo file conterrà le date di rilascio e i nomi delle release:
cut -f 1,1 distros-by-date.txt > distros-names.txt
paste distros-dates.txt distros-names.txt > distros-key-names.txt
head distros-key-names.txt
2) Il secondo file conterrà le date di rilascio e i numeri di versione:
`cut -f 2,2 distros-by-date.txt > distros-vernums.txt`
paste distros-dates.txt distros-vernums.txt > distros-key-vernums.txt
head distros-key-vernums.txt
3) Entrambi i file hanno la data di rilascio come campo comune. Uniamoli:
join distros-key-names.txt distros-key-vernums.txt | head
È importante che i file siano ordinati in base al campo chiave, affinché join
funzioni correttamente.
Confronto di file di testo
1) Creiamo due file di prova:
cat > file1.txt <<EOF
a
b
c
d
EOF
cat > file2.txt <<EOF
b
c
d
e
EOF
2) Possiamo confrontarli con comm
:
comm file1.txt file2.txt
comm -12 file1.txt file2.txt
In questo caso si sopprimono le colonne 1 e 2.
3) Uno strumento più complesso è diff
:
diff file1.txt file2.txt
Con il contesto:
diff -c file1.txt file2.txt
Il formato unificato è più conciso:
diff -u file1.txt file2.txt
4) Per creare un file patch di solito si usa l'opzione Naur
:
diff -Naur file1.txt file2.txt > patchfile.txt
cat patchfile.txt
Si può usare il comando patch
per applicare un file di patch:
patch file1.txt patchfile.txt
cat file1.txt
Ora file1.txt
ha lo stesso contenuto di file2.txt
.
Editare on the fly (al volo)
1) Il programma tr
viene utilizzato per traslitterare i caratteri:
echo "lettere minuscole" | tr a-z A-Z
Più caratteri possono essere convertiti in un singolo carattere:
echo "lettere minuscole" | tr [:lower:] A
Con l'opzione -d
si possono cancellare i caratteri:
echo "lettere minuscole" | tr -d e
Con l'opzione -s
si possono comprimere i caratteri ripetuti:
echo "aaabbbccc" | tr -s ab
echo "abcabcabc" | tr -s ab
2) Come altro esempio, usiamo tr
per implementare la codifica ROT13
(dove ogni carattere viene spostato di 13 posizioni nell'alfabeto):
echo "testo segreto" | tr a-zA-Z n-za-mN-ZA-M
Per decodificare, eseguire la stessa traduzione una seconda volta:
echo "grfgb frtergb" | tr a-zA-Z n-za-mN-ZA-M
3) Il programma sed
significa stream editor.
echo "fronte" | sed 's/fronte/retro/'
Il comando s
sta per sostituzione.
echo "fronte" | sed 's_fronte_retro_'
Il carattere immediatamente successivo a s
diventa il delimitatore.
I comandi in sed
possono essere preceduti da un indirizzo:
echo -e "fronte \nretro" | sed '1s/fronte/retro/'
echo -e "fronte \nretro" | sed '2s/fronte/retro/'
4) Proviamo altri esempi su distros.txt
:
sed -n '1,5p' distros.txt
L'opzione -n
non stampa le righe per impostazione predefinita, mentre il comando p
stampa solo le righe nell'intervallo indicato.
sed -n '/SUSE/p' distros.txt
Stampa solo le righe che corrispondono all'espressione regolare indicata.
sed -n '/SUSE/!p' distros.txt
Stampa solo le righe che non corrispondono all'espressione regolare.
5) Il comando s
sostituisce per impostazione predefinita solo la prima occorrenza su una riga corrispondente:
echo "aaabbbccc" | sed 's/b/B/'
Possiamo aggiungere il modificatore g
(globale) per sostituire tutte le occorrenze:
echo "aaabbbccc" | sed 's/b/B/g'
6) Cambiamo il formato della data da MM/GG/AAAA a AAAA-MM-GG su distros.txt
:
sed -E 's#([0-9]{2})/([0-9]{2})/([0-9]{4})$#\3-\1-\2#' distros.txt
Per prima cosa, usiamo l'opzione E, --regexp-extended
perché ci sono molti caratteri speciali come (
, )
, {
, }
e l'escape di tutti questi caratteri con un \
renderebbe l'espressione regolare molto disordinata e illeggibile.
Il delimitatore è il cancelletto (#
).
Quindi, si racchiudono tra parentesi le parti della regexp che corrispondono al mese ([0-9]{2}
), al giorno ([0-9]{2}
) e all'anno ([0-9]{4}
).
Le stringhe che corrispondono a una sottoespressione possono essere utilizzate nel metodo di sostituzione sostituzione in questo modo:
\n
, dove n
è il numero della sottoespressione corrispondente (coppia di parentesi).
7) È possibile dare diversi comandi allo stesso programma sed
, come questo:
echo "aaabbbccc" | sed -e 's/b/B/g' -e 's/a/A/g'
Tuttavia, a volte è preferibile elencare questi comandi in un file di script sed
, e poi richiamare questo script. Ad esempio:
cat <<EOF > distros.sed
\#script sed per produrre un rapporto sulle distro
1 i\Rapporto sulla distribuzione Linux
s|([0-9]{2})/([0-9]{2})/([0-9]{4})$|\3-\1-\2|
y|abcdefghijklmnopqrstuvwxyz|ABCDEFGHIJKLMNOPQRSTUVWXYZ|
EOF
cat distros.sed
sed -E -f distros.sed distros.txt
La prima riga è un commento.
Poi, il comando i
inserisce qualcosa prima della prima riga.
Il comando s
cambia il formato della data.
Infine, il comando y
traslittera i caratteri minuscoli in maiuscoli, in modo simile al comando tr
.
Tuttavia, a differenza di tr
, non riconosce gli intervalli di caratteri o le classi di caratteri, quindi bisogna elencare tutte le lettere dell'alfabeto.
aspell
1) File di testo normale:
cat foo.txt
aspell check foo.txt
cat foo.txt
2) File HTML:
cat foo.html
aspell check foo.html
cat foo.html
Se l'estensione del file non è .html
possiamo forzare la modalità html con l'opzione -H
.
Per maggiori informazioni sul comando aspell :
aspell --help | less
LEZIONE 8
- Archiviazione (gzip, bzip2, tar, zip, rsync)
- Networking (ping, traceroute, ip, netstat, wget, curl, ssh, scp, sftp)
- Filesystem (mount, umount, fdisk, mkfs, dd)
- Gestione dei pacchetti (apt)
Archiviazione e backup
1) Possiamo usare gzip
e bzip2
per comprimere uno o più file:
ls -l /etc > pippo.txt
ls -lh pippo.*
gzip pippo.txt
ls -lh pippo.*
gunzip pippo.txt
ls -lh pippo.*
ls -l /etc | gzip > pippo.txt.gz
gunzip -c pippo.txt.gz
zcat pippo.txt.gz | less
zless pippo.txt.gz
bzip2 pippo.txt
ls -lh pippo.*
bunzip2 pippo.txt.bz2
ls -lh pippo.*
2) Possiamo usare tar
per archiviare i file.
Creiamo una directory di prova:
mkdir -p testdir/dir-{001..100}
touch testdir/dir-{001..100}/file-{A..Z}
ls testdir/
ls testdir/dir-001/
Creare un archivio tar dell'intera directory:
tar -c -f testdir.tar testdir
tar -cf testdir.tar testdir
ls -lh
L'opzione -c
significa create
e l'opzione -f
è per il nome del file dell'archivio.
L'opzione -t
è usata per elencare il contenuto dell'archivio, mentre -v
è per la visualizzazione verbosa:
tar -tf testdir.tar | less
tar -tvf testdir.tar | less
Ora estraiamo l'archivio in una nuova posizione:
mkdir foo
cd foo
tar -xf ../testdir.tar
ls
tree -C | less -r
cd .. ; rm -rf foo/
3) Per impostazione predefinita, tar
rimuove l'iniziale /
dai nomi assoluti di file:
echo $(pwd)/testdir
tar cf testdir2.tar $(pwd)/testdir
tar tf testdir2.tar | less
mkdir foo
tar xf testdir2.tar -C foo/
tree foo -C | less -r
rm -rf foo
4) Possiamo estrarre dall'archivio solo alcuni file (non tutti i file):
mkdir foo
cd foo
tar tf ../testdir.tar testdir/dir-001/file-A
tar xf ../testdir.tar testdir/dir-001/file-A
tree
tar xf ../testdir.tar testdir/dir-002/file-{A,B,C}
tree
Si possono anche usare le --wildcards
, come in questo caso:
tar xf ../testdir.tar --wildcards 'testdir/dir-*/file-A'
tree -C | less -r
cd .. ; rm -rf foo
5) A volte è utile combinare tar
con find
e gzip
:
find testdir -name 'file-A'
find testdir -name 'file-A' -exec tar rf testdir3.tar '{}' '+'
tar tf testdir3.tar | less
find testdir -name 'file-B' -exec tar rf testdir3.tar '{}' '+'
tar tf testdir3.tar | less
L'opzione r
serve per aggiungere file a un archivio.
find testdir -name 'file-A' | tar cf - -T - | gzip > testdir.tgz
Il primo -
fa sì che tar invii l'output a stdout invece che a un file.
L'opzione -T
o --files-from
include nell'archivio solo i file elencati nel file dato.
In questo caso stiamo leggendo l'elenco dei file da -
, che significa che stdin è l'elenco dei file provenienti dal comando find
.
Poi passiamo l'output di tar
a gzip
per comprimerlo.
Possiamo anche usare le opzioni z
o j
per comprimere l'archivio:
find testdir -name 'file-A' | tar czf testdir.tgz -T -`
find testdir -name 'file-A' | tar cjf testdir.tbz -T -`
ls -lh
L'opzione j
utilizza la compressione bzip2
, invece di bzip
.
6) Il programma zip
è sia uno strumento di compressione che un programma di realizzazione:
zip -r testdir.zip testdir
ls -lh
L'opzione -r
è per la ricorsione.
mkdir -p foo
cd foo
unzip ../testdir.zip
tree | less
unzip -l ../testdir.zip testdir/dir-007/file-*
unzip ../testdir.zip testdir/dir-007/file-*
cd .. ; rm -rf foo
7) Possiamo usare rsync
per sincronizzare file e directory:
rsync -av testdir foo
ls foo
rsync -av testdir foo
Si noti che nel secondo caso non viene copiato alcun file perché rsync
rileva che non ci sono differenze tra l'origine e la destinazione.
touch testdir/dir-099/file-Z
rsync -av testdir foo
Con l'opzione --delete
si possono anche cancellare i file presenti nella directory di destinazione che non sono presenti nella directory di origine.
rm testdir/dir-099/file-Z
rsync -av testdir foo
ls foo/testdir/dir-099/file-Z
rsync -av --delete testdir foo
ls foo/testdir/dir-099/file-Z
rsync
può essere usato anche in rete, di solito in combinazione con ssh
.
Networking
1) Strumenti di base: Per elencare gli indirizzi IP dei device:
ip address
ip addr
ip a
Per mostare l'indirizzo IP di un particolare device:
ip addr show lo
Per mostrare la tabella di istradamento:
ip route
ip r
Per inviare un segnale di ping ad un indirizzo (in questo caso 3 tentativi):
ping -c 3 8.8.8.8
ping -c 3 linuxcommand.org
Per sapere qualcosa di più su un particolare IP e le impostazioni DNS:
dig linuxcommand.org
dig linuxcommand.org +short
Per tracciare il percorso che un pacchetto segue per raggiungere l'host:
traceroute linuxcommand.org
tracepath linuxcommand.org
2) Per scaricare i file possiamo usare wget
o curl
:
wget http://linuxcommand.org/index.php
less index.php
wget -O index.html 'http://linuxcommand.org/index.php'
less index.html
curl http://linuxcommand.org/index.php
curl http://linuxcommand.org/index.php > index.html
3) Netcat è un semplice strumento per la comunicazione in rete.
Usiamolo per ascoltare la porta 12345
:
nc -l 12345
Apriamo un altro terminale: pwd
Nel secondo terminale connettiamoci alla stessa porta in questo modo:
nc localhost 12345
Ora, ogni riga digitata qui viene inviata e visualizzata all'altro terminale:
Hello network
The quick brown fox jumped over the internet
Controlliamo l'altro terminale: test
Interrompiamo con Ctrl-c
.
Questo può sembrare banale, ma invece di localhost
avremmo potuto usare un nome o un IP di un server reale e connetterci ad esso da remoto.
Può essere usato per verificare che la porta TCP 12345
sul server a accessibile dal client (nel caso in cui ci sia un firewall, ad esempio).
Per controllare una porta UDP si può aggiungere l'opzione -u
a entrambi i comandi.
Può anche essere usato come semplice strumento per il trasferimento di file:
nc -l 12345 > file.txt
# esecuzione su T1
cd
# esecuzione su T2
nc -w 3 localhost 12345 < /etc/passwd
ls file.txt
# esecuzione su T1
cat file.txt
Come altro esempio, combiniamolo con tar
per trasferire un'intera directory:
mkdir cptest
cd cptest
nc -l 12345 | tar xzpf -
Spostiamoci sul terminale 2
ls
cd ~/Materiali/testdir
tar czpf - . | nc -w 3 localhost 12345
ls
cd ..
Ritorniamo sul terminale 1 e rimuoviamo la cartella cptest dopo averla esplorata:
ls
cd ..
rm -rf cptest
Rete: ssh
1) Un altro strumento di rete è ssh
, che può essere usato per accedere a un sistema remoto, eseguire comandi da remoto e altro ancora.
Per prima cosa creiamo un account utente (locale):
useradd -m -s /bin/bash user1
echo user1:pass1 | chpasswd
Effettuiamo il login:
ssh user1@localhost
ls -al
exit
Possiamo anche usare ssh
per eseguire un comando da remoto:
ssh user1@localhost ls -al
ssh user1@localhost whoami
ssh user1@localhost ls .*
ssh user1@localhost 'ls .*'
2) Scrivere una password ogni volta che si usa ssh
diventa rapidamente noioso.
Possiamo invece usare le chiavi, che sono più semplici e sicure.
Per prima cosa generiamo una coppia di chiavi pubbliche/private:
ssh-keygen --help
ssh-keygen -t ecdsa -q -N '' -f ~/.ssh/key1
L'opzione `N '' fa sì che venga generata una chiave che non ha una passphrase.
ls -al ~/.ssh/key1*
cat ~/.ssh/key1
cat ~/.ssh/key1.pub
Per poter effettuare il login al server con questa chiave, dobbiamo inviare la parte pubblica al server:
ssh-copy-id -i ~/.ssh/key1.pub user1@localhost
Ora proviamo a fare il login usando la chiave privata come file di identità:
ssh -i ~/.ssh/key1 user1@localhost
ls -al
cat .ssh/authorized_keys
exit
cat ~/.ssh/key1.pub
Si può notare che la chiave pubblica è stata aggiunta a .ssh/authorized_keys
sul server.
Il risultato è ancora migliore. Aggiungiamo questa configurazione a ~/.ssh/config
:
cat > ~/.ssh/config << EOF
Host server1
Hostname 127.0.0.1
User user1
IdentityFile ~/.ssh/key1
EOF
cat ~/.ssh/config
Ora possiamo semplicemente utilizzare ssh
con il nome server1
, senza dover
specificare il nome host (o l'IP) del server, il nome utente, il file di identità, ecc.
Verranno ottenuti automaticamente dal file di configurazione.
ssh server1
exit
ssh server1 whoami
3) Usare scp
, sftp
, rsync
ecc.
Tutti questi strumenti utilizzano un tunnel SSH per una comunicazione sicura con il server.
Ora che abbiamo un facile accesso ssh
al server, possiamo usare facilmente questi strumenti:
touch foo.txt
scp foo.txt server1:
ssh server1 ls -l
ssh server1 touch bar.txt
ssh server1 ls -l
scp server1:bar.txt .
ls -l bar.txt
sftp
:
sftp server1
ls
help
quit
rsync
:
ls testdir
rsync -av testdir server1:
ssh server1 ls
ssh server1 ls testdir
Filesystem
1) Creare un dispositivo a blocchi virtuale
Linux supporta uno speciale dispositivo a blocchi chiamato dispositivo loop, che mappa un file normale su un dispositivo a blocchi virtuale. Questo permette di utilizzare il file come file system virtuale.
1) Creare un file di dimensioni 1G:
fallocate -l 1G disk.img
du -hs disk.img
2) Creare un dispositivo loop con questo file (bisogna essere superuser) :
losetup -f disk.img
L'opzione -f
trova un dispositivo di loop inutilizzato.
3) Trovare il nome del dispositivo di loop che è stato creato:
losetup -a
losetup -a | grep disk.img
2) Creare un filesystem XFS
Assicurarsi che il pacchetto xfsprogs
sia installato:
apt install xfsprogs
2) Creare un filesystem XFS sul file immagine:
mkfs.xfs -m reflink=1 -L test disk.img
Il metadato -m reflink=1
indica al comando di abilitare i reflink, e -L test
imposta l'etichetta del filesystem.
3) Creare una directory:
mkdir mnt
4) Montare il dispositivo loop su di esso:
mount /dev/loop0 mnt
mount | grep mnt
5) Controllare l'utilizzo del filesystem:
df -h mnt/
Si noti che sono utilizzati solo 40M.
3) Copiare i file con '--reflink'.
1) Creare per prova un file di 100 MB (con dati casuali):
cd mnt/
dd if=/dev/urandom of=test bs=1M count=100
2) Verificare che ora ci siano 140M di spazio su disco utilizzato:
df -h .
3) Creare una copia del file (con i reflink abilitati):
cp -v --reflink=always test test1
4) Controllare la dimensione di ogni file:
ls -hsl
Ognuno di essi è di 100M e in totale ci sono 200M di dati.
5) Tuttavia, se si controlla l'utilizzo del disco, si vedrà che entrambi i file occupano ancora lo stesso spazio di prima 140M:
df -h .
Questo dimostra la funzione di risparmio di spazio dei reflink. Se il file fosse stato abbastanza grande, avremmo notato anche che la copia di reflink non richiede tempo, viene eseguita istantaneamente.
4) Cleanup
1) Smontare e cancellare la directory di prova mnt/
:
cd ..
umount mnt/
rmdir mnt/
2) Cancellare il dispositivo di loop:
losetup -a
losetup -d /dev/loop0
3) Rimuovere il file utilizzato per creare il dispositivo loop:
rm disk.img
Gestione dei pacchetti
apt update
apt upgrade
apt list emacs*
apt show emacs
apt install emacs
emacs
apt remove emacs
LEZIONE 9
Prima di iniziare con lo scripting bash (nella prossima lezione), faremo una rapida introduzione agli editor Vim ed Emacs.
Nozioni di base su Vim
1) Avvio e chiusura.
vim
Il carattere tilde (~
) all'inizio di una riga significa che non c'è testo in quella riga.
Per uscire premere :q
2) Modalità di modifica.
Avviamolo di nuovo, passandogli il nome di un file inesistente:
rm -f foo.txt
vim foo.txt
Vim ha una modalità di comando e una modalità di inserimento. Nella modalità comando i tasti digitati vengono interpretati come comandi. Nella modalità insert possiamo aggiungere testo al file.
All'avvio, Vim si trova nella modalità comando. Per passare alla modalità insert diamo il comando:
i
Notate lo stato -- INSERISCI --
in basso.
Ora inseriamo del testo:
The quick brown fox jumped over the lazy dog.
Per uscire dalla modalità insert e tornare alla modalità command, premere ESC
.
Per salvare le modifiche al file:
:w
3) Spostamento del cursore.
In modalità di comando, è possibile spostarsi con i tasti:
h
-- sinistral
-- destraj
-- in bassok
-- su
Premete alcune volte h
e l
.
Si può anche usare:
- 0
-- all'inizio della riga
- $
-- alla fine della riga
- w
-- all'inizio della parola o del carattere di punteggiatura successivo
- W
-- all'inizio della parola successiva (ignora la punteggiatura alla fine di una parola)
- b
-- indietro di una parola o di un carattere di punteggiatura
- B
-- indietro di una parola (ignora la punteggiatura alla fine di una parola)
Provateli un paio di volte.
Se un comando è preceduto da un numero, verrà ripetuto altrettante volte.
Ad esempio, 3w
è uguale a premere w
3 volte.
Si possono anche usare le frecce (sinistra, destra, su, giù).
4) Modifica di base.
Con il comando i
si inizia a inserire il testo nella posizione corrente del cursore.
Per aggiungere testo dopo la posizione corrente si può usare il comando a
.
Per iniziare ad aggiungere testo alla fine della della riga, si usa il comando A
.
Ora digitate:
È stato bello.
Linea 2
Riga 3
Linea 4
Linea 5
Quindi premere ESC
per tornare alla modalità di comando, quindi :w
per salvare (scrivere su file).
- Vai alla prima riga:
1G
- Andare all'ultima riga:
G
. - Andare alla terza riga:
3G
.
Quindi:
- Aprire una nuova riga al di sotto di quella corrente:
o
- Annullare:
ESC
eu
- Apre una nuova riga sopra quella corrente:
O
- Annullamento:
ESC
eu
Altri comandi di Vim
1) Cancellare il testo.
Per cancellare il testo in Vim si possono utilizzare i comandi x
, che cancella il il carattere sul cursore e d
.
1G5w
#Questo comando ci porta all'inizio della prima riga e poi avanti di 5 parole.
Premi x
alcune volte per cancellare alcuni caratteri, poi premete u
per annullare.
Premi 3x
per cancellare 3 caratteri, poi u
per annullare.
Prova anche questi e vedi cosa fanno:
dW
eu
5dW
eu
d$
eu
d0
eu
dd
eu
3dd
eu
dG
eu
d4G
eu
.
2) Tagliare, copiare e incollare.
Quando si cancella del testo, in realtà è come se si tagliasse, perché la parte cancellata viene inserita in un buffer incolla
e può essere incollata da qualche altra parte. Per incollarla dopo il cursore si può usare il comando p
(minuscolo),
per incollarlo prima del cursore possiamo usare la P
maiuscola. Prova:
5x
p
uu
3x
P
uu
5dw
$
p
uu
d$
0
p
u
P
uu
dd
p
u
P
uu
Al posto di d
possiamo usare il comando y
(yank) per copiare il testo.
yw
p
u
P
u
5yw
P
u
yy
p
u
3yy
P
u
p
u
yG
P
u
y$
0
P
u
Per unire una riga con la successiva possiamo usare J
:
3G
J
J
uu
3) Ricerca e sostituzione.
Per trovare un carattere nella riga corrente, premete f
e il carattere:
1G
fa
;
Per spostare il cursore all'occorrenza successiva di una parola o di una frase, si usa il comando /
.
Digitate una parola o una frase dopo di esso e poi date Invio:
/
then Linea
Per trovare la corrispondenza successiva, premere n
:
n
n
n
Per sostituire Linea
con linea
nell'intero file utilizzare:
:% s/Linea/linea/g
È simile al comando sed
. Inizia con un intervallo di righe in cui effettuare la sostituzione.
In questo esempio, %
denota l'intero file ed è lo stesso di 1,$
(dalla prima riga all'ultima).
Si può anche chiedere una conferma aggiungendo il modificatore c
alla fine del comando:
:1,$ s/linea/Linea/gc
Le opzioni sono:
y
-- sìn
-- noa
-- tuttiq
-- escil
-- ultimo (sostituisce questo e abbandona)Ctrl-e
/Ctrl-y
-- scorrere verso il basso e verso l'alto, per vedere il contesto
4) Commentare e decommentare blocchi negli script
Per commentare i blocchi in vim:
- premere
Esc
(per uscire dalla modalità di modifica o da un'altra modalità) - premere
ctrl
+v
(modalità blocco visuale) - usare i tasti freccia
↑/↓
per selezionare le righe desiderate (non evidenzierà tutto - va bene così!) Maiusc
+i
(I maiuscola)- inserisce il testo desiderato, ad esempio
#
- premere
Esc
Per decommentare i blocchi in vim:
- premere
Esc
(per uscire dalla modalità di modifica o da un'altra modalità) - premere
ctrl
+v
(modalità blocco visuale) - utilizzare i tasti freccia
↑/↓
per selezionare le righe da decommentare. -
se si desidera selezionare più caratteri, utilizzare uno dei seguenti metodi o combinarli:
- utilizzare i tasti freccia sinistra/destra per selezionare più testo
- per selezionare pezzi di testo, usare i tasti freccia
shift
+←/→
- è possibile premere ripetutamente i tasti di cancellazione sottostanti, come un normale pulsante di cancellazione
-
premere
d
ox
per eliminare i caratteri, ripetutamente se necessario
Modifica di più file
1) Spesso è utile modificare più di un file alla volta.
Usciamo da vim:
:q!
Creiamo un altro file di prova:
ls -l /usr/bin > ls-output.txt
Avviamo vim con entrambi i file di prova come argomento:
vim foo.txt ls-output.txt
Per vedere l'elenco dei buffer (file aperti):
:buffers
Per passare al buffer successivo premere :bn
:
:bn
:bn
:bn
:bn
Per passare al buffer precedente premere :bp
:
:bp
:bp
:bp
Si può anche passare a un altro buffer in questo modo:
:buffer 2
:buffer 1
2) È anche possibile aggiungere file alla sessione di editing corrente.
:q!
vim foo.txt
:e ls-output.txt
:buffers
3) Durante la modifica di più file, è possibile copiare una porzione di un file in un altro file che si sta modificando. È facile farlo utilizzando i soliti comandi "yank" e "paste".
:buffer 1
1G
yy
:buffer 2
1G
p
:q!
4) È anche possibile inserire un intero file in un file che si sta modificando.
vim ls-output.txt
3G
:r foo.txt
:w foo1.txt
Abbiamo salvato il buffer nel file foo1.txt
, ma stiamo ancora modificando il primo file
e ogni ulteriore modifica verrà effettuata su di esso.
:q!
Tutorial
1) Esiste un tutorial di Vim che può essere avviato con:
vimtutor
2) Per un tutorial su Emacs, avviare emacs con emacs -nw
, poi premere Ctrl-h
e t
.
Oppure spostarsi (con la freccia in basso su Emacs tutorial
e poi premere Invio.
3) Altri tutorial online:
-
https://openvim.com/
-
https://www.gnu.org/software/emacs/tour/
LEZIONE 10
In questa LEZIONE vedremo: - come creare ed eseguire uno script bash - come produrre del testo in uscita da uno script bash - variabili, costanti e funzioni di uno script bash
Gli script o altri file pronti della lezione si trovano nella cartella Materiali/lesson10
Creare ed eseguire uno script
1) Creiamo uno script che stampi "Hello World!".
vim hello.sh
i
#!/bin/bash
# questo è il primo script
echo 'Hello World!'
ESC
:wq
ls -l hello.sh
cat hello.sh
cat hello.sh | bash
bash hello.sh
Lo stiamo inviando a bash
, e bash interpreta ed esegue i comandi all'interno dello script.
2) Rendiamo lo script eseguibile:
chmod +x hello.sh
ls -l hello.sh
Proviamo a eseguirlo:
hello.sh
Dice "comando non trovato". Questo perché la shell cerca il comando in alcune directory,
che sono elencate nella variabile d'ambiente PATH
:
echo $PATH
Non c'è nessun comando hello.sh
in nessuna di queste directory, quindi la shell non può trovare tale comando.
Per risolvere questo problema si può aggiungere la directory corrente a PATH
:
PATH="$(pwd):$PATH"
echo $PATH
hello.sh
Un altro modo è quello di indicare a bash il percorso del comando, in questo modo:
./hello.sh
Quando diamo un percorso al comando (./
in questo caso), la shell non usa la variabile PATH
, ma cerca di trovare il comando nel percorso indicato.
3) Lo shebang.
Il carattere #
è di solito chiamato hash, mentre !
è di solito chiamato bang.
Insieme si chiamano shebang e si collocano all'inizio di uno script (senza righe vuote e senza spazi vuoti prima).
Indicano alla shell come interpretare lo script. Nel nostro caso /bin/bash
viene dopo di essi, quindi lo script sarà inviato a /bin/bash
per interpretarlo.
Lo stesso modo può essere utilizzato per interpretare uno script Python, ecc. Per esempio, nel caso di uno script Python, la prima riga sarebbe come questa:
#!/usr/bin/python3
.
4) I commenti sono indicati con un #
. Tutto ciò che viene dopo un #
è considerato un commento e viene ignorato dall'interprete bash.
echo 'Ciao' # questo è un commento
Avvio di un progetto
Inizieremo a scrivere uno script che genera un rapporto su vari stati e statistiche del sistema. Questo rapporto sarà in formato HTML.
1) Il primo passo consiste nello scrivere un programma (script) che generi una pagina HTML di base nello standard output.
Un esempio di pagina HTML di base si trova nel file page.html
:
cat page.html
Si può visualizzare la pagina col browser web non grafico lynx:
lynx page.html
Per uscire:
qy
mv page.html sys_info.sh
vim sys_info.sh
Inseriamo all'inizio di ogni riga echo " :
:1,$ s/^/echo "/
Inseriamo alla fine di ogni riga " :
:1,% s/$/"/
Spostiamoci all'inizio del file e aggiungiamo una riga:
1G
O
Incolliamo all'inizio dello script:
#!/bin/bash
# Programma per produrre una pagina di informazioni sul sistema
ESC
:wq
chmod +x sys_info.sh
./sys_info.sh
./sys_info.sh > sys_info.html
lynx sys_info.html
qy
2) Si può rendere questo script più semplice e chiaro utilizzando un singolo echo
:
vim sys_info.sh
:6,$ s/echo "//
:5,$-1 s/"$//
:wq
./sys_info.sh
Una stringa quotata può contenere newline e quindi contenere più righe di testo.
3) Inseriamo alcuni dati nel report:
vim sys_info.sh
:% s/Page Title/System Information Report/
:% s#Page body#<h1>System Information Report</h1>#
:wq
./sys_info.sh
4) Possiamo usare una variabile per evitare la ripetizione del testo "System Information Report"
vim sys_info.sh
:% s/System Information Report/$title/
Creiamo una nuova riga vuota sopra echo ed inseriamo la variabile:
/echo
O
title="System Information Report"
ESC
:wq
./sys_info.sh
Variabili e costanti
1) Le variabili in bash non devono essere dichiarate, si usano e basta:
foo="yes"
echo $foo
Qui abbiamo un'espansione di shell, che è la stessa di: echo yes
echo $foo1
È la prima volta che la shell vede la variabile foo1
, tuttavia non si lamenta, ma si limita a crearla e a darle un valore vuoto.
Questo significa che bisogna fare attenzione all'ortografia dei nomi delle variabili, altrimenti si possono ottenere risultati strani.
touch foo.txt
foo=foo.txt
foo1=foo1.txt
cp $foo $fool
Abbiamo sbagliato a scrivere il secondo argomento, quindi la shell lo espande a una stringa vuota e si ottiene un errore da cp
.
2) Per indicare le costanti in bash, per convenzione, si usano nomi di variabili in maiuscolo:
vim sys_info.sh
:% s/$title/$TITLE/g
:% s/title=/TITLE=/
:/^TITLE=/ s/Report/Report per $HOSTNAME/
:wq
./sys_info.sh
Abbiamo anche usato la variabile d'ambiente HOSTNAME
. Le variabili d'ambiente sono considerate come costanti, quindi sono in maiuscolo.
In realtà, c'è un modo per assicurarsi che una variabile non possa cambiare (è una costante), anche se non viene usato di frequente:
sed -i sys_info.sh -e 's/TITLE=/declare -r TITLE=/'
cat sys_info.sh
./sys_info.sh
L'opzione -r
di declare
significa "sola lettura". Quindi, non è possibile assegnare un valore a questa variabile.
3) Quando si assegna un valore a una variabile, non ci devono essere spazi intorno al segno di uguale:
a=z
echo $a
Le espansioni della shell vengono applicate al valore, prima dell'assegnazione:
b="una stringa"
c="una stringa e $b"
echo $c
d=$(ls -l foo.txt)
echo $d
e=$((5 * 7))
echo $e
f="stringa di testo"
echo $f
echo -e $f
help echo
È possibile eseguire più assegnazioni su una singola riga:
a=5 b="una stringa"
echo $a $b
4) Durante l'espansione, i nomi delle variabili possono essere delimitati da parentesi graffe {}
,
che sono necessarie in alcuni casi. Ad esempio:
filename="myfile"
touch $filename
mv $filename$ $filename1
Quello che vogliamo è rinominare il file in myfile1
, ma la shell interpreta filename1
.
interpreta filename1
come una variabile, che naturalmente non è stata
non è ancora stata assegnata ed è vuota. Dovremmo fare così
mv $filename ${filename}1
ls -l myfile1
5) Aggiungiamo un timestamp al report, usando variabili e costanti:
date +"%x %r %Z"
echo $USER
vim sys_info.sh
/TITLE=
o
CURRENT_TIME=$(date +"%x %r %Z")
TIMESTAMP="Generato il $CURRENT_TIME da $USER"
ESC
e :w
/<h1>
Yp
:s/h1/p/g
:s/TITLE/TIMESTAMP/
:wq
./sys_info.sh
Here documents
Un here document è una forma aggiuntiva di reindirizzamento dell'I/O in cui si incorpora un testo nel nostro script e lo si inserisce nello standard input di un comando. Funziona così:
comando << token
. . . . .
testo
. . . . .
token
dove comando è un comando che accetta lo standard input e token è una stringa usata per indicare la fine del testo incorporato. Dovrebbe trovarsi all'inizio della riga e non deve avere spazi di separazione.
1) Modifichiamo lo script per utilizzare un here document:
vim sys_info.sh
/echo
O
Inseriamo la riga:
cat << EOF
ESC
Ci spostiamo in fondo allo script e aggiungiamo una riga:
Go
e aggiungiamo la riga:
EOF
ESC
Eliminiamo le stringhe echo " :
:%s/echo "//
e poi le virgolette in fondo all'ultima riga html:
Gk$x
:wq
./sys_info.sh
Invece di usare echo
, lo script ora usa cat
e un documento here.
2) Il vantaggio di un here document è che all'interno del testo si possono usare liberamente gli apici singoli e doppi, poiché non sono interpretati dalla shell come delimitatori di una stringa. Per esempio:
foo="un testo qualsiasi"
cat << EOF
$foo
"$foo"
'$foo'
\$foo
EOF
La shell tratta le virgolette come caratteri ordinari.
3) Vediamo anche che le variabili all'interno del testo sono espanse. Per evitare l'espansione delle variabili, possiamo racchiudere il token tra virgolette:
cat << "EOF"
$foo
"$foo"
'$foo'
\$foo
EOF
cat << 'EOF'
$foo
"$foo"
'$foo'
\$foo
EOF
4) Innanzitutto scarichiamo nella nostra cartella di test gli script ftp1.sh
e ftp2.sh
.
NOTA: Modificare prima i due script, inserendo una versione di debian attuale in FTP_PATH (consultare il sito http://ftp.nl.debian.org/debian/dists/). Se si è dietro un proxy fare in modo di uscire diretti!
Gli here document possono essere usati con qualsiasi comando che accetti lo standard input.
Per esempio, possiamo usarlo con ftp
per recuperare un file:
vim ftp1.sh
:q!
./ftp1.sh
Se cambiamo l'operatore di reindirizzamento da <<
a <<-
, la shell ignorerà i caratteri di tabulazione iniziali nel documento.
Questo consente al documento here di essere indentato, migliorando così la leggibilità.
vim ftp2.sh
:q!
./ftp2.sh
Funzioni di shell
Le funzioni possono essere dichiarate in una di queste due forme, che sono equivalenti:
nome della funzione { comandi return }
nome () { comandi return }
1) Un semplice esempio di funzione si trova nello script fun.sh
:
vim fun.sh
:q!
./fun.sh
2) All'interno di una funzione si possono usare variabili locali:
vim local-vars.sh
:q!
./local-vars.sh
3) Mostriamo alcune informazioni aggiuntive nella pagina del report, utilizzando le funzioni. Vorremmo visualizzare informazioni su:
- Tempo di attività e carico del sistema.
- Spazio su disco.
- Spazio nella home.
Definiamo una funzione per ciascuna di esse:
vim sys_info.sh
/cat
O
Incolliamo le righe seguenti:
report_uptime () {
return
}
report_disk_space () {
return
}
report_home_space () {
return
}
ESC
e :w
Richiamiamo queste funzioni all'interno del body html:
/TIMESTAMP
o
Incolliamo le righe seguenti:
$(report_uptime)
$(report_disk_space)
$(report_home_space)
ESC
e :wq
./sys_info.sh
4) Vediamo che ogni funzione è sostituita da una riga vuota e non sappiamo cosa stia succedendo. Mostriamo alcuni feedback da ogni funzione:
vim sys_info.sh
/report_uptime
o
echo "Eseguita la funzione report_uptime."
ESC
/report_disk_space
o
echo "Eseguita la funzione report_disk_space."
ESC
/report_home_space
o
echo "Eseguita la funzione report_home_space."
ESC
e :wq
./sys_info.sh
5) Ora forniamo i dati reali:
uptime
df -h .
du -hs $HOME
vim sys_info.sh
/report_uptime
jddO
cat <<- _EOF_
<h2>Tempo di attività del sistema</h2>
<pre>$(uptime)</pre>
_EOF_
ESC
/report_disk_space
JddO
cat <<- _EOF_
<h2>Utilizzo dello spazio su disco</h2>
<pre>$(df -h .)</pre>
_EOF_
ESC
/report_home_space
JddO
cat <<- _EOF_
<h2>Utilizzo dello spazio della home</h2>
<pre>$(du -hs $HOME)</pre>
_EOF_
ESC
e :wq
./sys_info.sh
6) Controllare nel browser:
./sys_info.sh > sys_info.html
lynx sys_info.html
qy
LEZIONE 11
In questa LEZIONE vedremo:
- come fare una diramazione con
if
- leggere l'input da tastiera
Gli script o altri file pronti della lezione si trovano nella cartella Materiali/lesson11
Diramazione con if
L'istruzione if ha la seguente sintassi:
se _comando_; allora
_comando_
[elif _comando_; allora
_comando_...]
[else
_comando_]
fi
1) I comandi (compresi gli script e le funzioni di shell) restituiscono uno stato di uscita. Per convenzione, uno stato di uscita pari a zero indica successo e qualsiasi altro valore indica un fallimento.
ls -d /usr/bin
echo $?
# la cartella /usr/bin esiste e si può elencare
ls -d /bin/usr
echo $?
# la cartella /bin/usr non esiste e non si può elencare
I comandi incorporati true
e false
non fanno nulla, se non restituire uno stato di uscita:
true
echo $?
false
echo $?
2) L'istruzione if
valuta il successo o il fallimento dei comandi, in base al loro stato di uscita:
if true; then echo "È vero."; fi
if false; then echo "È vero."; fi
Se un elenco di comandi segue if
, l'ultimo comando dell'elenco viene valutato:
if false; true; then echo "È vero."; fi
if true; false; then echo "È vero"; fi
3) Il comando utilizzato più frequentemente con if
è test
, che esegue una serie di controlli e confronti.
touch foo.txt
if test -e foo.txt; then echo "File exists"; fi
if [ -e foo.txt ]; then echo "File exists"; else echo "File does not exist"; fi
Il comando [
è equivalente a test
(richiede ]
come ultimo argomento).
rm -f foo.txt
if [ -e foo.txt ]; then echo "Il file esiste"; else echo "Il file non esiste"; fi
4) Vediamo un esempio di script che testa i file:
./test-file.sh
vim test-file.sh
Notate che il parametro $FILE
è quotato all'interno dell'espressione.
Questo non è richiesto, ma è una difesa contro il rischio che il parametro sia vuoto o contenga solo spazi bianchi.
Si noti anche il comando exit
alla fine. Può opzionalmente accettare un numero come argomento,
che diventa lo stato di uscita, il successo o il fallimento, dello script.
Senza un argomento, il valore predefinito è lo stato di uscita dell'ultimo comando eseguito.
Se il comando exit
non è presente, lo stato di uscita dello script sarà quello dell'ultimo comando eseguito.
:q!
Vediamo un esempio simile che utilizza invece una funzione:
vim test-file-fun.sh
Notate che invece di exit
, una funzione può usare return
per indicare lo stato di uscita della funzione.
:q!
./test-file-fun.sh
5) Un esempio con il test delle stringhe:
./test-string.sh
vim test-string.sh
Si noti che quando si verifica un errore (ANSWER
è vuoto), si stampa il messaggio di errore in stderr
a.
il messaggio di errore su stderr
, reindirizzando l'output di echo
(>&2
).
(>&2
). Restituiamo anche un codice di uscita di 1 con exit 1
.
:q!
Un esempio simile con il test dei numeri interi:
vim test-integer.sh
:q!
./test-integer.sh
Per maggiori dettagli sui test disponibili si veda la guida:
help test | less
help [
Altre costruzioni di test
1) Le versioni moderne di bash includono un comando composto che agisce come una
sostituzione migliorata di test: [[ espressione ]]
.
Ha anche un operatore per la corrispondenza con le espressioni regolari: =~
.
vim test-integer2.sh
./test-integer2.sh
Un'altra caratteristica aggiuntiva di [[ ]]
è che l'operatore `==' supporta la corrispondenza dei pattern
nello stesso modo in cui lo fa l'espansione del nome del percorso:
FILE=foo.bar
if [[ $FILE == foo.* ]]; then echo "$FILE corrisponde al pattern 'foo.*'"; fi
2) Oltre al comando composto [[ ]]
, bash fornisce anche il comando composto (( ))
, utile per operare sui numeri interi.
if ((1)); then echo "È vero."; fi
if ((0)); then echo "È vero."; fi
if ((2)); then echo "È vero."; fi
Con questo comando di test possiamo semplificare un po' lo script precedente:
vim test-integer2a.sh
./test-integer2a.sh
diff -u test-integer2.sh test-integer2a.sh
.
Si noti che non si usa il segno $
per riferirsi alle variabili all'interno di (( ))
.
Inoltre, al posto di -eq
usiamo l'operatore ==
, al posto di -lt
usiamo l'operatore <
, ecc.
Questo rende la sintassi un po' più naturale.
3) Possiamo usare gli operatori logici per creare espressioni complesse.
Per l'espressione test
(e [ ]
) gli operatori logici sono -a
(AND), -o
(OR) e !
(NOT).
Per i comandi [[ ]]
e (( ))
gli operatori logici sono: &&
, ||
e !
.
vim test-integer3.sh
./test-integer3.sh
L'opzione -n
del comando echo
indica di non stampare una newline dopo la stringa.
Si noti che poiché test
e [
sono trattati come comandi (a differenza di [[
e ((
che sono costrutti speciali della shell),
ogni argomento dato loro deve essere separato da uno spazio.
Inoltre, le parentesi che raggruppano le espressioni logiche devono essere escaped in questo modo: \(
e \)
,
altrimenti la shell le interpreterà come qualcos'altro (hanno un significato speciale in shell).
Di solito è più conveniente usare [[
invece di test
o [
.
4) È possibile utilizzare gli operatori &&
(AND) e ||
(OR) per l'esecuzione condizionale di un comando.
Possono essere usati in questo modo:
comando1 && comando2
Prima viene eseguito il comando1
. Se (e solo se) ha successo, viene eseguito anche il comando2
.
comando1 || comando2
Per prima cosa viene eseguito il comando1
. Se (e solo se) fallisce, viene eseguito anche il comando2
.
Per esempio:
mkdir temp && cd temp
[[ -d temp ]] || mkdir temp
Il primo è equivalente a:
if mkdir temp; then cd temp; fi
La seconda è equivalente a:
if [[ -d temp ]]; then : ; else mkdir temp; fi
Il comando :
è un comando nullo, che significa "non fare nulla".
Senza di esso si otterrebbe un errore di sintassi.
cd
Lettura dell'input da tastiera
1) Lo script test-integer2.sh
, che abbiamo visto precedentemente, ha il valore di INT
,
quindi è necessario modificare lo script per per testare un altro valore.
Possiamo renderlo più interattivo utilizzando il comando read
:
vim read-integer.sh
./read-integer.sh
0
./read-integer.sh
7
./read-integer.sh
4
./read-integer.sh
3
./read-integer.sh
-8
Il comando read assegna l'input alla variabile int
.
Se non viene fornito alcun nome della variabile, assegna l'input alla variabile REPLY
.
2) Il comando read
può anche ottenere più nomi di variabili, come in questo esempio:
vim read-multiple.sh
In questo script, vengono assegnati e visualizzati fino a cinque valori.
:q!
./read-multiple.sh
a b c d e
./read-multiple.sh
a b
./read-multiple.sh
a b c d e f g
3) Può anche ottenere alcune opzioni:
help read | less
Con l'opzione -p
possiamo fornire una stringa di prompt:
read -p "Inserire uno o più valori >"
a b c
echo "REPLY = '$REPLY'"
L'opzione -s
può essere usata per un input silenzioso e -t
per impostare un timeout.
Vediamole in un esempio che cerca di leggere una password:
vim read-user-pass.sh
./read-user-pass.sh
Se non si digita una password entro 10 secondi, il comando read
andrà in timeout con un codice di uscita di errore.
4) L'input fornito a read
viene diviso dalla shell. Esiste una variabile di shell denominata IFS
(Internal Field Separator) che contiene un elenco di separatori.
Per impostazione predefinita, contiene uno spazio, un tab e un carattere newline. Ognuno di essi può separare gli elementi l'uno dall'altro.
Se si vuole modificare il modo in cui l'input viene separato in campi campi, si può cambiare il valore di IFS
.
vim read-ifs.sh
Si noti che abbiamo impostato IFS=":"
prima di chiamare read
.
La shell permette che una o più assegnazioni di variabili avvengano immediatamente prima di un comando.
Queste assegnazioni modificano l'ambiente per il comando che segue.
L'effetto dell'assegnazione è temporaneo, per la durata del comando.
È come fare quanto segue, ma in modo più conciso:
OLD_IFS="$IFS"
IFS=":"
read user pw uid gid name home shell <<< "$file_info"
IFS="$OLD_IFS"
L'operatore <<<
indica una here string. Una stringa here è come un documento here document, solo che è più breve e consiste in una singola stringa.
Dobbiamo usarlo perché read
non funziona bene con una pipe (ad esempio: echo "$file_info" | read ...
)
:q!
./read-ifs.sh
xyz
Si tenga presente che il file /etc/passwd contiene solo gli utenti locali (come root) e non gli eventuali utenti di rete
./read-ifs.sh
root
Esempi
1) Vediamo un esempio di programma che convalida il suo input:
vim validate.sh
:q!
Provate a farlo un paio di volte con input diversi:
./validate.sh
2) Vediamo un esempio di programma guidato da un menu:
vim menu.sh
Notate l'uso del comando exit
in questo script.
È usato per evitare che lo script esegua codice non necessario dopo l'esecuzione di un'azione.
:q!
Provate a ripetere l'operazione un paio di volte.
3) Come esercizio, provate a modificare questi esempi in modo che al posto di [[... ]]
e ((...)
, utilizzino il comando test
.
Suggerimento: Usate grep per valutare le espressioni regolari e valutare lo stato di uscita.
LEZIONE 12
In questa LEZIONE vedremo:
- i cicli con while
/until
- ramificazione con case
- parametri posizionali
Gli script o altri file pronti della lezione si trovano nella cartella Materiali/lesson12
Cicli con while e until
La sintassi del comando while
è la seguente:
while _comando_; do _comando_; done
1) Un semplice esempio:
vim while-count.sh
:q!
./while-count.sh
2) Possiamo utilizzare un ciclo while
per migliorare il programma del menu della lezione precedente:
vim while-menu.sh
Racchiudendo il menu in un ciclo while, siamo in grado di far sì che il programma ripeta la visualizzazione del menu dopo ogni selezione.
Il ciclo continua finché REPLY
non è uguale a 0
e il menu viene visualizzato di nuovo, dando all'utente l'opportunità di effettuare un'altra selezione.
Alla fine di ogni azione viene eseguito il comando sleep
, in modo che il programma si fermi per qualche secondo per consentire di vedere i risultati della
selezione prima di cancellare lo schermo e visualizzare nuovamente il menu.
Una volta che REPLY
è uguale a 0
, il che indica la selezione "quit", il ciclo termina e l'esecuzione continua con la riga successiva a done
.
:q!
./while-menu.sh
3) All'interno di un ciclo in bash possiamo usare break
e continue
.
vim while-menu2.sh
:q!
./while-menu2.sh
4) Il ciclo until
è molto simile al ciclo while
, ma con una condizione negativa (fino a quando non ...).
vim until-count.sh
:q!
./until-count.sh
5) Possiamo anche leggere lo standard input con while
e until
:
cat distros.txt
vim while-read.sh
Il ciclo while
continuerà fino a quando il comando read
avrà successo nel ricevere input da stdin
e noi reindirizziamo stdin per ottenere i dati dal file distros.txt
(usando l'operatore <
alla fine del comando while
).
./while-read.sh
Si può anche usare una pipe (|
) per reindirizzare lo stdin:
vim while-read2.sh
Si noti che stiamo interrompendo i comandi lunghi aggiungendo un \
alla fine di una riga,
in modo da rendere il programma più leggibile e chiaro.
./while-read2.sh
Diramazione con case
Il comando case
è un comando a scelta multipla.
1) Vediamo un esempio che implementa un programma di menu con case
:
vim case-menu.sh
Abbiamo già visto questo esempio in precedenza, implementato con if
, ed è chiaro che con case
è molto più semplice.
./case-menu.sh
case
tenta una corrispondenza con i pattern specificati. Quando viene trovata una corrispondenza, vengono eseguiti i comandi associati allo schema specificato.
Dopo che è stata trovata una corrispondenza, non vengono tentate altre corrispondenze.
2) Gli schemi utilizzati da case sono gli stessi utilizzati dall'espansione dei nomi di percorso (pathname). Ad esempio:
a)
-- corrisponde al carattere "a".[[:alpha:]])
-- corrisponde a qualsiasi carattere alfabetico???)
-- corrisponde a 3 caratteri*.txt)
-- Corrisponde a qualsiasi cosa che finisca in.txt
.*)
-- corrisponde a qualsiasi cosa
È buona norma includere *)
come ultimo pattern in un comando case
.
per catturare qualsiasi valore che non corrisponda a uno schema precedente.
Vediamo un esempio di script con i pattern:
vim case-patterns.sh
./case-patterns.sh
x
./case-patterns.sh
B2
./case-patterns.sh
foo.txt
./case-patterns.sh
xyz
./case-patterns.sh
ab
3) È anche possibile combinare più pattern utilizzando il carattere verticale come separatore. Vediamo un programma di menu modificato che utilizza le lettere invece delle cifre per la selezione del menu:
vim case-menu-l.sh
Notate come i nuovi schemi permettano di inserire sia lettere maiuscole che minuscole.
./case-menu-l.sh
4) Quando un pattern viene trovato, vengono eseguite le azioni corrispondenti e ;;
si assicura che l'elaborazione venga interrotta (senza cercare le corrispondenze agli schemi successivi).
Se invece si vuole provare a far corrispondere anche questi, si può usare ;;&
, come in questo esempio:
vim case4.sh
./case4.sh
a
./case4.sh
X
./case4.sh
+
Parametri posizionali
1) La shell fornisce un insieme di variabili chiamate parametri posizionali che contengono le singole parole nella riga di comando.
Testiamole con un semplice script:
vim posit-param.sh
./posit-param.sh
$(pwd)/posit-param.sh
Notate che $0
contiene la prima parola del comando, che è il nome e il percorso del comando stesso.
./posit-param.sh a b c d
La variabile speciale $#
contiene il numero di argomenti.
Se è necessario utilizzare più di 9 argomenti, si può usare ${10}
, ${11}
ecc. per accedervi.
2) Il comando shift
fa sì che tutti i parametri si "spostino" verso il basso di uno ogni volta che viene eseguito.
vim posit-param2.sh
In questo esempio c'è un ciclo che valuta il numero di argomenti rimanenti e continua finché ce n'è almeno uno.
./posit-param2.sh a b c d
./posit-param2.sh a b c d e f g
3) Ecco un altro esempio:
vim file-info.sh
Questo programma visualizza il tipo di file (determinato dal comando file
) e lo stato del file (dal comando stat
) di un file specificato.
Controlla il primo argomento e, se non esiste, esce con un messaggio di errore che mostra come utilizzare questo script.
Il comando basename
ottiene solo il nome del file (scartando il percorso).
./file-info.sh
./file-info.sh posit-param2.sh
./file-info.sh .
./file-info.sh xyz
4) I parametri posizionali possono essere utilizzati anche con le funzioni.
vim file-info-fun.sh
Notate che $0
contiene sempre il percorso completo della prima voce della riga di comando (cioè il nome del programma), anche all'interno di una funzione.
Si noti anche che FUNCNAME
è una variabile che contiene sempre il nome della funzione corrente.
./file-info-fun.sh
5) La shell fornisce due variabili speciali che contengono l'elenco di tutti i parametri posizionali.
Sono $*
e $@
. Proviamo un esempio che mostra le loro differenze:
vim posit-param3.sh
./posit-param3.sh
Si vede che sia $*
che $@
forniscono 4 parametri. "$*"
fornisce un
singolo parametro e "$@"
restituisce i due parametri originali.
parametri originali. Questo accade perché $*
è una lista di stringhe con tutti i parametri,
mentre $@
è un array di tutti i parametri.
In ogni caso, il costrutto più utile sembra essere "$@"
perché conserva l'elenco originale dei parametri e questo è ciò che serve nella maggior parte dei casi.
Un esempio
Proviamo a migliorare il programma sys_info.sh
, che abbiamo iniziato a costruire in una precedente LEZIONE,
aggiungendo alcuni parametri e opzioni. Vogliamo essere in grado di:
- dirgli di salvare l'output in un file specifico (invece di inviarlo allo stdout), utilizzando le opzioni
-f file
o--file file
. - Dirgli di chiedere interattivamente un nome di file per salvare l'output. Questa opzione deve essere specificata da
-i
o--interactive
. - Usare le opzioni
-h
o--help
per far sì che il programma fornisca in output informazioni sul suo utilizzo.
1) C'è un piccolo repo git nell'archivio sys_info.tgz
, apriamolo:
tar xfz sys_info.tgz
cd sys_info/
ls -al
git log --oneline
git tag
2) Prendiamo prima la versione iniziale dello script (che è stata sviluppato in una lezione precedente):
git checkout -q 1.initial
git status
vim sys_info.sh
./sys_info.sh
Vediamo alcune modifiche e miglioramenti:
3) Racchiudere in una funzione l'ultima parte (che genera la pagina HTML):
git checkout -q 2.write_html_page
git diff 1.initial
4) Aggiungere una funzione che visualizzi l'utilizzo del programma:
git checkout -q 3.usage
git diff 2.write_html_page
5) Aggiungere del codice che legga le opzioni della riga di comando:
git checkout -q 4.process_options
git diff 3.usage
Utilizziamo un ciclo while
e shift
per elaborare tutte le opzioni.
All'interno del ciclo si usa case
per far corrispondere l'opzione con una di quelle che ci si aspetta.
Se l'opzione è -f
(o --file
), interpretiamo il parametro successivo come un file e lo impostiamo nella variabile filename
.
Se l'opzione è -i
(o --interactive
), viene impostata la variabile interactive
a 1
(altrimenti rimarrà vuota).
Si noti che le azioni corrispondenti a -h | --help)
e *)
sono molto simili: visualizzano l'uso e escono dal programma.
Tuttavia il secondo caso è considerato un errore, perché c'è un'opzione sconosciuta/non supportata, quindi l'uso viene inviato a stderr (>&2
)
e il programma esce con il codice 1
(errore).
6) Se viene fornita l'opzione -i
o (--interactive
), il programma dovrebbe ottenere un nome di file in modo interattivo (dalla tastiera).
Vediamo il codice che lo fa:
git checkout -q 5.interactive
git diff 4.process_options
Questo codice viene eseguito solo se la variabile globale interactive
non è vuota.
C'è un ciclo while
infinito che cerca di leggere il nome del file nella variabile globale filename
.
Controlla che il valore dato non sia vuoto e che tale file non esista già.
Se esiste già un file di questo tipo, si chiede di nuovo se si può sovrascrivere il file o meno.
Utilizziamo il ciclo in modo da poter chiedere di nuovo un altro nome di file se quello dato non è adatto,
e ripetiamo l'operazione fino a quando non abbiamo un nome di file adatto (memorizzato nella variabile filename
).
7) Vediamo ora il codice che produce la pagina HTML:
git checkout -q 6.output_html_page
git diff 5.interactive
Se la variabile filename
è vuota, allora la pagina HTML verrà inviata a stdout, come in precedenza.
Altrimenti il programma cercherà di inviarla al file indicato (usando il reindirizzamento).
Il programma si assicura anche che sia possibile scrivere sul file, provando a creare prima un file vuoto.
8) Infine, studiamo l'ultima versione del programma e testiamola alcune volte.
git checkout master
vim sys_info.sh
:set tabstop=8
Si noti che abbiamo inserito quasi tutto il codice all'interno di funzioni.
C'è una funzione main()
che chiama alcune altre funzioni, poi queste funzioni ne chiamano altre e così via.
La funzione main()
viene chiamata alla fine del programma, in questo modo:
main "$@"
In questo modo ci si assicura che tutti i parametri forniti al programma vengano passati alla funzione main.
La funzione main, a sua volta, li passa tutti alla funzione process_options
, in questo modo:
process_options "$@"
Test dell'esempio
1) Vediamo l'utilizzo del programma:
./sys_info.sh -h
./sys_info.sh --help
2) Vediamo che possiamo anche chiamarlo senza alcun parametro. Proviamo con questo metodo:
./sys_info.sh
./sys_info.sh > report1.html
lynx report1.html
qy
3) Possiamo scrivere l'output in un file usando l'opzione -f
o --file
:
./sys_info.sh -f report2.html
cat report2.html
./sys_info.sh --file report3.html
4) Proviamo anche il metodo interattivo:
./sys_info.sh -i
report1.html
n
report2.html
y
./sys_info.sh --interactive
report3.html
n
report4.html
lynx report4.html
qy
LEZIONE 13
In questa lezione vedremo:
-
il ciclo con
for
-
espansione dei parametri
-
valutazione ed espansione aritmetica
Ciclo con for
1) Con for
possiamo eseguire il ciclo (looping) di un elenco di parole:
for i in A B C D; do echo $i; done
for i in {A..D}; do echo $i; done
for i in *.sh; do echo "$i"; done
In questi casi utilizza le espansioni della shell.
L'espansione dell'ultimo file può fallire se non ci sono file di questo tipo, in tal caso shell restituirà solo *.sh
(invece di un elenco di file corrispondenti).
Per evitare che ciò accada, si può riscrivere l'ultimo esempio in questo modo:
for i in *.sh; do [[ -e "$i" ]] && echo "$i"; done
2) Vediamo un esempio che trova la parola più lunga in un file:
vim longest-word.sh
In realtà può prendere uno o più file come parametri e processare ciascuno di essi.
Questo è implementato dal ciclo while
([[ -n "$1" ]] controlla che la variabile non sia vuota):
while [[ -n "$1" ]]; do
if [[ -r "$1" ]]; then
# . . . . .
# . . . . .
fi
shift
done
Il if
controlla che il file sia leggibile (altrimenti viene saltato).
L'elenco delle parole del file è prodotto dal comando strings "$1"
.
Utilizziamo una sostituzione di comando per usare questo elenco nell'istruzione for:
for i in $(strings "$1"); do
Si noti che non stiamo racchiudendo $(strings "$1")
tra doppi apici, altrimenti verrebbero trattati come una singola stringa,
il che non è quello che vogliamo.
Proviamo:
echo *
./longest-word.sh *
3) Vediamo una versione leggermente modificata dell'esempio precedente:
vim longest-word2.sh
La modifica consiste nel sostituire il ciclo while
con un ciclo for
come questo:
for i; do
if [[ -r "$i" ]]; then
# . . . . .
# . . . . .
fi
done
Poiché stiamo utilizzando la variabile i
per il ciclo esterno, nel ciclo interno abbiamo sostituito i
con j
.
Quando si omette l'elenco delle parole, for
utilizzerà per impostazione predefinita i parametri posizionali.
echo *
./longest-word2.sh *
4) for
ha anche un'altra forma, che è simile a quella del C (e di molti altri linguaggi):
for ((i=0; i<5; i=i+1)); do echo $i; done
Come sappiamo, il costrutto ((...))
è utilizzato per le espressioni aritmetiche, e al suo interno non si usa un $
davanti alle variabili.
5) Apportiamo anche una piccola modifica al programma sys_info.sh
che abbiamo visto nella scorsa LEZIONE:
vim sys_info.sh
Solo report_home_space ()
(l'ultima funzione) è stata modificata.
Fornisce maggiori dettagli per la home directory di ogni utente e comprende il numero totale di file e sottodirectory in ciascuna di esse.
Vengono inoltre utilizzate alcune variabili locali e utilizziamo printf
(invece di echo
) per formattare parte dell'output.
./sys_info.sh > report.html
lynx report.html
qy
Espansioni di variabili
1) Abbiamo già visto che a volte abbiamo bisogno di racchiudere i nomi delle variabili tra parentesi graffe, per evitare qualsiasi confusione:
a="foo"
echo "$a_file"
echo "${a}_file"
2) Ottenere un valore predefinito se la variabile non è impostata (o è vuota):
${parametro:-parola}
foo=
echo ${foo:-"sostituisci valore se non impostato"}
echo $foo
foo=bar
echo ${foo:-"sostituisci il valore se non impostato"}
echo $foo
3) Assegnare un valore predefinito se la variabile non è impostata (o è vuota):
${parametro:=parola}
foo=
echo ${foo:="valore predefinito se non impostato"}
echo $foo
foo=bar
echo ${foo:="valore predefinito se non impostato"}
echo $foo
Nota: I parametri posizionali e altri parametri speciali non possono essere assegnati in questo modo.
4) Esce con un messaggio di errore se il parametro non è impostato o è vuoto:
${parametro:?parola}
foo=
echo ${foo:? "il parametro è vuoto"}
echo $?
foo=bar
echo ${foo:? "il parametro è vuoto"}
echo $?
5) Restituisce il valore dato solo se il parametro non è vuoto:
${parametro:+parola}
foo=
echo ${foo:+"sostituisci il valore se impostato"}
foo=bar
echo ${foo:+"sostituisci il valore se impostato"}
6) Restituisce i nomi delle variabili che iniziano con un prefisso:
${!prefisso*}
o ${!prefisso@}
echo ${!BASH*}
Operazioni con le stringhe
Lunghezza della stringa:
${#parametro}
Per parametro si intende la variabile che contiene la stringa intera.
foo="This string is long."
echo "'$foo' è lunga ${#foo} caratteri."
Tuttavia, ${#@}
e ${#*}
danno il numero dei parametri posizionali.
2) Estrarre una sottostringa:
${parametro:offset}
Offset è lo spostamento verso destra a partire dal primo carattere.
${parametro:offset:lunghezza}
echo ${foo:5}
echo ${foo:5:6}
${foo: -5}
Se l'offset è negativo si conta dall'ultimo carattere verso sinistra.
${foo: -5:2}
Si noti che è necessario uno spazio prima di -
, per evitare di confondersi con un valore predefinito.
3) Rimuovere del testo all'inizio e alla fine:
${parametro#pattern}
Taglia fino alla prima ricorrenza del pattern partendo da sinistra
${parametro##pattern}
Taglia fino alla seconda ricorrenza del pattern partendo da sinistra
${parametro%pattern}
Taglia fino alla prima ricorrenza del pattern partendo da destra
${parametro%%pattern}
Taglia fino alla seconda ricorrenza del pattern partendo da destra
foo=file.txt.zip
echo ${foo#*.}
echo ${foo##*.}
echo ${foo%.*}
echo ${foo%%.*}
4) Sostituzione:
${parametro/pattern/stringa}
Viene sostituita la prima ricorrenza del pattern
${parametro//pattern/stringa}
Vengono sostituite tutte le ricorrenze del pattern
${parametro/#pattern/stringa}
Viene sostituita la prima ricorrenza del pattern
${parametro/%pattern/stringa}
Viene sostituita l'ultima ricorrenza del pattern
foo=XYZ.XYZ.XYZ
echo ${foo/XYZ/xyz}
echo ${foo//XYZ/xyz}
echo ${foo/%XYZ/xyz}
echo ${foo/#XYZ/xyz}
Se la sostituzione viene omessa, il pattern corrispondente verrà cancellato.
echo ${foo/XYZ}
echo ${foo//XYZ}
echo ${foo/%XYZ}
echo ${foo/#XYZ}
5) Modifichiamo l'esempio precedente di parola più lunga
per usare ${#j}
invece di $(echo -n "$j" | wc -c)
per ottenere la lunghezza di una parola:
diff -u longest-word3.sh longest-word2.sh
vim longest-word3.sh
ls -l /usr/bin > dirlist-usr-bin.txt
./longest-word3.sh dirlist-usr-bin.txt
Non è solo più semplice, ma anche molto più efficiente:
time ./longest-word3.sh dirlist-usr-bin.txt
time ./longest-word2.sh dirlist-usr-bin.txt
6) Conversione del case (da maiusciola a minuscola e viceversa):
foo=ABCD
echo ${foo,}
echo ${foo,,}
foo=abcd
echo ${foo^}
echo ${foo^^}
Possiamo anche dichiarare una variabile per forzare il contenuto ad essere maiuscolo o minuscolo:
declare -u foo
foo=aBcD
echo $foo
declare -l foo
foo=aBcD
echo $foo
unset foo
foo=aBcD
echo $foo
Valutazione ed espansione aritmetica
Abbiamo già visto $((espressione))
, dove espressione è un'espressione aritmetica.
È correlato al comando composto ((...))
che viene utilizzato per la valutazione aritmetica (test di verità).
Qui vedremo altri operatori aritmetici ed espressioni.
1) Per impostazione predefinita, i numeri sono trattati come decimali (base 10). Ma possiamo utilizzare anche numeri ottali (base 8), esadecimali (base 16), ecc.
Decimale: echo $((99))
Ottale: echo $((077))
Esadecimale: echo $((0xff))
Binario: echo $((2#11))
Base 7: echo $((7#66))
2) Operatori aritmetici:
echo $((5 + 2))
echo $((5 - 2))
echo $((5 * 2))
echo $((5 ** 2))
echo $((5 / 2))
echo $((5 % 2))
Un esempio:
vim modulo.sh
./modulo.sh
3) Assegnazione:
foo=
echo $foo
if (( foo = 5 )); then echo "È vero."; fi
echo $foo
Il segno =
qui sopra effettua un'assegnazione, e questa assegnazione è riuscita.
Per verificare l'uguaglianza si può usare ==
.
Altri operatori di assegnazione sono: +=
, -=
, *=
, /=
, %=
.
Esistono anche operatori incrementali/decrementali: ++
e --
foo=1
echo $((foo++))
echo $foo
Viene mostrato il valore prima dell'ultimo incremento
foo=1
echo $((++foo))
echo $foo
Viene mostrato il valore comprensivo dell'ultimo incremento
Vediamo una versione modificata dell'esempio modulo.sh
:
vim modulo2.sh
diff -u modulo.sh modulo2.sh
./modulo2.sh
4) Esistono anche alcuni operatori che lavorano a livello di bit:
~
-- Nega tutti i bit di un numero.<<
-- Sposta a sinistra tutti i bit di un numero.>>
-- Sposta tutti i bit di un numero verso destra.&
-- Esegue un'operazione AND su tutti i bit di due numeri.|
-- Esegue un'operazione OR su tutti i bit di due numeri.^
-- Esegue un'operazione OR esclusiva su tutti i bit di due numeri.
Esistono anche operatori di assegnazione corrispondenti (ad esempio, <<=
) per tutte le operazioni tranne la negazione bitwise.
Vediamo un esempio che stampa le potenze di 2:
for ((i=0;i<8;++i)); do echo $((1<<i)); done
I bit vengono spostati verso destra di tanti posti quant'è il valore di i.
5) Il comando composto ((...))
supporta anche gli operatori di confronto:
==
, !=
, <
, <=
, >
, >=
, &&
(AND logico), ||
(OR logico).
Supporta anche l'operatore ternario: expr1?expr2:expr3
.
Se l'espressione expr1
ha una valutazione non nulla (aritmeticamente vera), allora expr2
; altrimenti expr3
.
Le espressioni logiche seguono le regole della logica aritmetica;
cioè, espressioni che valgono zero sono considerate false, mentre espressioni non nulle sono considerate vere.
Il comando composto ((...))
mappa i risultati nei normali codici di uscita della shell.
if ((1)); then echo "true"; else echo "false"; fi
if ((0)); then echo "true"; else echo "false"; fi
L'operatore ternario è come un'istruzione compatta if/then/else
:
a=0
((a<1?++a:--a))
echo $a
((a<1?++a:--a))
echo $a
a=$((a<1?a+1:a-1))
echo $a
6) Vediamo un esempio più completo di utilizzo di operatori aritmetici in uno script che produce una semplice tabella di numeri.
vim arith-loop.sh
./arith-loop.sh
7) Per aritmetiche complesse possiamo usare bc
, che è una calcolatrice a precisione arbitraria.
bc <<< '2 + 2'
echo '2 + 2' | bc
Questo esempio di script calcola i pagamenti mensili del mutuo:
vim loan-calc.sh
./loan-calc.sh 135000 0.0775 180
Questo esempio calcola la rata mensile di un prestito di 135.000 dollari al 7,75% per 180 mesi (15 anni).
Si noti la precisione della risposta. Questa è determinata dal valore dato alla variabile speciale scale
nello script bc
.
Per maggiori dettagli su bc
vedere:
man bc
o info bc
.
LEZIONE 14
In questa LEZIONE vedremo: - array - comandi di gruppo - subshell - sostituzione di processo - trappole - esecuzione asincrona - pipe denominate
Array
Gli array sono variabili che contengono più di un valore alla volta.
1) Per iniziare:
a[0]=foo
echo ${a[0]}
days=(Sun Mon Tue Wed Thu Fri Sat)
echo ${days[0]}
echo ${days[*]}
echo $days[*]
echo $days
Se non viene fornito alcun indice, viene restituito il primo elemento.
days=( [3]=Wed [4]=Thu [5]=Fri [6]=Sat [0]=Sun [1]=Mon [2]=Tue)
echo ${days[*]}
La lista viene ordinata in base agli indici, se assegnati.
2) Vediamo un esempio che conta i file in base al tempo di modifica e li mostra in una tabella. Uno script di questo tipo potrebbe essere usato per determinare quando un sistema è più attivo.
./hours.sh
./hours.sh /usr/bin/
vim hours.sh
???
Per ottenere l'ora dell'ultima modifica dei file si usa il comando
stat
:
stat --help | less
stat -c %y *
stat -c %y * | cut -c 12-13
Utilizziamo l'ora di modifica come indice per l'array.
3) Visualizzazione dell'intero contenuto di un array:
animali=("un cane" "un gatto" "un pesce")
for i in ${animali[*]}; do echo $i; done
for i in ${animali[@]}; do echo $i; done
for i in "${animali[*]}"; do echo $i; done
for i in "${animali[@]}"; do echo $i; done
Si noti che questo è simile al comportamento dell'array di parametri posizionali: $*
, $@
, "$*"
, "$@"
.
4) Il numero di elementi dell'array:
a[100]=foo
echo ${#a[@]}
C'è un solo elemento nell'array.
echo ${#a[100]}
Questa è la lunghezza dell'elemento 100.
Ricordate che $#
è il numero di parametri posizionali.
5) Trovare chiavi e valori di un array:
foo=([2]=a [4]=b [6]=c)
for i in "${foo[@]}"; do echo $i; done
for i in "${!foo[@]}"; do echo $i; done
6) Aggiungere elementi alla fine di un array:
pippo=(a b c)
echo ${pippo[@]}
pippo+=(d e f)
echo ${pippo[@]}
7) Non è così difficile,con un po' di codice, ordinare un array :
vim array-sort.sh
./array-sort.sh
8) Per cancellare un array, usare il comando unset
:
foo=(a b c d e f)
echo ${pippo[@]}
unset foo
echo ${foo[@]}
Può anche essere usato per cancellare singoli elementi di un array.
pippo=(a b c d e f)
echo ${pippo[@]}
unset 'pippo[2]'
echo ${pippo[@]}
Notate che l'elemento dell'array deve essere virgolettato per evitare che la shell esegua l'espansione del nome del percorso.
9) Notate inoltre che l'assegnazione di un valore vuoto a un array non svuota il suo contenuto:
pippo=(a b c d e f)
pippo=
echo ${pippo[@]}
Questo perché qualsiasi riferimento a una variabile di un array senza un indice si riferisce all'elemento zero dell'array. Ad esempio:
foo=(a b c d e f)
echo ${foo[@]}
foo=A
echo ${foo[@]}
10) Gli array associativi utilizzano stringhe piuttosto che numeri interi come indici dell'array:
declare -A colors
colors["red"]="#ff0000"
colors["green"]="#00ff00"
colors["blue"]="#0000ff"
Gli array associativi devono essere creati con declare -A
.
Ai suoi elementi si accede allo stesso modo degli array indicizzati a interi:
echo ${colors["green"]}
Comandi di gruppo. Sottogruppi. Sostituzioni di processo.
1) Comandi di gruppo e sottogruppi.
Comando di gruppo: { comando1; comando2; [comando3; ... ;] }
.
Subshell: (comando1; comando2; [comando3;...])
A causa del modo in cui bash implementa i comandi di gruppo, le parentesi graffe devono essere separate dai comandi da uno spazio e l'ultimo comando deve essere terminato con un punto e virgola o con una newline prima della parentesi graffa di chiusura.
I comandi di gruppo e le subshell sono entrambi utilizzati per gestire il reindirizzamento:
date > foo.txt
ls -l > output.txt
echo "Elenco di foo.txt" >> output.txt
cat foo.txt >> output.txt
{ ls -l; echo "Elenco di foo.txt"; cat foo.txt; } > output.txt
(ls -l; echo "Elenco di foo.txt"; cat foo.txt) > output.txt
{ ls -l; echo "Elenco di foo.txt"; cat foo.txt; } | less
2) Vediamo un esempio che stampa un elenco dei file presenti in una directory, insieme ai nomi del proprietario del file e del gruppo proprietario. Alla fine dell'elenco, lo script stampa un conteggio del numero di file appartenenti a ciascun proprietario e gruppo.
./array-2.sh /usr/bin
vim array-2.sh
3) Un comando di gruppo ({ . . . }
) esegue tutti i suoi comandi nella shell corrente.
Una subshell (( . . . )
), come suggerisce il nome, esegue i suoi comandi in una copia figlia della shell corrente.
Ciò significa che l'ambiente ambiente viene copiato e data a una nuova istanza della shell.
Quando la subshell esce, la copia dell'ambiente viene persa,
quindi qualsiasi modifica apportata all'ambiente della subshell (compresa l'assegnazione di variabili) viene persa.
Pertanto, nella maggior parte dei casi, a meno che uno script non richieda una subshell, i comandi di gruppo sono preferibili alle alle subshell.
I comandi di gruppo sono più veloci e richiedono meno memoria.
Abbiamo già visto che l'uso di read
con pipe non funziona come ci si aspetterebbe.
echo "pippo" | read
echo $REPLY
Questo perché la shell esegue il comando dopo la pipe (read
in questo caso) in una subshell.
Il comando read
assegna un valore alla variabile REPLAY
nell'ambiente della subshell,
ma una volta terminata l'esecuzione del comando, la subshell e il suo ambiente vengono distrutti.
Pertanto, la variabile REPLAY
della shell corrente è ancora non assegnata (non ha un valore).
4) Per ovviare a questo problema, la shell fornisce una forma speciale di espansione, chiamata sostituzione di processo (process substitution).
Per i processi che producono output standard si presenta come segue:
<(lista-di-comandi)
Per i processi che utilizzano l'input standard si presenta come segue:
>(lista-di-comandi)
Per risolvere il nostro problema con read
possiamo utilizzare la sostituzione dei processi come questo:
read < <(echo "foo")
echo $REPLY
Ciò che sta accadendo è che la sostituzione di processo ci permette di trattare l'output di una subshell come un file ordinario ai fini del reindirizzamento.
echo <(echo "foo")
Usando echo
vediamo che l'output della subshell viene fornito da un file chiamato /dev/fd/63
.
5) Vediamo un esempio di loop read
che elabora il contenuto di una directory
di un elenco di directory creato da una subshell:
vim pro-sub.sh
Dato che stiamo usando read
, non possiamo usare una pipe per inviare i dati ad essa.
./pro-sub.sh
./pro-sub.sh /usr/bin | less
Trappole. Esecuzione asincrona. Pipe denominati.
1) Sappiamo che i programmi possono rispondere ai segnali. Possiamo aggiungere questa capacità anche ai nostri script. Bash fornisce un meccanismo per questo scopo, noto come trap (trappola). Vediamo un semplice esempio:
vim trap-demo.sh
Quando si preme Ctrl-c durante l'esecuzione dello script, quest'ultimo intercetta il segnale e risponde eseguendo il comando echo
.
Proviamo:
./trap-demo.sh
Premere Ctrl-c un paio di volte e vedere cosa succede.
2) È più conveniente dire a trap
di chiamare una funzione in risposta a un segnale, invece di un comando complesso.
Vediamo un altro esempio:
vim trap-demo2.sh
Si noti l'inclusione di un comando di uscita in ciascuna delle funzioni di funzioni di gestione dei segnali. Senza un'uscita, lo script continuerebbe dopo aver completato la funzione.
./trap-demo2.sh
Premere Ctrl-c.
3) Bash ha un comando integrato che aiuta a gestire l'esecuzione asincrona .
Il comando wait
fa sì che uno script genitore si metta in pausa finché un processo specificato (cioè lo script figlio) non termina.
Questo può essere spiegato meglio con un esempio. Avremo bisogno di due script, uno script padre e uno script figlio:
vim async-child.sh
Questo è un semplice script che viene eseguito per 5 secondi.
vim async-parent.sh
Da questo script si lancia lo script figlio.
Poiché stiamo aggiungendo &
dopo di esso, lo script genitore non aspetterà che termini l'esecuzione del figlio,ma continuerà la sua esecuzione.
Entrambi gli script sono in parallelo.
Subito dopo aver lanciato il figlio, il genitore utilizza la variabile speciale $!
per ottenere l'ID del processo (PID) del figlio.
Questa variabile contiene sempre il PID dell'ultimo job messo in background.
Poi, più avanti nello script genitore, si usa il comando wait
per fermare l'esecuzione del genitore, finché lo script figlio non è terminato.
Proviamo:
./async-parent.sh
Tutti i messaggi in uscita dal genitore sono preceduti da Parent:
e tutti i messaggi emessi dal figlio sono preceduti dal prefisso Child:
.
Questo ci aiuta a capire il flusso di esecuzione.
4) Le Named pipes si comportano come file, ma in realtà formano buffer first-in first-out (FIFO). Come per le pipe ordinarie (senza nome), i dati entrano da una parte ed escono dall'altra.
Con le pipe denominate è possibile impostare qualcosa di simile a questo:
processo1 > named_pipe
, e questo: processo2 < named_pipe
e si comporterà come: processo1 | processo2
.
L'unica differenza è che processo1
e processo2
vengono eseguiti nella shell corrente, non in una subshell,
il che rende le named pipe più utili, anche se sono un po' meno convenienti dell'uso dell'operatore pipe (|
).
Una named pipe può essere creata con il comando mkfifo
:
mkfifo pipe1
ls -l pipe1
Si noti che la prima lettera nel campo degli attributi è "p", che indica che si tratta di una pipe con nome.
ls -l > pipe1 &
cat < pipe1
Questo è simile a:
ls -l | cat
Tuttavia, la pipe denominata è più flessibile, perché i due comandi collegati dalla pipe possono essere eseguiti anche su terminali diversi.
Infatti questi due esempi non sono la stessa cosa:
echo "abc" | read
echo $REPLY
echo "abc" > pipe1 &
read < pipe1
echo $REPLY
Rimuoviamo pipe1
:
rm pipe1
5) Un altro esempio con un pipe denominato:
mkfifo pipe1
while true; do read line < pipe1; echo "You said: '$line'"; done &
echo Hi > pipe1
echo Hello > pipe1
echo "The quick brown fox jumped over the lazy dog." > pipe1
fg
Premere Ctrl-c per interrompere il ciclo while
.
rm pipe1
LEZIONE 15
-
Un semplice script che riceve come argomento l'URL di una pagina web e restituisce tutti gli URL presenti in quella pagina.
-
Un semplice script che riceve come argomento l'URL di una pagina web e restituisce un elenco delle 100 parole usate più frequentemente al suo interno.
-
Uno script che calcola i numeri di Fibonacci, utilizzando un algoritmo iterativo.
-
Uno script che risolve il problema delle Torri di Hanoi, utilizzando un algoritmo ricorsivo.
-
Uno script che raccoglie pagine web da Internet, a partire da una pagina principale, estrae le parole in ogni pagina e crea un elenco di parole.
Esempi 1
1) Questo è un semplice script che riceve come argomento l'URL di una pagina web e restituisce tutti gli URL presenti in quella pagina:
vim get_urls.sh
./get_urls.sh
url=http://linuxcommand.org/
./get_urls.sh $url
Vediamo come funziona:
wget -qO- $url
wget -qO- $url | grep -Eoi '<a [^>]+>'
L'opzione -E
è per la sintassi estesa della regexp, -o
è per la visualizzare solo la parte corrispondente, e -i
è per la visualizzazione case insensitive.
L'espressione [^>] corrisponde a tutti i caratteri compresi tra l'inizio <a
e la prima ricorrenza del simbolo >
.
Estraiamo tutti i tag anchor (<a>).
wget -qO- $url | grep -Eoi '<a [^>]+>' | grep -Eo 'href="?([^\"]+)"?'
Estrazione dell'attributo href
.
wget -qO- $url \
| grep -Eoi '<a [^>]+>' \
| grep -Eoi 'href="?[^\"]+"?' \
| grep -v 'mailto:' \
| sed -e 's/"//g' -e 's/href=//'
2) Questo è un semplice script che riceve come argomento l'URL di una pagina web e restituisce un elenco delle 100 parole più usate al suo interno:
vim get_words.sh
./get_words.sh
url=https://en.wikipedia.org/wiki/Linux
./get_words.sh $url
./get_words.sh $url | less
./get_words.sh $url | wc -l
wget -qO- $url \
| tr "\n" ' ' \
| sed -e 's/<[^>]*>/ /g' \
| sed -e 's/&[^;]*;/ /g' \
| tr -cs A-Za-z\' '\n' \
| tr A-Z a-z \
| less
wget -qO- $url \
| tr "\n" ' ' \
| sed -e 's/<[^>]*>/ /g' \
| sed -e 's/&[^;]*;/ /g' \
| tr -cs A-Za-z\' '\n' \
| tr A-Z a-z \
| sort \
| uniq -c \
| less
wget -qO- $url \
| tr "\n" ' ' \
| sed -e 's/<[^>]*>/ /g' \
| sed -e 's/&[^;]*;/ /g' \
| tr -cs A-Za-z\' '\n' \
| tr A-Z a-z \
| sort \
| uniq -c \
| sort -k1,1nr -k2 \
| sed 100q \
| less
Esempi 2
1) Questo è uno script che calcola i numeri di Fibonacci, utilizzando un algoritmo iterativo:
vim fibo.sh
./fibo.sh
for i in {0..10}; do ./fibo.sh $i; done
./fibo.sh 100
2) Questo è uno script che calcola il fattoriale di un numero, utilizzando un algoritmo ricorsivo:
vim fattoriale.sh
./fattoriale.sh <<< 0
.fattoriale.sh <<< 1
./fattoriale.sh <<< 5
for i in {0..10}; do ./fattoriale.sh $i; done
3) Questo è uno script che risolve il problema delle Torri di Hanoi, utilizzando un algoritmo ricorsivo:
vim hanoi.sh
./hanoi.sh <<< 0
./hanoi.sh <<< 1
./hanoi.sh <<< 2
./hanoi.sh <<< 3
./hanoi.sh <<< 4
./hanoi.sh <<< 5
Per una spiegazione della logica dello script si può consultare il Tutorial torri di Hanoi :
Esempi 3
Quello seguente uno script bash che raccoglie pagine web da Internet, partendo da una pagina principale, estrae le parole in ogni pagina e crea un elenco di parole. Per funzionare, è necessario che lynx e w3m possano accedere liberamente alla rete senza essere bloccati da un proxy (nel caso fare in modo che vengano sbloccati).
vim crawler.sh
Il file tmp/todo.txt
contiene un elenco di URL, uno per riga, che devono essere visitati.
Inizialmente vi si aggiunge l'URL principale che è stato dato come argomento.
Il file tmp/done.txt
contiene un elenco di URL, uno per riga, che sono già stati visitati.
Il file tmp/words.txt
contiene un elenco delle parole che sono state raccolte fino a quel momento, una per riga e ordinate in ordine alfabetico.
Esiste un ciclo infinito in cui vengono eseguiti questi passaggi:
1) Ottenere il primo URL da tmp/todo.txt
(e cancellarlo dal file).
2) Se non è valido (non inizia con http://
o https://
), continuare con l'URL successivo.
3) Se è valido, estrarre tutti gli URL di questa pagina e aggiungerli a tmp/todo.txt
, per poterli visitare in seguito.
4) Estrarre tutte le parole da questa pagina e unirle a tmp/words.txt
.
5) Aggiungere questo URL a tmp/done.txt
, in modo da non elaborarlo di nuovo.
Questo ciclo infinito non viene mai interrotto, ma c'è un timer che interrompe il programma dopo un certo numero di secondi di esecuzione.
./crawler.sh
apt install -y w3m
./crawler.sh
url=https://en.wikipedia.org/wiki/Linux
./crawler.sh $url
less tmp/words.txt
Aumentiamo il RUNTIME
:
sed -i crawler.sh -e '/^RUNTIME=/ c RUNTIME=100'
rm -rf tmp/
./crawler.sh $url
cat tmp/words.txt | wc -l
less tmp/words.txt