phpsu/shellcommandbuilder

用于创建 shell 命令的流体构建器

2.0.0 2020-09-07 17:33 UTC

This package is auto-updated.

Last update: 2024-09-23 11:11:35 UTC


README

Latest Version Software License Build Status Coverage Status Type Coverage Status Infection MSI Quality Score Total Downloads

以流体面向对象的方式创建基本和更复杂的 shell 命令。这使得在可读和可调试的层面对 bash 的一般机制进行抽象变得非常直接。

本库的参考依据为GNU Bash 参考手册
如果您需要在此库中添加更多来自该参考的功能,请随时创建问题或拉取请求。

概念

想象一下,您想创建以下 bash 命令:a && b | c || d |& f && (g && h) || {i || j;}

您可以通过创建一个 ShellBuilder 对象,然后从左到右按指令读取命令来实现这一点。

<?php

use PHPSu\ShellCommandBuilder\ShellBuilder;

$builder = new ShellBuilder();
$builder
    ->add('a')
    ->and('b')
    ->pipe('c')
    ->or('d')
    ->pipeWithForward('f')
    ->and(
        $builder->createGroup()->add('g')->and('h')
    )
    ->or(
        $builder->createGroup(true)->add('i')->or('j')
    );

目录

  1. 安装
  2. 用法
    1. 简单命令
    2. 管道、列表和重定向
    3. 复杂命令
    4. 条件表达式
    5. 协进程
  3. 特殊命令
  4. 贡献
  5. 测试

安装

您可以通过 composer 将此库添加到您的项目中

composer require phpsu/shellcommandbuilder

然后将其包含到您的类/文件中。

<?php

use PHPSu\ShellCommandBuilder\ShellBuilder;

$builder = new ShellBuilder();

简介

本库简化为以下三个主要组件

  • ShellBuilder
  • ShellCommand
  • ShellWord

ShellBuilder 是将一组命令粘合在一起的粘合剂。这种粘合剂是控制运算符之一,如 ||&&
命令由 ShellCommand 类表示。ShellCommand 负责参数和选项等。
ShellCommand 由 ShellWords 组成,它们代表构成命令的标记。

让我们看看一个例子

echo "hello world" | grep -e "world"

这一行是一个包含两个 ShellCommand 的 ShellBuilder 对象

  • echo "hello world
  • grep -e "world"

这些通过 | 运算符连接
拆分这些命令会得到以下 ShellWords

用法

简单命令

大部分 API 被标记为内部,它旨在通过 ShellBuilder 类访问。
这使得从单一基础构建简单和更复杂的命令变得非常简单。
此外,ShellBuilder 还具有工厂式方法,可以帮助立即从上到下构建命令。

这意味着,创建一个 ShellBuilder 可能看起来像这样

$builder = new ShellBuilder();

或者这样

$builder = ShellBuilder::new();

可以像这样创建一个 ShellCommand

$command = ShellBuilder::command('name-of-command');

或者,如果已经有一个 ShellBuilder 对象可用,可以像这样

/** @var \PHPSu\ShellCommandBuilder\ShellBuilder $builder */
$builder->createCommand('name-of-command');

让我们看一下之前的命令,并逐步构建它。

echo "hello world" | grep -e "world"

注意:每个步骤都作为代码中的注释写入

<?php
use PHPSu\ShellCommandBuilder\ShellBuilder;


// 1. First we create the command `echo`
$echo = ShellBuilder::command('echo');

// 2. "hello world" is an argument, that we can add like this:
$echo->addArgument('Hello World');

// 3. we create the `grep` command
$grep = ShellBuilder::command('grep');

// 4. and add the option '-e "world"'.
// the single hyphen that is before the option "e" marks it as a *short* option (addShortOption)
// Having two hyphens like --color makes it a regular option (addOption)
$grep->addShortOption('e', 'world');

// 5. Now we need combine those two commands together
// We do that, by creating a ShellBuilder
$builder = ShellBuilder::new();

// 6. And then adding the echo-command into it
$builder->add($echo);

// 7. Earlier we saw, that these two commands where held together by the pipe-Operator
// This can be accomplished by using the pipe-Method 
$builder->pipe($grep);

// 8. To use this command in e.g. shell_exec, you can convert it into a string and use it
shell_exec((string)$builder); // -> echo 'hello world' | echo -e 'world'

注意:每个参数和选项默认都会进行转义。

所有方法都实现了流畅的接口。对于此库,这意味着您可以通过将所有内容链接起来重写上面的示例。

<?php

use PHPSu\ShellCommandBuilder\ShellBuilder;

$builder = ShellBuilder::new()
    ->createCommand('echo')
    ->addArgument('Hello World')
    ->addToBuilder()
    ->pipe(
        ShellBuilder::command('grep')
            ->addShortOption('e', 'world')            
    );
shell_exec((string)$builder); // -> echo 'hello world' | echo -e 'world'

createCommand 将当前 ShellBuilder 传递到 ShellCommand 实例中。通过 addToBuilder,可以再次访问该 ShellBuilder,并且命令将自动添加到 ShellBuilder 中。目前这仅适用于 and

管道、列表和重定向

ShellBuilder 是表示命令之间关系的表示。无论是按顺序执行命令,还是连接输入和输出。

让我们看看以下假例
a; b && c | d || e |& f 2>&1
它说明了连接命令的多种方式。

重新构建此命令可能看起来像这样

<?php

use PHPSu\ShellCommandBuilder\ShellBuilder;

$builder = new ShellBuilder();
// adding the initial command
$builder->add('a');
// adding the next command for `;`
$builder->add('b');
// combining with and --> `&&` 
$builder->and('c');
// piping the output --> `|`
$builder->pipe('d');
// combining with or --> `||`
$builder->or('e');
// piping the output including the error --> `|&`
$builder->pipeWithForward('f');
// redirect stderr to stdout --> `2>&1`
$builder->redirectErrorToOutput();

完整的方法列表可以在以下位置找到: API 文档

复杂命令

这个库背后的理念是使生成更大和更复杂的shell命令更易于阅读和维护。
以下示例取自PHPsu。这个命令将远程源数据库同步到本地数据库。

ssh -F 'php://temp' 'hostc' 'mysqldump --opt --skip-comments --single-transaction --lock-tables=false -h '\''database'\'' -u '\''root'\'' -p '\''root'\'' '\''sequelmovie'\'' | (echo '\''CREATE DATABASE IF NOT EXISTS `sequelmovie2`;USE `sequelmovie2`;'\'' && cat)' | mysql -h '127.0.0.1' -P 2206 -u 'root' -p 'root'

首先,我们需要考虑这个命令由哪些组件组成。这导致了以下命令

ssh -F 'php://temp' 'hostc'

mysqldump --opt --skip-comments --single-transaction --lock-tables=false -h 'database' -u 'root' -p 'root' 'sequelmovie'

echo 'CREATE DATABASE IF NOT EXISTS `sequelmovie2`;USE `sequelmovie2`;'

cat

mysql -h '127.0.0.1' -P 2206 -u 'root' -p 'root'

现在,我们在PHP中构建它

<?php

use PHPSu\ShellCommandBuilder\ShellBuilder;

$builder = new ShellBuilder();
// creating the first command.
// The 'true' removes the connection between ShellBuilder and ShellCommand and makes it anonymous.
// This is the same result as ShellBuilder::command()
$mysqlDump = $builder->createCommand('mysqldump', true)
// adding the options and short-options
    ->addOption('opt')
    ->addOption('skip-comments')
    ->addOption('single-transaction')
// the signature of options have four variables
// 'lock-tables' is the name of the option --> "--lock-tables"
// the string 'false' is the value --> "--lock-tables 'false'"
// the third variable disables escaping --> "--lock-tables false"
// the fourth variable turns the space between name and value into '=' --> "--lock-tables=false"
    ->addOption('lock-tables', 'false', false, true)
    ->addShortOption('h', 'database')
    ->addShortOption('u', 'root')
    ->addShortOption('p', 'root')
    ->addArgument('sequelmovie')
    ->addToBuilder();
$builder->createCommand('ssh')
    ->addShortOption('F', 'php://temp')
    ->addArgument('hostc')
// SubCommand is technically an argument, that always escapes the output
    ->addSubCommand(
        $mysqlDump->pipe(
// 'createGroup' flags a ShellBuilder to wrap the commands in braces e.g (echo "hello world")
            $mysqlDump->createGroup()
            ->createCommand('echo')
            ->addArgument('CREATE DATABASE IF NOT EXISTS `sequelmovie2`;USE `sequelmovie2`;')
            ->addToBuilder()
            ->and('cat')
        )
    )
    ->addToBuilder()
    ->pipe(
        $builder->createCommand('mysql')
            ->addShortOption('h', '127.0.0.1')
// disabling escaping here: --> "-P 2206"
            ->addShortOption('P', '2206', false)
            ->addShortOption('u', 'root')
            ->addShortOption('p', 'root')
    )
;

接下来,我们看看如何实现进程和命令替换。以下是一个模拟示例。它创建一个当前目录及其所有子目录中所有php文件的列表,按大小排序并丰富。这个文件列表被重定向到以当前月份命名的txt文件。

cat <(ls -1ARSsD | grep ".*\.php") >> $(date +%B).txt

以下是PHP中的样子

use PHPSu\ShellCommandBuilder\ShellBuilder;

$builder = ShellBuilder::new()
    ->createCommand('cat')
// the false at the end prints the argument unescaped
    ->addArgument(
        ShellBuilder::new()
// turning all commands within this builder into a process substitution --> <(command ...)
// the same would work with `createCommandSubstition` resulting in something like this $(command ...)
        ->createProcessSubstition()
        ->createCommand('ls')
// currently combining short-options has to be done manually, although it could change in the future
// but doing it like this will always be possible, since it's impossible to evaluate the correctness
// without having the man-page of all the commands available
        ->addShortOption('1ARSsD')
        ->addToBuilder()
        ->pipe(
            ShellBuilder::command('grep')
            ->addArgument('.*\.php')
        ),
        false
    )
    ->addToBuilder()
// redirects stdout from the previous command and pushes it into stdin of the next command
// if redirected into a file, the true at the end changes the type to appending instead of overwriting --> "a >> b"
    ->redirectOutput(
        ShellBuilder::new()
            ->createCommand('date')
            ->addArgument('+%B', false)
// this is similar to the process/command-substitition from above but here it is applied on a command instead
// toggling means that instead of taking true or false as an argument it flips the internal state back and forth
            ->toggleCommandSubstitution()
            ->addToBuilder()
            ->addFileEnding('txt'),
        true
    )
;

条件表达式

条件表达式目前仍在开发中。基本API存在,但整体使用方式可能会改变,特别是涉及到转义时。

有多种条件表达式类型可用于构建表达式。它们基于Shell语法Bash参考

以下表达式类型存在

  • 算术:ArithmeticExpression::class
  • 文件:FileExpression::class
  • Shell:ShellExpression::class
  • 字符串:StringExpression::class

让我们看看两个例子

  • 1:如果文件不为空,则只执行命令
  • 2:如果变量大于5,则只执行命令
# 1:
[[ -s test.php ]] && echo "hello";

# 2: 
a=6; [[ "$a" -gt "5" ]] && echo "hello";

# 3: 
a=`cat file.txt`; [[ "$a" -gt "5" ]] && echo "hello";
use PHPSu\ShellCommandBuilder\ShellBuilder;
use PHPSu\ShellCommandBuilder\Conditional\FileExpression;
use PHPSu\ShellCommandBuilder\Conditional\ArithmeticExpression;

# 1:
ShellBuilder::new()
    ->add(FileExpression::create()->notEmpty('test.php'))
    ->and(ShellBuilder::command('echo')->addArgument('hello'))
;

# 2:
ShellBuilder::new()
    // adding a variable "a" with the value "6"
    // the third argument replaces $() through backticks --> a=$(cat) ~> a=`cat`
    // the fourth argument sets escpaing to false.
    // Escaping is disabled for commands as value.
    ->addVariable('a', '6', false, false)
    ->add(ArithmeticExpression::create()->greater('$a', '5'))
    ->and(ShellBuilder::command('echo')->addArgument('hello'))
;

# 3:

ShellBuilder::new()
    ->addVariable('a',
        ShellBuilder::new()
        ->createCommand('cat')
        ->addNoSpaceArgument('file')
        ->addToBuilder()
        ->addFileEnding('txt'),
        true // enable backticks
    )
    ->add(ArithmeticExpression::create()->greater('$a', '5')->escapeValue(true))
    ->and(ShellBuilder::command('echo')->addArgument('hello'))
;

协进程

要后台运行命令,ShellBuilder类支持coproc关键字。
这个关键字允许命令在子shell中异步运行,并且可以与管道和重定向结合使用。

有关协进程的更多信息,请参见Bash参考

以下是一个例子: {coproc tee {tee logfile;} >&3 ;} 3>&1
这将在后台启动tee并将它的输出重定向到stdout

use PHPSu\ShellCommandBuilder\Definition\GroupType;
use PHPSu\ShellCommandBuilder\ShellBuilder;

// we first create a new ShellBuilder, that will be wrapped in the group-syntax that does not open a subshell
// -> { command-list ;}
$builder = new ShellBuilder(GroupType::SAMESHELL_GROUP);
// then we set that builder to be asynchronous.
// the second argument of this method gives the coprocess a name.
// default is no name
// -> coproc [NAME] command
$builder->runAsynchronously(true)
    ->createCommand('tee')
    ->addArgument(
// createGroup again wraps it into a group-syntax and the true indicates, that is is in the same-shell notation
// false would open a subshell like e.g ( command ).
// default is false
        $builder->createGroup(true)
            ->createCommand('tee')
            ->addArgument('logfile', false)
            ->addToBuilder(),
        false
    )
    ->addToBuilder()
// redirectDescriptor is the more powerful way of writing redirects between File Descriptors
// argument 1: command that we redirect from/to
// argument 2: direction of the redirect (true: >&, false <&)
// argument 3: file descriptor before redirection
// argument 4: file descriptor after redirection
// the example below would render: >&3
    ->redirectDescriptor('', true, null, 3);
ShellBuilder::new()->add($builder)->redirectDescriptor('', true, 3, 1);            

如果您想将单个命令或命令列表重定向到后台,可以在命令末尾附加一个反斜杠&来实现。

所以你可能想这样做: ./import-script & ./import-script2 &

然后,可以这样实现

<?php
use PHPSu\ShellCommandBuilder\ShellBuilder;

ShellBuilder::new()->add('./import-script')->async('./import-script2')->async();

特殊

模式类 - ShellWord解析

模式类验证字符串输入是否为有效的Bourne Shellwords。它基于其在Ruby和Rust语言中的等效实现。
它接收一个字符串,并应用shell的单词解析规则将其拆分为一个数组。

use PHPSu\ShellCommandBuilder\Definition\Pattern;

Pattern::split('three blind mice');
// ['three', 'blind', 'mice']

Pattern::split尊重转义和引号,并且只在这些之外进行分割

use PHPSu\ShellCommandBuilder\Definition\Pattern;

Pattern::split('/home/user/dev/hallo\ welt.txt');
// ['/home/user/dev/hallo welt.txt']

Pattern::split('a "b b" a');
// ['a', 'b b', 'a']

如果输入无效,该方法将抛出异常。
例如,以下有一个不匹配的引号

use PHPSu\ShellCommandBuilder\Definition\Pattern;
Pattern::split("a \"b c d e");
// ShellBuilderException::class
// The given input has mismatching Quotes

调试ShellBuilder

有时需要更好地理解输出为什么是这样渲染的。
在这种情况下,所有类都实现了__toArray()-方法,该方法接受当前类状态并将其打印为数组。此外,ShellBuilder还实现了jsonSerializable。它本身调用__toArray-方法,并作为向客户端输出的一种快捷方式。

如果您在ShellBuilder上调用__toArray,它将通过所有命令并将它们转换为数组。这样,您就有了一个代表您想要执行的命令列表的深层嵌套结构。

贡献

安装以贡献

git clone git@github.com:phpsu/ShellCommandBuilder.git
cd ShellCommandBuilder
composer install

测试

composer test

您还可以在保存时立即检查您所做的任何更改是否影响了您的测试

composer test:watch

类型检查正在使用 psalm 进行。

composer psalm

如果您看到低的 变异评分指标 (MSI) 值,可以显示逃逸的变异

composer infection -- -s

安全性

如果发现任何与安全相关的问题,请通过电子邮件 git@cben.co 联系。

鸣谢

许可证

MIT 许可证 (MIT)。请参阅 许可证文件 获取更多信息。