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), offrendo dall'altro la possibilità 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'utilizzo 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).)
A 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 infine, 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 utilizzando 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'interprete 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 struttura "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 directory 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/..
coincide 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 directory 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 identificare 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'utilizzo 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'estensione 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) emettesse
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 interpretare come segue: il comando cat legge dal file
test
il suo standard input, emette quindi il contenuto di tale file
sul suo standard output che viene utilizzato 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. Innanzitutto
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 estensione in .c.txt
. Il comando
basename seguito da un file e da una estensione produce in output
il nome del file senza estensione. 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'architettura di un sistema Unix. L'idea base è estremamente semplice: avere una tecnica efficiente, 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 semplici 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, avremo:
$ 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 eliminare 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 ciascun 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 separati
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 esempio, 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 complicato, potete almeno utilizzarli 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 effettivamente; 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
significa permesso in lettura, w
in scrittura e x
in esecuzione/attraversamento mentre il trattino significa permesso mancante.
Il primo gruppo riguarda l'utente, il secondo 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 terminare (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.