Impariamo con degli esempi:
Bash ed ImageMagik, esempio di come rinominare foto utilizzando le proprietà dei file (data e ora di scatto);
Nome file tutto in maiuscolo;
Rimuovere una parte dal nome file indesiderata;
Succede spesso, usando la mia fotocamera digitale, che voglia rinominare una certa quantità di foto contenute in una cartella sfruttando i dati salvati nella foto.
Sfuttando il software ImageMagick capace di gestire gli Exif data e Bash si può creare un programma in poche righe che fa al caso nostro.
Procediamo per passi.
1.1 Creare un file bash da eseguire
Prima di tutto non è opportuno fare un comando lunghissimo tutto in una riga di codice. Con la shell di Linux è semplice creare un file (di testo) eseguibile.
a)
Creare il file ed editarlo:
gedit PhotoRename.sh &
NOTA:
Il simbolo & permette di eseguire il comando in background (nello specifico l'editor di testo rimarrà attivo e si potrà continuare a lavorare nel terminale)
NOTA:
L'estensione del file creato non è importante, di norma di utilizza l'estensione ".sh" per i file eseguibili da shell
b)
La prima riga del file deve essere la seguente:
#!/bin/bash
NOTA:
"#!" è lo shebang o sha-Bang e va inserito, per l'appunto, nella prima riga del file eseguibile. Sta ad indicare che l'interprete da utilizzare per eseguire il codice che segue è quello riportato, ovvero "/bin/bash" sta ad indicare l'interprete Bash. Equivale ad eseguire il file con la seguente riga di comando:
bash PhotoRename.sh
Esistono svariati altri interpreti oltre il bash, ad esemio: "/bin/sh" utilizza l'interprete shell Bourne di cui Bash ne è l'estensione (ma noi usiamo Bash!).
NOTA:
In seguito si utilizzerà il carattere "#" per inserire un commento nel file di testo, attenzione a lo shebang con un commento.
Renderlo eseguibile:
chmod +x PhotoRename.sh
c)
Per eseguirlo bisogna indicare il percorso del file:
./PhotoRename.sh
NOTA:
"./" è la directory corrente
Per non dover copia-incollare il file ed eseguirlo nella directory corrente si può sfruttare uno dei percorsi di path salvati nella variabile d'ambiente $PATH, per sapere quali directory sono fruibili (senza dover modificare la variabile di cui sopra) la si visualizzi usando il comando:
echo $PATH
io di norma copio i file nella directory seguente:
/usr/local/bin
NOTA:
Altra variabile d'ambiente che può tornare utile è $PWD che ritorna la directory corrente.
1.2 Ricaviamo dai file immagine la stringa formattata contenente data e ora
Dobbiamo capire come ricavare dal file immagine una stringa formattata in modo appropriato che diverrà il suffisso del nostro nuovo nome file.
a)
Usando il comando identify di ImageMagick ricaviamo data ed ora, segue un esempio di utilizzo:
era77@era77-PC:~/Immagini/Test$ identify -format "%[exif:DateTime]" DSC_0010.JPG
2015:12:07 15:02:18
b)
Ora vogliamo sostituire lo spazio tra data ed ora con il simbolo at ("@") e sostituire i due punti (":") con degli underscore ("_").
Il comando Bash che fa al caso nostro è tr (translate), utilizzarlo per sostituire caratteri è molto semplice, basta passargli come parametro il carattere da sostiuire e successivamente il suo sostituto:
era77@era77-PC:~/Immagini/Test$ identify -format "%[exif:DateTime]" DSC_0010.JPG| tr ':' '_'
2015_12_07 15_02_18
era77@era77-PC:~/Immagini/Test$ identify -format "%[exif:DateTime]" DSC_0010.JPG| tr ' ' '@' |tr ':' '_'
2015_12_07@15_02_18
NOTA:
Il simbolo "|" (barra verticale) sta ad identificare una pipeline, questo significa che il risultato del comando (output) viene passato come ingresso (input) al comando successivo. Alla luce di ciò è evidente il perchè il simbolo "|" in ambito Unix viene chiamato comunemente pipe.
NOTA:
C'è un metodo alternativo per raggiungere lo stesso risultato che mi permette di parlare di un altro comando Bash. Vedere la riga di comando che segue.
era77@era77-PC:~/Immagini/Test$ identify -format "%[exif:DateTime]" DSC_0010.JPG | awk '{print $1"@"$2}'|tr ':' '_'
2015_12_07@15_02_18
Avrete notato l'uso del comando awk che serve per ricercare e sostituire testo, avrete altresì notato che la stringa ritornata è esattamente la stessa. La parte difficoltosa da comprendere in prima battuta è l'opzione:
'{print $1"@"$2}'
che sta ad indicare che l'output del comanda sarò formato dalla prima e dalla seconda sottostringa della stringa di input con in mezzo il carattere "@". Il comando in questione è complesso ed ha moltissime opzioni per maggiori informazioni a riguardo vi consiglio di far riferimento ad una delle tante guide che potete trovare in rete ad esempio qui.
c)
Per comodità è conveniente salvare la stringa ricavata in una variabile. Questa operazione è semplice:
era77@era77-PC:~/Immagini/Test$ DateTime="$( identify -format "%[exif:DateTime]" DSC_0010.JPG |tr ' ' '@' |tr ':' '_')"
era77@era77-PC:~/Immagini/Test$ echo $DateTime
2015_12_07@15_02_18
Nella prima riga di comando (che come noterete non dà nessun output) si assegna ad una variabile di nome arbitrario (è stato scelto nell'esempio DateTime) la stringa ricavata al punto precedente, nota importante, visto e considerato che i comandi ritornano una stringa, per effettuarne l'assegnazione è stato usato il simbolo "=" (come ci si potrebbe aspettare) seguito da " "$( ...)" ".
NOTA:
Si noti che nella seconda riga di comando, per visualizzare il contenuto di una variabile, si è utilizzato il comando echo seguito dal nome della variabile preceduta dal simbolo "$" (il carattere speciale $ avvisa l'interprete dei comandi che quel che segue è una variabile).
d)
A questo punto abbiamo il prefisso del nome file desiderato in una variabile. Ma il file non è unico, anzi, a questo punto quindi dovremmo reiterare il procedimento per tutti i file contenuti nella directory. Ci viene in aiuto il costrutto for. La sintassi è la seguente:
for ImageFile in *.JPG
do commands
done
La variabile del ciclo ImageFile (nome arbitrario) indicizzerà uno per uno tutti i file con estensione JPG contenuti nella directory corrente.
NOTA:
"*.JPG" significa "tutti i file jpg nella directory" , "*" è il noto carattere speciale che sta ad indicare per l'appunto "tutti".
NOTA:
Se si volessero più file con estensioni diverse si possono elencare separate da virgola fra parentesi graffe e con ";" prima del do:
*.{JPG,jpg,JPEG,jpeg,PNG,png};
NOTA:
Per utilizzare nel seguito la variabile indice si dovrà utilizzare il carattere speciale "$", prima del nome variabile ($ImageFile)
NOTA:
Il carattere ";" corrisponde al fine linea / accapo / comando (stile C). Quindi o si indenta come sopra (più leggibile) oppure si può scrivere tutto in una riga sfruttando il carattere ";".
1.3 Nome file univoco
Visto e considerato che ci possono essere più immagini scattate nello stesso secondo e bene fornire un nome immagine con un indice univoco.
Per fare questo creaiamo una variabile che fungerà anche da contatore di numero di foto rinominate.
Per dichiararla ed inizializzarla basta scrivere quanto segue:
NumPhoto=0;
NOTA:
Ricordatevi di non frapporre spazi prima o dopo il simbolo di uguale ("=") e di mettere il punto e virgola (";") alla fine della riga di codice.
Ad ogni iterazione del ciclo for la aggiorneremo incrementandola di un'unità scrivendo questa riga di codice:
NumPhoto=$((NumPhoto+1));
NOTA:
Si noti l'uso del carattere dollaro ("$") seguito dalle parentesi più esterne per assegnare il valore alla variabile nonchè l'uso delle paentesi più interne per effettuare il calcolo.
Per generare la parte finale del nome file a questo punto si utilizarà la riga di codice:
NameNum="$(printf "Img_%04d" $NumPhoto)"
Si assegnerà dunque ad una variabile (NameNum) di tipo striga (si noti a tal proposito l'uso per l'asegnazione di "$( .... )" ) l'output del comando printf. Il comando printf deriva direttamente dal C e ne mantiene la stessa sintassi, nell'esempio si crea una stringa con prefisso Img_ seguito da una variabile di tipo intero di lunghezza fissa di 4 cifre su cui viene scritto il valore della variabile NumPhoto.
1.4 Rinominiamo i file
Unendo le due stringhe di cui ai punti 1.2 e 1.3, separate da un trattino, otteniamo il nome file finale:
NewFieName="$(printf "%s-%s.JPG" $DateTime $NameNum)"
Per rinominare i file si usa il comando mv , da notare che tale comando è usato normalmente per effettuare lo spostamento (move) di un file, nel caso che la directory di destinazione sia la stessa della sorgente rinomina semplicemente il file:
mv "$ImageFile" "$NewFieName"
Riportiamo come output a monitor, per tutti i file che andremo a scandire nella directory durante il processo, il nome originario e quello finale nonché, alla fine dell'elaborazione, la stampa a video del numero di file rinominati:
for ImageFile in *.JPG; do
...
echo $ImageFile " -> " $NewFieName
...
done
echo "Number of photo renamed: "$NumPhoto
Il listato del file eseguibile alla fine sarà il seguente:
#!/bin/bash
#
# Script used for rename Photo in a directory
# New name depend to Exif data (date and time)
#
# Author: Ruffo Alberto Emilio
# Version: 0.0_1
NumPhoto=0;
for ImageFile in *.{JPG,jpg,JPEG,jpeg,PNG,png}; do
DateTime="$( identify -format "%[exif:DateTime]" $ImageFile|tr ' ' '@'|tr ':' '_')"
NameNum="$(printf "Img_%04d" $NumPhoto)"
NewFieName="$(printf "%s-%s.JPG" $DateTime $NameNum)"
echo $ImageFile " -> " $NewFieName
mv "$ImageFile" "$NewFieName"
NumPhoto=$((NumPhoto+1));
done
echo "Number of photo renamed: "$NumPhoto
Provando ad eseguire il file si avrà un risultato simile a quello sotto riportato:
era77@era77-PC:~/Immagini/Test$ ./PhotoRename.sh
DSC_0001.JPG -> 2015_12_07@15_01_59-Img_0000.JPG
DSC_0002.JPG -> 2015_12_07@15_02_13-Img_0001.JPG
DSC_0010.JPG -> 2015_12_07@15_01_58-Img_0002.JPG
DSC_004.JPG -> 2015_12_07@15_02_18-Img_0003.JPG
DSC_007.JPG -> 2015_12_07@15_02_16-Img_0004.JPG
Number of photo renamed: 5
Ora copiando il file in una delle directory di path possiamo agevolmente rinominare tutti i file immagine in ogni directory del nostro PC con un semplicissimo comando da noi creato. POTENZA DEL PINGUINO!
Come noto l'ambiente Linux è "Case Sensitive" ovvero lettere maiuscole e minuscole non sono per niente la stessa cosa, questo vale ovviamente anche e sopratutto per i nomi dei file.
Potrebbe essere comodo un bello script in bash che trasformi il nome dei file contenuti in una directory tutti in maiuscolo. Si riprenda il punto 1.1 di cui sopra per cominciare (io ho chiamato il file "UpperCase.sh"), poi basterà scrivere il seguente codice.
#!/bin/bash
#
# Script used for rename file with upper case characters
#
# Author: Ruffo Alberto Emilio
# Version: 0.0_1
for File in *; do
NewName="$(echo "$File"| tr [:lower:] [:upper:])"
echo $File "->" $NewName
mv "$File" "$NewName"
done
Si usano i comandi "move" (mv) e "translate" (tr) già usati nel punto precedente.
Come già detto questo script rinomina tutti file contenuti nella directory convertendo le lettere minuscole nelle rispettive maiuscole, il viceversa mi pare banale da attuare.
Riporto un esempio di ciò che accade se si lancia il file:
era77@ERA77PC:~/Documenti$ ls
AaBbCc.TxT FFFggg.tXt
era77@ERA77PC:~/Documenti$ UpperCase.sh
AaBbCc.TxT -> AABBCC.TXT
FFFggg.tXt -> FFFGGG.TXT
era77@ERA77PC:~/Documenti$ ls
AABBCC.TXT FFFGGG.TXT
Può tornare utile avendo una serie di file rimuovere una parte costante del nome file (magari una lunga serie di file con nome auto-generato).
Si abbiano ad esempio i seguenti file in una data directory.
ruffo@ubuntu:~/TestFileRename$ ls -1
Test_11_BlahBlahBlah.txt
Test_12_BlahBlahBlah.txt
Test_13_BlahBlahBlah.txt
Test_1_BlahBlahBlah.txt
Test_21_BlahBlahBlah.txt
Test_2_BlahBlahBlah.txt
Test_33_BlahBlahBlah.txt
Test_3_BlahBlahBlah.txt
ruffo@ubuntu:~/TestFileRename$
Un suffisso uguale per tutti i file è decisamente poco utile ed è mia intenzione rimuoverlo grazie ad uno script!
Generiamo il solito file bash eseguibile ed editiamolo:
ruffo@ubuntu:~/TestFileRename$ touch FileNameCut.sh
ruffo@ubuntu:~/TestFileRename$ chmod +x FileNameCut.sh
ruffo@ubuntu:~/TestFileRename$ gedit FileNameCut.sh &
Il codice dello script è riportato di seguito, viene richiesta durante l'esecuzione l'estensione del file ed il suffisso da rimuovere.
#!/bin/bash
#
# Script used for rename file with constant suffix (to be removed)
#
# Author: Ruffo Alberto Emilio
# Version: 0.0_1
echo -n "File extension : "
read -r ext
count=$(ls -1q *.$ext 2>/dev/null | wc -l) #do not print on standard error in case files with given extension are not present
if [ $count -eq 0 ]; #Check there at least one file with given extension
then
echo "No files found"
exit 1
fi
echo "Found " $count " files"
echo
echo -n "Suffix to be removed : "
read -r suffix
suffix="$suffix.$ext" #Add file extension
for file in *.$ext; do #For each file with given extension
suffix_removed=${file/%$suffix} #remove suffix
new_file="$suffix_removed.$ext" #Add file extension
echo $file " -> " $new_file
mv $file $new_file #rename file
done
echo
Una volta eseguito si ha il seguente output e la rimozione del suffisso dai file.
ruffo@ubuntu:~/TestFileRename$ ./FileNameCut.sh
File extension : txt
Found 8 files
Suffix to be removed : _BlahBlahBlah
Test_11_BlahBlahBlah.txt -> Test_11.txt
Test_12_BlahBlahBlah.txt -> Test_12.txt
Test_13_BlahBlahBlah.txt -> Test_13.txt
Test_1_BlahBlahBlah.txt -> Test_1.txt
Test_21_BlahBlahBlah.txt -> Test_21.txt
Test_2_BlahBlahBlah.txt -> Test_2.txt
Test_33_BlahBlahBlah.txt -> Test_33.txt
Test_3_BlahBlahBlah.txt -> Test_3.txt
ruffo@ubuntu:~/TestFileRename$
Partendo dall'esempio al paragrafo precedente .
Si abbiano ad esempio i seguenti file in una data directory.
ruffo@ubuntu:~/TestFileRename$ ls -1
Test_11.txt
Test_12.txt
Test_13.txt
Test_1.txt
Test_21.txt
Test_2.txt
Test_333.txt
Test_3.txt
ruffo@ubuntu:~/TestFileRename$
Altra cosa fastidiosissima per il sottoscritto è che i file non sono nell'ordine "naturale" (quantomeno per l'uomo) in quanto il valore numerico del nome del file non ha numero di caratteri uguale per tutti i file (ci sono insomma un pò di zeri da aggiungere ai posti giusti). Anche qui, via di script Bash!
ruffo@ubuntu:~/TestFileRename$ touch FileNumbered.sh
ruffo@ubuntu:~/TestFileRename$ chmod +x FileNumbered.sh
ruffo@ubuntu:~/TestFileRename$ gedit FileNumbered.sh &
Il codice da scrivere è il seguente. Non è a prova di bomba, ma per i file che uso di norma va più che bene, prima cerco quante cifre ha il numero più lungo, poi le cancello e le rimpiazzo con lo stesso numero di prima eventualmente con qualche "0" in più all'inizio.
#!/bin/bash
#
# Script used for renumber file with numbers with constant number of digits
#
# Author: Ruffo Alberto Emilio
# Version: 0.0_1
echo -n "File extension : "
read -r ext
count=$(ls -1q *.$ext 2>/dev/null | wc -l) #do not print on standard error in case files with given extension are not present
if [ $count -eq 0 ]; #Check there at least one file with given extension
then
echo "No files found"
exit 1
fi
echo "Found " $count " files"
suffix=".$ext" #File extension
#Find maximum number of digits for number in file names
max_num_digit=0;
for file in *.$ext; do #For each file with given extension
ext_removed=${file/%$suffix} #remove suffix
num_str=$(echo $ext_removed | tr -dc '0-9') #get number from file
if [ -n "$num_str" ] #String not empty
then
len_num_str=${#num_str}
if [ $max_num_digit -lt $len_num_str ];
then
max_num_digit=$len_num_str
fi
fi
done
echo "Max number of digits : " $max_num_digit
#Add as many "0" as need
for file in *.$ext; do #For each file with given extension
ext_removed=${file/%$suffix} #remove suffix
num_str=$(echo $ext_removed | tr -dc '0-9') #get number from file
if [ -n "$num_str" ] #String not empty
then
len_num_str=${#num_str} #Number of digits of this numebr
#Necessary to add "0"?
if [ $len_num_str -lt $max_num_digit ];
then
while [ $len_num_str -lt $max_num_digit ]
do
num_str="0$num_str"
len_num_str=$(( $len_num_str + 1 ))
done
fi
file_new=$(echo $ext_removed | tr -d '0-9') #remove numbers
file_new="$file_new$num_str.$ext"
echo $file "->" $file_new
if [ "$file" != "$file_new" ];
then
mv $file $file_new
fi
fi
done
echo
Segue l'esempio dell'ouput di esecuzione.
ruffo@ubuntu:~/TestFileRename$ ./FileNumbered.sh
File extension : txt
Found 8 files
Max number of digits : 3
Test_11.txt -> Test_011.txt
Test_12.txt -> Test_012.txt
Test_13.txt -> Test_013.txt
Test_1.txt -> Test_001.txt
Test_21.txt -> Test_021.txt
Test_2.txt -> Test_002.txt
Test_333.txt -> Test_333.txt
Test_3.txt -> Test_003.txt
ruffo@ubuntu:~/TestFileRename$