Como você executa um comando nativo arbitrário a partir de uma string?

208

Posso expressar minha necessidade com o seguinte cenário: Escreva uma função que aceite uma string para ser executada como um comando nativo.

Não é uma idéia muito longe buscada: se você estiver em interface com outros utilitários de linha de comando de outras partes da empresa que fornecem um comando para executar literalmente. Como você não controla o comando, você precisa aceitar qualquer comando válido como entrada . Estes são os principais soluços que não consegui superar facilmente:

  1. O comando pode executar um programa vivendo em um caminho com um espaço nele:

    $command = '"C:\Program Files\TheProg\Runit.exe" Hello';
  2. O comando pode ter parâmetros com espaços neles:

    $command = 'echo "hello world!"';
  3. O comando pode ter marcações simples e duplas:

    $command = "echo `"it`'s`"";

Existe alguma maneira limpa de fazer isso? Eu só consegui conceber soluções generosas e feias, mas para uma linguagem de script, acho que isso deve ser simples.

Johnny Kauffman
fonte

Respostas:

318

Invoke-Expression, também conhecido como iex. A seguir, trabalharemos nos seus exemplos 2 e 3:

iex $command

Algumas strings não serão executadas como estão, como no exemplo 1, porque o exe está entre aspas. Isso funcionará como está, porque o conteúdo da string é exatamente como você a executaria diretamente em um prompt de comando do Powershell:

$command = 'C:\somepath\someexe.exe somearg'
iex $command

No entanto, se o exe estiver entre aspas, você precisará da ajuda &para executá-lo, como neste exemplo, como executado na linha de comando:

>> &"C:\Program Files\Some Product\SomeExe.exe" "C:\some other path\file.ext"

E então no script:

$command = '"C:\Program Files\Some Product\SomeExe.exe" "C:\some other path\file.ext"'
iex "& $command"

Provavelmente, você poderia lidar com quase todos os casos, detectando se o primeiro caractere da sequência de comandos é ", como nesta implementação ingênua:

function myeval($command) {
    if ($command[0] -eq '"') { iex "& $command" }
    else { iex $command }
}

Mas você pode encontrar outros casos que precisam ser invocados de maneira diferente. Nesse caso, você precisará usartry{}catch{} , talvez para tipos / mensagens de exceção específicos, ou examinar a sequência de comandos.

Se você sempre receber caminhos absolutos em vez de relativos, não deverá ter muitos casos especiais, se houver, fora dos 2 acima.

Joel B Fant
fonte
3
Aliasing é ótimo. Lembre-se, se você se mudar para outra máquina ou enviar esse script para outra pessoa, o alias provavelmente não será configurado. Prefira os nomes completos das funções do PowerShell.
precisa
1
@ Doug: Na maioria das vezes eu faço isso ou uso os aliases internos (especialmente por questões de brevidade na linha de comando). O problema evalé meio brincalhão, porque é assim que é chamado em muitas outras linguagens de script, e essa não é a primeira pergunta que eu vi sobre onde alguém não tinha idéia invoke-expression. E o caso do OP parece apenas um script interno.
Joel B Fant
Eu tentei isso, mas ele não funciona com: $ command = '"C: \ Arquivos de programas \ Windows Media Player \ mplayer2.exe" "H: \ Áudio \ Música \ Stevie Wonder \ Stevie Wonder - Stevie Wonder - Superstition.mp3" ''
Johnny Kauffman
3
Você pode ter que colocar um "&" ou "." assinar antes do comando real se não for powershell-natal, por exemploInvoke-Expression "& $command"
Torbjörn Bergstedt
Vitórias de Torbjörn Bergstedt! O mágico extra "&" resolveu meu problema! Como resultado, estou feliz e confusa.
Johnny Kauffman
19

Consulte também este relatório do Microsoft Connect sobre essencialmente como é difícil usar o PowerShell para executar comandos do shell (oh, a ironia).

http://connect.microsoft.com/PowerShell/feedback/details/376207/

Eles sugerem o uso --% como uma maneira de forçar o PowerShell a parar de tentar interpretar o texto à direita.

Por exemplo:

MSBuild /t:Publish --% /p:TargetDatabaseName="MyDatabase";TargetConnectionString="Data Source=.\;Integrated Security=True" /p:SqlPublishProfilePath="Deploy.publish.xml" Database.sqlproj
Luke Puplett
fonte
1
Ah, e a Microsoft quebrou a internet e o link não é mais válido.
Johan Boulé 24/05
1
O link acima está em archive.org em web.archive.org/web/20131122050220/http://…
mwfearnley
4

A resposta aceita não estava funcionando para mim ao tentar analisar o registro para desinstalar seqüências de caracteres e executá-las. Afinal, eu não precisava da ligação, Invoke-Expressionafinal.

Finalmente me deparei com este belo modelo para ver como executar seqüências de desinstalação:

$path = 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
$app = 'MyApp'
$apps= @{}
Get-ChildItem $path | 
    Where-Object -FilterScript {$_.getvalue('DisplayName') -like $app} | 
    ForEach-Object -process {$apps.Set_Item(
        $_.getvalue('UninstallString'),
        $_.getvalue('DisplayName'))
    }

foreach ($uninstall_string in $apps.GetEnumerator()) {
    $uninstall_app, $uninstall_arg = $uninstall_string.name.split(' ')
    & $uninstall_app $uninstall_arg
}

Isso funciona para mim, principalmente porque $appé um aplicativo interno que eu sei que só terá dois argumentos. Para cadeias de desinstalação mais complexas, convém usar o operador de junção . Além disso, eu apenas usei um mapa de hash, mas, na verdade, você provavelmente desejaria usar uma matriz.

Além disso, se você tiver várias versões do mesmo aplicativo instaladas, esse desinstalador percorrerá todas elas de uma só vez, o que confunde MsiExec.exe, então é isso também.

Droogans
fonte
1

Se você deseja usar o operador de chamada, os argumentos podem ser uma matriz armazenada em uma variável:

$prog = 'c:\windows\system32\cmd.exe'
$myargs = '/c','dir','/x'
& $prog $myargs

O operador de chamada também trabalha com objetos ApplicationInfo.

$prog = get-command cmd
$myargs = -split '/c dir /x'
& $prog $myargs
js2010
fonte