ptlis/shell-command

壳命令执行的基本包装器。

1.3.0 2021-03-13 20:46 UTC

README

一个友好的开发者壳命令执行包装器。

以下目标激发了该包的创建

  • 使用 命令模式 封装执行壳命令所需的数据,允许命令在以后传递和执行。
  • 维护一个无状态的对象图,允许(例如)从单个命令启动多个运行进程。
  • 提供同步和异步使用的干净 API。
  • 可以将运行进程包装在承诺中,以允许轻松组合。

Build Status codecov Latest Stable Version

安装

从终端

$ composer require ptlis/shell-command

使用方法

构建器

该软件包包含一个命令构建器,提供了一种简单、安全的方法来构建命令。

use ptlis\ShellCommand\CommandBuilder;

$builder = new CommandBuilder();

构建器将在构造时尝试确定您的环境,您可以通过指定环境作为第一个参数来覆盖此操作

use ptlis\ShellCommand\CommandBuilder;
use ptlis\ShellCommand\UnixEnvironment;

$builder = new CommandBuilder(new UnixEnvironment());

注意:此构建器是不可变的 - 方法调用必须链接,并以对 buildCommand 的调用结束,如下所示

$command = $builder
    ->setCommand('foo')
    ->addArgument('--bar=baz')
    ->buildCommand()

设置命令

首先,我们必须提供要执行的命令

$builder->setCommand('git')             // Executable in $PATH
    
$builder->setCommand('./local/bin/git') // Relative to current working directory
    
$builder->setCommand('/usr/bin/git')    // Fully qualified path

$build->setCommand('~/.script.sh')      // Path relative to $HOME

如果命令不可定位,则抛出 RuntimeException

设置进程超时

超时(以微秒为单位)设置库在终止进程之前将等待多长时间。默认为 -1,表示永远不会强制终止。

$builder
    ->setTimeout(30 * 1000 * 1000)          // Wait 30 seconds

如果进程执行时间超过此值,则发送 SIGTERM;如果进程在此之后进一步等待 1 秒后仍未终止,则发送 SIGKILL。

设置轮询超时

设置轮询进程状态之间的等待时间(以微秒为单位)。默认为 1,000,000(1 秒)。

$builder
    ->setPollTimeout(30 * 1000 * 1000)          // Wait 30 seconds

设置工作目录

您可以设置命令的工作目录

$builder
    ->setCwd('/path/to/working/directory/')

添加参数

添加参数以调用命令(所有参数均被转义)

$builder
    ->addArgument('--foo=bar')

有条件地添加,取决于表达式的结果

$builder
    ->addArgument('--foo=bar', $myVar === 5)

添加多个参数

$builder
    ->addArguments([
        '--foo=bar',
        '-xzcf',
        'if=/dev/sda of=/dev/sdb'
    ])

有条件地添加,取决于表达式的结果

$builder
    ->addArguments([
        '--foo=bar',
        '-xzcf',
        'if=/dev/sda of=/dev/sdb'
    ], $myVar === 5)

注意:转义和原始参数按添加到构建器的顺序添加到命令中。这适用于对参数顺序敏感的命令。

添加原始参数

警告:不要将这些方法传递用户提供的数据!恶意用户可以轻松执行任意壳命令。

参数也可以在不转义的情况下应用

$builder
    ->addRawArgument("--foo='bar'")

有条件地,根据表达式的结果

$builder
    ->addRawArgument('--foo=bar', $myVar === 5)

添加多个原始参数

$builder
    ->addRawArguments([
        "--foo='bar'",
        '-xzcf',
    ])

有条件地,根据表达式的结果

$builder
    ->addRawArguments([
        '--foo=bar',
        '-xzcf',
        'if=/dev/sda of=/dev/sdb'
    ], $myVar === 5)

注意:转义和原始参数按添加到构建器的顺序添加到命令中。这适用于对参数顺序敏感的命令。

添加环境变量

在运行命令时可以设置环境变量

$builder
    ->addEnvironmentVariable('TEST_VARIABLE', '123')

有条件地,根据表达式的结果

$builder
    ->addEnvironmentVariable('TEST_VARIABLE', '123', $myVar === 5)

添加多个环境变量

$builder
    ->addEnvironmentVariables([
        'TEST_VARIABLE' => '123',
        'FOO' => 'bar'
    ])

有条件地,根据表达式的结果

$builder
    ->addEnvironmentVariables([
        'TEST_VARIABLE' => '123',
        'FOO' => 'bar'
    ], $foo === 5)

添加进程观察者

可以将观察者附加到启动的进程中。在这种情况下,我们添加了一个简单的记录器

$builder
    ->addProcessObserver(
        new AllLogger(
            new DiskLogger(),
            LogLevel::DEBUG
        )
    )

构建命令

构建器配置完成后,可以检索命令以执行

$command = $builder
    // ...
    ->buildCommand();

同步执行

要同步运行命令,请使用 runSynchronous 方法。这将返回实现 CommandResultInterface 的对象,编码了命令的结果。

$result = $command
    ->runSynchronous(); 

当您需要多次运行相同的命令时,可以简单地重复调用runSynchronous;每次调用都会运行命令并将结果返回到您的应用程序。

命令的退出码和输出可以通过此对象上的方法获得

$result->getExitCode();         // 0 for success, anything else conventionally indicates an error
$result->getStdOut();           // The contents of stdout (as a string)
$result->getStdOutLines();      // The contents of stdout (as an array of lines)
$result->getStdErr();           // The contents of stderr (as a string)
$result->getStdErrLines();      // The contents of stderr (as an array of lines)
$result->getExecutedCommand();  // Get the executed command as a string, including environment variables
$result->getWorkingDirectory(); // Get the directory the command was executed in 

异步执行

命令也可以异步执行,允许程序在等待结果的同时继续执行。

Command::runAsynchronous

runAsynchronous方法返回一个实现了ProcessInterface的对象,该对象提供了监控进程状态的方法。

$process = $command->runAsynchronous();

与同步API一样,当您需要多次运行相同的命令时,可以简单地重复调用runAsynchronous;每次调用都会运行命令并将表示进程的对象返回到您的应用程序。

进程 API

ProcessInterface提供了监控和操作进程状态和生命周期所需的方法。

检查进程是否已完成

if (!$process->isRunning()) {
    echo 'done' . PHP_EOL;
}

强制进程停止

$process->stop();

等待进程停止(这将阻塞脚本的执行,实际上使此操作同步)

$process->wait();

获取进程ID(如果进程已结束,则抛出\RuntimeException

$process->getPid();

从流中读取输出

$stdOut = $process->readStream(ProcessInterface::STDOUT);

提供输入(例如通过STDIN)

$process->writeInput('Data to pass to the running process via STDIN');

获取退出码(如果进程仍在运行,则抛出\RuntimeException

$exitCode = $process->getExitCode();

向进程发送信号(SIGTERM或SIGKILL)

$process->sendSignal(ProcessInterface::SIGTERM);

获取运行中的命令的字符串表示

    $commandString = $process->getCommand();

Process::getPromise

可以将shell命令执行的监控封装在ReactPHP Promise中。这为我们提供了一个灵活的执行模型,允许使用Promise::then进行链式调用,以及使用Promise::allPromise::somePromise::race及其相关方法进行聚合。

通过从Process实例调用getPromise方法来构建执行命令的promise。这将返回一个\React\Promise\Promise实例。

$eventLoop = \React\EventLoop\Factory::create();

$promise = $command->runAsynchonous()->getPromise($eventLoop);

ReactPHP EventLoop组件用于定期轮询正在运行的过程,以查看它是否已终止;一旦它已经终止,根据执行命令的退出码,promise将被解析或拒绝。

这种实现的效果是,一旦创建了您的promises、chains和aggregates,就必须调用EventLoop::run

$eventLoop->run();

这将阻塞进一步的执行,直到promises被解析/拒绝。

模拟

提供了命令和Builder接口的模拟实现,以帮助测试。

通过针对接口进行类型提示,而不是具体的实现,这些模拟可以注入并用于返回预配置的结果对象。

贡献

您可以通过向问题跟踪器提交问题、改进文档或提交拉取请求来做出贡献。对于拉取请求,我更希望保持代码风格和测试覆盖率,但我很乐意解决可能出现的任何小问题,以便请求可以合并。

已知限制

  • 仅支持UNIX环境。