Los comandos de terminal parecen cadenas normales, pero los espacios, comillas, barras invertidas y metacaracteres cambian la interpretación de cada argumento. Separar una línea con str.split() falla cuando una ruta contiene espacios, mientras construir comandos mediante concatenación puede introducir una vulnerabilidad de inyección. El módulo shlex en Python ofrece análisis léxico inspirado en shells Unix para dividir líneas, conservar argumentos entre comillas, reconstruir comandos y crear minilenguajes sencillos.
Esta guía explica shlex.split(), shlex.join(), shlex.quote() y la clase configurable shlex, además de una integración más segura con subprocess. Complementa nuestros artículos sobre listas en Python, archivos temporales, comparación de textos, PermissionError y archivos de texto.
Por qué str.split() no basta
Considera una línea con un archivo que contiene espacios:
comando = 'python informe.py --salida "Mi Informe.pdf"'
print(comando.split())El resultado conserva las comillas y separa incorrectamente el nombre. shlex.split() aplica reglas parecidas a un shell POSIX:
import shlex
argumentos = shlex.split(comando)
print(argumentos)
# ['python', 'informe.py', '--salida', 'Mi Informe.pdf']Cada elemento corresponde ahora a un argumento que puede entregarse a subprocess.run().
Ejecutar con subprocess y shell=False
El patrón más seguro consiste en pasar una lista sin invocar un shell:
import shlex
import subprocess
linea = 'python informe.py --salida "Mi Informe.pdf"'
args = shlex.split(linea)
resultado = subprocess.run(
args,
shell=False,
check=True,
capture_output=True,
text=True,
)La documentación oficial de subprocess recomienda secuencias de argumentos siempre que sea posible. Con shell=False, caracteres como punto y coma, ampersand y tuberías se pasan al programa en lugar de interpretarse como operadores del shell.
No ejecutes entrada arbitraria del usuario
shlex.split() analiza texto; no decide si un programa u opción está permitido. Este código sigue siendo peligroso:
linea = input("Comando: ")
subprocess.run(shlex.split(linea))El usuario puede solicitar cualquier ejecutable accesible. Los servicios deben definir acciones permitidas, validar parámetros y construir listas desde datos estructurados.
COMANDOS = {
"listar": ["python", "-m", "mi_app", "listar"],
"estado": ["python", "-m", "mi_app", "estado"],
}
accion = entrada.get("accion")
if accion not in COMANDOS:
raise ValueError("Acción no permitida")
subprocess.run(COMANDOS[accion], check=True)Cómo split() trata las comillas
Las comillas simples y dobles protegen espacios y algunos caracteres especiales:
import shlex
print(shlex.split("echo 'dos palabras'"))
print(shlex.split('echo "dos palabras"'))
print(shlex.split(r'echo archivo\ con\ espacios.txt'))En el modo POSIX predeterminado, las comillas se eliminan y los escapes se interpretan. Una comilla sin cerrar genera ValueError.
try:
argumentos = shlex.split('echo "texto incompleto')
except ValueError as error:
print("Línea inválida:", error)Comentarios con comments
Por defecto, shlex.split() no interpreta # como comentario.
linea = "ejecutar tarea # comentario"
print(shlex.split(linea))Para archivos de control sencillos, actívalos:
print(shlex.split(linea, comments=True))
# ['ejecutar', 'tarea']Elige conscientemente porque # puede formar parte de un archivo, etiqueta o argumento.
Reconstruir con shlex.join()
shlex.join() recibe una lista de tokens y crea una línea escapada para shells Unix.
import shlex
args = ["echo", "-n", "dos palabras", "archivo;inofensivo"]
linea = shlex.join(args)
print(linea)La función fue añadida en Python 3.8 y actúa como inversa práctica de split().
assert shlex.split(shlex.join(args)) == argsEs excelente para logs legibles. Si ya tienes la lista, pásala directamente al subproceso en vez de reconstruir una cadena para ejecutarla.
Escapar un token con quote()
shlex.quote() convierte una cadena en un token seguro para una línea de shell POSIX.
from shlex import quote
archivo = "informe; rm -rf ~"
linea = f"cat {quote(archivo)}"
print(linea)El punto y coma queda dentro de comillas. La documentación oficial de shlex advierte que esta protección no está garantizada en shells no POSIX, incluido Windows.
quote() no sustituye una lista
El escape manual se complica con varias capas, como SSH, sh -c o comandos remotos. Prefiere:
subprocess.run(["cat", archivo], shell=False, check=True)Usa quote() solo cuando una interfaz inevitable exige una única cadena interpretada por un shell POSIX.
Limitaciones en Windows
shlex modela sintaxis de shells Unix. cmd.exe y PowerShell tienen reglas propias de comillas, escapes, variables y operadores. Un valor protegido con shlex.quote() puede ser incorrecto o inseguro en Windows.
Para ejecutables, pasa una lista a subprocess.run(..., shell=False). Para comandos internos como dir, implementa lógica específica y evita combinar entrada no confiable con shell=True.
Usar la clase shlex como iterador
La función split() cubre casos comunes. Para una sintaxis personalizada, crea el lexer:
import shlex
lexer = shlex.shlex(
'copiar "archivo origen.txt" destino/',
posix=True,
)
lexer.whitespace_split = True
for token in lexer:
print(token)La entrada puede ser una cadena o un flujo con read() y readline().
Modo POSIX y compatibilidad
La clase shlex.shlex usa posix=False por compatibilidad histórica, mientras shlex.split() usa posix=True. El modo POSIX elimina comillas, interpreta escapes y admite cadenas vacías entre comillas.
import shlex
texto = 'comando "" "a b"'
print(list(shlex.shlex(texto, posix=False)))
print(list(shlex.shlex(texto, posix=True)))Define el modo explícitamente en parsers persistentes.
whitespace_split
Con whitespace_split=True, los tokens se separan principalmente por espacios y puntuación configurada.
lexer = shlex.shlex('enviar --nombre "Ana Silva"', posix=True)
lexer.whitespace_split = True
print(list(lexer))El resultado se parece a una lista de argumentos.
punctuation_chars
El parámetro punctuation_chars devuelve secuencias de operadores como tokens independientes.
lexer = shlex.shlex(
"tarea && validar || cancelar; fin",
posix=True,
punctuation_chars=True,
)
lexer.whitespace_split = True
print(list(lexer))Los caracteres ();<>|& se tratan como puntuación cuando vale True. También puedes proporcionar una cadena específica. La propiedad solo se define al crear el objeto.
Parsing no es validación semántica
El lexer puede devolver >>> aunque ningún shell soportado lo reconozca. Después de tokenizar, valida la gramática.
OPERADORES = {"&&", "||", ";"}
for token in lexer:
if token.startswith(("|", "&", ";")) and token not in OPERADORES:
raise ValueError(f"Operador inválido: {token}")Para lenguajes complejos, usa un parser formal.
Personalizar comentarios, espacios y palabras
Las instancias exponen commenters, whitespace, quotes, escape y wordchars.
lexer = shlex.shlex("clave=valor ; comentario", posix=True)
lexer.commenters = ";"
lexer.whitespace_split = True
print(list(lexer))Estas reglas permiten archivos de control simples. Documenta la sintaxis y crea pruebas de regresión.
Minilenguaje controlado
Un uso seguro es interpretar una sintaxis propia sin ejecutar un shell.
def interpretar(linea: str) -> dict:
tokens = shlex.split(linea, comments=True)
if not tokens:
return {"accion": "vacio"}
accion, *args = tokens
if accion not in {"copiar", "mover", "listar"}:
raise ValueError("Comando desconocido")
return {"accion": accion, "argumentos": args}La aplicación traduce tokens a operaciones internas y aplica permisos.
Inclusión de archivos y fuentes apiladas
La clase soporta fuentes de entrada apiladas y un atributo source. Un token puede solicitar leer otro archivo. Es potente, pero peligroso con rutas controladas por usuarios.
Restringe directorios, resuelve rutas, bloquea .., limita profundidad y detecta ciclos. Para configuración común, TOML, JSON o configparser suelen ser más claros.
Archivo y línea en errores
infile y lineno ayudan a crear diagnósticos. error_leader() produce un prefijo estilo compilador Unix.
lexer = shlex.shlex('copiar "sin cerrar', infile="tareas.conf", posix=True)
try:
list(lexer)
except ValueError as error:
print(lexer.error_leader() + str(error))Los mensajes con ubicación facilitan corregir archivos.
Unicode y nombres de archivo
El modo POSIX incluye caracteres Latin-1 en wordchars, pero los nombres Unicode modernos deben probarse. whitespace_split=True suele preservar mejor argumentos generales entre comillas.
No normalices nombres del sistema de archivos sin comprender la plataforma.
Logs seguros de comandos
shlex.join(args) crea una representación legible, pero los argumentos pueden contener contraseñas, tokens y rutas personales.
def ocultar(args):
resultado = list(args)
for indice, token in enumerate(resultado[:-1]):
if token in {"--password", "--token"}:
resultado[indice + 1] = "***"
return resultado
logger.info("Ejecutando: %s", shlex.join(ocultar(args)))Oculta antes de registrar y nunca reutilices la cadena del log como fuente de ejecución.
Probar round-trip y entradas hostiles
casos = [
["echo", "dos palabras"],
["cat", "archivo;seguro"],
["printf", "%s", ""],
["programa", "unicode-ñ", "a'b"],
]
for args in casos:
assert shlex.split(shlex.join(args)) == argsPrueba comillas sin cerrar, barras, líneas vacías, comentarios, Unicode, punto y coma, ampersands, pipes, sustituciones y comillas invertidas.
Errores frecuentes
- Usar
str.split()con líneas que contienen comillas. - Creer que tokenizar autoriza ejecutar.
- Construir una cadena para
shell=Truecuando sirve una lista. - Usar
shlex.quote()como escape multiplataforma. - Ejecutar directamente entrada del usuario.
- Confundir parsing con validación de operadores.
- Permitir inclusión de archivos sin restringir rutas.
- Registrar secretos.
Buenas prácticas
- Prefiere listas con
shell=False. - Usa
split()para texto POSIX confiable. - Usa
join()para mostrar y diagnosticar. - Usa
quote()solo para un token en un shell Unix inevitable. - Valida ejecutables, opciones y operadores.
- Separa parsers de ejecutores.
- Define sintaxis y límites para minilenguajes.
- Prueba Linux y Windows por separado.
Conclusión
El módulo shlex en Python resuelve la tokenización de líneas con comillas, escapes y sintaxis parecida a shells Unix. split() transforma texto en argumentos, join() crea una representación escapada y quote() protege un token en un contexto POSIX. La clase shlex permite analizadores configurables.
La regla central de seguridad es evitar una cadena de shell cuando una lista de argumentos es suficiente. El parsing correcto resuelve espacios y comillas, pero solo la validación explícita controla lo que puede ejecutarse. Con subprocess, shell=False, listas permitidas y pruebas hostiles, shlex ayuda a crear CLIs y minilenguajes sin abrir una vía de inyección de comandos.







