shlex - Analisi lessicale delle sintassi tipo shell

Scopo Analisi lessicale delle sintassi tipo shell.
Versione Python 1.5.2, con aggiunte nelle successive versioni.

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).

Stringhe Racchiuse tra Virgolette

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'
'?'

Commenti Annidati

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'
'.'

Divisione

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.']

Includere altre Sorgenti di Token

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\'"'

Gestione Errori

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."

Analisi POSIX contro Non-POSIX

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'"]

Vedere anche:

shlex
La documentazione della libreria standard per questo
cmd
Strumenti per costruire interpreti interattivi di comando
optparse
Analisi delle opzioni della riga di comando.
getopt
Analisi delle opzioni della riga di comando.