shlex en Python: comandos seguros

Publicado el: 02/08/2026
Tempo de leitura: 6 minutos
Terminal de comandos que representa parsing seguro con shlex en Python

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)) == args

Es 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)) == args

Prueba 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=True cuando 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.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Base de datos local que representa persistencia con shelve en Python
    Bibliotecas y Módulos
    Foto de perfil de Leandro Hirt da Academify

    shelve en Python: persistencia simple

    Aprende shelve en Python para persistir objetos, actualizar datos mutables, evitar riesgos de pickle y decidir cuándo migrar a SQLite.

    Ler mais

    Tempo de leitura: 5 minutos
    01/08/2026
    Documentos de texto que representan comparación de versiones con difflib en Python
    Bibliotecas y Módulos
    Foto de perfil de Leandro Hirt da Academify

    difflib en Python: compara textos y archivos

    Aprende difflib en Python para comparar textos, medir similitud, crear diffs unificados, informes HTML y sugerencias de nombres.

    Ler mais

    Tempo de leitura: 6 minutos
    01/08/2026
    Panel de gráficos que representa análisis estadístico de datos en Python
    Bibliotecas y Módulos
    Foto de perfil de Leandro Hirt da Academify

    statistics en Python: análisis de datos

    Aprende statistics en Python para media, mediana, desviación, cuantiles, correlación, regresión, NormalDist y KDE.

    Ler mais

    Tempo de leitura: 7 minutos
    31/07/2026
    Gráficos de fracciones que representan números racionales exactos en Python
    Bibliotecas y Módulos
    Foto de perfil de Leandro Hirt da Academify

    fractions en Python: números racionales

    Aprende fractions en Python para aritmética racional exacta, reducción automática, limit_denominator, formato y conversiones seguras.

    Ler mais

    Tempo de leitura: 6 minutos
    31/07/2026
    Calculadora y documentos que representan cálculos Decimal precisos en Python
    Bibliotecas y Módulos
    Foto de perfil de Leandro Hirt da Academify

    Decimal en Python: cálculos precisos

    Aprende Decimal en Python para cálculos exactos, dinero, quantize, redondeo, contextos y validación sin errores de float.

    Ler mais

    Tempo de leitura: 7 minutos
    30/07/2026
    Código digital que representa identificadores UUID únicos y ordenables en Python
    Bibliotecas y Módulos
    Foto de perfil de Leandro Hirt da Academify

    uuid en Python: IDs únicos y ordenables

    Aprende uuid en Python: versiones 4, 5, 6 y 7, validación, almacenamiento, IDs ordenables y buenas prácticas de seguridad.

    Ler mais

    Tempo de leitura: 7 minutos
    30/07/2026