Nel precedente articolo abbiamo imparato le basi della shell Bash. In questo articolo ci proponiamo di approfondire l’argomento.

Installazione di nuove applicazioni

Capita piuttosto frequentemente, lavorando con la riga di comando, di dover installare nuove applicazioni. Fortunatamente abbiamo a disposizione strumenti che rendono questa operazione agevole e veloce da eseguire. Le applicazioni sono disponibili come pacchetti scaricabili da repository on-line. Questi pacchetti possono esistere in diverse “forme”. Essendo l’articolo orientato alla shell Bash su un sistema operativo Ubuntu, useremo il formato debian e come strumento di management apt.

Un packet manager system è un insieme di strumenti che permette di distribuire, installare, rimuovere e aggiornare software in un sistema Linux. Purtroppo non c’è un tool unico per la gestione dei pacchetti, anzi quasi ogni distribuzione ne adotta uno diverso, ad esempio apt (Ubuntu/debian), rpm (red-hat).

I pacchetti sono generalmente formati da un singolo file nel quale, oltre al software con tutte le sue parti, sono contenuti anche molti metadati come la versione, il checksum e la lista delle dipendenze. Quest' ultime sono librerie e altri software che sono necessari per far funzionare l’applicazione.

Fino a pochi anni fa bisognava gestirle a mano ma fortunatamente i packet manager più moderni riescono ad automatizzare il processo.

Prima di procedere ad una nuova installazione è buona norma, se non l’avete già fatto di recente, eseguire l’update della lista locale dei software (scaricando le nuove informazioni dai repository). Si usa a questo scopo il comando:

$ sudo apt-get update

Dopo questa operazione, è probabile che alcuni pacchetti debbano essere aggiornati ad una versione più recente. Potete farlo usando:

$ sudo apt-get upgrade

Finalmente potete cimentarvi nell’installazione di un nuovo software. Scegliamo come esempio il divertente cowsay:

$ sudo apt-get install cowsay

al momento dell’esecuzione del comando, apt-get controllerà che cowsay sia presente in almeno uno dei repository conosciuti. In caso positivo andrà alla ricerca delle librerie necessarie al suo funzionamento (in gergo si dice che risolve le dipendenze) e mostrerà sullo schermo un riepilogo delle operazioni che saranno effettuate, chiedendovi un’ultima conferma. Se date il vostro assenso, cowsay (con tutte le sue dipendenze) sarà scaricato in locale e installato sul vostro sistema. Ora potete digitare:

$ cowsay "Ciao lettore”

Questo comando stampa in ascii-art una mucca(!) con un fumetto che recita una frase passata come parametro (nell’esempio “Ciao lettore”). Se ritenete (a torto!) che questo software non sia divertente e che occupi inutilmente spazio sul vostro hard-disk, potete rimuoverlo. Il comando è intuitivo:

$ sudo apt-get remove cowsay

Analogamente a quello che accadeva per l’installazione, verrà presentato un riepilogo delle operazioni che dovranno essere compiute e verrà chiesto di nuovo una conferma per procedere. Questa rimozione elimina il pacchetto ma non le eventuali configurazioni. Se siete proprio decisi a cancellare ogni traccia del software potete usare:

$ sudo apt-get --purge remove cowsay

Ora se volete divertirvi un po' con cowsay prima di passare ad argomenti più seri, provate a digitare quanto segue:

cowsay -f tux "Io non sono una mucca!”
cowsay -f dragon "Ladro! Ti fiuto e ti riconosco dall'odore. Odo il tuo respiro..."

I processi

In genere una applicazione blocca la shell fino a quando non ha terminato il proprio lavoro. In gergo si dice che il comando è stato eseguito in foreground (in primo piano). Se lanciamo un job particolarmente oneroso, saremo costretti ad aspettare che finisca (o ad aprire un altra shell). I moderni sistemi operativi sono multitasking, ovvero permettono di eseguire più operazioni contemporaneamente. Vorremmo poter sfruttare questa importantissima caratteristica anche quando usiamo Bash. Fortunatamente questo è possibile. Possiamo chiedere al sistema operativo di eseguire una certa applicazione in background (in “secondo piano”) inserendo un carattere ‘&' alla fine del comando. In questo modo il job continua ad essere attivo ma la shell torna subito disponibile. Supponiamo, ad esempio, di dover scompattare un file zip molto grande. Essendo una operazione lunga decidiamo di eseguirla in background:

$ unzip file-zip-molto-grande.zip &

Ora il file verrà unzippato ma noi potremo continuare il nostro lavoro (attenzione però! Se l’operazione in background prevede una stampa dei risultati a schermo, i messaggi verranno comunque visualizzati. Si può ovviare a questo usando flag appositi che sopprimano gli output). Per conoscere i processi che sono in secondo piano, possiamo usare il comando jobs

$ jobs
[1]+ Running    unzip file-zip-molto-grande.zip &

se vogliamo riportare un job in foreground, possiamo usare il comando fg seguito dal suo id (il numeretto tra parentesi quadre stampato da jobs)

$ fg 1
unzip file-zip-molto-grande.zip &

Se abbiamo giù avviato (non in background) una applicazione onerosa ci sono (almeno) due azioni che possiamo compiere: ctrl+c che ferma definitivamente il job, utile se il ci accorgiamo di aver dato un comando sbagliato o se per qualche motivo l’applicazione si è bloccata. ctrl+z permette di stoppare il job momentaneamente. Possiamo poi scegliere se mandare il processo in background utilizzando il comando bg o farlo continuare usando fg. Visto che stiamo parlando di programmi e processi: cosa fare quando uno di essi si blocca o si rifiuta di interagire con noi? Nei sistemi Windows esiste la famosa(!) combinazione di tasti ctrl+alt+canc che ci permette di accedere ad un task manager e chiudere manualmente i software ribelli. In Bash possiamo usare degli strumenti analoghi. Innanzitutto dobbiamo scoprire le applicazioni in esecuzione. Usiamo allo scopo il comando ps (-A è solo uno dei tanti switch).

$ ps -A
4724 ??         0:00.02 [...] 
4726 ??         0:00.07 [...]
4731 ??         0:00.05 [...]

L’output risultante è piuttosto ricco (nell’esempio l’abbiamo tagliato). Un valore che ci interessa particolarmente è il PID (la prima colonna). Esso identifica in modo univoco un processo. Se quest’ultimo non risponde, possiamo killarlo (in italiano sarebbe più corretto tradurre con terminare oppure chiudere… ma l’italianizzazione del termine inglese da più soddisfazione!). Lo si può fare usando il comando kill con il segnale -9 seguito dal PID del processo da cancellare:

$ kill -9 1234

Networking di base

ping

La funzione primaria di ping è quella di verificare se un dispositivo di rete è raggiungibile e di misurare la latenza di trasmissione (ovvero il tempo impiegato dai pacchetti per “andare” e “tornare”). La sintassi è semplicissima ping <ip/url-destinazione>:

$ ping it.wikipedia.org
64 bytes from 91.198.174.192: icmp_seq=1 ttl=58 time=11.1 ms
64 bytes from 91.198.174.192: icmp_seq=2 ttl=58 time=11.4 ms
[...]

Il comando continua ad inviare pacchetti finchè non lo stoppiamo esplicitamente usando ctrl+c.

wget

wget è un “downloader non interattivo”. Permette cioè di scaricare file da internet (via http o ftp) senza richiedere azioni dell’utente. Come esempio, scarichiamo da github un file di testo contenente l’elenco delle parole italiane (lo useremo in un esempio successivo).

$ wget https://github.com/napolux/paroleitaliane/raw/master/paroleitaliane/280000_parole_italiane.txt

Come potete vedere la sintassi di base è molto semplice: wget seguito dall’url della risorsa da prelevare. Premendo invio vedrete di messaggi seguiti da una barra (in ascii-art) che mostra in tempo reale i progressi del download. Naturalmente (ma ormai dovreste averci fatto l’abitudine) è possibile complicarsi la vita a piacimento usando alcune delle decine di opzioni a disposizione. Facciamo un paio di esempi, rimandando il lettore alle man-page per approfondimenti.

$ wget --timeout=10 --limit-rate=8k https://github.com/.../28000_parole_italiane.txt 
$ wget -c https://github.com/.../28000_parole_italiane.txt 

Nel primo caso settiamo un timeout per terminare il comando nel caso non vengano ricevuti dati per più di 10 sec. Inoltre limitiamo la velocità di download a 8k (per rivivere cosa si provava 15 anni fa quando ancora si navigava su internet con un modem 56kbps…) Nel secondo caso invece possiamo recuperare un download interrotto per qualsiasi motivo. Supponete di aver scaricato solo il 40% del file e poi la connessione è caduta. Con il flag -c possiamo chiedere di riprendere il download dal punto in cui si era fermato. Questa opzione è utile anche se volete prelevare file molto grossi dividendo le sessioni di download.

ssh

ssh è un client per il login remoto in rete sicura. Tradotto in parole più semplici, è un tool che vi permette di connettervi da riga di comando ad un server remoto. Sia in ambito lavorativo che in quello “personale”, è una operazione che vi troverete a fare di frequente. Se ad esempio comprate un VPS o un servizio Cloud quasi sicuramente dovrete connettervi ad esso con ssh per amministrarlo. Per fortuna il comando è semplice e intuitivo. Basta specificare l’utente con il quale volete loggarvi e il server.

$ ssh david@example.com

Se tutto procede bene, vi verrà chiesta la password dell’utente e poi… sarete liberi di fare qualsiasi tipo di danno sulla macchina remota. Per terminare la sessione basterà digitare exit. In genere il comando mostrato è sufficiente per la maggior parte dei server. Potrebbero però presentarsi delle situazioni non standard. Vi farà piacere sapere che ssh ha diverse decine di opzioni per fronteggiare quei casi. Le man-page, come al solito, potranno darvi tutti i dettagli.

scp

Dopo aver preso il comando di una macchina remota con ssh, potrebbe nascere l’esigenza di caricare o scaricare file da quel server. Potreste ad esempio voler installare un software che avete sviluppato in locale, oppure prelevare i log di un certo servizio, in modo da poterli analizzare. In entrambi i casi ci sarà di aiuto scp. Questo comando permette di fare l’upload o il download di file da una macchina remota. Vediamo come.

$ scp 28000_parole_italiane.txt david@server-remoto.com:/home/david

con questo comando chiediamo l’upload del file 28000_parole_italiane.txt (che deve trovarsi nella directory corrente… altrimenti dovete specificare il suo path). Il comando va letto in questo modo: david@ rappresenta l’utente con cui eseguire il login sul server server-remoto.com il server remoto verso il quale inviare il file (potete anche usare un indirizzo ip) :/home/david è invece il path assoluto della cartella sul server nella quale il file verrà salvato Naturalmente l’utente deve avere i permessi di scrittura nella directory di destinazione. Per il download la procedura è molto simile:

$ scp david@server-remoto.com:/home/david/test-image.png /home/david/file-scaricati

Analogamente al caso precedente, ci connettiamo al server “server-remoto.com” con l’utente david e chiediamo il download del file /home/david/test-image.png. Questo file sarà salvato in locale nella cartella /home/david/file-scaricati. Potete intuire che, con una sintassi perfettamente analoga, sarà possibile anche spostare file tra due server. Come al solito, scp supporta molte opzioni. Ad esempio se il nostro server non è in ascolto sulla porta standard, possiamo usare il flag -P per indicare la nuova porta da usare.

$ scp -P nuova-porta [...]

Se volete invece spostare intere directory potete usare il flag -r.

curl

Formalmente curl è un client che permette di prelevare o caricare file in un server. Nella pratica si rivela un vero scrigno di strumenti preziosi per il web. Per uno sviluppatore, in particolare, si rivela utilissimo in fase di test di web-services. Vediamo in primo luogo la sua sintassi di base (useremo il server fittizio iop.webservice):

$ curl "http://iop.webservice"

Interroghiamo il servizio che risponde all’URL “myservice.com” e attendiamo la sua risposta. Il codice ricevuto (sia esso un html, un json o un xml) verrà stampato sullo schermo. In molte occasioni può essere utile salvare l’output su un file. Basta usare l’opzione -o seguita dal path del file da salvare:

$ curl -o response.json "http://iop.webservice"

Con l’opzione -i -v possiamo ottenere anche gli header dettagliati scambiati con il server. Questo può essere particolarmente utile in fase di debug del servizio:

$ curl -i -v "http://iop.webservice"

Spesso per accedere ad un certo servizio c’è bisogno di autenticarsi. Nel caso di basic authentication si può inviare username e password usando l’opzione -u:

$ curl -u username:password http://iop.webservice/login

se viene specificato solo lo username, curl si preoccuperà di chiedervi anche la password. Fin ora abbiamo usato solo chiamate di tipo GET. Possiamo però usare anche tutti gli altri verbi. Basterà usare il parametro -X seguito da una stringa opportuna (GET, POST, PUT, DELETE, …)

$ curl -X GET "http://iop.webservice?id=123"
$ curl -X DELETE "http://iop.webservice/id/123"

-X GET è l’opzione di default (quindi avremmo potuto ometterla nella prima riga). Nel caso di richiesta POST, possiamo passare i parametri usando l’opzione -d.

$ curl -X POST "http://iop.webservice/login" -d "username=myun&password=mypw"

è anche possibile codificare i dati in modo che siano adatti ad essere inviati sul web:

$ curl -X POST "http://iop.webservice/post" --data-urlencode "stringa da codificare"

Se abbiamo invece bisogno di inviare un file binario, possiamo utilizzare l’opzione –data-binary e il nome del file da inviare preceduto da @:

$ curl -X PUT "http://iop.webservice/documents" --data-binary @data-file-name.txt

Per includere header extra alla nostra richieste (ad esempio il content-type) possiamo utilizzare il parametro -H:

$ curl -X POST "http://iop.webservice" -H "Content-Type: application/json" -d "{'data':'hello!'}"

Per fare l’upload di un file possiamo usare in parametro -F che in pratica simula l’invio di un form (multipart-post-data)

$ curl "http://iop.webservice/post" -F "uploaded_file=@nomefilelocale.ext" -F "name=test-upload" 

Redirecting & pipe

Nella shell ci sono sempre almeno tre file aperti. I loro nomi sono criptici: stdin, stdout, stderr e vengono di solito associati a dei numeri: stdin=0, stdout=1, stderr=2. Perché ne abbiamo bisogno? Riflettiamo sul funzionamento di una shell: di solito interagiamo con essa inserendo comandi con la tastiera e otteniamo delle risposte sottoforma di testi stampati sullo schermo. Bene: stdin è il canale di comunicazione che permette alla bash di leggere i nostri comandi ed è associato di default alla tastiera. Stdout è invece il dispositivo verso il quale vengono riversate gli output dei comandi e, di default, è associato allo schermo. Stderr è invece il canale sul quale vengono mandati tutti i messaggi di errore. Anch’esso è normalmente associato allo schermo. Questi canali sono configurabili cosa che si rivela utile in molte circostanze. Vediamo alcuni esempi. Il comando du è utile per capire velocemente come è distribuita l’occupazione di spazio sul nostro hard-disk.

$ du -h -d1 /

abbiamo chiesto di calcolare la capienza di ciascuna cartella contenuta in root (path: ‘/'). con il flag -h chiediamo un output human-readable mentre con -d1 imponiamo di limitare l’analisi al primo livello dell’albero delle directory. Se provate ad eseguire il comando otterrete, oltre al risultato atteso, anche una nutrita serie di righe di errore. E' un comportamento normale, dato che da utenti comuni non avete i permessi per accedere ad alcune cartelle nel filesystem. Ma è anche piuttosto fastidioso dato che la stampa di questi errori compromette la leggibilità. Possiamo ovviare a questo eseguendo il redirect degli errori (ovvero di stderr) verso uno speciale dispositivo di nome /dev/null che semplicemente scarta tutte le informazioni che gli arrivano (una sorta di buco nero nel sistema!)

$ du -h -d1 / 2> /dev/null 

Con il simbolo ‘2>' abbiamo chiesto di redirigere stderr (che, se ricordate, ha numero 2) verso /dev/null, che è come dire di buttare via tutti gli errori. Otterremo in output le sole informazioni rilevanti. Potremmo voler conservare questo risultato in un file, per confrontarlo in futuro con altre rilevazioni.

$ du -h -d1 / 2> /dev/null > result-2016-06-10.txt

con il simbolo ‘>' abbiamo indicato di redirigere lo stdout verso un file e non verso lo schermo come sarebbe stato di default. Usando ‘»' possiamo invece accodare il contenuto al file esistente, invece di ricrearlo ogni volta.

$ du -h -d1 / 2> /dev/null >> result-2016-06-10.txt

Notate che scrivere ‘>' o ‘»' è equivalente a scrivere ‘1>' o ‘1»'. Sempre riguardo i canali di comunicazione, un altro concetto importante da affrontare è quello di pipe. E' possibile concatenare vari comandi in modo che l’output del primo sia usato come input del secondo. Un “classica” applicazione delle pipe è quella che si fa con less (che, ricordiamo, permette di navigare il contenuto testuale di un file). Supponiamo di avere un comando che ha un output molto lungo, come ad esempio il ps -A di cui abbiamo parlato in precedenza. L’output di questo comando ci mostra decine di righe. Vorremmo poterlo vedere un po' alla volta:

$ ps -A | less

Il simbolo della pipe è ‘|'. L’output del comando a sinistra viene usato come input di quello a destra. Possiamo anche mettere in pipe più comandi contemporaneamente:

$ ls /home/david/Downloads/ | sort -f | less

sort ordina le righe che riceve in input. Il suo switch -f specifica l’ordinamento case-insensitive. Otteniamo dunque un elenco navigabile dei file contenuti nella cartella /home/david/Downloads ordinati alfabeticamente. Ricerca

Find

find è uno dei tool più utili a disposizione della shell. Formalmente questo comando cerca, in un albero di directory, i file che rispondano a particolari requisiti e su di essi compie delle operazioni. Questa definizione è molto generale… proviamo a chiarirla con qualche esempio. Nella sua forma base, find elenca tutti gli oggetti che trova in un albero di cartelle:

$ find /home/david

questo comando stampa su schermo tutti i file e le cartelle contenute in /home/david. Questo comportamento può essere utile in alcuni (rari) casi. Proviamo a renderlo più interessante specificando dei filtri:

$ find /home/david -name "test”

Ora verranno cercati, in /home/david e in tutte le sue sottodirectory, i file e le cartelle di nome test. Per avere più flessibilità è possibile usare le wild-cards:

$ find /home/david -iname "*.pdf”

abbiamo chiesto una ricerca di tutti i file con estensione pdf. Notate anche che abbiamo usato l’opzione -iname l’equivalente case-insensitive di -name. Se invece volessimo cercare i file che non hanno estensione pdf, potremmo usare la negazione

$ find /home/david ! -name "*.pdf”

Find ha decine di altre opzioni. Non possiamo parlare di tutte (per questo potete consultare la pagina man relativa). Ci limiteremo a riportare alcuni esempi tra quelli più significativi per uno sviluppatore. Cerchiamo tutti i file pdf che siano stati creati entro un certo tempo (ad esempio gli oggetti non più vecchi di 30 minuti). Useremo l’opzione -cmin:

$ find /home/david -iname "*.pdf” -cmin -30

-cmin accetta un valore numerico positivo o negativo. Se scriviamo -30 richiediamo tutti i file creati al massimo 30 minuti fa. Con +30 otterremo quelli creati da almeno 30 minuti. Può essere utile anche porre una limitazione alla ricerca nelle sottocartelle. Di default find cerca nella directory passata come parametro e in tutte le sue sottodirectory. Potremmo però voler limitare la profondità di visita dell’albero. Possiamo fare questo usando le opzioni -maxdepth e -mindepth. La prima specifica fino a che livello dell’albero si deve arrivare. La seconda il livello da cui partire per la ricerca. Usando opportunamente queste opzioni, possiamo ad esempio chiedere a find di cercare solo nella cartella corrente senza considerare le sottocartelle:

$ find /home/david -iname "*.pdf" -maxdepth 1

Oppure si può richiedere di cercare sono nelle cartelle figlie di quella corrente (senza spingersi più in basso):

$ find /home/david -iname "*.pdf" -maxdepth 2 -mindepth 2

In pratica abbiamo chiesto di cercare file pdf a partire dal livello 2 e di non andare oltre quel livello.

grep

grep (acronimo che sta per “general regular expression print”) serve a cercare in un file le righe di testo che corrispondono a un modello specificato tramite regular-expression. La sua invocazione restituisce un elenco delle righe in cui è stata trovata una corrispondenza. Facciamo un esempio con la word-list che abbiamo scaricato in un paragrafo precedente, rinominata per convenienza in dict_it. Con il seguente comando cerchiamo nel file tutte le righe che contengono la parola programmazione:

$ grep programmazione dict_it
microprogrammazione
monoprogrammazione
multiprogrammazione
programmazione
riprogrammazione  

Sfruttando le espressioni regolari possiamo ottenere risultati più sofisticati. Ad esempio possiamo chiedere tutte le parole che iniziano e finiscono con la lettera h:

$ grep "^h.*h$"  dict_it
hashish
hindemith
hurrah

Alcune utili opzioni sono: -i ricerca case insensitive -w ricerca l’intera parola -v ricerca le righe che non rispondono al pattern -c conta le righe trovate

Grep risulta particolarmente utile se usato in congiunzione con le pipe. Riprendendo un esempio precedente, quello che determinava l’occupazione di spazio delle cartelle con il comando du. Vorremmo filtrare l’output in modo da poter vedere solo le cartelle che superano 1 GB di grandezza.

$ du -h -d1 / 2> /dev/null | grep -E "^ *\d\d?,?\d?G" 

L’espressione regolare passata a grep ci mostra solo le righe che iniziano con un numero (eventualmente con la virgola) seguito dal simbolo ‘G' che indica Gb. E' possibile anche mettere in pipe più comandi. Ad esempio se volessimo trovare le 5 directory che occupano più spazio nel nostro hard-disk, potremmo scrivere un comando del genere:

$ du -h -d1 / 2>/dev/null | sort -nr | head -n 5

Con il primo pipe ( | sort -nr) stiamo ordinando le righe risultanti dal comando du. Con il secondo selezioniamo solo le prime 5 righe dell’output precedente.

Cos’altro imparare?

Nei due articoli dedicati alla Bash (questo e quello pubblicato sul precedente numero della rivista) abbiamo trattato solo una piccola parte degli strumenti disponibili nella shell. Abbiamo scelto quelli più utili nel lavoro di tutti i giorni, in modo da rendere subito produttivi anche i lettori che non erano mai entrati in contatto con la command line. A seconda del proprio ruolo ciascuno potrà trarre vantaggio da un certo numero di comandi piuttosto che da altri. Ad esempio un sistemista potrà trovare più utili i tool riguardanti i processi e il networking (ping, ssh,…). Uno front-end developer invece trarrà più vantaggio da curl e magari installerà alcuni dei software per gestire da shell le sue creazioni (npm, yeoman, git). Il vantaggio della riga di comando è racchiuso proprio nella sua versatilità.

David Ciamberlano