Introduzione a GNU/Linux

Questi brevissimi appunti sono stati redatti nel 2002 come supporto per il corso “Introduzione a GNU/Linux” e sono ispirati agli appunti “Linux4IOI” predisposti in occasione degli allenamenti della squadra italiana che ha partecipato alle IOI (International Olympiad in Informatics).

La scelta di GNU/Linux come (sistema di tipo) Unix illustrato in questi appunti corrisponde al duplice desiderio di mettere da un lato gli studenti in grado di sperimentare direttamente le nozioni introdotte durante le lezioni di laboratorio (dal momento che GNU/Linux è installabile, senza costo, sui più comuni PC), offredo dall’altro la possibiltà di illustrare i concetti profondi e gli strumenti potenti che sono tipici di Unix.

L’oggetto centrale della discussione sarà l’interprete dei comandi, denominato shell, ossia il programma che il sistema esegue non appena un utente si collega ad esso e che costituisce, per così dire, la principale interfaccia tramite la quale Unix colloquia con l’utente.

Attraverso la discussione del funzionamento della shell saranno introdotti alcuni concetti elementari riguardo a: filesystem, processi e sistema dei permessi che caratterizzano Unix.

Infine, particolare attenzione sarà dedicata all’utilizzo dei filtri, basati sulla nozione di redirezione dell’input/output, che costituiscono uno degli strumenti più potenti messi a disposizione da Unix.

L’ultilizzo dei filtri sarà mostrato attraverso una breve antologia di utilità tra le più comuni del toolset standard di Unix per la manipolazione di file di testo.

Documentazione on-line

Di seguito accenneremo ad alcuni comandi e informazioni ma, per ovvie ragioni di spazio e tempo, le informazioni potranno risultare scarne e talvolta approssimative. Come fare a reperire maggiori dettagli? Unix è in generale molto povero di messaggi diagnostici ed informativi quando eseguite un comando: in generale, si assume che nessun messaggio voglia dire tutto bene, ma come fare se non sappiamo quali opzioni usare, o a cosa serve un comando?

I comandi man e info offrono un aiuto poderoso in questo caso. Se li invochiamo seguiti dal nome di un comando, essi visualizzano le informazioni che il sistema mette a disposizione circa quel comando. Ad esempio, con man man otterremo un aiuto su come utilizzare il comando man stesso:

$ man man

NAME
       man - format and display the on-line manual pages
       manpath - determine user's search path for man pages

SYNOPSIS
       man  [-acdfFhkKtwW]  [-m  system] [-p string] [-C config_file] [-M path] [-P pager]
       [-S section_list] [section] name ...

DESCRIPTION
       man formats and displays the on-line manual pages.  This version  knows  about  the
       MANPATH  and  (MAN)PAGER  environment variables, so you can have your own set(s) of
       personal man pages and choose whatever program you like to  display  the  formatted
       pages.  If section is specified, man only looks in that section of the manual.  You
       may also specify the order to search the sections for entries and which  preproces-
       sors  to run on the source files via command line options or environment variables.
       If name contains a / then it is first tried as a filename, so that you can  do  man
       ./foo.5 or even man /cd/foo/bar.1.gz.

OPTIONS
       -C  config_file
              Specify  the  man.conf  file  to  use; the default is /etc/man.config.  (See
              man.conf(5).)

Ad una breve descrizione del comando segue la sua “sinossi”, ovvero la sintassi secondo la quale può essere invocato. Usualmente si ha il comando (nell’esempio: man) seguito da alcune opzioni, ciascuna usualmente preceduta da uno o due trattini -, (ancora nell’esempio: [-acdfFhkKtwW] [-m system] [-p string] [-C config_file] [-M path] [-P pager] [-S section_list]) e in fine, dagli argomenti (sempre nell’esempio: [section] name ...).

Alcune convenzioni sintattiche molto comuni sono l’uso delle parentesi quadre [] che indicano che quello che racchiudono è opzionale (ossia che può essere presente, o meno) e l’uso dei puntini di sospensione ... che indicano la possibilità di ripetere l’elemento che li precede per un numero indefinito di volte. Osservate che usualmente, tranne se diversamente specificato, le opzioni possono essere raggruppate e precedute da un singolo trattino (invece che da uno per ogni opzione).

Alla sinossi segue una descrizione dettagliata (che nell’esempio è stata troncata) sia del comando che del significato delle opzioni e degli argomenti. Utilizzando man per tutti i comandi che saranno presentati in seguito potrete chiarirvi le idee sui dettagli e su tutto quello che la sintesi non permetterà di raccontare di seguito.

Potete “muovervi” nella lettura del manuale utilizzando le frecce, o la barra spaziatrice, e potete terminare la lettura premendo il tasto q. (In realtà, quando leggete il manuale state utilzzando oltre al comando man un comando di visualizzazione che vi permette di “muovervi” nel documento che costituisce il manuale, tale comando (che si chiama usualmente pager) può essere less, o more. Se volete imparare come utilizzarlo... man less!).

Il comando info offre talvolta informazioni più accurate ed esaustive, ma è meno semplice da utilizzare. Dopo averlo invocato potete avere istruzioni sul suo funzionamento premendo contemporaneamente il tasto ctrl ed il tasto h, mentre potete uscire premendo il tasto q.

Se volete avere una copia in formato PDF di una pagina del manuale (per poterla stampare e leggere con calma), potete usare l’opzione -t, (ed il ps2pdf per convertire l’output da PostScript a PDF) come ad esempio in:

man -t man | ps2pdf - man.pdf

aprendo quindi il file man.pdf con il vostro viewer preferito per i PDF.

L’interprete dei comandi

Una delle più comuni shell nel mondo Unix (e quindi Linux, ma, in particolare, che avrete a disposizione alle IOI) è la bash. Potete accertarvi di questo fatto chiedendo al sistema l’elenco dei processi attivi tramite il comando ps, ad esempio, sul mio sistema in questo momento si ha:

$ ps
  PID TTY          TIME CMD
 1088 pts/3    00:00:00 bash
 7136 pts/3    00:00:00 ps

dove il segno di $ è il cosiddetto “prompt” ossia il segnale tramite il quale la bash indica che è pronta a ricevere comandi dall’utente, ps è il comando che ho dato (in seguito assumeremo sempre che sulla riga del prompt ci sia il comando che dovete dare per ottenere il risultato citato nell’esempio) e, sulle righe seguenti, c’è l’output del comando, che indica sulla prima riga (in fondo) il nome bash come il nome di uno dei processi in esecuzione.

Moltissime delle cose che diremo in questi appunti restano vere anche usando altri interpreti di comandi, in quanto sono del tutto standard e valide in generale. Ad ogni modo, assumiamo che d’ora in poi useremo sempre solo la bash; se il comando precedente avesse riportato un esito diverso (ossia, se la stringa bash non dovesse comparire affatto), potete eseguire manualmente l’inteprete mediante il comando bash. (Oppure potete cambiare definitivamente la shell che il sistema eseguirà ad ogni collegamento con il comando chsh).

Identificare i file

Unix ha un filesystem gerarchico, il che vuol dire che tutti i suoi file sono organizzati in una struttra “ad albero” a partire da una directory radice (per l’appunto, la root del filesystem) che, come ogni directory del sistema, può contenere a sua volta altre directory, oppure file. Ad esempio, parte del filesystem della mio sistema è (secondo il comando tree):

/
|-- bin
|-- boot
|-- dev
|-- etc
|-- home
|   |-- ilenia
|   `-- santini
|       |-- Linux4IOI.html
|-- lib
|   |-- modules
|-- mnt
|   |-- cdrom
|   |-- floppy
|-- opt
|   |-- jdk
|-- root
|-- tmp
|-- usr
`-- var
    |-- log
    |   |-- httpd
    |-- spool
        |-- mail
            |-- santini.mbox
            |-- ilenia.mbox
            |-- root.mbox

Potete osservare (alcune) sottodirectory della root / come bin, usr, var e la directory home dove sono contenuti i dati degli utenti, con le sue sottodirectory ilenia e santini; sempre a titolo di esempio, sono riportati anche alcuni file. Linux4IOI.html, santini.mbox, ilenia.mbox e root.mbox.

Una directory, o file, viene identificato tramite il percorso che è necessario fare per raggiungerla. Se tale percorso parte dalla radice, viene detto assoluto, ad esempio, il percorso assoluto del file Linux4IOI.html è /home/santini/Linux4IOI.html e il percorso assoluto della directory mail è /var/spool/mail.

È però possibile una “scorciatoia” per consentire una più rapida identificazione delle directory, o file. A tale scopo, ad ogni processo (e quindi, anche alla shell) è associata una directory corrente che può essere una qualunque directory del sistema. Un processo può pertanto identificare una directory, o file, tramite il percorso che è necessario fare per raggiungerla a partire dalla directory corrente, in questo caso, il percorso viene detto relativo. Ancora per esempio, se la directory corrente della shell fosse /home, allora esso potrebbe identificare il file Linux4IOI.html mediante il percorso relativo santini/Linux4IOI.html.

Osservate che è immediato comprendere se un percorso è assoluto, o relativo: è del primo tipo se inizia con /, mentre è del secondo tipo altrimenti. Ed è anche immediato comprendere cosa identifica un percorso relativo: a partire da esso si può infatti sempre ottenere il percorso assoluto corrispondente semplicemente anteponendogli il percorso (assoluto) della direcotory corrente. Nell’esempio, /home seguito da santini/Linux4IOI.html identifica /home/santini/Linux4IOI.html. Tenete sempre ben presente che il percorso relativo dipende dalla directory corrente che (vedremo come) può essere mutata e comunque dipende dal processo corrente. L’unica “identificazione permanente” di un file (indipendente dai vari processi e dalle rispettive directory correnti), è il suo percorso assoluto.

Sono possibili altre scorciatoie. Ogni directory contiene infatti due directory speciali, denominate . e .., la prima è un sinonimo per la directory stessa, mentre la seconda è un sinonimo per la directory “padre” (ossia quella che la precede nel percorso assoluto). Così, ad esempio, /home/santini/.. conincide con /home. La comodità di tali scorciatoie appare immediatamente considerando i percorsi relativi. Supponiamo che la directory corrente sia /home/ilenia, allora il percorso relativo ../santini/Linux4IOI.html identifica il file /home/santini/Linux4IOI.html.

Per finire, osserviamo che a ciascun utente del sistema è usualmente associata una directory particolare, detta home directory, nella quale l’utente può mantenere i propri dati. All’atto del collegamento col sistema (login), viene eseguita la shell e gli viene associata come directory corrente la sua home directory.

Tutte le volte che nel seguito si userà il termine percorso (per identificare una directory, o file) faremo quindi riferimento ai concetti qui introdotti; in particolare, nella documentazione, i termini path, pathname e anche dir, file, o filename si riferiscono spesso all’identificazione (tramite percorso assoluto, o relativo) di directory e file.

La directory corrente

Come detto in precedenza, ad ogni processo è associata una directory del sistema, detta directory corrente. In particolare, la bash mette a disposizione alcuni comandi per conoscere l’attuale directory corrente e per modificarla.

Il comando pwd restituisce la directory corrente, così, sul mio sistema, ad esempio (in questo momento):

$ pwd /home/santini

Il comando cd serve a mutare la directory corrente in quella indicata, ad esempio:

$ cd /home

rende /home la directory corrente della shell. Se eseguito senza argomenti, il comando rende la home directory la directory corrente. Una cosa molto comoda da utilizzare è l’argomento -, che riporta la directory corrente all’ultima directory corrente che precede quella attuale, ossia se ad esempio la directory corrente fosse /home, allora i comandi:

$ cd santini
$ cd -

avrebbero come effetto quello di far mutare la direcotory corrente a /home/santini e poi di nuovo a /home. Questo è molto comodo per “tornare sui propri passi”

Osserviamo che esistono due comandi, pushd e popd che consentono di cambiare la directory corrente mantenendo uno stack delle directory attraversate, controllate sul manuale per ulteriori dettagli.

Manipolare directory e file

Copiare e spostare directory e file è immediato quando si ha chiaro come il sistema li identifica. Il comando per copiare è cp e quello per spostare è mv. La sinossi di entrambi è il comando seguito da un elenco di file e/o directory. Se l’elenco comprende due soli elementi, allora sarà copiato/spostato il primo sul secondo, se invece l’elenco comprende più elementi, allora l’ultimo elemento deve identificare una directory; in questo caso, tutto quello che è identificato dagli elementi dell’elenco (tranne l’ultimo) verrà copiato/spostato nella directory indicata dall’ultimo argomento.

Ad esempio, se la directory corrente è /home/ilenia e vogliamo copiare il file Linux4IOI.html al suo interno useremo il comando:

$ cp ../santini/Linux4IOI.html .

dove avremo usato le scorciatoie .. e . rispettivamente per reperire Linux4IOI.html usando un percorso relativo e per indicare (sempre con un percorso relativo) la directory corrente.

Similmente, se volessimo muovere le directory modules e jdk sotto santini potremmo usare il comando:

$ mv /lib/modules /opt/jdk /home/santini

dove abbiamo fatto uso soltanto di percorsi assoluti.

Per creare una directory, usate il comando mkdir seguito dal nome della directory.

Cancellare file è molto semplice (quindi pericoloso), basta usare il comando rm seguito dalla lista dei file da cancellare. Usato con l’opzione -i il comando chiede conferma prima di ogni cancellazione.

Si possono solamente cancellare directory vuote (questo per sicurezza), tramite il comando rmdir seguito dal nome della directory.

Se volete farvi male, potete cancellare ricorsivamente un intero sottoalbero con l’opzione di azione ricorsiva di rm. Essa può discendere molte directory e quindi chiedere potenzialmente l’approvazione a molte cancellazioni, per ridurre la sua verbosità si usa in generale in congiunzione con l’opzione -f che forza il comando a cancellare senza fare questioni. Se siete alla frutta e volete eliminare un sottoalbero intero, usate quindi rm -rf seguito dal nome del sotto albero. Ma attenti, in Unix non c’è modo di tornare sui vostri passi.

Ultimo, ma certo non meno importante, è il comando ls per elencare il contenuto di una directory (di quella corrente se lanciato senza argomenti, oppure di tutte le directory elencate come argomenti). Esso ha molte opzioni (al solito il manuale è illuminante). Vi ricordo almeno l’opzione -l che aumenta il numero di informazioni riportate per ogni file.

L’espansione dei caratteri speciali

È possibile specificare (comodamente) più di un file per volta come argomento di un comando? La shell mette a disposizione un comodo stratagemma per farlo: l’uso dei caratteri speciali ? e *. Se tali caratteri vengono utilizzati per indentificare una directory o file la shell, prima di eseguire il comando, sostituisce i percorsi che contengono tali caratteri con i percorsi di tutti i file che soddisfano alcune regole. Nel caso del carattere ?, la regola è che al posto di quel carattere ci sia (esattamente) un carattere qualunque, mentre nel caso di * è che al posto di quel carattere ci sia un numero non nullo di caratteri qualunque (in entrambi i casi, il carattere / mantiene il suo significato di separare, lungo il percorso, le directory e non viene quindi mai considerato tra i possibili caratteri qualunque).

Attenzione, questo processo di sostituzione è effettuato dalla shell e pertanto il comando che verrà eseguito non ha alcuna conoscenza del fatto che gli argomenti che gli sono stati passati derivano da tale sostituzione. Per renderci conto di questo fatto useremo il comando echo, che ha come effetto quello di copiare nel suo output l’elenco (immutato) dei suoi argomenti. Ad esempio:

$ echo ciao come stai
ciao come stai

ora, se la directory corrente fosse /home, il comando:

$ echo *
ilenia santini

ha l’output prodotto nell’esempio in quanto, prima di essere eseguito, la shell avrà sostituito * (una specificazione di percorso relativo) con tutti i possibili percorsi relativi (che hanno /home come directory corrente). Similmente, il comando:

$ echo /var/*
/var/log /var/spool

ha come output l’elenco dei possibili percorsi (assoluti) che iniziano con /var/ e sono seguiti da una successione arbitraria di caratteri.

In relazione a quanto detto nella sottosezione precedente, quindi, per copiare (o spostare) un insieme di file sotto una directory, possiamo usare lo stratagemma appena descritto. Ad esempio:

$ cp /var/spool/*.mbox /home/santini

copia i file santini.mbox, ilenia.mbox e root.mbox dalla directory /var/spool alla directory /home/santini

Programmare semplici script

Un aspetto fondamentale dell’utlizzo del calcolatore è che ci può essere d’aiuto evitando farci eseguire compiti ripetitivi. Anche la shell ha un meccanismo che ci permette di raccogliere in semplici script (piccoli programmi) sequenze di comandi che ci ritroviamo spesso ad eseguire.

Uno script di shell è semplicemente un file “eseguibile” (ovvero che ha i permessi di esecuzione per l’utente che intende utilizzarlo) con una speciale intestazione. Senza entrare nel merito dei permessi del filesystem, che costituirebbero argomento a sé stante, diciamo molto brevemente che qualunque file sulla cui prima riga compaia:

#!/bin/bash

dove /bin/bash si assume essere il percorso della bash, e per cui sia stato eseguito il comando:

$ chmod u+x file

dove file è il nome del file in questione, è uno script di shell. Questo significa che si può utilizzare file come un comando del sistema e che questo comporterà che tutte le linee del file che seguono quella di intestazione saranno eseguite in sequenza dalla shell.

La shell mette a disposizione alcuni costrutti tipici dei linguaggi di programmazione che possono essere utilizzati negli script e dalla linea di comando (sebbene, in questo caso, sia più difficile utilizzarli perché talvolta sono lunghi e complessi da editare). Per prima cosa osserviamo che è possibile utilizzare delle variabili, esse non debbono essere dichiarate in precedenza, hanno nomi alfanumerici e vengono assegnate come var=value dove var è il nome della variabile e value è il suo valore. Per riferirsi ad una variabile, viceversa, bisogna anteporre al suo nome il simbolo del dollaro, ossia, secondo l’assegnamento precedente, $var viene sostituita dalla shell con value.

In uno script, le variabili 0, 1, 2... corrispondono al nome dello script ed ai suoi argomenti. Così, ad esempio, se lo script echonum2 contiene:

#!/bin/sh
echo uno $1
echo due $2

e la directory corrente fosse /var/spool/mail, allora si avrebbe:

$ econnum2 *
uno santini.mbox
due ilenia.mbox

dove osservate che sebbene lo script sia elementare (non contenga alcuna gestione del carattere *) esso elenca con successo i primi due file contenuti nella sua directory corrente.

Il costrutto maggiormente utilizzato nella shell è quello di for che permette di eseguire ripetutamente una porzione di codice in cui una variabile assume di volta in volta valori distinti.

La sintassi (che potete trovare nel manuale) è:

for name [ in word ] ; do list ; done

che fa sì che la lista di comandi list venga eseguita varie volte con la variabile name che assume i valori determinati dall’espansione di word.

Facciamo un esempio, supponiamo che la directory corrente della shell sia /var/spool/mail, allora:

$ for file in *; do echo ciao file $file; done
ciao file santini.mbox
ciao file ilenia.mbox
ciao file root.mbox

dove notate che è presente una linea per ogni file, visto che il comando echo viene eseguito ripetutamente.

La bash mette a disposizione una ulteriore sintassi per il for simile a quella C:

for (( expr1 ; expr2 ; expr3 )) ; do list; done

in questo modo, possono essere eseguiti semplicemente cicli in cui la variabile assume valori numerici. Ad esempio:

$ for (( i = 0; i < 5; i++ )); do echo $i; done
0
1
2
3
4

Se la versione della bash che usate non consentisse questa sintassi, potete usare il comando seq e la sintassi $() come discusso in seguito in un esempio.

Entrambe le forme del comando sono molto utili quando si voglia copiare una medesima operazione su più file. Dopo la sezione sulla redirezione vedremo, come esempio, un modo semplice di mutare l’estenzione ad un gruppo di file.

Un ulteriore costrutto, molto utile nel caso degli script (ma forse meno usuale sulla linea di comando) è quello dell’if. La sua sintassi è:

if list; then list; [ elif list; then list; ] ... [ else list; ] fi

dove, a seconda dell’exit status della prima lista di comandi (presto impareremo cosa sia) vengono eseguite o la lista di comandi del ramo then, o quella del ramo else. Spesso la prima lista di comandi è in realtà un’espressione condizionale costruita facendo uso dell’operatore [] della bash. Per ulteriori dettagli controllate nel manuale della bash alla voce CONDITIONAL EXPRESSIONS (per cercare una cosa in una pagina di manuale visualizzata con less premete dapprima la barra / e poi inserite la stringa da cercare seguita da invio; per trovare la successiva occorrenza, inserite la barra seguita subito da invio). Le espressioni condizionali si possono combinare logicamente per mezzo degli operatori not !, and && e or ||.

Ad esempio, la seguente espressione darà si in output se il primo argomento dello script è uguale alla variabile pippo e se esiste un file così identificato:

if [ "$1" = "$pippo" ] && [ -e "$pippo" ]; then
    echo si
else
    echo no
fi

osservate che il punto e virgola è un separatore e può sempre essere sostituito con un a capo.

Redirezione e input/output standard

Ad ogni processo di Unix sono associati implicitamente tre file, cosiddetti standard input, output ed error. Dal primo di essi, il processo ricava il suo ingresso, o input, e sul secondo produce tutte le sue uscite, o output. Il terzo file serve al programma per segnalare degli speciali output che indicano una condizione di errore, o comunque straordinaria.

Questo vuol dire che se un programma tenta di leggere o scrivere senza specificare esplicitamente dei file per tali operazioni (ad esempio, in C, usando le funzioni di libreria puts, printf, getchar o scanf), esso leggerà dallo standard input e scriverà sullo standard output.

Normalmente, quando eseguite un comando tramite la shell il comando riceverà il suo standard input “dalla tastiera” e invierà il suo standard output (ed error) “sul terminale” (le virgolette stanno ad indicare che ci sono di mezzo cose più sofisticate del semplice e diretto hardware, ma per la discussione attuale, questo grado di precisione è sufficiente). Così, se eseguite ad esempio il comando cat che ha l’effetto di concatenare sul suo standard output i file che sono stati specificati come argomento, avrete che, ad esempio:

$ cat /var/spool/mail/santini.mbox
From: ilenia@localhost
To: santini@localhost
Subject: Buon lavoro alle IOI!
Date:...

ovvero, comparirà “sul terminale” il contenuto (qui troncato) della mia casella di posta. Se ora eseguite il comando senza argomenti, esso leggerà, invece di /var/spool/mail/santini.mbox, lo standard input:

$ cat
ciao come stai
ciao come stai
^D

dove la prima linea è stata scritta dall’utente e la seconda è prodotta per effetto del comando cat che invia sul suo standard output quanto a letto e della shell che lo visualizza (“sul terminale”). Il carattere che compare scritto come ^D corrisponde alla pressione simultanea dei tasti ctrl e d, che ha come effetto quelo di segnalare la fine del file (ossia, di emettere un segnale di EOF allo standard input).

Redirezione da/a file

La shell rende possibile redirigere questi file standard da e verso altri file del sistema, utilizzando i caratteri speciali < e >. Più precisamente, se invocando un comando aggiungiamo sulla linea di comando < seguito dall’identificazione di un file, allora il comando leggerà il file specificato come il suo standard input; viceversa, se aggiungiamo > seguito dall’identificazione di un file, allora il comando scriverà sul file specificato il suo standard output.

Ad esempio, dopo l’esecuzione del comando:

$ cat >test
ciao come stai
^D

il file test (che viene creato se non esisteva prima di aver dato il precedente comando), conterrà esattamente le parole ciao come stai che, nell’esempio precedente, erano state emesse “sul terminale”. Parimenti, il comando:

$ cat <test
ciao come stai

emetterà “sul terminale” l’input che ricaverà dal file test invece che “dalla tastiera”.

È possibile redirigere anche lo standard error, mediante i caratteri speciali 2>, questo può essere comodo per distinguere facilmente, durante l’esecuzione di un programma, l’output “normale” dagli errori che altrimenti la shell invierebbe indistintamente “sul terminale”. Ad esempio, se il comando pasticcio (che ovviamente non troverete nel manuale) emetesse alcune righe di output e di errore mescolate come segue:

$ pasticcio
questo è un errore
questo invece va bene
di nuovo un errore
un altro errore
ma chiudiamo in bellezza

dove è chiaro cosa funzioni o meno, potremmo rendere meno ambiguo il suo output eseguendolo come segue:

$ pasticcio >output 2>errori

dopo di che potremmo trovare nel file output le righe:

questo invece va bene
ma chiudiamo in bellezza

e nel file errori, le righe:

questo è un errore
di nuovo un errore
un altro errore

il che può risultare molto comodo per ispezionare e osservare con calma gli errori prodotti da un programma (come, ad esempio, un compilatore!).

Pipe tra comandi

Per fare qualche altro esempio, introduciamo il comando wc che conta il numero di linee, parole e bytes presenti nel suo input. Ad esempio:

$ wc <test
      1       3      15 test

che indica che ci sono una linea, tre parole e quindici byte in test.

Un ulteriore meccanismo di redirezione della shell consente di “concatenare” l’esecuzione di più comandi facendo in modo che lo standard output di un comando sia collegato allo standard input del successivo. Il carattere tramite il quale si specifica questa intenzione è | che deve essere interposto tra i comandi che si intendono concatenare.

Per continuare con l’esempio precedente, consideriamo:

$ cat <test | wc
      1       3      15 test

che possiamo intepretare come segue: il comando cat legge dal file test il suo standard input, emette quindi il contenuto di tale file sul suo standarad output che viene utlizzato come standard input dal comando wc che quindi effettua il suo conteggio.

Ovviamente, tutto si può combinare, così ad esempio:

$ cat <test | wc >out

farà sì che i conteggi vengano memorizzati nel file out.

Ancora redirezione

Una cosa che talvolta è molto utile è usare come argomento di un comando l’output di un altro comando. Supponiamo, ad esempio, di voler produrre un output simile a quello di wc, ma più grazioso. Inanzitutto limitiamoci al conteggio delle parole, che otterremo con l’opzione -w. Quello che vorremo è aggiungere una dicitura in italiano. Consideriamo il seguente script:

#!/bin/bash
echo il file $1 contiene
wc -w <$1
echo parole

se lo eseguissimo, avremmo l’output diviso su tre righe (una per comando) che può essere antiestetico (in questo caso, ma può essere diverso da quello di cui abbiamo bisogno in generale).

Se racchiudiamo uno (o più comandi di una pipe) in $() ed usiamo tale espressione come argomento, allora la shell prima di eseguire il comando di cui tale espressione è argomento eseguirà i comandi tra parentesi e sostituirà l’espressione con l’output dei comandi eseguiti. Ecco la soluzione per il nostro script:

#!/bin/bash
echo il file $1 contiene $(wc -w <$1) parole

Per concludere, torniamo alla promessa che avevamo fatto qualche tempo fa. Supponiamo di voler mutare l’estensione di un insieme di file da .c.txt semplicemente a .c. Il comando che possiamo usare è:

for i in *.c.txt; do echo mv $i $(basename $i .c.txt).c; done | bash

dove, il ciclo di for eseguirà echo mv $i $(basename $i .c.txt).c una volta per ogni file con estenzione in .c.txt. Il comando basename seguito da un file e da una estensione produce in output il nome del file senza estenzione. Quindi, prima della pipe, il ciclo for avrà prodotto in output una sequenza di comandi del tipo mv X.c.txt X.c con al posto di X tutti i file di cui voglio cambiare l’estensione. Ora: se il comando fa quello che deve (posso osservare l’output di tutti gli echo quante volte voglio e correggere per bene la sintassi del comando), posso usare una pipe per dare il tutto in pasto a un nuovo interprete di comandi che eseguirà per me l’elenco di mv.

Questo è un modo abbastanza generale (e sicuro) di procedere. Se dovete dare una sequenza di comandi simili costruite un ciclo for che faccia l’echo di un comando costruito a partire dalla variabile del ciclo. Solo quando sarete sicuri di non aver sbagliato, fate il pipe in una shell.

Per finire, un caso molto comune di uso della forma di redirezione appena discussa è per la costruzione di cicli. Se la sintassi “alla C” del for fosse non disponibile (può succedere alle IOI), potete cavarvela con il comando seq che genera sul suo standard output una sequenza di numeri. Ad esempio:

$ for i in $(seq 0 5); do echo $i; done
0
1
2
3
4

Ora, sempre più difficile! La bash mette a disposizione un modo per creare una sorta di “file temporaneo” consentendoci di “trattare come un file” lo standard output di un comando (o di una sequenza di comandi collegati da pipe). Per fare questo è sufficiente racchiudere il filtro tra i caratteri <( e ). Questo è molto utile nei casi in cui vogliamo passare l’output di un filtro ad un comando che però non opera leggendo lo standard input, ma opera su file (dato il loro nome, o percorso); questo è molto comune per quei comandi che, ad esempio, operano su più di un file per volta: in tal caso (essendo unico lo standard input) non avrebbe senso usare la redirezione. Ad esempio, possiamo concatenare due o più file con il comando cat semplicemente indicando il loro nome come argomento del comando; se il file a.txt contiene una lettera “a” e così il file b.txt contiene una “b”, avremo:

$ cat a.txt b.txt
a
b

Supponiamo ora di voler concatenare due liste di numeri ottenute con seq; una soluzione potrebbe essere:

$ seq 1 3 > primitre
$ seq 4 6 > altritre
$ cat primitre altrire
1
2
3
4
5
6

Una soluzione alternativa, che non richieda la creazione dei due file primitre e altritre è possibile tramite l’uso di <() in luogo dei file:

$ cat <(seq 1 3) <(seq 4 6)
1
2
3
4
5
6

Filtri

Il concetto di filtro è assolutamente centrale nell’artchitettura di un sistema Unix. L’idea base è estremamente semplice: avere una tecnica efficente, uniforme e semplice che consenta la comunicazione tra processi (la redirezione e le pipe) e sviluppare piccoli programmi con compiti ben specifici e circoscritti.

Osserviamo, per inciso, che questo è l’esatto opposto dei correnti sistemi operativi della famiglia Windows dove in genere si hanno programmi molto sofisticati che svolgono numerose funzioni, ma che molto difficilmente possono interoperare tra loro.

Facciamo un esempio per essere più chiari. Immaginiamo di volere un elenco dei dieci processi in esecuzione da più lungo tempo nel sistema, assieme al nome dell’utente che li sta eseguendo ed il relativo tempo di esecuzione. Apparentemente è un compito complesso: con Windows dovrei cercare un programma che faccia proprio quello che voglio, oppure verificare se qualche programma di gestione dei processi ha qualche opzione che mi consenta di ottenere il risultato desiderato, oppure, in fine, decidermi a scrivere io stesso il programma studiando le API del sistema operativo che consentono di accedere alle informazioni sui processi in esecuzione. Non sembra essere una cosa in nessun caso banale.

Con Unix la cosa è relativamente più semplice. Come prima cosa, quello che cerco riguarda i processi, quindi vediamo se il comando ps offre le informazioni che cerco. Dopo un po’ di studio del manuale, decido che posso usarlo con alcune opzioni per ottenere qualcosa del tipo:

$ ps whauxS
...
root       602  9.0 12.8 19500 8060 ?        R    07:50  53:50 /etc/X11/X -auth /var/gdm/:0.Xauth :0
root       603  0.0  0.0  3800    0 ?        SW   07:50   0:00 [gdm]
santini    609  0.0  0.0  1944    0 ?        SW   07:53   0:00 [Default]
santini    656  0.0  0.0  1892    0 ?        SW   07:53   0:00 [.xsession]
santini    664  0.0  0.7  7180  492 ?        S    07:53   0:01 gnome-session
santini    665  0.0  0.1  2316  124 ?        S    07:53   0:00 ssh-agent /home/santini/.xsession-ssh
...

dove si vede sia il nome dell’utente (la prima colonna), che il nome del processo (l’ultima colonna) ed il tempo da cui è in esecuzione (la penultima colonna) e dove i puntini ... indicano che ho ritagliato un esempio di alcune righe tra le decine riportate da ps. Ora voglio isolare le sole informazioni che mi interessano, siccome la spaziatura su ogni riga è costante (è come se fosse un file con record e campi di lunghezza fissa), posso usare il comando cut per prelevare le colonne che mi interessano, che sono i caratteri (-b, per bytes) da 0 a 9 e quelli dopo il 56esimo. Vediamo come fare la cosa con una pipe:

$ ps whauxS | cut -b 0-9, 56-
...
root       53:50 /etc/X11/X -auth /var/gdm/:0.Xauth :0
root        0:00 [gdm]
santini     0:00 [Default]
santini     0:00 [.xsession]
santini     0:01 gnome-session
santini     0:00 ssh-agent /home/santini/.xsession-ssh
...

wow, ci siamo quasi. Ora, come dicevo, vorrei i venti che girano da più tempo: per prima cosa, quindi, ordino i processi in ordine inverso di tempo di esecuzione, mi può aiutare il comando sort al quale chiedo di ordinare numericamente (-n), in ordine inverso (-r) ed in base alla seconda colonna (-k 2):

$ ps whauxS |  cut -b 0-9,56- |  sort -rnk2
...
root       53:50 /etc/X11/X -auth /var/gdm/:0.Xauth :0
santini     0:01 gnome-session
santini     0:00 [.xsession]
santini     0:00 ssh-agent /home/santini/.xsession-ssh
santini     0:00 [Default]
root        0:00 [gdm]
...

come vedete, il processo gnome-session è risalito, visto che era in esecuzione da almeno un secondo. Ora, non voglio tutte le linee, ma solo la prima decina, per questo c’è il comando head che mi permette di prendere solo un certo numero di righe a partire dalla prima. Soluzione al mio problema, quindi:

$ ps whauxS |  cut -b 0-9,56- |  sort -rnk2 | head -10
root      104:48 init [5]
santini    55:44 deskguide_applet --activate-goad-server deskguide_applet --goad-fd 10
root       54:59 /etc/X11/X -auth /var/gdm/:0.Xauth :0
santini    10:52 xscreensaver -no-splash -timeout 20 -nice 10
xfs         0:22 xfs -droppriv -daemon
santini     0:51 xemacs
santini     0:26 /usr/bin/sawfish --sm-client-id=default2
santini     0:20 bash
santini     0:17 tasklist_applet --activate-goad-server tasklist_applet --goad-fd 10
santini     0:09 panel --sm-client-id default7

Ho ottenuto il risultato cercato senza scrivere una linea di codice e senza cercare un programma che facesse proprio esattamente quello che volevo io. Certo, è necessario conoscere i comandi Unix ps, cut, sort e head, direte voi, che può non essere semplice. Questo è vero, ma si tratta di programmi del tutto generali, che qualunque utente Unix conosce bene e che ha usato in mille contesti per realizzare mille filtri come quello qua sopra. Non si tratta di apprendere ogni volta un comando diverso, una sintassi diversa, dei menù diversi. Si tratta semplicemente di dotarsi di un arsenale con poche armi, molto sempici e molto generali che è molto semplice mettere assieme.

Ora vedremo alcuni di questi strumenti utilizzati nella costruzione di semplici filtri che vi possono essere utili durante la partecipazione alle IOI. L’esposizione è volutamente stringata e priva di dettagli, usatela come puntatore verso la documentazione on-line e come ispirazione per la sperimentazione. Molti dei comandi discussi di seguito appartengono al pacchetto denominato textutils su cui potete avere maggiori informazioni tramite il comando info textutils.

Selezionare righe e colonne

head e tail, come abbiamo visto, si può ottenere solo un certo numero di righe dall’inizio del file. Il comando tail permette di selezionare un certo numero di righe a partire dalla fine del file; tramite l’opzione +n (dove n è un numero) si possono ottenere tutte le righe a partire dall’n-esima (ossia, scartando le prime n-1). Così, ad esempio, se test contiene i numeri da uno a dieci uno per riga si ha:

$ head -2 <test
1
2
$ tail -3 <test
8
9
10
$ tail +7 <test
7
8
9
10

purtroppo non esiste per head l’analogo dell’opzione +n, così se volete avere tutte le righe scartando le ultime n-1 dovete usare uno stratagemma basato sul comando tac (si chiama al contrario del comando cat) che concatena il suo input e lo emette in output con le righe “al contrario” (ossia dall’ultima alla prima). Per avere tutte le righe di un file tranne le ultime n usate quindi tac out | tail +n | tac; sempre nell’esempio precedente:

$ tac <out | tail +8 | tac
1
2
3

Un fratello di tac è il comando rev che inverte l’ordine in cui compaiono i caratteri di ciascuna riga; assieme a cut (che abbiamo già visto e su cui presto torneremo) questo comando è molto comodo (utilizzato due volte, similmente a come abbiamo fatto sopra per tac) per selezionare i campi di una riga a partire dagli ultimi, invece che dai primi.

grep, un approccio completamente diverso alla selezione di righe da un file è reso possibile dal comando grep. Esso permette di selezionare le righe non in base alla loro posizione, ma al loro contenuto. Esso può essere specificato tramite una espressione regolare; il caso più elementare di una espressione regolare è del semplice testo: in questo caso, verranno emesse tutte e sole le linee che contengono quel testo. Ad esempio, se il file mesi contiene l’elenco dei nomi dei mesi, avremmo:

$ grep r <mesi
febbraio
marzo
aprile
settembre
ottobre
novembre
dicembre

che, come dice il proverbio, sono i mesi preferibili in cui mangiare le rane, per via della r. Espressioni regolari più generali consentono di selezionare le linee usando dei “caratteri speciali” similmente a come la shell espande i percorsi dei file che contengono * e ?. Per avere maggiori informazioni sulle espressioni regolari leggete la sezione REGULAR EXPRESSIONS del comando grep. Una opzione molto comoda è -v che consente di invertire l’effetto della selezione, ossia di emettere solo le linee che non soddisfano l’espressione regolare (o, semplicemente, che non contengono il testo specificato).

cut, abbiamo visto che questo comando permette di selezionare in base ai byte, esso può anche essere usato per record con campi di lunghezza variabile, purché delimitati da un singolo carattere. In questo caso, si specifica il delimitatore con l’opzione -d e l’elenco dei campi separato da virgole con l’opzione -f. Se il carattere separatore è presente più di una volta tra i campi (come, ad esempio, lo è lo spazio nell’output di ls) può essere utile elimiare tali duplicati ricorrendo all’opzione -s del comando tr che sarà discusso nella prossima sezione. Come esempio si consideri la pipe:

$ rev /etc/passwd | cut -d: -f1 | rev
/usr/bin/false
/bin/sh
/bin/csh
/bin/bash

che mostra l’ultimo campo del file /etc/passwd, un file che contiene varie informazioni riguardo agli utenti del sistema e che è ha una linea per ogni utente, con vari campi separati dal carattere : e dove l’ultimo campo (ossia il primo, se la riga è rovesciata) è il path della shell che l’utente ha scelto di usare.

paste, come c’è da aspettarsi, se è possibile tagliare un file secondo le sue colonne, è anche possibile incollare diversi file facendoli risultare come le colonne di un nuovo file che li raggruppi. Ad esempio, per ottenere un file di tre colonne, due che riportino nomi e proprietari di alcuni file con estensione .txt e l’altra che ne contenga il numero di linee possiamo fare ls -l *.txt | tr -s ' ' | cut -d' ' -f3,9 per ottenere le prime due colonne (notate l’uso congiunto di tr -s ' ' e cut -d' ' per usare lo spazio come singolo delimitatore), quindi fare wc -l *.txt | tr -s ' ' | cut -d' ' -f 2 per ottenere la colonna dei numeri di linea, in fine (usando <() invece di due file temporanei):

$ paste <(ls -l *.txt | tr -s ' ' | cut -d' ' -f3,9) <(wc -l *.txt | tr -s ' ' | cut -d' ' -f 2)
18      santini a.txt
21      santini b.txt
33      santini c.txt

Semplici manipolazioni

sort, uniq questi due comandi sono molto utili perché consentono il primo di ordinare il contenuto di un file (lessicograficamente, o secondo l’ordine numerico) e il secondo, di emettere, a partire da un file ordinato contenente linee ripetute, una sola linea per ripetizione. Ad esempio, il comando who emette la lista di tutte le login attive, in questo modo, un utente che si è collegato più volte, compare su diverse linee. Se vogliamo sapere chi sono (o contare) gli utenti distinti collegati al sistema possiamo allora fare:

$ who | cut -d' ' -f1 | sort | uniq
santini

dove con la sequenza di pipe dapprima consideriamo solo il nome degli utenti (il primo campo della riga, separato dagli altri tramite uno spazio), poi mettiamo in ordine il file in modo da evidenziare in ordine le linee ripetute e, in fine, emettiamo solo gli utenti distinti. Se ci interessasse solo il loro numero, e non il loro nome, potremmo fare:

$ who | cut -d' ' -f1 | sort | uniq | wc -l
1

Le opzioni -d e -c di uniq consentono, rispettivamente, di ottenere le sole linee duplicate, o il conteggio di quante volte ogni linea sia duplicata.

sed, tr talvolta può essere molto utile sostituire tutte le occorrenze di una parola, o di un carattere, con un’altra parola, o carattere. Il comando sed è un “editor di stream”, ossia può effettuare quelle modifiche che normalmente un utente esegue con un editor normale su di un file operando invece tramite pipe. Tale comando è quindi molto potente (controllate il manuale se volete), una delle sue funzioni è quella che in un editor si chiamerebbe “cerca e rimpiazza”. La sintassi, in questo caso è:

sed 's/prima/dopo/g;'

che, per ogni linea del suo standard input che contiene la parola prima emette sul suo standard output la stessa linea con la parola dopo al posto di prima (in realtà, è possibile specificare sostituzioni molto più potenti, e complicate, tramite espressioni regolari). Se, per caso, la parola che volete modificare contiene il carattere / potete usare la stessa sintassi di prima dove avrete rimpiazzato il carattere con qualunque altro carattere (tutte e tre le volte che compare nell’espressione). Ad esempio:

$ ls /var/spool/mail/* | sed 's|/var/spool/mail/|mailbox: |g;'
mailbox: santini.mbox
mailbox: ilenia.mbox
mailbox: root.mbx

Come caso particolare, la sostituzione sed 's/prima//g;' ha come effetto di cancellare tutte le occorrenze della parola prima. Se le due parole sono costituite da un solo carattere, possiamo ottenere lo stesso effetto con il comando tr, in particolare,:

tr set1 set2

se set1 e set2 sono due insiemi dello stesso numero di caratteri, il comando sostituirà ordinatamente le occorrenze di ciacun carattere del primo insieme con quello del secondo. Ad esempio:

$ echo ciao | tr aeiou 12345
c314

Alcune opzioni molto comode del comando sono -d che cancella tutte le occorrenze dei caratteri del primo insieme e -s che dapprima sostituisce le occorrenze multiple dei caratteri del primo insieme con una occorrenza singola. Così, ad esempio,:

$ echo 'ciao   come te      la   passi' | tr -s ' ' ':'
ciao:come:te:la:passi

che è molto comodo per “normalizzare” una linea in cui i campi siano serparati da uno o più spazi (trasformandola, nell’esempio, in una linea in cui i campi sono separati da :).

awk, talvolta vorremmo eseguire una semplice operazione per ogni linea di un file come, ad esempio, sommare i valori che compaiono in una certa colonna. In questo caso, il comando awk può essere d’aiuto, sebbene esso sia in generale molto potente e complicato (controllate il manuale, al solito). La sintassi generale è awk 'selettore { istruzioni } ' dove il selettore indica su quali linee eseguire le istruzioni. Il selettore può venir omesso, nel qual caso le istruzioni si applicheranno ad ogni riga, altrimenti può essere una espressione regolare racchiusa tra barre e le istruzioni verranno eseguite solo sulle linee che soddisfano tale espressione, oppure BEGIN, o END per indicare, rispettivamente, che le istruzioni vanno eseguite all’inizio, o alla fine del file. Le istruzioni sono molto simili a quelle usuali di un linguaggio di programmazione (si veda il manuale per maggiori dettagli); ogni riga è suddivisa in campi (normalmente separati da uno o più spazi, o segni di tabulazione) e le istruzioni possono riferirsi ai campi tramite le variabili $1, $2… Tornando al nostro esempio, supponiamo di voler sommare la dimensione dei file con estensione *.jpg contenuti nella directory corrente. A tale scopo possiamo usare il seguente comando:

$ ls -l | awk 'BEGIN { tot = 0; } /jpg/ { tot = tot + $5 } END { print tot; } '
2631021

join terminiamo la discussione con un comando molto utile per “mettere assieme” due file basandoci sul contenuto delle loro linee. Immaginiamo di avere due file che contengono diverse righe su ciascuna delle quali sono presenti vari campi separati da un carattere fissato. Ad esempio, il file /etc/passwd visto in precedenza contiene una riga per utente con diversi campi separati da :, tra i quali il primo e il terzo sono rispettivamente la stringa ed il numero che il sistema usa per identificare l’utente. Ad esemepio, sul mio sistema, si ha:

$ cut -d: -f1,3 /etc/passwd
nessuno:500
ilenia:502
santini:501

Consideriamo ora la pipe:

$ ls -ln | awk '{print $3":"$9}'
503:nostro.txt
501:mio.txt
502:suo.txt

che produce l’elenco dei file della directory corrente preceduti dal numero dell’utente che li possiede (si veda la prossima sezione per maggiori dettagli sulla nozione di proprietario di un file). Se volessimo conoscere non tanto il numero, quanto il nome del proprietario di ogni file dovremmo “mettere assieme” i due file “incollando” le righe che hanno nel posto riservato al numero, il medesimo valore. Per farlo dobbiamo per prima cosa mettere in ordine (alfabetico, anche se qui ci interessano dei numeri) i due file. Possiamo farlo con le pipe:

$ cut -d: -f1,3 /etc/passwd | sort -t: -k2,2
nessuno:500
santini:501
ilenia:502
$ ls -ln | awk '{print $3":"$9}' | sort -t: -k1,1
501:mio.txt
502:suo.txt
503:nostro.txt

dove abbiamo usato i parametri -t e -k per indicare a sort che volevamo ordinare in base al secondo, o al primo campo, e che i campi sono separati da :. Il comando join ci permette di unire tali file, una volta che specifichiamo quale sia il campo che vogliamo usare per identificare le righe corrispondenti. Nel nostro caso di tratta del secondo campo del primo file e del primo campo del secondo file, mentre i campi sono separati dal solito carattere. Ecco come si conclude quest’ultimo complicato esempio:

$ join -t: -1 2 -2 1 <(cut -d: -f1,3 /etc/passwd | sort -t: -k2,2) <(ls -ln | awk '{print $3":"$9}' | sort -t: -k1,1)
501:santini:mio.txt
502:ilenia:suo.txt

Osservate che mancano all’appello una riga del primo e una del secondo file, infatti per esse manca la corrispondenza tra i campi che abbiamo scelto per ottenere l’unione. L’opzione -a consente di recuperare anche tali linee (che non verranno però “incollate” a nessuna linea dell’altro file). In questo modo, il comando join può essere usato, invece che per “mettere assieme” due file, per trovarne in un certo senso l’intersezione, o la differenza. Ma questo è lasciato per esercizio alla vostra curiosità.

diff, cmp senza entrare nei dettagli, questi due comandi sono in grado di mostrare le differenze, o di individuare almeno la loro presenza, tra due file; anche se il loro uso per manipolare file è molto compilcato, potete almeno utilzzarli per avere una indicazione sulla somiglianza tra due file. Questo vi può essere utile ad esempio per confrontare l’output che vi aspettate debba avere un programma con quello che ha effettivamete; l’exit code (che sarà spiegato in seguito), unitamente alle espressioni condizionali della shell, può essere usato in uno script che controlli se il vostro programma si comporta come vi aspettate, o meno.

Proprietà e permessi

Utenti e gruppi

Senza entrare tanto nel dettaglio, ogni sistema Unix, essendo condiviso da più utenti, ha un modo per stabilire chi può fare cosa. Molto semplicemente, ogni utente è identificato in modo univoco dal sistema tramite un numero intero, detto user identity (uid) e gli utenti sono raccolti in gruppi anch’essi identificati univocamente da un numero, detto group identity (gid). Se volete sapere chi siete e a che gruppo appartenete, potete usare il comando id, nel mio caso:

$ id
uid=500(santini) gid=100(users) groups=100(users), 98(admin)

in genere è l’amministratore del sistema che definisce i gruppi e decide a che gruppo appartenete.

Proprietà di file e directory

Tutti i file e le directory del sistema sono associati ad un utente e ad un gruppo che li possiedono; questo permette di regolare da che utente, o da che gruppo di utenti, essi possano essere scritti, letti, eseguiti (nel caso di file, o attraversati (nel caso delle directory). Tramite ls -l è possibile conoscere queste informazioni:

$ ls -l /home/*

da questa lista, nella parte sinistra, si possono osservare i permessi -rwxr-x----, mentre più a destra sono le indicazioni sull’utente e gruppo proprietari del file. Interpretare i permessi è semplice, sono tre gruppi di tre lettere, r singifica permesso in lettura, w in scrittura e x in esecuzione/attraversamento mentre il trattino significa permesso mancante. Il primo gruppo riguarda l’utente, il sencondo il gruppo ed il terzo tutti gli utenti del sistema.

Il comando chmod permette di modificare i permessi di un file, mentre il comando access permette di verificare se chi lo invoca ha determinati permessi su un dato file. Ad esempio:

$ if access -w /; then echo si; fi

darà quasi certamente output nullo, visto che un utente normale non ha permesso di scrittura nella root directory del sistema. Fate riferimento al manuale per la sintassi ed il significato di tali comandi.

I processi

Alcuni file del sistema possono essere eseguiti, sia nel senso che contengono del codice che il sistema sa come eseguire (ossia, si tratta di codice binario, o di sorgenti per qualche tipo di interprete installato nel sistema) che nel senso che hanno gli appropriati permessi di esecuzione. Se specifichiamo uno di tali file come primo elemento della riga di comando, il sistema lo eseguirà. Così come la directory corrente permette di specificare in modo abbreviato certi file, l’interprete conserva sempre una lista di directory (separate da due punti) nelle quali cercare i comandi, questa variabile si chiama PATH. Sul mio sistema:

$ echo $PATH
/bin:/usr/bin:/usr/local/bin:/home/santini/bin

il che vuol dire che se scrivessi pippo sulla linea di comando il sistema cercherebbe un file eseguibile di nome /bin/pippo, o /usr/bin/pippo e via discorrendo ed eseguirebbe il primo trovato. Se voglio eseguire un comando nella directory corrente posso agire in due modi: la cosa più sicura è invocarlo precedendolo con ./, ovvero nell’esempio precedente, come ./pippo; altrimenti, potrei aggiungere . alla lista in PATH, ma questo può essere rischioso dal punto di vista della sicurezza.

Un file eseguibile (nel senso precedente), una volta che viene invocato dalla riga di comando si “attiva” e diventa un processo del sistema.

Si può ottenere l’elenco di tutti i processi attivi nel sistema con il comando ps di cui abbiamo visto diversi esempi sin qui. Invocato senza opzioni, riporta solo i comandi attivati dalla login corrente. Invocato con argomento aux (si tratta in verità di opzioni, ma per cui va omesso il trattino), esso restituisce l’elenco di tutti i processi attivi con l’indicazione dell’utente che li sta eseguendo. Se il vostro nome utente è pippo, con:

$ ps aux | grep pippo

potete conoscere la lista di tutti i vostri processi attivi nel sistema. Una volta che un processo è attivo, potete terminarlo inviandogli un messaggio denominato term, o kill. Per fare questo potete usare il comando kill che accetta come argomento il numero che identifica il processo nel sistema, detto process identity (pid). Per scoprire il pid potete usare il comando ps, dove il pid è il primo numero che compare su ogni riga. Se quindi il processo che volete uccidere avesse pid 123, ad esempio, potete usare:

$ kill 123

per chiedere “gentilmente” al processo di temrinare (segnale term), oppure:

$ kill -9 123

per ordinargli di cessare immediatamente l’esecuzione (segnale kill). La differenza sta nel fatto che con il primo segnale alcuni programmi (come ad esempio gli editor), prima di terminare salvano il lavoro parzialmente svolto, mentre con il secondo segnale sono costretti ad uscire immediatamente, potenzialmente causando una perdita di dati non ancora salvati. Il secondo segnale si rende però necessario qualora il programma sembri non rispondere al primo segnale.

Su alcuni sistemi, il comando killall seguito da un nome di comando permette di inviare un segnale (term, o kill se usate l’opzione -9) a tutti i processi che sono stati invocati con il nome di quel comando.

Non temete di molestare gli altri utenti: il sistema è ben protetto e vi consente di inviare messaggi (quindi di terminare) soltanto i processi che avete istanziato voi stessi.

Mettiamo assieme le cose

Ad ogni processo sono associati un utente ed un gruppo che sono solitamente l’utente ed il gruppo del processo che lo ha invocato: in Unix tutti i processi sono “figli” di qualche altro processo (e sono, non c’è bisogno di dirlo, organizzati in un albero).

Ogni processo, alla sua terminazione, ritorna un valore intero (detto exit status) che viene comunemente utilizzato per indicare al processo “padre” l’esito della computazione; di solito il valore 0 sta ad indicare una esecuzione terminata senza errori, mentre valori positivi stanno ad indicare diverse possibili condizioni anomale di terminazione. Il manuale di ogni comando che abbiamo visto specifica, per ciascuno di essi, quale sia il significato dei suoi valori d’uscita.

Confrontando l’utente e gruppo associati ad un processo e l’utente e il gruppo associati ad un file è possibile scoprire cosa può fare quel processo a quel file, se può leggerlo, scriverlo, o eseguirlo.

Questo meccanismo sarebbe molto limitato se non fosse possibile, in qualche modo, far sì che un processo abbia utente e gruppo diversi da quelli del processo che lo ha invocato. Cambiare l’utente e il gruppo di un processo è quindi ovviamente possibile, ma si rimanda alla lettura del libro consigliato per scoprire i dettagli della questione.