Linux per le Olimpiadi Internazionali di Informatica

0 Introduzione

Questi brevissimi appunti sono realizzati come supporto per gli allenamenti della squadra italiana che parteciperà alle IOI (International Olympiad in Informatics).

Gli studenti che partecipano agli allenamenti sono invitati per ogni dubbio, domanda, o richiesta di chiarimento a contattarmi all'indirizzo santini@di.unimi.it (senza ritegno).

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 il sistema colloquia con l'utente.

Attraverso la discussione del funzionamento della shell saranno accennati acluni concetti elementari riguardo la gestione dei processi e del filesystem, nonché le tecniche di redirezione dell'input/output che sono alla base dell'utilizzo dei filtri.

Per concludere, saranno presentati alcuni specifici filtri con esempi di utilizzo legati specificamente alla partecipazione alle IOI.

Ecco di seguito il contenuto di questo documento:

  1. Documentazione on-line
  2. L'interprete dei comandi
    1. Identificare i file
    2. La directory corrente
    3. Manipolare directory e file
    4. L'espansione dei caratteri speciali
    5. Programmare semplici script
  3. Redirezione dell'input/output
    1. Redirezione da/a file
    2. Pipe tra comandi
    3. Ancora redirezione
  4. Filtri
    1. Selezionare righe e colonne
    2. Semplici manipolazioni
  5. Miscellanea
    1. Permessi e processi
    2. Trucchi e comandi utili
  6. Approfondimenti ed ulteriori letture

1 Documentazione on-line

Di seguito saranno accennati 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 usuali 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.

Alla fine di questi appunti, nella sezione Approfondimenti ed ulteriori letture, troverete puntatori ad ulteriore documentazione e indicazioni su come proseguire nella vostra eslorazione di Linux.

2 L'interprete dei comandi

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

2.1 Identificare i file

Unix ha un filesystem gerarchico, il che vuol dire che tutti i suoi files 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 files. 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 direcotry home dove sono contenuti i dati degli utenti, con le sue sottodirectory ilenia e santini; sempre a titolo di esempio, sono riportati anche alcuni files: 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 files. 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. 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 eseguito 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 files.

2.2 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.

2.3 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 direcotry. 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.

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 files è molto semplice (quindi pericoloso), basta usare il comando rm seguito dalla lista dei files 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.

2.4 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 (escluso il carattere / che mantiene il suo significato di separare, lungo il percorso, le direcotry).

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 files sotto una directory, possiamo usare lo strataggemma 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

2.5 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 se 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ù dificcile 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, anno 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 files 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 si 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 avesse questa sintassi, potete usare il comando seq e la sintassi $() come discusso in sequito in un esempio.

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

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 code della prima lista di comandi 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.

3 Redirezione dell'input/output

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 files 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 sulla tastiera e , la seconda è l'effetto del comando cat che invia sul suo standard output quanto a letto e della shell che lo visualizza ("sul terminale"). Il carattere ^D corrisponde alla pressione simultanea di ctrl e del tasto d, che ha come effetto quelo di segnalare la fine del file (ossia, di emettere un segnale di EOF allo standard input).

3.1 Redirezione da/a file

La shell rende possibile ridirigere 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!).

3.2 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à si che i conteggi vengano memorizzati nel file out.

3.3 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 files 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 di 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

4 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 anno programmi molto sofisticati che svolgono numerose funzioni, ma che molto difficilmente possono interoperare tra loro.

Facciamo un esempio per essere più chiari. Immginiamo 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 snessun 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.

4.1 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 al contrario. 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

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
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. Il comando awk discusso nella prossima sezione può essere d'utilità quando i campi sono divisi da uno o più spazi bianchi e/o da tabulatori (in questo caso, il comando cut è di scarso aiuto).

4.2 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 | uinq
sntini
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 | uinq | wc -l
1

sed, tr talvolta può essee 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 rimpiazzza". 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 l'uso di cut è reso difficoltoso dal fatto che i campi sono separati, o da uno o più spazi, o da uno o più "caratteri tipo spazio" (come il tabulatore, o simili). In questo caso, il comando awk può essere d'aiuto, sebbene esso sia in generale molto potente e complicato (controllate il manuale, al solito). Nella sintassi

awk '/testo/ {print $1,$2... }'
può essere visto come una combinazione di grep e cut, nel senso che esso stamperà i campi specificati dai numeri per tutte e solo le linee che contengono la stringa testo. Anche qui le espressioni regolari possono aumentare l'espressività del comando, così come, omettendo la parte tra barre, tutte le linee saranno considerate. Ad esempio
$ ps aux | awk '/root/ {print $11}'
stamperà tutti i processi in esecuzione dell'utente root.

5 Miscellanea

5.1 Permessi e processi

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 appartente. Noto questo, tutti i file e le directory del sistema hanno un utente ed un gruppo che li possiedono e quindi permessi che regolano da che utente e da che gruppo di utenti essi possano essere scritti, letti, eseguiti (nel caso di files) 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. Fate riferimento al manuale per la sintassi ed il significato di tali comandi. Una discussione approfondita dei permessi, del loro significato e delle implicazioni che hanno sull'uso del sistema è al di la dello scopo di questi appunti. Nella sezione Approfondimenti ed ulteriori letture potete trovare puntatori a documentazione che spiega in maggior dettaglio tutte queste cose.

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 files, 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 che troverebbe. 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.

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 ritorna 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 lisa 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.

5.2 Trucchi e comandi utili

diff, comm, cmp (per verificare i file di test)

gcc -S (per vedere cosa fa delle macro)

strings, nm, gdb (debugging)

6 Approfondimenti ed ulteriori letture

La raccolta più autorevole e completa di documenti on-line su Linux si trova sul sito Linux Documentation Project. In particolare, una guida per iniziare (nello spirito di questi appunti, comprendente una miniguida di Emacs) è The Linux Users' Guide, disponibile in vari formati e anche in italiano.

Molte distribuzioni mettono a disposizione dei buoni documenti su come iniziare ad utilizzarle, per esempio, la Debian Guide relativa alla distribuzione Debian, oppure la The Official Red Hat Linux Getting Started Guide della Red Hat sono un buon punto dove cominciare (a proseguire questi appunti).

Più difficile il discorso sulla carta stampata. In generale molti libri sono reimpaginazioni, o traduzioni, del materiale che potete trovare gratuitamente sul sito del Linux Documentation Project: in questo caso, considerate bene se vale davvero la pena di spendere (spesso un sacco) di soldi solo per avere su carta quello che potete avere gratis sul monitor.