La shell, oltre ad essere una comoda interfaccia per il sistema, è un potente linguaggio di scripting. Le sequenze di comandi possono infatti essere salvate in un file e successivamente richiamate come comandi.
Il presente documento è un estratto delle innumerevoli funzioni della shell BASH, a titolo di introduzione alla programmazione ed all'uso. Per una trattazione completa si veda il manuale GNU. ogni osservazione o correzione è benvenuta (matteochiocciolamatteolucarellipuntonet)
Importante: lo shell-scripting, come molti altri linguaggi di script, è case-sensitive (cioè distingue tra maiuscole e minuscole) e NON IGNORA I BLANK, attenzione quindi a spazi, tab, return, ecc.
Un programma per shell è un semplice file di testo (eventualmente con estensione .sh). E' utilizzabile come un qualsiasi altro comando se reso eseguibile
chmod 755 ./mioscript ./mioscript
Altrimenti può venire invocato passandolo come argomento alla shell (in tal caso non deve necessariamente essere eseguibile).
sh mioscript
La prima riga deve contenere il percorso dell'interprete, quindi ogni script inizierà con la dicitura:
#!/bin/sh
Alcuni Unix richiedono anche uno spazio dopo il "!".
Il segno "$" fa sì che il valore seguente venga "espanso" (valutato). Per le variabili questo significa che al momento dell'assegnazione utilizzeremo il solo nome mentre al momento dell'utilizzo dovremo far precedere il nome dal segno di dollaro. La valutazione delle variabili avviene anche nelle stringhe tra virgolette. L'espansione ha molti altri effetti, che si apprenderanno gradualmente.
VALORE=3 echo $VALORE
Va notato che le variabili di shell non richiedono dichiarazione di tipo. Ad una stessa variabile è possibile assegnare successivamente valori di ogni tipo (numerico, stringa, ecc). E' quindi possibile realizzare array misti.
Nei nomi delle variabili sono ammessi tutti i caratteri alfanumerici e l'underscore. Il primo carattere deve essere alfabetico. Per convenzione si utilizzano normalmente caratteri maiuscoli.
Per evitare ambiguità la variabile può essere delimitata dalle parentesi graffe
VALORE=3 echo ${VALORE}alpha
NOTA: Non vanno lasciati spazi intorno al segno "=" di un assegnamento.
\ (backslash)
\$
è il cattere dollaro $
è il carattere speciale già visto).# (cancelletto)
; (puntoevirgola)
for F in *; do echo $F; done
| (pipe)
ls -l | grep .txt
> (maggiore)
ls -l > ls.txt
2>
2>&1
&>
kwrite &> /dev/null
< (minore)
&& (and logico)
|| (or logico)
& (ecommerciale)
` (apice inverso)
`comando`
equivale quindi a a $(comando)
I comandi possono essere raggruppati tramite le parentesi tonde () o
tramite le parentesi graffe {}.
I comandi raggruppati sono eseguiti come se fossero un singolo comando
(quindi, ad esempio possono essere redirezionati in blocco).
Il raggruppamento nelle parentesi tonde fa si che i comandi vengano
eseguiti da una nuova shell figlia (subshell).
Nota: le parentesi graffe si ottengono dalla tastiera italiana
premendo AltGr+7 e AltGr+0.
I parametri posizionali sono gli argomenti passati allo script o alla funzione al momento della chiamata. Quindi se lo script "ordina.sh" viene chiamato:
ordina.sh /home/user/documenti
All'interno dello script il primo parametro posizionale ($0) varrà "ordina.sh" il secondo ($1) varrà "home/user/documenti".
$n
: n-esimo parametro posizionale ($0,$1,..)
$*
: lista dei parametri posizionali in un unica stringa
$@
: lista dei parametri posizionali come array di stringhe
$#
: numero dei parametri posizionali
All'interno di uno script possono essere definite delle funzioni tramite una sintassi analoga al linguaggio C:
[ function ] name () { command-list; }
Gli argomenti della chiamata saranno i parametri posizionali all'interno della funzione. Non vanno quindi specificati nella definizione della funzione, e il loro numero può essere variabile. Le variabili all'interno della funzione hanno scope locale (cioè non sono definite all'esterno).
ES: tipica verifica di inzio script: se non sono presenti due parametri, oppure i due parametri non sono quelli previsti, stampa una breve nota di utilizzo ed esce.
print_usage() { echo "Questo script va chiamato con 2 parametri" echo "Che possono essere \"-c path\"" echo " oppure \"-l path\"" } [ $# != 2 ] && print_usage && exit 1 [ $1 != "-l" ] && [ $1 != "-c" ] && print_usage && exit 2
GROUPS
: elenco dei gruppi dell'utente corrente
HOME
: home dell'utente corrente
HOSTNAME,HOSTTYPE
: nome e tipo macchina
IFS
: lista dei caratteri separatori utilizzati
OLDPWD
: directory precedente
PATH
: percorsi di ricerca comandi
PPID
: pid del processo padre
PS1
: prompt
PWD
: directory corrente
RANDOM
: numero casuale (tra 0 e 32767)
UID
: id dell'utente corrente
$?
: valore di ritorno dell'ultimo processo eseguito$$
: PID (id del processo) della shell corrente
$!
: PID dell'ultimo processo lanciato in background
I comandi che seguono sono un estratto dei comandi built-in della shell.
Per ottenere maggiori informazioni utilizzare man bash
.
alias|unalias [name[=value] ...]
alias ls="ls -l"
assegna di default il flag -l al comando ls.
break
cd [dir|-]
continue
declare [-a] [name[=value]]
echo [-neE] [arg ...]
exec [-c] [-a name] [command [arguments]]
exit [n]
logout [n]
pwd
read [-a arr] [-d ch] [-n n] [-p str] [-t t] [-u fd] [name ...]
ask_user () { while [ 0 ]; do read -p "$1 (y/n)? " ANS [ $ANS == "y" ] && return 0 [ $ANS == "n" ] && return 1 done }
umask [mode]
source|. scriptfile
I comandi che seguono sono eseguibili indipendenti dalla shell.
Vengono generalmente considerati presenti in ogni sistema, e quindi
comunemente utilizzati all'interno degli script. Ognuno dei comandi
presenta molte opzioni, per approfondire si vedano le corrispondenti
pagine di manuale. I comandi illustrati risiedono generalmente in
/bin e /usr/bin. Nella maggioranza dei sistemi esiste una pagina di
manuale specifica per ogni comando visibile con man comando
.
awk
ls -l | awk '{print $6}'
stampa solo il sesto campo (la data) dell'output di lsecho $A | awk '{print tolower($0)}'
converte $A in caratteri minuscoli
cat
cat file1 file2 file3 > file123
(unisce i tre file in uno)
cdrecord/mkisofs/growisofs
chmod/chown/chgrp
cp
date
df [-h]
du [file o directory]
grep
gzip/bzip2
host
lp/lpr
lp doc.ps
stampa il file postscript comando|lp
invia alla stampante l'otput del comando
ln [-s] file destinazione
ls [-al]
mail/mailto
mkdir [-p] path
mv
ps
rm [-r]
seq [-s STR][-f FRM] first [incr] last
sleep n[smhd]
sort [file]
echo -e "alpha\ngamma\nbeta\nkappa\ndelta" | sort
sync
tar
tar xf archivio.tar
scompatta l'archivio nella dir corrente tar cjf archivio.tar.bz2 ./dir
crea l'archivio compresso con la directory dirtr
tr -d '\015' < file.txt > file.2.txt
elimina i CR dal contenuto del file (converte un testo da formato DOS a formato UNIX) tr --squeeze-repeats ' ' < file
elimina tutte le ripetizioni di spazi, lasciando solo il primo
wget UrlSorgente [path_destinazione]
bg [job]
fg [job]
jobs [-rs]
kill [job]
wait [job o pid]
Un test si può realizzare con una delle seguenti forme (gli spazi sono necessari!):
[ test ] test test
Alcuni dei test possibili sono:
espressioni su file (es:-a filename
):
-a|-e
: se esiste
-b
: se è un block device
-c
: se è un char device
-d
: se è una directory
-f
: se è un file normale
-h|-L
: se è un link simbolico
-p
: se è una pipe
-r
: se è leggibile
-s
: se non è vuoto (0 bytes)
-w
: se è scrivibile
-x
: se è eseguibile
-S
: se è una socket
-N
: se è stato modificato dopo l'ultima lettura
file1 -nt file2
):
-nt
: se file1 è più nuovo di file2 (modifica)
-ot
: se file1 è più vecchio di file2
-ef
: se file1 e file2 hanno lo stesso inode
(cioè sono hard link ad uno stesso contenuto)
-Z string
):
-z
: se è una stringa di lunghezza zero
-n
: se è una stringa di lunghezza non zero
str1 == str2
):
==
: se sono uguali
!=
: se sono diverse
[ ! -f nomefile ]
: vero se file NON è un file normale. test && comando
: se test è vero esegui comando test || comando
: se test non è vero esegui comando
[ $VAR ] && echo "VAR=$VAR"
Es: controlla il corretto numero di argomenti:
[ $# != 2 ] && echo "sintassi errata" && exit 1
Es: se non esiste l'eseguibile mysqld termina:
test -x /usr/sbin/mysqld || exit 0
if test then commands [elif test; then commands] [else commands] fi
case word in pattern [| pattern]...) command command ;; pattern [| pattern]...) command-list ;; . . esac
select name [in words ...] do commands done
for (( i=0 ; i<MAX ; i++ )) do commands done
for name [in words ...] do commands done
until test do commands done
while test do commands done
Es: se /bin/comando è eseguibile eseguilo, altrimenti stampa un errore
if [ -x /bin/comando ]; then /bin/comando else echo "ERRORE: non posso eseguire /bin/comando" fi
Es: stampa numeri crescenti all'infinito
COUNT=0 while [ 0 ]; do echo $COUNT (( ++COUNT )) done
Es: attesa di 5 sec con (semplice) progress-bar
for loop in 3 2 1 0 ; do sleep 1; echo -n "$loop"; done
Es: memorizza il nome dei file presenti nella directory config nell'array TPCONF
TPCOUNT = 0 declare -a TPCONF for CONFFILE in ./config/* do TPCONF[$TPCOUNT]=$CONFFILE (( TPCOUNT++ )) done
Es: countdown
COUNT=20 until [ $COUNT -lt 10 ]; do echo count: $COUNT let COUNT-=1 done
Es: stampa una stringa descrittiva dell'architettura di processore
case $( uname -m ) in i[3456]86 ) echo "architettura Intel o simili";; alpha ) echo "architettura Alpha";; arm ) echo "architettura Arm";; * ) echo "Altro";; esac
Es: Selezione di un file con menù
select SCELTA in ./* do [ -z $SCELTA ] || break done echo Hai scelto $SCELTA
Una delle funzionalità più potenti della shell è nella ricerca di file e directory. E' possibile utilizzare nei path alcuni caratteri speciali:
*
: qualsiasi stringa, anche nulla
?
: qualsiasi singolo carattere, non nullo
?(lista)
: nessuno o uno dei path in lista
*(lista)
: nessuno, uno o più dei path in lista
+(lista)
: uno o più dei path in lista
@(lista)
: solo uno dei path in lista
!(lista)
: tutti eccetto i path in lista
[xxx..]
: uno qualsiasi tra i caratteri specificati
ES: conteggia e memorizza in un array i file presenti nella directory ./conf/
declare -a TPCONF TPCOUNT=0 for CONFFILE in ./conf/* do TPCONF[$TPCOUNT]=$CONFFILE (( TPCOUNT++ )) done
Alcune delle espansioni illustrate hanno una sintassi particolarmente criptica. Permettono, con un po' di pratica, sostituzioni quasi impossibili con altri linguaggi. Per una migliore comprensione si vedano gli esempi. NOTA: Le espansioni sono quasi sempre annidate, quindi i parametri vengono a loro volta espansi.
{xxx..}
~
(tilde)${parameter:-word}
${parameter:=word}
${parameter:?word}
${parameter:+word}
${#parameter}
Es: echo a{d,c,b}e =output=> ade ace abe
Es: sostituisce gli spazi con underscore all'interno dei nomi dei file nella directory corrente (rinominandoli)
for FILE in * do NUOVONOME=${FILE// /_} mv "$FILE" $NUOVONOME done
Es: suddivide un path nei suoi componenti e li assegna ad un array: (l'espressione sembra complessa, ma è veramente potente)
PATH="/home/utente/doc/documento.pdf" declare -a COMP=( "\""${PATH//\//"\" \""}"\"" ) # ciclo di stampa dell'array ottenuto for i in `/usr/bin/seq 1 $((${#COMP[*]}-1))` do echo ${COMP[$i]} done
La Bash incorpora molte funzioni di parsing delle stringhe, anche se con una certa mancanza di omogeneità. Alcune delle seguenti funzioni sono utilizzate anche in altri contesti (la prima ad esempio, applicata ad un array ne restituisce il numero di elementi). Molte altre operazioni sono possibili tramite i comandi tr ed expr.
${#stringa}
${stringa%regexp}
${stringa##regexp}
${stringa%%regexp}
${stringa#regexp}
${string:offset:length}
${parameter:offset}
${string/sub1/sub2}
${string//sub1/sub2}
${string/#sub1/sub2}
${string/%sub1/sub2}
foo=/tmp/my.dir/filename.tar.gz len = ${#foo} # ritorna la lunghezza della stringa path = ${foo%/*} # ritorna /tmp/my.dir file = ${foo##*/} # ritorna filename.tar.gz base = ${file%%.*} # ritorna filename ext = ${file#*.} # ritorna tar.gz ${file/%.tar.gz/.tgz} # ritorna filename.tgz $(echo $file | tr '[A-Z]' '[a-z]') # ritorna FILENAME.TAR.GZ
NOTA IMPORTANTE: la bash esegue calcoli solo su numeri interi, quindi ad esempio 5/2=2. Per avere calcoli in virgola mobile è necessario utilizzare un calcolatore esterno (dc, bc, ecc).
Un'espressione aritmetica si può valutare utilizzando la seguente forma:
(( expr ))
Esistono inoltre i due comandi expr
e bc
, per il cui approfondimento si rimanda ai rispettivi manuali.
Le operazioni possibili sono:
x++ ++x
: post e pre incremento
x-- --x
: post e pre decremento
+,-,*,/
: somma,sottrazione,prodotto,divisione
**,%
: elevamento a potenza,resto
! ~
: negazione logica e bitwise
<< >>
: shift bitwise
<= >= < >
: confronto
== !=
: uguaglianza e disuguaglianza
&,|,^
: bitwise AND,OR,XOR
&&,||
: AND e OR logici
test?b:c
: valutazione condizionale
(se test allora vale b, altrimenti c)
Per parentesi annidate si utilizzano le parentesi semplici:
(( 5 * ( 2 - $NUM ) ))
il $
è necessario, come sempre, per le assegnazioni e per l'output:
SUM=$(( $A + $B )) echo $(( 5+3 ))
mentre non è necessario per incremento e decremento:
(( COUNTER++ ))
La doppia parentesi tonda va utilizzata anche per test di tipo numerico:
(( $A >= $B )) && echo "B non è più grande di A"
Anche se per i test di confronto funziona anche la parentesi quadra (che però, in realtà, effettua un confronto tra stringhe):
[ $A == $B ] && echo "A è uguale a B"
La bash permette l'uso di array monodimensionali, che possono essere anche misti (cioè contenere valori di tipo differente).
# definizione ARY=( uno due tre quattro ) # stampa il numero di elementi di un array echo "${#ARY[*]}" # stampa due elementi dopo il terzo echo "${ARY[@]:3:2}" #stampa il 2 elemento echo "${ARY[1]}" # stampa l'intero array echo "${ARY[*]}" # ciclo di stampa sugli elementi di un array for (( I=0 ; I<${#ARY[*]} ; I++ )) do echo "elemento[$I]: ${ARY[$I]}" done
;
.
echo "errore" >> /dev/stderr echo "output" >> /dev/stdoutQuando non si vorranno più vedere gli avvisi (ma solo gli errori) sarà sufficiente avviare lo script con:
script.sh 1>> /dev/null
getopt
):
while [ $# -gt 0 ] do case "$1" in -h | --help ) print_usage && exit 0;; -v | --version ) echo "$VERSION" && exit 0;; -t | --test ) TESTFLAG=1;; -c | --config ) CONFIGFILE=$2;; * ) break ;; # altri flag e azioni possibili esac shift done
OLDIFS=$IFS IFS=$'\x0A' FILEARRAY=( $(ls -dtr *) ) IFS=$OLDIFS
/bin/bash -x nomescript
#!/bin/bash -x
ARRNAME=$TYPE
eval NEWARRAY=( \${$ARRNAME[*]} )
while [ 0 ]; do echo "Insert new password for $USERTOMODIFY" stty -echo read -p "password: " PWD1 echo read -p "password verify: " PWD2 echo if [ $PWD1 == $PWD2 ]; then break; fi echo "Password doesn't match. Try again" done stty echo echo "$USERTOMODIFY:$PWD1" | chpasswd