martedì 26 novembre 2013

BASH - Guida all'automazione: Organizzare i files scaricati.

È rapido scaricare i files che si trovano sul web, salvarli nella directory Downloads, usarli, e poi dimenticarci che esistono.
Capita spesso che però la directory diventi troppo incasinata per gestirne il contenuto, anche solo il trovare un file scaricato un po' di tempo fa, se nella directory ci sono più di un centinaio di elementi, diventa lento e tedioso.

Bash ci viene in aiuto di nuovo, impariamo come si può, usando un semplice comando presente in tutte le distribuzioni linux, determinare il contenuto di un file (con un discreto margine di errore), e spostarlo nella directory che ci interessa.

Passo 1: Riconoscere l'obiettivo

Il comportamento che ci interessa ottenere, è il seguente:
Lanciando il comando, deve spostare i files che "riconosce" dalla directory corrente ad alcune directory definite a seconda del tipo di file.

Passo 2: Identificazione del problema

Il problema nel gestire uno spostamento "automatico" è che il sistema deve essere in grado di classificare il tipo di files, questo obiettivo lo raggiungiamo con uno strumento già presente nella maggior parte delle distribuzioni Linux: il comando "file":
[fanfurlio@magliettabianca]$ file *
01.jpg:             JPEG image data, JFIF standard 1.01
accessitest.odt:    OpenDocument Text
arcreactor.xcf:     GIMP XCF image data, version 0, 1200 x 1200, RGB Color
dosinst.iso:        # ISO 9660 CD-ROM filesystem data 'DOSINST' (bootable)
fonts.txt:          ASCII text
organizefiles.sh:   Bourne-Again shell script, ASCII text executable
python.pdf:         PDF document, version 1.4
re_your_brains.mp3: Audio file with ID3 version 2.3.0, contains: MPEG ADTS, layer III, v1, 192 kbps, 44.1 kHz, Stereo
splash.mov:         ISO Media, Apple QuickTime movie
syslinux.png:       PNG image data, 640 x 480, 8-bit/color RGB, non-interlaced
tastiera.zip:       Zip archive data, at least v1.0 to extract
Vending.apk:        Java Jar file data (zip)
Il comando non fa altro che riconoscere il tipo di file dal suo contenuto, con un discreto margine di errore, che il nostro script dovrà compensare.
Prima di tutto rendiamo l'elenco di tipi di file più gestibile da script, usando lo switch "--mime-type":
[fanfurlio@magliettabianca]Downloads$ file --mime-type *
01.jpg:             image/jpeg
accessitest.odt:    application/vnd.oasis.opendocument.text
arcreactor.xcf:     image/x-xcf
dosinst.iso:        application/x-iso9660-image
fonts.txt:          text/plain
organizefiles.sh:   text/x-shellscript
python.pdf:         application/pdf
re_your_brains.mp3: audio/mpeg
splash.mov:         video/quicktime
syslinux.png:       image/png
tastiera.zip:       application/zip
Vending.apk:        application/jar
Con questo output sarà molto più facile determinare i tipi di file, ed escludere ciò che non ci interessa.
Prima di partire con la creazione dello script è importante aver ben chiaro il nostro obiettivo:
I file immagine devono essere spostati nella directory Pictures,
I file video devono essere spostati nella directory Videos,
I file musicali devono essere spostati nella directory Music,
I file di documento devono essere spostati nella directory Documents,
Gli script shell devono essere spostati nella directory Shellscripts.

Passo 3: Creazione dell'automazione

Per ottenere il comportamento che ci interessa è opportuno mischiare un po' delle conoscenze acquisite con i precedenti script, prendiamo in esame una delle righe di output del comando file:
01.jpg:             image/jpeg
Ci rendiamo facilmente conto, guardando tutto l'elenco, che se vogliamo recuperare i file immagine, sarà sufficiente filtrare per "image/" tramite grep:
[fanfurlio@magliettabianca]Downloads$ file --mime-type * | grep "image"
01.jpg:             image/jpeg
arcreactor.xcf:     image/x-xcf
dosinst.iso:        application/x-iso9660-image
syslinux.png:       image/png
Mi rendo conto che in questo modo ottengo i file immagine, ma ne ho uno di troppo, in realtà a me interessano solo i file che hanno come mimetype "image/QUALCOSA", quindi provo ad affinare il filtro:
[fanfurlio@magliettabianca]Downloads$ file --mime-type * | grep "image/"
01.jpg:             image/jpeg
arcreactor.xcf:     image/x-xcf
syslinux.png:       image/png
Direi che ci siamo, vediamo un po' se può andare lo stesso sistema per gli altri tipi di file:
[fanfurlio@magliettabianca]Downloads$ file --mime-type * | grep "video/"
splash.mov:         video/quicktime

[fanfurlio@magliettabianca]Downloads$ file --mime-type * | grep "text/"
fonts.txt:          text/plain
organizefiles.sh:   text/x-shellscript

[fanfurlio@magliettabianca]Downloads$ file --mime-type * | grep "audio/"
re_your_brains.mp3: audio/mpeg
Un'altra cosa che salta subito all'occhio, è che nel filtrare "text/" ha lasciato passare anche uno script bash, correttamente aggiungo, poiché sempre di file di testo si tratta, ma nel mio caso preferisco che vengano trattati separatamente, quindi dovrò ricordarmi di gestire l'eccezione.

Direi che a questo punto abbiamo abbastanza dati per iniziare a scrivere il nostro script, eccolo qua:
#!/bin/bash
config[0]="image/::/home/fanfurlio/Pictures";
config[1]="video/::/home/fanfurlio/Videos";
config[2]="audio/::/home/fanfurlio/Music";
config[3]="application/pdf::/home/fanfurlio/Documents";
config[4]="text/plain:.sh:/home/fanfurlio/Shellscripts";
config[5]="text/plain::/home/fanfurlio/Documents";

for confline in "${config[@]}"; do
    mimetype=$(echo $confline | cut -d':' -f1);
    extension=$(echo $confline | cut -d':' -f2);
    destination=$(echo $confline | cut -d':' -f3);
    file --mime-type * | grep "${mimetype}" | grep "${extension}:" | while read line; do
        nomefile=$(echo $line | cut -d':' -f1);
        mv ${nomefile} ${destination}/${nomefile};
    done
done
Esaminiamo un paio di punti che possono essere un po' poco immediati:

config[4]="text/plain:.sh:/home/fanfurlio/Shellscripts";
Questa riga dichiara una variabile come "vettore", cioè una variabile che contiene più variabili, raggruppate con lo stesso nome, ci è comodo raggrupparle per poterle richiamare con lo stesso nome, come vedremo fra poco.
config[4]
Indica a Bash che vogliamo accedere ad un solo indice, cioè ad uno "spazio" contenuto all'interno del nostro vettore, allo stesso tempo facciamo capire a Bash che la variabile config dovrà essere un vettore.
="___"
Inserisce una "stringa", cioè una serie di lettere e numeri, all'interno della variabile.
___:___:___
Raggruppo queste due porzioni per far notare che ho "spezzato" la nostra stringa in 3 parti, usando come separatore i :, questo ci permette di inserire 3 informazioni sulla stessa riga.
È importante quando si sceglie un separatore stare attenti a scegliere un carattere che non venga MAI usato all'interno delle informazioni che vogliamo separare, per ovvi motivi.
text/plain
Prima delle 3 informazioni che ci interessa: il tipo di file che vogliamo spostare nella directory apposita.
.sh
Seconda informazione, in questo script facoltativa (vedremo fra poco perché), l'estensione del file.
/home/fanfurlio/Shellscripts
Terza informazione, la directory in cui vogliamo che il file venga spostato.

Un altro punto da esaminare da vicino è la definizione del ciclo:
for confline in "${config[@]}"; do
In soldoni questa riga spiega a Bash che vogliamo ripetere il blocco fra do e done tante volte quanti elementi sono contenuti nel vettore config, vediamo come:
for
Iniziamo un ciclo, leggendo questa parola chiave Bash saprà che vogliamo ripetere un blocco di istruzioni più volte, e si aspetterà nelle prossime parole una definizione di come fare.
confline
Come in tutti i linguaggi di programmazione, il ciclo "for" si usa quando vogliamo ciclare i valori di una specifica variabile, questa parola determina come vogliamo chiamare la variabile che conterrà i valori all'interno del ciclo.
in
Questa altra parola chiave indica a Bash che il prossimo parametro indicherà cosa deve finire nella variabile specificata appena prima.
${config[@]}
Questa parola altro non significa che "Ogni elemento nel vettore che si chiama config".
do
Questa è l'ultima parola chiave, ed indica a Bash l'inizio del blocco di istruzioni da ripetere.

Questo ciclo si eseguirà per ogni elemento che abbiamo inserito nel vettore config, assegnando di volta in volta il valore del singolo elemento alla variabile confline.

Il resto dovrebbe essere abbastanza chiaro ormai, utilizziamo una subshell $() per eseguire un comando echo dell'elemento di configurazione, recuperando tramite cut i 3 parametri di configurazione che ci interessano.
file --mime-type *| grep "${mimetype}"| grep "${extension}:"| while read line; do
Ultima parte di non immediata comprensione, anche se tutte le parti le abbiamo già viste in funzione: file --mime-type *
Questo è il comando che abbiamo esplorato prima di iniziare, sappiamo che ci fornisce il tipo di ogni file che trova nella directory corrente.
grep "${mimetype}"
Siccome ci interessano di volta in volta solo i files di uno specifico tipo, questo filtro lascia passare solo le righe che contengono il mimetype fornito.
grep "${extension}:"
Questa è una ulteriore "raffinazione" del filtro, se si ha una specifica estensione di file che vogliamo selezionare (come nell'esempio l'estensione .sh), possiamo usare questo secondo filtro.
Ci si rende rapidamente conto che se la variabile $extension (proveniente dal secondo parametro di configurazione) è vuota, il filtro lascerà passare tutte le righe dell'elenco senza dar fastidio.
while
Passiamo tutto l'elenco filtrato al ciclo while che abbiamo già usato la volta scorsa.
read
read è un comando linux che legge una riga dall'input (in questo caso il nostro elenco filtrato), e immagazzina la riga nella variabile che gli passiamo.
line
La variabile in cui read metterà la riga che gli arriva.
do
Il ciclo while farà si che il blocco si ripeta per ogni riga dell'elenco, passandoci l'intera riga nella variabile line.

Infine, si recupera dalla riga dell'elenco il nome del file (la porzione che precede i due punti, come abbiamo potuto vedere dalle prove), e si lancia il comando mv che provvederà a spostare il file nella directory che abbiamo definito come destinazione.
Lo script segue volutamente una precedenza, le configurazioni vengono lette in sequenza, così da permettere di spostare un file (ad esempio il file "organizefiles.sh") PRIMA che una configurazione più generica lo sposti nella directory sbagliata, semplicemente inserendo una configurazione più specifica per quel tipo di file.