Executando comandos shell no 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++