Tenho um executável que recebe vários argumentos de linha de comando e quero escrever um .cmd que execute algumas verificações e depois passe todos os argumentos para o executável.
Como exemplo, veja o executável echo_args.exe
que é este aplicativo Rust:
use std::env;
fn main() {
// Collect the command line arguments into a vector
let args: Vec<String> = env::args().collect();
// Iterate over the arguments and print each one
for arg in args.iter() {
println!("{}", arg);
}
}
O fato de ser Rust não tem relação com a questão, mas dessa forma você pode reproduzir minha situação.
Chamando este aplicativo diretamente do PowerShell:
echo_args.exe "hello `"`"world`"`""
Impressões, como esperado:
echo_args.exe
hello ""world""
Chamando este aplicativo diretamente do prompt de comando:
echo_args.exe "hello """"world"""""
Impressões, como esperado:
echo_args.exe
hello ""world""
Em ambos os casos, é usada uma maneira correta de escapar aspas duplas, apropriada ao processador de comandos.
Entretanto, se eu escrevo um test.cmd
arquivo, encontro o problema de que ele se comporta de forma diferente quando chamado pelo PowerShell ou pelo Prompt de Comando.
A solução ingênua é:
@echo off
set exe=echo_args.exe
"%exe%" %*
E embora isso funcione quando chamado do Prompt de Comando:
test "hello """"world"""""
Não vem do PowerShell porque:
test "hello `"`"world`"`""
Agora imprime:
echo_args.exe
hello "world"
Observe que as aspas duplas são "comidas" pelo processador de comando que executa o .cmd (ou seja, cmd.exe
). Então, o Powershell passa para o hello ""world""
to cmd.exe
(como faria para echo_args.exe
) e cmd.exe
deduz as aspas duplas e passa hello "world"
para o .exe.
Agora, percebo que posso evitar isso escrevendo um test.ps1
que execute a mesma função, para que o .cmd não seja chamado pelo PowerShell, e para fins práticos isso é bom, mas minha pergunta é: existe uma maneira de escrever o .cmd para que ele se comporte corretamente, independentemente de ser iniciado cmd.exe
automaticamente com o PowerShell ou se for encontrado e executado diretamente pelo Prompt de Comando?
Detectar se cmd.exe
foi iniciado do PowerShell parece propenso a erros e complicado. E não vejo uma maneira direta de (re)construir os argumentos no .cmd que evite esse problema (já que o problema acontece essencialmente antes mesmo que o código seja executado). O que estou esquecendo? Perguntei a alguns LLMs (ChatGPT 4o e Claude Sonnet 3.5), mas eles insistem em provar sua inutilidade para problemas que exigem alguma nuance e apresentam uma série infinita de não soluções.
A triste realidade no Windows PowerShell (a edição legada, fornecida com o Windows e exclusiva para Windows, do PowerShell, cuja versão mais recente é a 5.1), bem como nas versões (agora obsoletas) do PowerShell (Core) 7 (até a v7.2.x), é que uma camada extra e manual
\
de escape de caracteres incorporados"
é necessária em argumentos passados para programas externos .Isso foi corrigido no PowerShell v7.3+ , com exceções seletivas no Windows. Portanto, no PowerShell 7.3+ , suas chamadas funcionam conforme o esperado , EXCETO se você chamar um arquivo em lote , entre outros programas legados.
O comportamento antigo e quebrado ainda está disponível como opt-in e, por padrão, ainda se aplica seletivamente a certos programas , notavelmente arquivos em lote. Você pode evitar isso definindo a
$PSNativeCommandArgumentPassing
variável de preferência para'Standard'
, mas isso vem com ressalvas ; veja a próxima seção.Veja esta resposta para mais detalhes.
Portanto, no Windows PowerShell (e nas versões obsoletas do PowerShell 7.2; no PowerShell 7.3+, as chamadas funcionam de forma análoga com as
\
instâncias removidas ):Como alternativa, como nenhuma interpolação de string é necessária no seu caso, usar uma string literal
'...'
( ) elimina a necessidade de escape do PowerShell :Entretanto, mesmo quando uma string expansível
"..."
(interpoladora) ( ) é necessária, há uma maneira de evitar a necessidade de escape do PowerShell por meio do uso da variante here-string (invariavelmente multilinha)Finalmente, para fins de completude, ambas as edições do PowerShell (todas as versões) oferecem
--%
, o chamado stop-parsing token , que essencialmente copia o que o segue verbatim para a linha de comando do processo construída nos bastidores. Isso acarreta limitações severas, no entanto, notavelmente a incapacidade de referenciar variáveis do PowerShell - veja a seção inferior desta resposta para detalhes:Advertência:
Advertência sobre o PowerShell 7.3+ ao invocar um arquivo em lote :
Como o comportamento antigo e quebrado por padrão (
$PSNativeCommandArgumentPassing
defaults to'Windows'
) ainda se aplica a arquivos em lote (entre outros interpretadores legados), as soluções alternativas acima ainda são necessárias por padrão no PowerShell 7.3+.Conforme observado, a configuração
$PSNativeCommandArgumentPassing = 'Standard'
desativa esse comportamento e executa\"
o escape de"
caracteres incorporados para todos os programas externos nos bastidores, incluindo arquivos em lote.No entanto, o uso de
\"
pode causar problemas em arquivos em lote : comocmd.exe
não reconhece a\"
como escape ,cmd.exe
metacaracteres como&
inside\"...\"
embedded em geral"..."
podem resultar em erros de sintaxe; além disso, ao processar argumentos individuais usando a linguagem em lote (em vez de apenas passar todos os argumentos para outro programa, com$*
), apenas""
o escape -escaping é reconhecido; a proposta mencionada abaixo teria evitado esse problema usando""
o escape -escaping para arquivos em lote nos bastidores.Há outra armadilha , que não está relacionada
$PSNativeCommandArgumentPassing
e afeta todas as versões de ambas as edições do PowerShell: como o PowerShell só emprega"..."
o encapsulamento se o valor literal a ser passado contiver espaços na linha de comando do processo, uma chamada comobatch.cmd 'http://example.org?foo=1&bar=2'
from PowerShell quebra o arquivo em lote, porque este último recebe o argumentohttp://example.org?foo=1&bar=2
unquoted .batch.cmd '"http://example.org?foo=1&bar=2"'
.Novamente, a proposta mencionada a seguir teria evitado esse problema.
O problema #15143 do GitHub é uma proposta detalhada que teria evitado toda essa confusão no futuro, por meio de acomodações seletivas para intérpretes legados como
cmd.exe
e CLIs não padrão comomsiexec.exe
. Infelizmente, não deu em nada.Quanto às suas observações e perguntas :
Conclui-se do exposto acima que o Windows PowerShell é o culpado, então você deve compensar seu comportamento problemático na invocação .
Não, não é
cmd.exe
isso que "come" as aspas duplas, é, na verdade, a maneira quebrada como o Windows PowerShell coloca o valor literal que ele analisou de acordo com suas regras de sintaxe na linha de comando do processo que ele usa para chamar programas externos nos bastidores.cmd.exe
Ele passa tudo o que recebe no estado em que se encontra , com apenas uma interpretação mínima (nenhuma no caso em questão).Especificamente, a linha de comando do processo - quebrado - que o Windows PowerShell constrói é:
Isso está quebrado, porque o (Windows) PowerShell - que necessariamente deve passar uma string com aspas duplas para programas externos, já que somente essa forma de aspas pode ser entendida por CLIs do Windows - negligencia escapar do
"
.Ou seja, para passar verbatim
hello ""world""
para um programa externo, ou"hello """"world"""""
ou, mais tipicamente,"hello \"\"world\"\""
deve ser colocado na linha de comando do processo.O PowerShell 7.3+ agora executa esse escape usando
\"
(mas, como observado, por padrão, não para arquivos em lote ).