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)) == argsEla é 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)) == argsInclua 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=Truequando 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.







