Scopo | Analisi lessicale delle sintassi tipo shell. |
Versione Python | 1.5.2, con aggiunte nelle successive versioni. |
A partire dal 1 gennaio 2021 le versioni 2.x di Python non sono piu' supportate. Ti invito a consultare la corrispondente versione 3.x dell'articolo per il modulo shlex
Il modulo shlex implementa una classe per analizzare semplici sintassi tipo shell. Può essere usato per scrivere un proprio linguaggio di programmazione domain specific , o per analizzare stringhe racchiuse tra virgolette (un compito più complesso di quello che può sembrare a prima vista).
Un problema comune quando si lavora con un testo in input è quello di identificare una sequenza di parole virgolettate come singola entità. Dividere la stringa in base alle virgolette non sempre fornisce il risultato sperato, specialmente se si sono livelli di virgolette annidati. Si prenda ad esempio il testo seguente:
Questa stringa ha delle "virgolette" e degli 'apici singoli' incorporati in essa, ed anche "un esempio 'annidato'".
Un approccio ingenuo potrebbe essere quello di tentare di costruire una espressione regolare per trovare le parti di testo al di fuori delle virgolette per separarle da quelle all'interno delle medesime, o viceversa. Detto approccio sarebbe inutilmente complesso e prono ad errori risultanti da casi limite come gli apostrofi od anche errori di sintassi. Una soluzione migliore è quella di usare un vero e proprio analizzatore, tipo quello fornito dal modulo shlex . Di seguito un semplice esempio che stampa i token identificati nel file di input:
import readline
import logging
import shlex
import sys
if len(sys.argv) != 2:
print 'Per favore specificare un nome di file nella riga di comando.'
sys.exit(1)
filename = sys.argv[1]
body = file(filename, 'rt').read()
print 'ORIGINALE:', repr(body)
print
print 'TOKEN:'
lexer = shlex.shlex(body)
for token in lexer:
print repr(token)
Quando si esegue su dati che contengono virgolette annidate, l'analizzatore produce l'elenco di token che ci si aspetta:
$ python shlex_example.py quotes.txt ORIGINALE: 'Questa stringa ha delle "virgolette" e degli \'apici singoli\' incorporati in essa, ed anche "un esempio \'annidato\'".' TOKEN: 'Questa' 'stringa' 'ha' 'delle' '"virgolette"' 'e' 'degli' "'apici singoli'" 'incorporati' 'in' 'essa' ',' 'ed' 'anche' '"un esempio \'annidato\'"' '.'
Vengono gestiti anche gli apici singoli, tipo gli apostrofi. Se si passa questo file in input:
Questa stringa contiene l'apostrofo incorporato vero?
Il token con l'apostrofo incorporato non costituisce un problema:
$ python shlex_example.py apostrophe.txt ORIGINALE: "Questa stringa contiene l'apostrofo incorporato vero?" TOKEN: 'Questa' 'stringa' 'contiene' "l'apostrofo" 'incorporato' 'vero' '?'
Visto che l'analizzatore è inteso per l'uso con linguaggi di comando, deve gestire i commenti. Nella modalità predefinita qualsiasi testo che segua un # è considerato parte di un commento, quindi ignorato. A causa della natura dell'analizzatore, solo i prefissi di commento a singolo carattere sono supportati. L'insieme dei caratteri di commento usati possono essere configurati attraverso la proprietà commenters
$ python shlex_example.py comments.txt ORIGINALE: "Questa riga viene riconosciuta\n# Ma quest'altra viene ignorata.\nQuesta linea viene elaborata." TOKEN: 'Questa' 'riga' 'viene' 'riconosciuta' 'Questa' 'linea' 'viene' 'elaborata' '.'
Se occorre semplicemente dividere una stringa esistente nei token che la compongono, la funzione
split()
è un semplice wrapper attorno all'analizzatore.
import shlex
text = """Questo testo ha "parti virgolettate" al suo interno."""
print 'ORIGINALE:', repr(text)
print
print 'TOKEN:'
print shlex.split(text)
Il risultato è una lista:
$ python shlex_split.py ORIGINALE: 'Questo testo ha "parti virgolettate" al suo interno.' TOKEN: ['Questo', 'testo', 'ha', 'parti virgolettate', 'al', 'suo', 'interno.']
La classe shlex comprende parecchie proprietà di configurazione le quali consentono al programmatore di controllarne il comportamento. La proprietà source abilita la funzionalità di riutilizzo del codice (o della configurazione) consentendo ad un flusso di token di includerne un altro. E' un comportamento simile a quella dell'operatore della shell Bourne source , da qui il nome.
import shlex
text = """Questo testo dice di includere source quotes.txt prima di continuare."""
print 'ORIGINALE:', repr(text)
print
lexer = shlex.shlex(text)
lexer.wordchars += '.'
lexer.source = 'source'
print 'TOKEN:'
for token in lexer:
print repr(token)
Si noti la stringa source quotes.txt incorporata nel testo originale. Visto che la proprietà source di lexer viene impostata a "source", quando la parola chiave viene rilevata il nome del file che segue viene incluso automaticamente. Per fare sì che il nome del file appaia come singolo token, occorre aggiungere il carattere . all'interno dell'elenco dei caratteri che sono inclusi nelle parole (altrimenti " quotes.txt " avrebbe prodotto tre token: " quotes ", " . " e " txt ". Il risultato è:
$ python shlex_source.py ORIGINALE: 'Questo testo dice di includere source quotes.txt prima di continuare.' TOKEN: 'Questo' 'testo' 'dice' 'di' 'includere' 'Questa' 'stringa' 'ha' 'delle' '"virgolette"' 'e' 'degli' "'apici singoli'" 'incorporati' 'in' 'essa' ',' 'ed' 'anche' '"un esempio \'annidato\'"' '.' 'prima' 'di' 'continuare.'
La funzionalità "source" usa un metodo chiamato
sourcehook()
per caricare la sorgente addizionale in input, in questo modo si può derivare
shlex
per fornire la propria implementazione per caricare dati da qualsiasi parte.
import shlex
text = """|Col 1||Col 2||Col 3|"""
print 'ORIGINALE:', repr(text)
print
lexer = shlex.shlex(text)
lexer.quotes = '|'
print 'TOKEN:'
for token in lexer:
print repr(token)
In questo esempio ogni cella della tabella viene racchiusa tra barre verticali:
$ python shlex_table.py ORIGINALE: '|Col 1||Col 2||Col 3|' TOKEN: '|Col 1|' '|Col 2|' '|Col 3|'
E' anche possibile controllare i caratteri whitespace usati per la divisione delle parole. Se si modifica l'esempio shlex_example.py per includere punti e virgole, come segue:
import shlex
import sys
if len(sys.argv) != 2:
print 'Per favore specificare un nome di file nella riga di comando.'
sys.exit(1)
filename = sys.argv[1]
body = file(filename, 'rt').read()
print 'ORIGINALE:', repr(body)
print
print 'TOKEN:'
lexer = shlex.shlex(body)
lexer.whitespace += '.,'
for token in lexer:
print repr(token)
Il risultato cambia in:
$ python shlex_whitespace.py quotes.txt ORIGINALE: 'Questa stringa ha delle "virgolette" e degli \'apici singoli\' incorporati in essa, ed anche "un esempio \'annidato\'".' TOKEN: 'Questa' 'stringa' 'ha' 'delle' '"virgolette"' 'e' 'degli' "'apici singoli'" 'incorporati' 'in' 'essa' 'ed' 'anche' '"un esempio \'annidato\'"'
Quando l'analizzatore giunge alla fine del suo input prima che tutte le stringhe tra virgolette siano chiuse, solleva
ValueError
. Quando questo succede, è utile esaminare alcune delle proprietà dell'analizzatore mantenute mentre viene elaborato l'input. Ad esempio
infile
fa riferimento al nome del file che si sta elaborando (che potrebbe essere diverso dal file originale, se un file chiama "source" verso un altro). Quando l'errore viene scoperto
lineno
riporta la riga. Tipicamente
lineno
è la fine del file, che potrebbe essere ben lontano dalla prima virgoletta. L'attributo
token
contiene il buffer di testo che non è stato ancora incluso in un token valido. Il metodo
error_leader()
produrre un prefisso di messaggio in uno stile simile a quello dei compilatori Unix, che consente ad editor tipo emacs di analizzare l'errore e portare l'utente direttamente a quella riga non valida.
import shlex
text = """Questa riga è a posto.
Questa riga ha un "virgolettato non completato.
Anche questa riga è a posto.
"""
print 'ORIGINALE:', repr(text)
print
lexer = shlex.shlex(text)
print 'TOKEN:'
try:
for token in lexer:
print repr(token)
except ValueError, err:
first_line_of_error = lexer.token.splitlines()[0]
print 'ERRORE:', lexer.error_leader(), str(err), 'dopo "' + first_line_of_error + '"'
L'esempio di cui sopra produce questo risultato:
$ python shlex_errors.py ORIGINALE: 'Questa riga \xc3\xa8 a posto.\nQuesta riga ha un "virgolettato non completato.\nAnche questa riga \xc3\xa8 a posto.\n' TOKEN: 'Questa' 'riga' 'va' 'bene' '.' 'Questa' 'riga' 'ha' 'un' ERRORE: "None", line 4: No closing quotation dopo ""virgolettato non completato."
Il comportamento predefinito per l'analizzatore è quello di usare uno stile retro compatibile che non è conforme alle specifiche POSIX. Per ottenere un comportamento POSIX si imposta il parametro posix quando si costruisce l'analizzatore.
import shlex
for s in [ 'Da"Non"Separare',
'"Da"Separare',
'Escape \e Carattere non tra virgolette',
'Escape "\e" Carattere tra virgolette',
"Escape '\e' Carattere tra apici singoli",
r"Escape '\'' \"\'\" singolo apice",
r'Escape "\"" \'\"\' virgolette',
"\"'Elimina uno strato extra di virgolette'\"",
]:
print 'ORIGINALE :', repr(s)
print 'non-POSIX:',
non_posix_lexer = shlex.shlex(s, posix=False)
try:
print repr(list(non_posix_lexer))
except ValueError, err:
print 'errore(%s)' % err
print 'POSIX :',
posix_lexer = shlex.shlex(s, posix=True)
try:
print repr(list(posix_lexer))
except ValueError, err:
print 'errore(%s)' % err
print
Ecco qualche esempio delle differenze nel comportamento dell'analizzatore
$ python shlex_posix.py ORIGINALE : 'Da"Non"Separare' non-POSIX: ['Da"Non"Separare'] POSIX : ['DaNonSeparare'] ORIGINALE : '"Da"Separare' non-POSIX: ['"Da"', 'Separare'] POSIX : ['DaSeparare'] ORIGINALE : 'Escape \\e Carattere non tra virgolette' non-POSIX: ['Escape', '\\', 'e', 'Carattere', 'non', 'tra', 'virgolette'] POSIX : ['Escape', 'e', 'Carattere', 'non', 'tra', 'virgolette'] ORIGINALE : 'Escape "\\e" Carattere tra virgolette' non-POSIX: ['Escape', '"\\e"', 'Carattere', 'tra', 'virgolette'] POSIX : ['Escape', '\\e', 'Carattere', 'tra', 'virgolette'] ORIGINALE : "Escape '\\e' Carattere tra apici singoli" non-POSIX: ['Escape', "'\\e'", 'Carattere', 'tra', 'apici', 'singoli'] POSIX : ['Escape', '\\e', 'Carattere', 'tra', 'apici', 'singoli'] ORIGINALE : 'Escape \'\\\'\' \\"\\\'\\" singolo apice' non-POSIX: errore(No closing quotation) POSIX : ['Escape', '\\ \\"\\"', 'singolo', 'apice'] ORIGINALE : 'Escape "\\"" \\\'\\"\\\' virgolette' non-POSIX: errore(No closing quotation) POSIX : ['Escape', '"', '\'"\'', 'virgolette'] ORIGINALE : '"\'Elimina uno strato extra di virgolette\'"' non-POSIX: ['"\'Elimina uno strato extra di virgolette\'"'] POSIX : ["'Elimina uno strato extra di virgolette'"]