shlex - Analisi delle Sintassi Stile Shell
Scopo: Analisi lessicale delle sintassi Stile shell.
Il modulo shlex implementa una classe per analizzare semplici sintassi tipo shell. Può essere usato per scrivere un proprio linguaggio di programmazione domain specific (DSL), o per analizzare stringhe racchiuse tra apici (un compito più complesso di quello che può sembrare a prima vista).
Elaborare Stringhe Racchiuse tra Apici
Un problema comune quando si lavora con un testo in input è quello di identificare una sequenza di parole racchiuse tra apici come singola entità. Dividere la stringa in base agli apici non sempre fornisce il risultato sperato, specialmente se si sono livelli di apici annidati. Si prenda ad esempio il testo seguente:
Questa stringa contiene "doppi apici" ed 'apici singoli' incorporati in essa, e 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 degli apici per separarle da quelle all'interno degli stessi, o viceversa. Detto approccio sarebbe inutilmente complesso e prono a errori risultanti da casi limite come gli apostrofi o anche errori ortografici. 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 usando la classe shlex
:
# shlex_example.py
import shlex
import sys
if len(sys.argv) != 2:
print('Prego specificare un nome file da riga di comando.')
sys.exit(1)
filename = sys.argv[1]
with open(filename, 'r') as f:
body = f.read()
print('ORIGINALE: {!r}'.format(body))
print()
print('TOKEN:')
lexer = shlex.shlex(body)
for token in lexer:
print('{!r}'.format(token))
Quando si esegue su dati che contengono apici all'interno, l'analizzatore produce l'elenco di token attesi.
$ python3 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 isolati, 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:
$ python3 shlex_example.py apostrophe.txt ORIGINALE: "Questa stringa contiene l'apostrofo incorporato vero?\n" TOKEN: 'Questa' 'stringa' 'contiene' "l'apostrofo" 'incorporato' 'vero' '?'
Rendere le Stringhe Sicure per le Shell
La funzione quote()
esegue l'operazione inversa, utilizzando sequenze di escape per gli apici esistenti e aggiungendo gli apici mancanti per le stringhe in modo da renderne l'uso sicuro in comandi di shell.
# shlex_quote.py
import shlex
examples = [
"Annidato'SingoloApice",
'Annidato"DoppioApice',
'Annidato Spazio',
'~CarattereSpeciale',
r'BarraRove\sciata',
]
for s in examples:
print('ORIGINALE : {}'.format(s))
print('TRA APICI : {}'.format(shlex.quote(s)))
print()
E' comunque più sicuro usare una lista di argomenti quando si sta usando subprocess.Popen
ma in situazioni dove non è possibile, quote()
fornisce una qualche protezione assicurando che i caratteri speciali, spazi, ritorni a capo, avanti riga siano propriamente provvisti di sequenze di escape.
$ python3 shlex_quote.py ORIGINALE : Annidato'SingoloApice TRA APICI : 'Annidato'"'"'SingoloApice' ORIGINALE : Annidato"DoppioApice TRA APICI : 'Annidato"DoppioApice' ORIGINALE : Annidato Spazio TRA APICI : 'Annidato Spazio' ORIGINALE : ~CarattereSpeciale TRA APICI : '~CarattereSpeciale' ORIGINALE : BarraRove\sciata TRA APICI : 'BarraRove\sciata'
Commenti Incorporati
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 può essere configurato attraverso la proprietà commenters
.
$ python3 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 di Stringhe in Token
Se occorre semplicemente dividere una stringa esistente nei token che la compongono, la funzione split()
è un semplice wrapper attorno all'analizzatore.
# shlex_split.py
import shlex
text = """Questo testo ha "parti tra apici" al suo interno."""
print('ORIGINALE: {!r}'.format(text))
print()
print('TOKEN:')
print(shlex.split(text))
Il risultato è una lista:
$ python3 shlex_split.py ORIGINALE: 'Questo testo ha "parti tra apici" al suo interno.' TOKEN: ['Questo', 'testo', 'ha', 'parti tra apici', 'al', 'suo', 'interno.']
Includere altre Sorgenti di Token
La classe shlex
comprende parecchie proprietà di configurazione che ne controllano il comportamento. La proprietà source
abilita la funzionalità di riutilizzo del codice (o della configurazione) consentendo a un flusso di token di includerne un altro. E' un comportamento simile a quello dell'operatore source
della Bourne shell, da qui il nome.
# shlex_source.py
import shlex
text = "Questo testo dice di passare a source quotes.txt prima di continuare."
print('ORIGINALE: {!r}'.format(text))
print()
lexer = shlex.shlex(text)
lexer.wordchars += '.'
lexer.source = 'source'
print('TOKEN:')
for token in lexer:
print('{!r}'.format(token))
La stringa source quotes.txt
nel testo originale riceve un trattamento speciale. Visto che la proprietà source
del lexer viene impostata a "source"
, quando la parola chiave viene rilevata, il nome del file che segue è incluso automaticamente. Per fare sì che il nome del file appaia come singolo token, occorre aggiungere il carattere .
deve essere aggiunto all'elenco dei caratteri che sono inclusi nelle parole (altrimenti "quotes.txt
" avrebbe prodotto tre token: "quotes
", ".
" e "txt
". Il risultato è:
$ python3 shlex_source.py ORIGINALE: 'Questo testo dice di passare a source quotes.txt prima di continuare.' TOKEN: 'Questo' 'testo' 'dice' 'di' 'passare' 'a' '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 una sottoclasse di shlex
per fornire la propria implementazione per caricare dati da locazioni diverse dai file.
Controllare il Parser
In un esempio precedente si dimostrava che la modifica del valore di wordchars
controlla quali caratteri sono inclusi nelle parole. E' anche possibile impostare il carattere per quotes
per usare apici alternativi o addizionali. Ogni valore deve essere singolo, non è possibile avere caratteri di apertura e chiusura diversi (come le parentesi, ad esempio).
# shlex_table.py
import shlex
text = """|Col 1||Col 2||Col 3|"""
print('ORIGINALE: {!r}'.format(text))
print()
lexer = shlex.shlex(text)
lexer.quotes = '|'
print('TOKEN:')
for token in lexer:
print('{!r}'.format(token))
In questo esempio ogni cella della tabella viene racchiusa tra barre verticali:
$ python3 shlex_table.py ORIGINALE: '|Col 1||Col 2||Col 3|' TOKEN: '|Col 1|' '|Col 2|' '|Col 3|'
E' anche possibile controllare spazi, ritorni a capo, tabulazioni usati per la divisione delle parole.
# shlex_whitespace.py
import shlex
import sys
if len(sys.argv) != 2:
print('Prego specificare un nome di file da riga di comando.')
sys.exit(1)
filename = sys.argv[1]
with open(filename, 'r') as f:
body = f.read()
print('ORIGINALE: {!r}'.format(body))
print()
print('TOKEN:')
lexer = shlex.shlex(body)
lexer.whitespace += '.,'
for token in lexer:
print('{!r}'.format(token))
Se l'esempio in shlex_example.py
viene modificato per includere punti e virgole, il risultato cambia.
$ python3 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 apici siano chiuse, solleva una eccezione 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 lontana dal primo apice. 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 a editor tipo emacs di analizzare l'errore e portare l'utente direttamente a quella riga non valida.
# shlex_errors.py
import shlex
text = """Questa riga è a posto.
Questa riga ha un "apice non chiuso.
Anche questa riga è a posto.
"""
print('ORIGINALE: {!r}'.format(text))
print()
lexer = shlex.shlex(text)
print('TOKEN:')
try:
for token in lexer:
print('{!r}'.format(token))
except ValueError as err:
first_line_of_error = lexer.token.splitlines()[0]
print('ERRORE: {} {}'.format(lexer.error_leader(), err))
print('segue {!r}'.format(first_line_of_error))
L'esempio di cui sopra produce questo risultato:
$ python3 shlex_errors.py ORIGINALE: 'Questa riga è a posto.\nQuesta riga ha un "apice non chiuso.\nAnche questa riga è a posto.\n' TOKEN: 'Questa' 'riga' 'è' 'a' 'posto' '.' 'Questa' 'riga' 'ha' 'un' ERRORE: "None", line 4: No closing quotation segue '"apice non chiuso.'
Elaborazione 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 imposti l'argomento posix
quando si costruisce l'analizzatore.
# shlex_posix.py
import shlex
examples = [
'Da"Non"Separare',
'"Da"Separare',
'Ignorare il Carattere \e se non tra apici',
'Ignorare il Carattere "\e" se tra doppi apici',
"Ignorare il Carattere '\e' se tra singoli apici",
r"Ignorare '\'' \"\'\" tra singoli apici",
r'Ignorare "\"" \'\"\' tra doppi apici',
"\"'Togliere uno strato di apici supplementare'\"",
]
for s in examples:
print('ORIGINALE : {!r}'.format(s))
print('non-POSIX : ', end='')
non_posix_lexer = shlex.shlex(s, posix=False)
try:
print('{!r}'.format(list(non_posix_lexer)))
except ValueError as err:
print('errore({})'.format(err))
print('POSIX : ', end='')
posix_lexer = shlex.shlex(s, posix=True)
try:
print('{!r}'.format(list(posix_lexer)))
except ValueError as err:
print('error({})'.format(err))
print()
Ecco qualche esempio delle differenze nel comportamento dell'analizzatore.
$ python3 shlex_posix.py ORIGINALE : 'Da"Non"Separare' non-POSIX : ['Da"Non"Separare'] POSIX : ['DaNonSeparare'] ORIGINALE : '"Da"Separare' non-POSIX : ['"Da"', 'Separare'] POSIX : ['DaSeparare'] ORIGINALE : 'Ignorare il Carattere \\e se non tra apici' non-POSIX : ['Ignorare', 'il', 'Carattere', '\\', 'e', 'se', 'non', 'tra', 'apici'] POSIX : ['Ignorare', 'il', 'Carattere', 'e', 'se', 'non', 'tra', 'apici'] ORIGINALE : 'Ignorare il Carattere "\\e" se tra doppi apici' non-POSIX : ['Ignorare', 'il', 'Carattere', '"\\e"', 'se', 'tra', 'doppi', 'apici'] POSIX : ['Ignorare', 'il', 'Carattere', '\\e', 'se', 'tra', 'doppi', 'apici'] ORIGINALE : "Ignorare il Carattere '\\e' se tra singoli apici" non-POSIX : ['Ignorare', 'il', 'Carattere', "'\\e'", 'se', 'tra', 'singoli', 'apici'] POSIX : ['Ignorare', 'il', 'Carattere', '\\e', 'se', 'tra', 'singoli', 'apici'] ORIGINALE : 'Ignorare \'\\\'\' \\"\\\'\\" tra singoli apici' non-POSIX : errore(No closing quotation) POSIX : ['Ignorare', '\\ \\"\\"', 'tra', 'singoli', 'apici'] ORIGINALE : 'Ignorare "\\"" \\\'\\"\\\' tra doppi apici' non-POSIX : errore(No closing quotation) POSIX : ['Ignorare', '"', '\'"\'', 'tra', 'doppi', 'apici'] ORIGINALE : '"\'Togliere uno strato di apici supplementare\'"' non-POSIX : ['"\'Togliere uno strato di apici supplementare\'"'] POSIX : ["'Togliere uno strato di apici supplementare'"]
Vedere anche:
- shlex
- La documentazione della libreria standard per questo
- cmd
- Strumenti per costruire interpreti di comando interattivi.
- argparse
- Elaborazione delle opzioni della riga di comando.
- subprocess
- Esegue comandi dopo l'elaborazione della riga di comando.