phpsu / shellcommandbuilder
用于创建 shell 命令的流体构建器
Requires
- php: >=7.2
- ext-json: *
Requires (Dev)
- infection/infection: ^0.15.3
- phpunit/php-invoker: ^2.0
- phpunit/phpunit: ^8.5
- pluswerk/grumphp-config: ^3.0
- spatie/phpunit-watcher: ^1.22
- vimeo/psalm: ^3.11
README
以流体面向对象的方式创建基本和更复杂的 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') );
目录
安装
您可以通过 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)。请参阅 许可证文件 获取更多信息。