shlex no Python: comandos e argumentos seguros

Publicado em: 02/08/2026
Tempo de leitura: 7 minutos
Terminal de comandos representando parsing seguro com shlex no Python

Comandos de terminal parecem apenas strings, mas espaços, aspas, barras invertidas e caracteres especiais mudam a forma como cada argumento é interpretado. Separar uma linha com str.split() falha assim que um caminho contém espaços, enquanto montar comandos por concatenação pode abrir uma vulnerabilidade de injeção. O módulo shlex no Python oferece análise lexical inspirada em shells Unix para dividir linhas, preservar argumentos entre aspas, reconstruir comandos e criar minilinguagens simples.

Neste guia, você aprenderá a usar shlex.split(), shlex.join(), shlex.quote() e a classe shlex, além de integrar argumentos com subprocess de forma mais segura. O conteúdo complementa nossos artigos sobre listas em Python, arquivos temporários, comparação de textos, PermissionError e leitura de arquivos.

Por que str.split() não basta

Considere uma linha que contém um arquivo com espaços:

comando = 'python relatorio.py --saida "Meu Relatório.pdf"'
print(comando.split())

O resultado mantém as aspas dentro dos tokens e separa o nome em partes inadequadas. shlex.split() aplica regras semelhantes às de um shell POSIX:

import shlex

argumentos = shlex.split(comando)
print(argumentos)
# ['python', 'relatorio.py', '--saida', 'Meu Relatório.pdf']

Agora cada elemento corresponde a um argumento que pode ser entregue a subprocess.run().

Executando com subprocess e shell=False

A opção mais segura é passar uma lista e não invocar um shell:

import shlex
import subprocess

linha = 'python relatorio.py --saida "Meu Relatório.pdf"'
args = shlex.split(linha)

resultado = subprocess.run(
    args,
    shell=False,
    check=True,
    capture_output=True,
    text=True,
)

A documentação oficial de subprocess recomenda sequências de argumentos sempre que possível. Com shell=False, metacaracteres como ponto e vírgula, & e | são passados ao programa como caracteres comuns, em vez de serem executados pelo shell.

Não execute diretamente entrada do usuário

shlex.split() faz parsing; ele não decide se um programa ou argumento é permitido. Este código continua perigoso:

linha = input("Comando: ")
subprocess.run(shlex.split(linha))

O usuário pode solicitar qualquer executável acessível. Para um serviço ou painel, defina comandos permitidos, valide opções e construa a lista a partir de dados estruturados.

COMANDOS = {
    "listar": ["python", "-m", "meu_app", "listar"],
    "status": ["python", "-m", "meu_app", "status"],
}

acao = entrada.get("acao")
if acao not in COMANDOS:
    raise ValueError("Ação não permitida")

subprocess.run(COMANDOS[acao], check=True)

Como shlex.split() trata aspas

Aspas simples e duplas protegem espaços e alguns caracteres:

import shlex

print(shlex.split("echo 'duas palavras'"))
print(shlex.split('echo "duas palavras"'))
print(shlex.split(r'echo arquivo\ com\ espaco.txt'))

No modo POSIX padrão, as aspas são removidas e os caracteres escapados são interpretados. Uma aspa não fechada gera ValueError, portanto trate entrada inválida explicitamente.

try:
    argumentos = shlex.split('echo "texto incompleto')
except ValueError as erro:
    print("Linha inválida:", erro)

Comentários com o parâmetro comments

Por padrão, shlex.split() não interpreta # como início de comentário.

linha = "executar tarefa # comentário"
print(shlex.split(linha))

Para arquivos de configuração simples, habilite comentários:

print(shlex.split(linha, comments=True))
# ['executar', 'tarefa']

Escolha conscientemente: em nomes de arquivos e hashtags, # pode fazer parte do argumento.

Reconstruindo com shlex.join()

shlex.join() recebe uma lista de tokens e gera uma linha escapada para shells Unix.

import shlex

args = ["echo", "-n", "duas palavras", "arquivo;inofensivo"]
linha = shlex.join(args)
print(linha)

A função foi adicionada no Python 3.8 e é a inversa prática de split() para tokens compatíveis.

assert shlex.split(shlex.join(args)) == args

Ela é excelente para logs legíveis. Ainda assim, se você já possui a lista, entregue-a diretamente ao subprocesso em vez de reconstruir uma string e executá-la.

Escapando um único token com quote()

shlex.quote() transforma uma string em um token seguro para uma linha de shell POSIX.

from shlex import quote

arquivo = "relatorio; rm -rf ~"
linha = f"cat {quote(arquivo)}"
print(linha)

O ponto e vírgula fica dentro de aspas, impedindo que vire separador de comandos em um shell Unix. A documentação oficial de shlex adverte que essa proteção não é garantida para shells não POSIX, incluindo o shell do Windows.

quote() não substitui uma lista

Escapar manualmente é mais complexo quando há múltiplas camadas, como SSH, sh -c ou comandos remotos. A regra preferida continua sendo:

subprocess.run(["cat", arquivo], shell=False, check=True)

Use quote() apenas quando uma interface exige inevitavelmente uma única string interpretada por um shell POSIX.

Limitações no Windows

shlex modela sintaxe de shells Unix. cmd.exe e PowerShell possuem regras próprias de aspas, escapes, variáveis e operadores. Uma string protegida com shlex.quote() pode ser insegura ou simplesmente incorreta no Windows.

Para programas executáveis, passe uma lista a subprocess.run(..., shell=False). Para comandos internos como dir, crie uma implementação específica para a plataforma e evite misturar entrada não confiável com shell=True.

Usando a classe shlex como iterador

A função split() cobre casos comuns. Para uma sintaxe personalizada, use a classe:

import shlex

lexer = shlex.shlex(
    'copiar "arquivo origem.txt" destino/',
    posix=True,
)
lexer.whitespace_split = True

for token in lexer:
    print(token)

O objeto lê de uma string ou de um fluxo que implemente read() e readline().

Modo POSIX e modo de compatibilidade

A classe shlex.shlex usa posix=False por padrão por compatibilidade histórica, enquanto shlex.split() usa posix=True. No modo POSIX, aspas são removidas, escapes são interpretados e strings vazias entre aspas são aceitas.

import shlex

texto = 'comando "" "a b"'
print(list(shlex.shlex(texto, posix=False)))
print(list(shlex.shlex(texto, posix=True)))

Defina o modo explicitamente em parsers persistentes para evitar mudanças acidentais de comportamento.

whitespace_split

Quando whitespace_split=True, tokens são separados principalmente por espaços e pela pontuação configurada.

lexer = shlex.shlex('enviar --nome "Ana Silva"', posix=True)
lexer.whitespace_split = True
print(list(lexer))

Essa configuração aproxima o resultado de uma lista de argumentos de linha de comando.

punctuation_chars

O parâmetro punctuation_chars permite devolver sequências de operadores como tokens separados.

lexer = shlex.shlex(
    "tarefa && validar || cancelar; fim",
    posix=True,
    punctuation_chars=True,
)
lexer.whitespace_split = True
print(list(lexer))

Os caracteres ();<>|& são tratados como pontuação quando o valor é True. Também é possível fornecer uma string específica. A propriedade só pode ser definida na criação.

Parsing não é validação semântica

O lexer pode devolver uma sequência como >>>, mesmo que nenhum shell a reconheça como operador válido. Depois de tokenizar, valide a gramática permitida.

OPERADORES = {"&&", "||", ";"}

for token in lexer:
    if token.startswith(("|", "&", ";")) and token not in OPERADORES:
        raise ValueError(f"Operador inválido: {token}")

Para uma linguagem complexa, use um parser formal em vez de tentar reproduzir um shell completo.

Personalizando comentários, espaços e palavras

Instâncias expõem atributos como commenters, whitespace, quotes, escape e wordchars.

lexer = shlex.shlex("chave=valor ; comentário", posix=True)
lexer.commenters = ";"
lexer.whitespace_split = True
print(list(lexer))

Modificar essas regras permite criar arquivos de controle simples. Documente a sintaxe e escreva testes, porque pequenas mudanças podem alterar tokens existentes.

Minilinguagem controlada

Um uso legítimo de shlex é interpretar uma sintaxe própria, sem executar diretamente um shell.

def interpretar(linha: str) -> dict:
    tokens = shlex.split(linha, comments=True)
    if not tokens:
        return {"acao": "vazio"}

    acao, *args = tokens
    if acao not in {"copiar", "mover", "listar"}:
        raise ValueError("Comando desconhecido")
    return {"acao": acao, "argumentos": args}

O programa transforma tokens em operações internas e aplica permissões próprias.

Inclusão de arquivos e source

A classe possui suporte a fontes empilhadas e um atributo source. Quando habilitado, um token pode solicitar leitura de outro arquivo. Isso é poderoso, mas perigoso com caminhos controlados pelo usuário.

Restrinja diretórios, resolva caminhos, bloqueie travessia com .., limite profundidade e detecte ciclos. Para configurações comuns, configparser, TOML ou JSON oferecem estruturas mais explícitas.

Mensagens de erro com arquivo e linha

Os atributos infile e lineno ajudam a gerar diagnósticos. error_leader() cria um prefixo no estilo de compiladores Unix.

lexer = shlex.shlex('copiar "sem fechar', infile="tarefas.conf", posix=True)
try:
    list(lexer)
except ValueError as erro:
    print(lexer.error_leader() + str(erro))

Mensagens com localização tornam arquivos de configuração muito mais fáceis de corrigir.

Unicode e nomes de arquivos

O modo POSIX inclui caracteres Latin-1 em wordchars, mas nomes Unicode modernos devem ser testados. whitespace_split=True costuma preservar melhor caminhos e argumentos gerais entre aspas.

Não normalize nomes de arquivo sem entender o sistema de arquivos. Formas Unicode visualmente iguais podem ter sequências de pontos de código diferentes.

Logs seguros de comandos

shlex.join(args) produz uma representação legível, mas logs podem conter senhas, tokens e caminhos pessoais.

def mascarar(args):
    resultado = list(args)
    for indice, token in enumerate(resultado[:-1]):
        if token in {"--password", "--token"}:
            resultado[indice + 1] = "***"
    return resultado

logger.info("Executando: %s", shlex.join(mascarar(args)))

Mascarar deve acontecer antes de registrar. A string de log não deve ser reutilizada para execução.

Testando round-trip e entradas hostis

casos = [
    ["echo", "duas palavras"],
    ["cat", "arquivo;seguro"],
    ["printf", "%s", ""],
    ["programa", "çãõ", "a'b"],
]

for args in casos:
    assert shlex.split(shlex.join(args)) == args

Inclua aspas não fechadas, barras invertidas, linhas vazias, comentários, Unicode e payloads com ;, &, |, $() e crases.

Erros frequentes

  • Usar str.split() para linhas com aspas.
  • Acreditar que tokenizar autoriza executar o comando.
  • Construir uma string para shell=True quando uma lista serviria.
  • Usar shlex.quote() como solução multiplataforma.
  • Executar diretamente o resultado de entrada do usuário.
  • Confundir parsing com validação de operadores.
  • Permitir inclusão de arquivos sem restringir caminhos.
  • Registrar argumentos secretos.

Boas práticas

  • Prefira listas de argumentos com shell=False.
  • Use split() para interpretar linhas POSIX confiáveis.
  • Use join() para exibição e diagnóstico.
  • Use quote() apenas para um token em shell Unix inevitável.
  • Valide executáveis, opções e operadores.
  • Separe parsers de executores.
  • Defina sintaxe e limites para minilinguagens.
  • Teste Linux e Windows separadamente.

Conclusão

O módulo shlex no Python resolve a tokenização de linhas com aspas, escapes e sintaxe semelhante a shells Unix. split() transforma texto em argumentos, join() cria uma representação escapada e quote() protege um token em contextos POSIX específicos. A classe shlex permite construir analisadores simples e configuráveis.

A principal regra de segurança é não depender de uma string de shell quando uma lista de argumentos atende ao caso. Parsing correto evita erros com espaços e aspas, mas somente validação explícita controla o que pode ser executado. Com subprocess, shell=False, listas permitidas e testes de entradas hostis, shlex ajuda a criar CLIs e minilinguagens úteis sem transformar texto em uma porta para injeção de comandos.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Conteúdo do artigo

    Artigos relacionados

    Banco de dados local representando persistência com shelve no Python
    Bibliotecas e Módulos
    Foto de perfil de Leandro Hirt da Academify

    shelve no Python: persistência simples

    Aprenda shelve no Python para persistir objetos, atualizar dados mutáveis, evitar riscos de pickle e saber quando migrar para SQLite.

    Ler mais

    Tempo de leitura: 5 minutos
    01/08/2026
    Documentos de texto representando comparação de versões com difflib no Python
    Bibliotecas e Módulos
    Foto de perfil de Leandro Hirt da Academify

    difflib no Python: compare textos e arquivos

    Aprenda difflib no Python para comparar textos, medir similaridade, criar diffs unificados, relatórios HTML e sugestões de nomes.

    Ler mais

    Tempo de leitura: 7 minutos
    01/08/2026
    Painel de gráficos representando análise estatística de dados no Python
    Bibliotecas e Módulos
    Foto de perfil de Leandro Hirt da Academify

    statistics no Python: análise de dados

    Aprenda statistics no Python para média, mediana, desvio padrão, quantis, correlação, regressão, NormalDist e KDE.

    Ler mais

    Tempo de leitura: 8 minutos
    31/07/2026
    Gráficos de frações representando números racionais exatos no Python
    Bibliotecas e Módulos
    Foto de perfil de Leandro Hirt da Academify

    fractions no Python: números racionais

    Aprenda fractions no Python para cálculos racionais exatos, simplificação, limit_denominator, formatação e conversões seguras.

    Ler mais

    Tempo de leitura: 7 minutos
    31/07/2026
    Calculadora e documentos representando cálculos decimais precisos no Python
    Bibliotecas e Módulos
    Foto de perfil de Leandro Hirt da Academify

    Decimal no Python: cálculos precisos

    Aprenda Decimal no Python para cálculos exatos, dinheiro, quantize, arredondamento, contextos e validação sem erros de float.

    Ler mais

    Tempo de leitura: 8 minutos
    30/07/2026
    Código digital representando identificadores UUID únicos e ordenáveis no Python
    Bibliotecas e Módulos
    Foto de perfil de Leandro Hirt da Academify

    uuid no Python: IDs únicos e ordenáveis

    Aprenda uuid no Python: versões 4, 5, 6 e 7, validação, bancos de dados, IDs ordenáveis e cuidados de segurança.

    Ler mais

    Tempo de leitura: 8 minutos
    30/07/2026