INDICE

Introduzione

Il presente corso è stato elaborato sulla base del materiale scaricato dal seguente Link che contiene il materiale per lezioni interattive create da Dashamir Hoxha nel corso del 2022. Il testo è stato tradotto dall'inglese all'italiano e solo in minima parte rielaborato ed adattato. 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.

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

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 whatisvisualizza 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 output
  • sort -- Ordina le righe dei file di testo
  • uniq -- Segnala o omette le righe ripetute
  • cut -- Estrarre sezioni da ogni riga di file
  • paste -- Unisce righe di file
  • join -- Unisce le righe di due file su un campo comune
  • comm -- Confronta due file ordinati riga per riga
  • diff -- Confronta i file riga per riga
  • patch -- Applica un file diff a un originale
  • tr -- Tradurre o cancellare caratteri
  • sed -- Editor di flusso per filtrare e trasformare il testo
  • aspell -- 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/lesson7con 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 di file assoluti:

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ò non sembrare molto impressionante, 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 -

ls # esecuzione su T2

cd testdir

tar czpf - . | nc -w 3 localhost 12345

ls

cd ..

ls # esecuzione su T1

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:

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 <<EOF >> ~/.ssh/config Host server1 Nome host 127.0.0.1 Utente 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. Li otterrà 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 anche 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:

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.

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. 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 (^ESC).

Per salvare le modifiche al file:

:w

3) Spostamento del cursore.

In modalità di comando, è possibile spostarsi con i tasti:

  • h -- sinistra
  • l -- destra
  • j -- in basso
  • k -- 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 (^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 e u
  • Apre una nuova riga sopra quella corrente: O
  • Annullamento: ^ESC e u

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

Premete x alcune volte per cancellare alcuni caratteri, poi premete u per annullare.

Premete 3x per cancellare 3 caratteri, poi u per annullare.

Provate anche questi e vedete cosa fanno:

  • dW e u
  • 5dW e u
  • d$ e u
  • d0 e u
  • dd e u
  • 3dd e u
  • dG e u
  • d4G e u.

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, per incollarlo prima del cursore possiamo usare la P maiuscola. Provate:

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 (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 -- no
  • a -- tutti
  • q -- esci
  • l -- 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 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 o x per eliminare i caratteri, ripetutamente se necessario

Modifica di più file

1) Spesso è utile modificare più di un file alla volta.

Uscire da vim:

:q!

Creiamo un altro file di prova:

ls -l /usr/bin > ls-output.txt

Avviare 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

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 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 questo comando in alcune directory, che sono elencate nella variabile d'ambiente 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 formn formato HTML.

1) Il primo passo consiste nello scrivere un programma (script) che generi una pagina HTML di base nello standard standard. Un esempio di pagina HTML di base si trova nel file pagina.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

:1,$ s/^/echo "/

:% s/$/"/

1G

O

Incollare 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/

/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 circondati 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=$(data +"%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 l'input standard 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 ed eventualmente aggiorniamo la versione di Debian in FTP_PATH . Gli here document possono essere usati con qualsiasi comando che accetti lo standard input. Per esempio, possiamo usarlo con ftp per recuperare un file:

: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(da copiare nella cartella di test):

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

Diramazione con if

L'istruzione if ha la seguente sintassi:

se comandi; allora
    comandi
[elif comandi; allora
    comandi...]
[else
    comandi]
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 $?

ls -d /bin/usr

echo $?

I comandi incorporati true e false non fanno nulla, se non restituire uno stato di uscita. 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 exitnon è 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-stringa.sh

vim test-stringa.sh

Si noti che quando si verifica un errore (ANSWER è vuoto), si stampa il messaggio di errore in stderra. 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 evase 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 questo, 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

./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. azione è stata eseguita.

: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

Cicli con while e until

La sintassi del comando while è la seguente:

while comandi; do comandi; 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 negata.

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. vengono eseguiti. Dopo che è stata trovata una corrispondenza, non vengono tentate altre 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 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 --interattivo.
  • 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

3) Vediamo alcune modifiche e miglioramenti:

Racchiudere in una funzione l'ultima parte (che genera la pagina HTML):

git checkout -q 2.write_html_page

git diff 1.initial

Aggiungere una funzione che visualizzi l'utilizzo del programma:

git checkout -q 3.usage

git diff 2.write_html_page

4) 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 --interattivo), 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).

5) Se viene fornita l'opzione -i o (--interattivo), 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).

6) 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.

7) 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 --interattivo

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