Executando comandos shell pelo PHP

Executar comandos pelo shell é um recurso poderoso, que deve ser usado com cuidado. No PHP existem diversas funções para fazer isso, como exec
, shell_exec
, system
, passthru
, proc_open
.
Lembrando que podemos desabilitar estas funções no php.ini
na diretiva disable_functions, inclusive se você não irá utilizá-las sempre é uma boa ideia manter desabilitada.
Vamos ver primeiro as funções exec
, shell_exec
, system
que são um pouco parecidas.
exec(string $command
, array &$output
= ?, int &$return_var
= ?): string
A função exec
executa um comando passado como primeiro parâmetro e retorna a última linha da saída dele. Se for passada uma variável como segundo parâmetro, ele irá atribuir a essa variável(ela é passada por referência) todas a linhas de saída como um array. Se for passada uma variável como terceiro parâmetro, ele irá atribuir o código de status de retorno do comando para ela.
Veja um exemplo de execução do comando ls
em um sistema linux.
$lastLine = exec('ls -la',$out ,$resultCode);
echo "Última linha: $lastLine" . PHP_EOL;
echo "ResultCode: $resultCode" . PHP_EOL;
echo "Saida completa do comando" . PHP_EOL;
print_r($out);
shell_exec(string $cmd
): string
A função shell_exec
executa um comando passado e retorna toda a saída deste comando como uma string.
echo shell_exec('ls -la');
A função shell_exec
tem o mesmo comportamento do operador backticks ou operador de execução, que executa como um comando o que estiver entre acentos graves ( ` ` ) .
echo `ls -la`;
system(string $command
, int &$return_var
= ?): string
A função system
executa um comando e exibe a saída como texto dele automáticamente, retornando a última linha. Se o segundo parâmetro for passado ele atribui para esta variável o código de status de retorno do comando para ela.
$out = system('ls -la', $resultCode);
echo PHP_EOL.PHP_EOL;
echo "Última linha: $out" . PHP_EOL;
echo "Código de Status: $resultCode";
passthru(string $command
, int &$return_var
= ?): void
A função passthru
executa um comando e exibe a saída crua automaticamente. Se o segundo parâmetro for passado, ele atribui para esta variável o código de status de retorno do comando para ela.
passthru('ls -la');
proc_open(string $cmd
, array $descriptorspec
, array &$pipes
, string $cwd
= ?, array $env
= ?, array $other_options
= ? ): resource
A função proc_open
como as outras executa um comando, mas ela permite um maior controle, fornecendo ponteiros para a entrada e saída, o envio de variáveis de ambiente para o processo e em qual diretório o comando irá ser executado.
$cmd: é o comando a ser executado.
$descriptorspec: é uma matriz contendo a descrição de como os pipes para fazer a comunicação com o processo serão criados. A chave representa o número do descritor e o valor representa como o PHP passará este descritor para o processo filho. A chave 0 é stdin, 1 é stdout, enquanto 2 é stderr. Os pipes suportados atualmente são file
e pipe
, sendo file um pipe nomeado(como um arquivo) ou um pipe anonimo. Junto com o tipo também informamos se o pipe é de leitura “r”, escrita “w” ou “append” “a”.
&$pipes: é um array onde os ponteiros de entrada e saida serão gravados.
$cwd
: O diretório onde será executado o comando. Se null será executado no diretório corrente.
$env
: um array com as variáveis de ambientes a serem passadas para o comando.
Para testar vamos criar um script PHP(para ficar mais fácil a reprodução) que lê um dois valores da entrada padrão e os exibe com uma variável de ambiente.
<?php
//script.php
$nome = fgets(STDIN);
echo "Entrada do nome: $nome" . PHP_EOL;
$sobrenome = fgets(STDIN);
echo "Entrada do sobrenome: $sobrenome" . PHP_EOL;
echo "Variável de ambiente: " .$_ENV['env_var'] . PHP_EOL;
throw new Exception('Um erro');
Agora vamos executar este script pelo phpcli utilizando a função proc_open
:
$descriptorspec = [
0 => ["pipe","r"], // stdin
1 => ["pipe","w"], // stdout
2 => ["file","./erros.txt", 'a'] //stderr
];
$env = ['env_var'=>'valor'];
$proc = proc_open("php script.php",
$descriptorspec,
$pipes,
'scripts',
$env
);
fwrite($pipes[0],"Rodrigo\n");
fwrite($pipes[0],"Aramburu\n");
while (!feof($pipes[1])) {
echo fgets($pipes[1]);
}
fclose($pipes[0]);
fclose($pipes[1]);
proc_close($proc);
Como podemos ver criamos na linha 1 a matriz de descritores, onde informamos que o primeiro pipe será um pipe anônimo de leitura, este será a entrada do programa, o segundo também será um pipe anônimo, mas de escrita sendo a saída do processo e por último um pipe nomeado, sendo um arquivo configurado para ser de append que será a saída de erro, “como um log”.
Na linha 7 criamos uma array com variáveis de ambiente para passar para o processo.
Na linha 9 executamos a função proc_open
passando o comando a ser executado, a nossa matriz de descritores criada na linha 1, a variável $pipes (que ao ser criado o processo, o pipes especificados serão atribuídos em forma de array), o diretório onde iremos executar nosso comando(obs. o script.php esta dentro de uma pasta script no diretório atual) e por fim o array de variáveis de ambiente.
Na linha 16 e 17 utilizamos a função fwrite
para escrever no pipe de leitura, isso pode parecer estranho, mas os pipes são do ponto de vista do processo, então escrevemos no pipe que o processo vai ler e lemos do pipe que o processo vai escrever. Na linha 18 fazemos um laço para ler todo o conteúdo do pipe de saída.
Por fim na linhas 22 e 23 fechamos os pipes de entrada e saida e na linha 24 encerramos o recurso do processo.
Lembrando que no script que estamos executando lançamos uma exceção para teste, a saída deste erro pode ser vista no arquivo erros.txt dentro da pasta script, visto que configuramos para o stderr ser passado para este arquivo.
Symfony Process
Embora o proc_open forneça bastante controle para a execução de um processo, podemos utilizar uma biblioteca para fornecer isso de uma forma mais fácil. O componente Process do Symfony pode ser utilizado de maneira independente, fornecendo uma API mais simples(para mim pelo menos) para executar comandos.
Primeiro instalamos o componente via composer.
composer require symfony/proces
E então criamos um script para executar um comando.
include './vendor/autoload.php';
use Symfony\Component\Process\Process;
$process = new Process(['ls', '-la']);
$process->run();
echo $process->getOutput();
Primeiro criamos um objeto Process
, ele recebe um array com o comando e seus parâmetros que desejamos executar, mas ele não é executado na criação do objeto, então chamamos o método run
que irá efetivamente iniciar a execução o comando. Para obtermos a saída do comando utilizamos o método getOutput
e para a saída de erro getErrorOutput
.
Também podemos especificar o diretório em que o comando irá ser executado passando-o como segundo parâmetro para o construtor do objeto Process, e variáveis de ambiente no terceiro parâmetro como um array.
Podemos executar o comando de forma assíncrona utilizando o método start
ao invés do run
e verificar se o processo ainda está em execução através do método isRunning
.
$process = new Process(['sleep','10']);
$process->start();
while($process->isRunning()){
echo "Ainda Executando, aguarde". PHP_EOL;
sleep(1);
}
echo $process->getOutput();
echo $process->getErrorOutput();
Para fazer a entrada de dados do processo, podemos passar as entradas para o quarto parâmetro do construtor, ele aceita uma string, um recurso stream ou um objeto Trasversable. Então a chamada do nosso script que usamos antes que recebe um nome e sobrenome poderia ser assim.
$process = new Process(['php', 'script.php'], '../scripts', ['env_var'=>'teste'], "Rodrigo\nAramburu");
$process->run();
echo $process->getOutput();
echo "-------------------". PHP_EOL;
echo $process->getErrorOutput();
Podemos ao invés de passar a entrada no construtor passa-lá através do método setInput
do objeto Process após ele ser criado.
Caso estivermos executando um processo de forma assíncrona podemos fazer a entrada de dados utilizando uma classe fornecida pelo componente chamada InputStream
. Veja o exemplo.
$process = new Process(['php', 'script.php'], '../scripts', ['env_var'=>'teste']);
$input = new InputStream();
$process->setInput($input);
$process->start();
$input->write('Rodrigo');
$input->write('Aramburu');
$input->close();
$process->wait();
echo $process->getOutput();
echo "-------------------". PHP_EOL;
echo $process->getErrorOutput();
Acima criamos um objeto InputStream
e adicionamos ao objeto Process, através do método setInput
, antes de iniciarmos a execução do processo. Após, chamamos o método write
para passar cada uma das entradas para o processo.
Quando um processo está sendo executado de forma assíncrona podemos utilizar o método wait
para esperar até que o processo seja executado até o final. Ou podemos esperar até que uma determinada saída seja passada através do método waitUntil
que recebe uma função que deve retornar true quando não seja mais necessário esperar.
$process->start();
$process->waitUntil(function ($type, $output) {
return $output === 'Entrada do nome:';
});
$input->write('Rodrigo');
Podemos também especificar um timeout para um processo no construtor do objeto Process(5º parâmetro), se o processo durar mais que o timeout estipulado ele irá lançar uma exceção(ProcessTimedOutException
).
$process = new Process(['sleep', '30'], null, null, null, 10);
$process->run();
O componente Process possui uma classe auxiliar(ExecutableFinder
) que ajuda a encontrar o caminho absoluto de um executável.
$finder = new ExecutableFinder();
$path = $finder->find('composer');
echo $path;
Bom essa foi uma pequena introdução a execução de comandos no shell, mais detalhes podem ser encontrados na página de documentação do PHP e na documentação do componente Process, que possui vários outros recursos.
Sempre lembrando que executar comandos shell pode ser um problema de segurança, principalmente se utilizar entradas que o usuário do sistema forneceu, então evite se puder senão valide e sanitize as entradas como se não houvesse amanhã, as funções escapeshellarg
e escapeshellcmd
podem ajudar. E se não for usar desabilite.
T++