shlex — Análisis de sintaxis de estilo shell

Propósito:Análisis léxico de sintaxis de estilo shell.

El módulo shlex implementa una clase para analizar sintaxis simples tipo shell. Se puede usar para escribir un lenguaje específico de dominio o para analizar cadenas entre comillas (una tarea que es más compleja de lo que parece en la superficie).

Analizar cadenas entrecomilladas

Un problema común cuando se trabaja con texto de entrada es identificar una secuencia de palabras entrecomilladas como una sola entidad. Dividir el texto entre comillas no siempre funciona como se esperaba, especialmente si hay niveles anidados de comillas. Toma el siguiente texto como ejemplo.

This string has embedded "double quotes" and
'single quotes' in it, and even "a 'nested example'".

Un enfoque ingenuo sería construir una expresión regular para encontrar las partes del texto fuera de las comillas para separarlas del texto dentro de las comillas, o viceversa. Eso sería innecesariamente complejo y propenso a errores resultantes de casos extremos como apóstrofes o incluso errores tipográficos. Una mejor solución es usar un analizador verdadero, como el que proporciona el módulo shlex. Aquí hay un ejemplo simple que imprime los tokens identificados en el archivo de entrada usando la clase shlex.

shlex_example.py
import shlex
import sys

if len(sys.argv) != 2:
    print('Please specify one filename on the command line.')
    sys.exit(1)

filename = sys.argv[1]
with open(filename, 'r') as f:
    body = f.read()
print('ORIGINAL: {!r}'.format(body))
print()

print('TOKENS:')
lexer = shlex.shlex(body)
for token in lexer:
    print('{!r}'.format(token))

Cuando se ejecuta en datos con comillas embebidas, el analizador produce la lista de tokens esperados.

$ python3 shlex_example.py quotes.txt

ORIGINAL: 'This string has embedded "double quotes" and\n\'singl
e quotes\' in it, and even "a \'nested example\'".\n'

TOKENS:
'This'
'string'
'has'
'embedded'
'"double quotes"'
'and'
"'single quotes'"
'in'
'it'
','
'and'
'even'
'"a \'nested example\'"'
'.'

Las comillas aisladas, como los apóstrofes, también se manejan. Considera este archivo de entrada.

This string has an embedded apostrophe, doesn't it?

El token con el apóstrofe embebido no es un problema.

$ python3 shlex_example.py apostrophe.txt

ORIGINAL: "This string has an embedded apostrophe, doesn't it?"

TOKENS:
'This'
'string'
'has'
'an'
'embedded'
'apostrophe'
','
"doesn't"
'it'
'?'

Hacer cadenas seguras para shells

La función quote() realiza la operación inversa, escapa de las comillas existentes y agrega comillas faltantes para que las cadenas sean seguras de usar en los comandos de shell.

shlex_quote.py
import shlex

examples = [
    "Embedded'SingleQuote",
    'Embedded"DoubleQuote',
    'Embedded Space',
    '~SpecialCharacter',
    r'Back\slash',
]

for s in examples:
    print('ORIGINAL : {}'.format(s))
    print('QUOTED   : {}'.format(shlex.quote(s)))
    print()

Por lo general, es más seguro usar una lista de argumentos cuando se usa subprocess.Popen, pero en situaciones donde eso no es posible quote() proporciona cierta protección al garantizar que los caracteres especiales y los espacios en blanco se entrecomillan correctamente .

$ python3 shlex_quote.py

ORIGINAL : Embedded'SingleQuote
QUOTED   : 'Embedded'"'"'SingleQuote'

ORIGINAL : Embedded"DoubleQuote
QUOTED   : 'Embedded"DoubleQuote'

ORIGINAL : Embedded Space
QUOTED   : 'Embedded Space'

ORIGINAL : ~SpecialCharacter
QUOTED   : '~SpecialCharacter'

ORIGINAL : Back\slash
QUOTED   : 'Back\slash'

Comentarios embebidos

Dado que el analizador está destinado a ser utilizado con lenguajes de comando, necesita manejar comentarios. Por defecto, cualquier texto que sigue a un # se considera parte de un comentario y se ignora. Debido a la naturaleza del analizador, solo se admiten prefijos de comentarios de un solo carácter. El conjunto de caracteres de comentario utilizado se puede configurar a través de la propiedad commenters.

$ python3 shlex_example.py comments.txt

ORIGINAL: 'This line is recognized.\n# But this line is ignored.
\nAnd this line is processed.'

TOKENS:
'This'
'line'
'is'
'recognized'
'.'
'And'
'this'
'line'
'is'
'processed'
'.'

División de cadenas en tokens

Para dividir una cadena existente en tokens de componentes, la función de conveniencia split() es una envoltura simple alrededor del analizador.

shlex_split.py
import shlex

text = """This text has "quoted parts" inside it."""
print('ORIGINAL: {!r}'.format(text))
print()

print('TOKENS:')
print(shlex.split(text))

El resultado es una lista.

$ python3 shlex_split.py

ORIGINAL: 'This text has "quoted parts" inside it.'

TOKENS:
['This', 'text', 'has', 'quoted parts', 'inside', 'it.']

Incluir otras fuentes de tokens

La clase shlex incluye varias propiedades de configuración que controlan su comportamiento. La propiedad source habilita una característica para la reutilización de código (o configuración) al permitir que una secuencia de tokens incluya otra. Esto es similar al operador source de shell Bourne, de ahí el nombre.

shlex_source.py
import shlex

text = "This text says to source quotes.txt before continuing."
print('ORIGINAL: {!r}'.format(text))
print()

lexer = shlex.shlex(text)
lexer.wordchars += '.'
lexer.source = 'source'

print('TOKENS:')
for token in lexer:
    print('{!r}'.format(token))

La cadena «source quotes.txt» en el texto original recibe un manejo especial. Dado que la propiedad source del lexer se establece en "source", cuando se encuentra la palabra clave, el nombre de archivo que aparece en la línea siguiente se incluye automáticamente. Para que el nombre de archivo aparezca como un token único, el carácter . debe agregarse a la lista de caracteres que se incluyen en las palabras (de lo contrario, «quotes.txt» se convierte en tres tokens «quotes», «.»,»txt»). Así se ve la salida.

$ python3 shlex_source.py

ORIGINAL: 'This text says to source quotes.txt before
continuing.'

TOKENS:
'This'
'text'
'says'
'to'
'This'
'string'
'has'
'embedded'
'"double quotes"'
'and'
"'single quotes'"
'in'
'it'
','
'and'
'even'
'"a \'nested example\'"'
'.'
'before'
'continuing.'

La función fuente utiliza un método llamado sourcehook() para cargar la fuente de entrada adicional, por lo que una subclase de shlex puede proporcionar una implementación alternativa que cargue datos desde ubicaciones que no sean archivos.

Control del analizador

Un ejemplo anterior demostró el cambio del valor wordchars para controlar qué caracteres se incluyen en las palabras. También es posible establecer el carácter quotes para usar comillas adicionales o alternativas. Cada comilla debe ser un solo carácter, por lo que no es posible tener comillas abiertas y cerradas diferentes (no paréntesis, por ejemplo).

shlex_table.py
import shlex

text = """|Col 1||Col 2||Col 3|"""
print('ORIGINAL: {!r}'.format(text))
print()

lexer = shlex.shlex(text)
lexer.quotes = '|'

print('TOKENS:')
for token in lexer:
    print('{!r}'.format(token))

En este ejemplo, cada celda de la tabla está envuelta en barras verticales.

$ python3 shlex_table.py

ORIGINAL: '|Col 1||Col 2||Col 3|'

TOKENS:
'|Col 1|'
'|Col 2|'
'|Col 3|'

También es posible controlar los espacios en blanco utilizados para dividir palabras.

shlex_whitespace.py
import shlex
import sys

if len(sys.argv) != 2:
    print('Please specify one filename on the command line.')
    sys.exit(1)

filename = sys.argv[1]
with open(filename, 'r') as f:
    body = f.read()
print('ORIGINAL: {!r}'.format(body))
print()

print('TOKENS:')
lexer = shlex.shlex(body)
lexer.whitespace += '.,'
for token in lexer:
    print('{!r}'.format(token))

Si el ejemplo en shlex_example.py se modifica para incluir punto y coma, los resultados cambian.

$ python3 shlex_whitespace.py quotes.txt

ORIGINAL: 'This string has embedded "double quotes" and\n\'singl
e quotes\' in it, and even "a \'nested example\'".\n'

TOKENS:
'This'
'string'
'has'
'embedded'
'"double quotes"'
'and'
"'single quotes'"
'in'
'it'
'and'
'even'
'"a \'nested example\'"'

Manejo de errores

Cuando el analizador encuentra el final de su entrada antes de que se cierren todas las cadenas entrecomilladas, genera ValueError. Cuando eso sucede, es útil examinar algunas de las propiedades mantenidas por el analizador mientras procesa la entrada. Por ejemplo, infile se refiere al nombre del archivo que se está procesando (que podría ser diferente del archivo original, si un archivo obtiene otro). El lineno informa la línea cuando se descubre el error. El lineno es típicamente el final del archivo, que puede estar muy lejos de la primera comilla. El atributo token contiene el búfer de texto que no está incluido en un token válido. El método error_leader() produce un prefijo de mensaje en un estilo similar a los compiladores de Unix, que permite a editores como emacs analizar el error y llevar al usuario directamente a la línea no válida.

shlex_errors.py
import shlex

text = """This line is ok.
This line has an "unfinished quote.
This line is ok, too.
"""

print('ORIGINAL: {!r}'.format(text))
print()

lexer = shlex.shlex(text)

print('TOKENS:')
try:
    for token in lexer:
        print('{!r}'.format(token))
except ValueError as err:
    first_line_of_error = lexer.token.splitlines()[0]
    print('ERROR: {} {}'.format(lexer.error_leader(), err))
    print('following {!r}'.format(first_line_of_error))

El ejemplo produce esta salida.

$ python3 shlex_errors.py

ORIGINAL: 'This line is ok.\nThis line has an "unfinished quote.
\nThis line is ok, too.\n'

TOKENS:
'This'
'line'
'is'
'ok'
'.'
'This'
'line'
'has'
'an'
ERROR: "None", line 4:  No closing quotation
following '"unfinished quote.'

Análisis POSIX versus no POSIX

El comportamiento predeterminado para el analizador es usar un estilo compatible con versiones anteriores que no sea compatible con POSIX. Para el comportamiento POSIX, establece el argumento posix cuando construyes el analizador.

shlex_posix.py
import shlex

examples = [
    'Do"Not"Separate',
    '"Do"Separate',
    'Escaped \e Character not in quotes',
    'Escaped "\e" Character in double quotes',
    "Escaped '\e' Character in single quotes",
    r"Escaped '\'' \"\'\" single quote",
    r'Escaped "\"" \'\"\' double quote',
    "\"'Strip extra layer of quotes'\"",
]

for s in examples:
    print('ORIGINAL : {!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('error({})'.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()

Aquí hay algunos ejemplos de las diferencias en el comportamiento de análisis.

$ python3 shlex_posix.py

ORIGINAL : 'Do"Not"Separate'
non-POSIX: ['Do"Not"Separate']
POSIX    : ['DoNotSeparate']

ORIGINAL : '"Do"Separate'
non-POSIX: ['"Do"', 'Separate']
POSIX    : ['DoSeparate']

ORIGINAL : 'Escaped \\e Character not in quotes'
non-POSIX: ['Escaped', '\\', 'e', 'Character', 'not', 'in',
'quotes']
POSIX    : ['Escaped', 'e', 'Character', 'not', 'in', 'quotes']

ORIGINAL : 'Escaped "\\e" Character in double quotes'
non-POSIX: ['Escaped', '"\\e"', 'Character', 'in', 'double',
'quotes']
POSIX    : ['Escaped', '\\e', 'Character', 'in', 'double',
'quotes']

ORIGINAL : "Escaped '\\e' Character in single quotes"
non-POSIX: ['Escaped', "'\\e'", 'Character', 'in', 'single',
'quotes']
POSIX    : ['Escaped', '\\e', 'Character', 'in', 'single',
'quotes']

ORIGINAL : 'Escaped \'\\\'\' \\"\\\'\\" single quote'
non-POSIX: error(No closing quotation)
POSIX    : ['Escaped', '\\ \\"\\"', 'single', 'quote']

ORIGINAL : 'Escaped "\\"" \\\'\\"\\\' double quote'
non-POSIX: error(No closing quotation)
POSIX    : ['Escaped', '"', '\'"\'', 'double', 'quote']

ORIGINAL : '"\'Strip extra layer of quotes\'"'
non-POSIX: ['"\'Strip extra layer of quotes\'"']
POSIX    : ["'Strip extra layer of quotes'"]

Ver también