Newsgroup: it.comp.os.linux.iniziare
Titolo: MOD : (DISAMINA) BASH : Test di un pattern all'interno di una stringa

(mi scuso per alcuni word-wrap nei commenti, ma non ho voglia di rimettere
mano alla loro formattazione)
Riflessioni sul modo di funzionare, in Bash, di:
- comando "test" (abbreviabile con /usr/bin/[ )
- "builtin 'test'" (abbreviabile con [ )
- builtin "[["
===========================================================================
# Giusto per scaldare i muscoli (del cervello), partiamo con una semplice 
# operazione di confronto fatta con i tre metodi.
# Qui il confronto avviene fra il valore ASCII di "3" e "4", mentre per
# confrontare valori numerici vanno usate le espressioni aritmetiche:
# (( ... )).
# Comando test
/usr/bin/test 3 -lt 4; echo $?
    0
/usr/bin/test 4 -lt 3; echo $?
    1
# Builtin test ("[")
[ 3 -lt 4 ]; echo $?
    0
[ 4 -lt 3 ]; echo $?
    1
# Builtin "[["
[[ 3 -lt 4 ]]; echo $?
    0
[[ 4 -lt 3 ]]; echo $?
    1
# Niente di speciale, no? Ok, allora veniamo al sodo, cioè vediamo come i 
# comandi di test rispondono alla domanda "esiste il pattern all'interno
della 
# stringa?"
# Ma prima: un piccolo check fatto con "grep" : 
VAR='AAA BBB CCC'
echo "$VAR" | grep B; echo $?
    AAA BBB CCC
    0
echo "$VAR" | grep -q B; echo $?
    0
echo "$VAR" | grep D; echo $?
    1
echo "$VAR" | grep -q D; echo $?
    1
# E fin qui è tutto chiaro: se 'sta roba fosse stata inserita dentro a 
# un costrutto "if", la condizione, vera o falsa, sarebbe consistita 
# nell'exit status del comando "grep":
if echo "$VAR" | grep -q B; then echo OK; else echo NON OK; fi
    OK
if echo "$VAR" | grep -q D; then echo OK; else echo NON OK; fi
    NON OK
# Vediamo ora il funzionamento di "test" inteso come buitin:
[ `echo "$VAR" | grep B` ]; echo $?
    bash: [: BBB: binary operator expected
    2
# Bestia che dolore !!!
# Che cos'è successo ?!? Vediamo.
# Che cosa gli ho chiesto? Gli ho chiesto: controlla se è vera l'espressione
che
# consiste nel risultato del seguente comando (sostituzione di comando
interna
# alle due parentesi quadre) e quindi dimmi il tuo exit code (echo $?).
# Che cosa otteniamo allora come espressione da valutare? Otteniamo "AAA BBB
CCC", 
# cioè come se avessimo scritto: [ AAA BBB CCC ]. Ma questo produce un
errore 
# di sintassi: la shell fa la sostituzione di comando ottenendo
# AAA BBB CCC , risultato sul quale fa il parsing e che viene quindi
interpretato
# come se si trattasse di due argomenti uniti da un operatore, da passare a
test 
# perché valuti lui l'espressione; ma test a questo punto rileva che BBB non
è 
# un operatore logico e ci sputa in un occhio con un messaggio di errore "2" 
# (il codice di errore di sintassi).
# Tuttavia, se si usano i doppi apici, allora si protegge la
# sostituzione di comando dal parsing che la suddividerebbe in singoli
argomenti;
# l'argomento diventa così uno solo (come se avessimo scritto [ "AAA BBB
CCC" ] )
# e, di conseguenza test non restituisce alcun errore perché, per default,
fa un
# test di stringa non vuota ("[ -n ... ]" , ovvero "test -n").
# Restituisce un exit code "0" se l'argomento è una stringa non vuota e un
# exit code "1" se l'argomento è una stringa vuota. In altre parole, se
# l'argomento... non c'è, il test restituisce di default 1.
# Ecco gli esempi:
[ "`echo "$VAR" | grep B`" ]; echo $? # il risultato della sostituzione di
    0                                 # comando è protetto, mediante i doppi
apici, 
                                      # dal successivo parsing che ne fa
                                      # sempre la shell
[ "`echo "$VAR" | grep -q B`" ]; echo $? # la shell fa subito la
sostituzione di
    1                                    # comando e la sintassi si salva
con i 
                                         # doppi apici che fanno sì che un
argomento
                                         # esista; tuttavia, siccome esso è
nullo, 
                                         # il test restituisce di default 1.
        # (((
        # perché ho detto "di default"? Perché "-n" è lo switch implicito.
        # Se con lo switch "-z" gli chiedo, invece, di controllare se
l'argomento 
        # è nullo mi restitusce 0 (cioè mi risponde che la condizione da
controllare 
        # è vera, ossia che l'argomento è inesistente):
 
        [ -z "`echo "$VAR" | grep -q B`" ]; echo $?
            0
 
        # )))
# Ripetiamo il comando di prima senza usare gli apici:
[ `echo "$VAR" | grep -q B` ]; echo $? # ovviamente, in questo caso
particolare
    1                                  # (comando che non restituisce alcun
output 
                                       # da sottoporre a valutazione) il
test
                                       # va a buon fine anche senza le
virgolette messe 
                                       # a protezione dal parsing della
shell,
                                       # perché il risultato della
sostituzione di 
                                       # comando è nullo (perché grep è in
                                       # 'quiet mode'), cioè è un argomento
nullo 
                                       # e quindi non c'è alcuna stringa da
                                       # proteggere da parsing: il test
restituisce 1
# Infatti il comando appena dato equivale al seguente test...
[ "" ]; echo $? # sintassi corretta, ma argomento nullo
    1
# ... ovvero...
[ -n "" ]; echo $?
    1
        # (((
        # "-n" è appunto lo switch di default di test
        # )))
# E adesso un piccolo rompicapo:
[ -n `echo "$VAR" | grep -q B` ]; echo $?
    0
# Come mai qui l'exit code è 0 ? Avevamo detto che essendo l'argomento nullo
i
# doppi apici non servono a niente perché al loro interno non c'è nulla... e
# allora?
#
# Allora, il fatto è che il test restituisce 0 (cioè ci dice che un
argomento
# esiste, ossia non è vuoto) perché in questo caso un argomento
effettivamente 
# c'è ed è... "-n": siccome il risultato del comando "grep -q" produce un
argomento
# vuoto, quello che sarebbe stato uno switch diviene esso stesso l'argomento
da
# valutare. Quindi, il test consiste nel controllare se la stringa "-n" non
sia
# vuota (come se facessimo "[ -n "-n" ]", dunque il risultato è "0".
#
# E per quest'altro switch ( "-z": controllo se la stringa è vuota), vale lo
# stesso discorso: test finisce per valutare che "-z" è una stringa non
vuota,
# come se facessimo "[ -n "-z" ]":
[ -z `echo "$VAR" | grep -q B` ]; echo $?
    0
# Ma chi non avesse riflettuto abbastanza sulle espansioni e sulla sintassi 
# (come era il mio caso, prima di riscrivere questo documento, anche se
neppure 
# ora, in tutta franchezza, posso dire di essere sicuro di quel che
dico :-) ) 
# avrebbe potuto pensare che il test con "-z" funziona correttamente, perché 
# il comando, essendo in "quiet mode" ("switch "-q"), non ha generato alcun 
# output e che di conseguenza lo switch che controlla se l'argomento è vuoto 
# ci risponde che esso è effettivamente vuoto (id est: non esiste).
# Niente di più sbagliato !!! Come abbiamo visto sopra, questa è
un'inferenza 
# logica errata, perché la controprova dà anch'essa risultato "0" (e qui 
# infatti io mi incartavo).
# Questo può essere un esempio di come affidandosi a dei "trial & error", 
# cioè ragionando per induzione, sia spesso arduo arrivare
# a capire la regola seguita da un comportamento: il comportamento di "test"
# dipende da quello della shell Bash, la quale fa prima le espansioni che
deve
# fare, ed entrambi i comportamenti sono condizionati dalla sintassi (uso 
# delle virgolette, uso degli escape, dei backtick, ecc.).
#
# Sempre rimanendo a questo caso specifico, SI NOTI CHE "[[" si comporta
invece
# diversamente da "[" :
[[ -n ]]; echo $?
    bash: unexpected argument `]]' to conditional unary operator
    bash: syntax error near `;'
# ... cioè: non essendo "-n" fra apici, dato che ciò che segue le doppie 
# parentesi quadre è una stringa che inizia con un "less",
# "[[" la considera come switch (cioè come una richiesta di verifica che la
stringa
# successiva non sia vuota) e pensa che le doppie parentesi quadre di
chiusura
# siano appunto l'argomento su cui fare la verifica di stringa non vuota.
Ma, a questo
# punto si imbatte in un errore di sintassi brutto quanto il diavolo,
# in quanto... non trova più le parentesi di chiusura.
#
# Ma... se le doppie parentesi quadre di chiusura fanno parte del comando,
come
# potrei procedere se volessi fare un test riguardante una stringa composta
da due
# parentesi? Semplice, dovrei usare l'escape:
[[ -n \]\] ]]; echo $?
    0
# Mentre se volessi effettivamente fare un test di "-n" , cioè far sì
che "-n"
# fosse interpretato come stringa da valutare, dovrei evitare che "[[" lo
# considerasse come uno switch. Potrei allora scrivere:
[[ \-n ]]; echo $?
    0
# ... oppure, usando i doppi apici, potrei fare così:
[[ "-n" ]]; echo $?
    0
        # (((
        # Sono forme del test di default, che equivalgono quindi a:
        [[ -n \-n ]]; echo $?
            0
        # e a...
        [[ -n "-n" ]]; echo $?
            0
        # )))
# Passiamo al comando "/usr/bin/test", che si comporta esattamente 
# come il "test" builtin (cioè come "[" ) e lo fa per gli stessi motivi.
/usr/bin/test `echo "$VAR" | grep B`; echo $?
    /usr/bin/test: BBB: binary operator expected
    2
/usr/bin/test `echo "$VAR" | grep D`; echo $? # l'argomento è nullo
perché "D"
    1                                         # non è presente nella stringa 
                                              # memorizzata in $VAR, quindi 
                                              # l'exit code è 1
/usr/bin/test `echo "$VAR" | grep -q B`; echo $? # l'argomento è nullo
perché
    1                                            # grep è in "quiet mode", 
                                                 # quindi l'exit code è 1
# Passiamo a "[["
        # (((
        # Ricordo che il contenuto di VAR è sempre la stringa "AAA BBB CCC"
        # )))
[[ `echo "$VAR" | grep B` ]]; echo $?
    0
# Oh, bene! Con "[[" non servono le virgolette perché "[[" sottrae
l'argomento (cioè
# il risultato del comando grep, ovvero "AAA BBB CCC") al parsing che ne
farebbe
# la shell: appena la sostituzione di comando sputa fuori un output, "[[" se
ne
# appropria. E siccome in questo caso esiste un output del comando "grep" ,
il test
# restituisce 0. Con "[[" non occorre proteggere gli argomenti dal parsing
che ne
# farebbe la shell: nel nostro caso "[[" prende il risultato della
sostituzione di
# comando come singolo argomento, anche se contiene spazi.
        # (((
        # però, attenzione... proviamo a controllare se "AAA BBB CCC" sia
        # uguale a "AAA BBB CCC" :
        [[ `echo "$VAR" | grep B` = AAA BBB CCC ]]; echo $?
            bash: syntax error in conditional expression
            bash: syntax error near `BBB'
        # ... Acc! Ha ragione: lui ha fatto il possibile, in assenza degli
apici
        # come può sapere quali siano i termini del confronto?
        # E allora, diciamoglielo (con gli apici, appunto):
        [[ "`echo "$VAR" | grep B`" = "AAA BBB CCC" ]]; echo $?
            0
 
        # Nel primo caso, assume che la sostituzione di comando generi
un'unica
        # stringa anche se essa contiene spazi, ma poi la confronta con AAA
e 
        # quindi non sa più che fare con BBB e CCC.
        # E infatti se si confrontano due espansioni di comando,
        # il test non produce messaggi di errori sintattici:
        [[ `echo "$VAR" | grep B` = `echo AAA BBB CCC` ]]; echo $?
            0
 
        # ... cioè gli è stato chiaro che voglio confrontare la prima
        # sostituzione di comando con la seconda, considerando i due 
        # output come unici argomenti da confrontare (e siccome 
        # "AAA BBB CCC" è effettivamente uguale a "AAA BBB CCC" ci 
        # restituisce l'exit code 0.
        # Infatti, come prova "a contrario" vediamo se capisce che "AAA BBB
CCC"
        # non contiene "DDD":
 
        [[ `echo "$VAR" | grep B` = `echo DDD` ]]; echo $?
            1
 
        # Ok, lo capisce.
 
        )))
[[ `echo "$VAR" | grep D` ]]; echo $?
    1
# Qui restituisce 1 anche se non ci sono le virgolette. Perché? Ma perché
non
# c'è alcun output del comando grep da valutare !
# Infatti (come fanno anche "/usr/bin/test" e "[" ), quando la stringa è
nulla,
# "[[" restituisce 1.
# La sostituzione di comando ha prodotto un argomento nullo, esattamente
come
# sarebbe stato usando grep in "quiet mode":
[[ `echo "$VAR" | grep -q B` ]]; echo $?
    1
[[ `echo "$VAR" | grep -q D` ]]; echo $?
    1
# (uffa) Finito? Manco per sogno ! :-P
#
# Torniamo alle espressioni di cui all'inizio (quelle usate per togliere il
# bloccasterzo al cervello). Per espressioni aritmetiche bisogna usare le 
# doppie parentesi tonde, ma al nostro scopo va bene anche confrontare due 
# stringhe (ovvero i loro valori ASCII).
#
# Bon. vediamo il comportamento del builtin test ("["):
[ 3 -lt 4 ]; echo $?
    0
[ 4 -lt 3 ]; echo $?
    1
... e fin qui niente di nuovo.
Ma... con i simboli di "minore" e "maggiore" cominciano i problemi:
[ 3 < 4 ]; echo $?
    bash: 4: No such file or directory
    1
[ 4 < 3 ]; echo $?
    bash: 3: No such file or directory
    1
# Infatti "[", a differenza di "[[", non accetta l'operatore "<" 
# (come neppure accetta altri simbili) al suo interno.
# Perché? Perché "[" dipende "in toto" dalla shell, cioè perché opera su
stringhe
# che sono il risultato di interpretazioni che sulle stesse stringhe prima
di
# tutto fa la shell. "[[" invece è costruito in modo che faccia
# da solo il parsing degli argomenti, sottraendoli dall'intervento della
shell.
# Ne consegue che, usando "[", siccome il simbolo "<" è per la shell una
# redirezione del contenuto di un file, è come se, nel primo caso, avessimo 
# detto alla shell di aprire il file 4 e di mandarne il contenuto allo
standard 
# input. Siccome il file 4 non esiste, la shell ci risponde 
# "No such file or directory". E poi? E poi l'espansione si ferma lì: la
shell 
# non esamina se 3 sia o meno un comando, in quanto l'errore termina le 
# operazioni da svolgere. A questo punto, una volta che la shell se ne è
uscita 
# con un messaggio di errore, l'argomento che resta a "[" da valutare è un
bel... 
# niente. E come già sappiamo sottoporre a test un niente comporta un exit
code 
# pari a 1.
#
# Per svolgere il test con "[" (se usassi "[[" sappiamo già che sarebbe
tutto
# più semplice) devo quindi proteggere l'argomento dall'intervento della
shell.
# Posso farlo con un escape prima del simbolo di "minore":
[ 3 \< 4 ]; echo $?
    0
[ 4 \< 3 ]; echo $?
    1
# ... oppure posso farlo anche facendo sì che l'espressione che voglio
valutare
# sia l'output di un comando (la sostituzione di comando fa sì che l'output
# divenga l'espressione da valutare):
[ `echo '3 < 4'` ]; echo $?
    0
[ `echo '4 < 3'` ]; echo $?
    1
# Nella precedente versione di questo documento, dove miagolavo :-) ancora
nel buio,
# mi chiedevo perché il comando "[" pareva accettare il simbolo di "minore"
solo a
# seguito di una sostituzione di comando; la risposta è quindi che la shell
fa la
# sostituzione di comando, fa il parsing anche del risultato e passa il
comando a
# "test" il quale, a questo punto, ottiene come espressione da valutare due
# argomenti da confrontare tramite il simbolo di "minore" e quindi il test 
# funzionerà correttamente.
# This is The End ?
[ This is The End ]; echo $?
    bash: [: too many arguments
    2
# ;-)
# Notare che con 3 blocchi, il messaggio di errore è diverso:
[ This is the ]; echo $?
    bash: [: is: binary operator expected
    2
# Perché prima troppi argomenti e ora si aspetta un operatore logico? 
# Facile: perché quando i blocchi che compongono l'espressione sono 3, test
assume 
# che quello in mezzo sia un operatore... ma "is" non è un operatore logico 
# (è solo un operatore di un altra grammatica)
[ "This is The End" ]; echo $?
    0
# :-)
[ `echo This is The End` ]; echo $?
    bash: [: too many arguments
    2
# ;-)
[[ This is The End ]]; echo $?
    bash: conditional binary operator expected
    bash: syntax error near `is'
# ;-)
# Notare che, con 3 o con 4 blocchi, il messaggio di errore è il medesimo:
[[ This is The ]]; echo $?
    bash: conditional binary operator expected
    bash: syntax error near `is'
# ...è un'altra "piccola" differenza comportamentale rispetto a "["
[[ "This is The End" ]]; echo $?
    0
# :-)
[[ `echo This is (really) The End` ]]; echo $?
    bash: command substitution: line 1: syntax error near unexpected token
`('
    bash: command substitution: line 1: `echo This is (really) The End'
    1
# >:-# ma vaffan'...
[[ `echo This is \(really\) The End` ]]; echo $?
    0
# :-)
==============================================================================

 # test funzionerà correttamente.
La risposta e' nell'ordine in cui la shell esegue le varie operazioni. Le
redirezioni avvengono prima della sostituzione di comando, ed e' per questo
che usare un comando il cui output contiene < o > funziona. Prova a giocare
un po' con eval e vedrai che le cose cambiano.
 
$ [ $(echo '3 < 4') ]; echo $?
0
$ [ $(eval echo '3 < 4') ]; echo $?
-bash: 4: No such file or directory
1